Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Equivalent to Blueprint's RunningText #14876

Open
2 tasks done
nmain opened this issue Mar 13, 2019 · 4 comments
Open
2 tasks done

Equivalent to Blueprint's RunningText #14876

nmain opened this issue Mar 13, 2019 · 4 comments
Labels
component: Typography The React component. new feature New feature or request waiting for 👍 Waiting for upvotes

Comments

@nmain
Copy link

nmain commented Mar 13, 2019

A <RunningText> component would apply typography styles to basic html elements in it, so something like this:

<RunningText>
  <h2>Foo</h2>
</RunningText>

Would behave like this:

<Typography variant="h2">Foo</Typography>
  • This is not a v0.x issue.
  • I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior 🤔

Within a <RunningText> all of the styles normally applied by <Typography> would be applied to the corresponding plain html elements.

Current Behavior 😯

If you want to render markdown in Material-UI, you have to use something like https://github.com/remarkjs/remark-react and its remarkReactComponents to make transformations like <h2> => <Typography variant="h2">. If you want to render arbitrary rich text from something like ProseMirror, you have to use something like https://github.com/remarkablemark/html-react-parser/ and its replace functionality to do the same. If you want to render rich text from a contenteditable with WYSIWYG, you're out of luck because there's no way to use <Typography> in the editing control of things like ProseMirror.

Examples 🌈

Blueprint.js is React component library unrelated to Material Design. It has a feature like this, documented here as "RUNNING_TEXT": https://blueprintjs.com/docs/#core/components/html

Context 🔦

I listed some specific use cases in Current Behavior, but basically any situation where you want to render basic document content that might be dynamically generated and where using lots of <Typography> elements would be difficult.

Benchmark

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 13, 2019

@nmain We don't have such a component. For the markdown problem. You have two options:

  1. markdown-to-jsx: You specify to a parser the React component to use for each type of element. You have a demo here:
    import React from 'react';
    import ReactMarkdown from 'markdown-to-jsx';
    import { withStyles } from '@material-ui/core/styles';
    import Typography from '@material-ui/core/Typography';
    import Link from '@material-ui/core/Link';
    const styles = theme => ({
    listItem: {
    marginTop: theme.spacing(1),
    },
    });
    const options = {
    overrides: {
    h1: { component: props => <Typography gutterBottom variant="h4" {...props} /> },
    h2: { component: props => <Typography gutterBottom variant="h6" {...props} /> },
    h3: { component: props => <Typography gutterBottom variant="subtitle1" {...props} /> },
    h4: { component: props => <Typography gutterBottom variant="caption" paragraph {...props} /> },
    p: { component: props => <Typography paragraph {...props} /> },
    a: { component: Link },
    li: {
    component: withStyles(styles)(({ classes, ...props }) => (
    <li className={classes.listItem}>
    <Typography component="span" {...props} />
    </li>
    )),
    },
    },
    };
    function Markdown(props) {
    return <ReactMarkdown options={options} {...props} />;
    }
    export default Markdown;

    This demo https://mui.com/material-ui/getting-started/templates/blog/ uses that strategy.
  2. markdown-to-html: You use a traditional html generator, then style the output. You have a demo here:
    import React from 'react';
    import PropTypes from 'prop-types';
    import clsx from 'clsx';
    import { connect } from 'react-redux';
    import marked from 'marked';
    import { withStyles } from '@material-ui/core/styles';
    import textToHash from 'docs/src/modules/utils/textToHash';
    import compose from 'docs/src/modules/utils/compose';
    import prism from 'docs/src/modules/components/prism';
    // Monkey patch to preserve non-breaking spaces
    // https://github.com/chjj/marked/blob/6b0416d10910702f73da9cb6bb3d4c8dcb7dead7/lib/marked.js#L142-L150
    marked.Lexer.prototype.lex = function lex(src) {
    src = src
    .replace(/\r\n|\r/g, '\n')
    .replace(/\t/g, ' ')
    .replace(/\u2424/g, '\n');
    return this.token(src, true);
    };
    const renderer = new marked.Renderer();
    renderer.heading = (text, level) => {
    // Small title. No need for an anchor.
    // It's reducing the risk of duplicated id and it's fewer elements in the DOM.
    if (level >= 4) {
    return `<h${level}>${text}</h${level}>`;
    }
    // eslint-disable-next-line no-underscore-dangle
    const escapedText = textToHash(text, global.__MARKED_UNIQUE__);
    return (
    `
    <h${level}>
    <a class="anchor-link" id="${escapedText}"></a>${text}` +
    `<a class="anchor-link-style" href="#${escapedText}">
    <svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="M46.9 13.9c-.5-.6-1.2-.94-2.07-.94h-6.67l1.86-8.98c.17-.85 0-1.7-.52-2.3-.48-.6-1.2-.94-2.07-.94-1.6 0-3.2 1.27-3.54 2.93l-.5 2.42c0 .07-.07.13-.07.2l-1.37 6.62H20.7l1.88-8.96c.16-.85 0-1.7-.53-2.3-.48-.6-1.2-.94-2.07-.94-1.65 0-3.2 1.27-3.56 2.93l-.52 2.58v.08l-1.37 6.64H7.3c-1.67 0-3.22 1.3-3.58 2.96-.16.86 0 1.7.52 2.3.48.6 1.2.93 2.07.93h6.97l-2 9.65H4c-1.67 0-3.22 1.27-3.56 2.94-.2.8 0 1.67.5 2.27.5.6 1.2.93 2.08.93H10l-1.84 9.05c-.2.84 0 1.67.52 2.3.5.6 1.25.92 2.08.92 1.66 0 3.2-1.3 3.55-2.94l1.94-9.33h11.22l-1.87 9.05c-.15.84.03 1.67.53 2.3.5.6 1.2.92 2.07.92 1.65 0 3.22-1.3 3.56-2.94l1.9-9.33h7c1.6 0 3.2-1.28 3.53-2.93.2-.87 0-1.7-.52-2.3-.48-.62-1.2-.96-2.05-.96h-6.7l2.02-9.65h6.93c1.67 0 3.22-1.27 3.56-2.92.2-.85 0-1.7-.5-2.3l-.04.03zM17.53 28.77l1.95-9.65H30.7l-1.97 9.66H17.5h.03z"/></svg>
    </a></h${level}>
    `
    );
    };
    const externs = [
    'https://material.io/',
    'https://www.styled-components.com/',
    'https://emotion.sh/',
    'https://getbootstrap.com/',
    ];
    renderer.link = (href, title, text) => {
    let more = '';
    if (externs.some(domain => href.indexOf(domain) !== -1)) {
    more = ' target="_blank" rel="noopener nofollow"';
    }
    // eslint-disable-next-line no-underscore-dangle
    const userLanguage = global.__MARKED_USER_LANGUAGE__;
    let finalHref = href;
    if (userLanguage !== 'en' && finalHref.indexOf('/') === 0) {
    finalHref = `/${userLanguage}${finalHref}`;
    }
    return `<a href="${finalHref}"${more}>${text}</a>`;
    };
    const markedOptions = {
    gfm: true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false,
    smartLists: true,
    smartypants: false,
    highlight(code, lang) {
    let language;
    switch (lang) {
    case 'diff':
    language = prism.languages.diff;
    break;
    case 'css':
    language = prism.languages.css;
    break;
    case 'ts':
    case 'tsx':
    language = prism.languages.typescript;
    break;
    case 'js':
    case 'jsx':
    default:
    language = prism.languages.jsx;
    break;
    }
    return prism.highlight(code, language);
    },
    renderer,
    };
    const styles = theme => ({
    root: {
    fontFamily: theme.typography.fontFamily,
    fontSize: 16,
    color: theme.palette.text.primary,
    '& .anchor-link': {
    marginTop: -96 - 29, // Offset for the anchor.
    position: 'absolute',
    },
    '& pre, & pre[class*="language-"]': {
    margin: '24px 0',
    padding: '12px 18px',
    backgroundColor: theme.palette.background.paper,
    borderRadius: theme.shape.borderRadius,
    overflow: 'auto',
    WebkitOverflowScrolling: 'touch', // iOS momentum scrolling.
    },
    '& code': {
    display: 'inline-block',
    lineHeight: 1.6,
    fontFamily: 'Consolas, "Liberation Mono", Menlo, Courier, monospace',
    padding: '3px 6px',
    color: theme.palette.text.primary,
    backgroundColor: theme.palette.background.paper,
    fontSize: 14,
    },
    '& p code, & ul code, & pre code': {
    fontSize: 14,
    lineHeight: 1.6,
    },
    '& h1': {
    ...theme.typography.h2,
    margin: '32px 0 16px',
    },
    '& .description': {
    ...theme.typography.h5,
    margin: '0 0 40px',
    },
    '& h2': {
    ...theme.typography.h4,
    margin: '32px 0 24px',
    },
    '& h3': {
    ...theme.typography.h5,
    margin: '32px 0 24px',
    },
    '& h4': {
    ...theme.typography.h6,
    margin: '24px 0 16px',
    },
    '& h5': {
    ...theme.typography.subtitle2,
    margin: '24px 0 16px',
    },
    '& p, & ul, & ol': {
    lineHeight: 1.6,
    },
    '& h1, & h2, & h3, & h4': {
    '& code': {
    fontSize: 'inherit',
    lineHeight: 'inherit',
    // Remove scroll on small screens.
    wordBreak: 'break-word',
    },
    '& .anchor-link-style': {
    opacity: 0,
    // To prevent the link to get the focus.
    display: 'none',
    },
    '&:hover .anchor-link-style': {
    display: 'inline-block',
    opacity: 1,
    padding: '0 8px',
    color: theme.palette.text.hint,
    '&:hover': {
    color: theme.palette.text.secondary,
    },
    '& svg': {
    width: '0.55em',
    height: '0.55em',
    fill: 'currentColor',
    },
    },
    },
    '& table': {
    width: '100%',
    display: 'block',
    overflowX: 'auto',
    WebkitOverflowScrolling: 'touch', // iOS momentum scrolling.
    borderCollapse: 'collapse',
    borderSpacing: 0,
    overflow: 'hidden',
    '& .prop-name': {
    fontSize: 13,
    fontFamily: 'Consolas, "Liberation Mono", Menlo, monospace',
    },
    '& .required': {
    color: theme.palette.type === 'light' ? '#006500' : '#9bc89b',
    },
    '& .prop-type': {
    fontSize: 13,
    fontFamily: 'Consolas, "Liberation Mono", Menlo, monospace',
    color: theme.palette.type === 'light' ? '#932981' : '#dbb0d0',
    },
    '& .prop-default': {
    fontSize: 13,
    fontFamily: 'Consolas, "Liberation Mono", Menlo, monospace',
    borderBottom: `1px dotted ${theme.palette.text.hint}`,
    },
    },
    '& thead': {
    fontSize: 14,
    fontWeight: theme.typography.fontWeightMedium,
    color: theme.palette.text.secondary,
    },
    '& tbody': {
    fontSize: 14,
    lineHeight: 1.5,
    color: theme.palette.text.primary,
    },
    '& td': {
    borderBottom: `1px solid ${theme.palette.divider}`,
    padding: '8px 16px 8px 8px',
    textAlign: 'left',
    },
    '& td:last-child': {
    paddingRight: 24,
    },
    '& td compact': {
    paddingRight: 24,
    },
    '& td code': {
    fontSize: 13,
    lineHeight: 1.6,
    },
    '& th': {
    whiteSpace: 'pre',
    borderBottom: `1px solid ${theme.palette.divider}`,
    fontWeight: theme.typography.fontWeightMedium,
    padding: '0 16px 0 8px',
    textAlign: 'left',
    },
    '& th:last-child': {
    paddingRight: 24,
    },
    '& tr': {
    height: 48,
    },
    '& thead tr': {
    height: 64,
    },
    '& strong': {
    fontWeight: theme.typography.fontWeightMedium,
    },
    '& blockquote': {
    borderLeft: `5px solid ${theme.palette.text.hint}`,
    backgroundColor: theme.palette.background.paper,
    padding: '4px 24px',
    margin: '24px 0',
    },
    '& a, & a code': {
    // Style taken from the Link component
    color: theme.palette.secondary.main,
    textDecoration: 'none',
    '&:hover': {
    textDecoration: 'underline',
    },
    },
    '& img': {
    maxWidth: '100%',
    },
    },
    });
    function MarkdownElement(props) {
    const { classes, className, dispatch, text, userLanguage, ...other } = props;
    // eslint-disable-next-line no-underscore-dangle
    global.__MARKED_USER_LANGUAGE__ = userLanguage;
    /* eslint-disable react/no-danger */
    return (
    <div
    className={clsx(classes.root, 'markdown-body', className)}
    dangerouslySetInnerHTML={{ __html: marked(text, markedOptions) }}
    {...other}
    />
    );
    /* eslint-enable */
    }
    MarkdownElement.propTypes = {
    classes: PropTypes.object.isRequired,
    className: PropTypes.string,
    dispatch: PropTypes.func,
    text: PropTypes.string,
    userLanguage: PropTypes.string.isRequired,
    };
    export default compose(
    connect(state => ({
    userLanguage: state.options.userLanguage,
    })),
    withStyles(styles, { flip: false }),
    )(MarkdownElement);

    The documentation https://mui.com/ uses this strategy

n°1 has the advantage of being fully idiomatic with React. The drawback is that it's x5 slower than n°2.

@nmain
Copy link
Author

nmain commented Mar 13, 2019

So what I'm proposing is a component that implements style rules similar to line 105 here:

https://github.com/mui/material-ui/blob/2f6a982aa74ffa46680798089ad20ed67ed0c5ae/docs/src/modules/components/MarkdownElement.js#L105-277

be added to MaterialUI.

@oliviertassinari
Copy link
Member

A similar concept component for benchmark: https://elastic.github.io/eui/#/display/text.

@oliviertassinari oliviertassinari removed the waiting for 👍 Waiting for upvotes label Nov 30, 2019
@olivercoad
Copy link

I'm also looking for something like this.
Bulma's content an another similar thing.

Is there any reason not to just copy MarkdownElement pretty much as-is and rename it to RunningText?
I guess remove the renderedMarkdown prop, and it might be nice to have add a prop that works like the reverse of variantMapping to map semantic elements to variants.

Would you accept a PR for something like this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: Typography The React component. new feature New feature or request waiting for 👍 Waiting for upvotes
Projects
None yet
Development

No branches or pull requests

3 participants