diff --git a/docs/src/modules/components/AppTableOfContents.js b/docs/src/modules/components/AppTableOfContents.js index 7cd92269a00257..217333658de183 100644 --- a/docs/src/modules/components/AppTableOfContents.js +++ b/docs/src/modules/components/AppTableOfContents.js @@ -1,13 +1,13 @@ /* eslint-disable react/no-danger */ import React from 'react'; import PropTypes from 'prop-types'; -import marked from 'marked/lib/marked'; import throttle from 'lodash/throttle'; import clsx from 'clsx'; import Box from '@material-ui/core/Box'; import { useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; +import { render as renderMarkdown } from 'docs/src/modules/utils/parseMarkdown'; import textToHash from 'docs/src/modules/utils/textToHash'; import DiamondSponsors from 'docs/src/modules/components/DiamondSponsors'; import Link from 'docs/src/modules/components/Link'; @@ -60,43 +60,6 @@ const useStyles = makeStyles((theme) => ({ active: {}, })); -const renderer = new marked.Renderer(); - -function setRenderer(itemsCollector, unique) { - renderer.heading = (text2, level) => { - const text = text2 - .replace( - /([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, - '', - ) // remove emojis - .replace(/<\/?[^>]+(>|$)/g, ''); // remove HTML - - if (level === 2) { - itemsCollector.current.push({ - text, - level, - hash: textToHash(text, unique), - children: [], - }); - } else if (level === 3) { - if (!itemsCollector.current[itemsCollector.current.length - 1]) { - throw new Error(`Missing parent level for: ${text}`); - } - - itemsCollector.current[itemsCollector.current.length - 1].children.push({ - text, - level, - hash: textToHash(text, unique), - }); - } - }; -} - -function getItemsServer(contents, itemsCollector) { - marked(contents.join(''), { renderer }); - return itemsCollector.current; -} - function getItemsClient(items) { const itemsClient = []; @@ -145,9 +108,40 @@ export default function AppTableOfContents(props) { const t = useSelector((state) => state.options.t); const itemsServer = React.useMemo(() => { - const itemsCollectorRef = { current: [] }; - setRenderer(itemsCollectorRef, {}); - return getItemsServer(contents, itemsCollectorRef); + const items = []; + const unique = {}; + + renderMarkdown(contents.join(''), { + heading: (text2, level) => { + const text = text2 + .replace( + /([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, + '', + ) // remove emojis + .replace(/<\/?[^>]+(>|$)/g, ''); // remove HTML + + if (level === 2) { + items.push({ + text, + level, + hash: textToHash(text, unique), + children: [], + }); + } else if (level === 3) { + if (!items[items.length - 1]) { + throw new Error(`Missing parent level for: ${text}`); + } + + items[items.length - 1].children.push({ + text, + level, + hash: textToHash(text, unique), + }); + } + }, + }); + + return items; }, [contents]); const itemsClientRef = React.useRef([]); diff --git a/docs/src/modules/components/MarkdownElement.js b/docs/src/modules/components/MarkdownElement.js index 1ca0f06935ca05..bd60873db434bc 100644 --- a/docs/src/modules/components/MarkdownElement.js +++ b/docs/src/modules/components/MarkdownElement.js @@ -2,44 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { useSelector } from 'react-redux'; -import marked from 'marked/lib/marked'; import { withStyles } from '@material-ui/core/styles'; import textToHash from 'docs/src/modules/utils/textToHash'; +import { render as renderMarkdown } from 'docs/src/modules/utils/parseMarkdown'; 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 `${text}`; - } - - // eslint-disable-next-line no-underscore-dangle - const hash = textToHash(text, global.__MARKED_UNIQUE__); - - return [ - ``, - ``, - text, - `', - ``, - ].join(''); -}; - const externs = [ 'https://material.io/', 'https://getbootstrap.com/', @@ -50,70 +17,6 @@ const externs = [ 'https://ui-kit.co/', ]; -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 !== '/size-snapshot') { - finalHref = `/${userLanguage}${finalHref}`; - } - - return `${text}`; -}; - -const markedOptions = { - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: false, - smartLists: true, - smartypants: false, - highlight(code, language) { - let prismLanguage; - switch (language) { - case 'ts': - prismLanguage = prism.languages.tsx; - break; - - case 'js': - case 'sh': - prismLanguage = prism.languages.jsx; - break; - - case 'diff': - prismLanguage = { ...prism.languages.diff }; - // original `/^[-<].*$/m` matches lines starting with `<` which matches - // - // we will only use `-` as the deleted marker - prismLanguage.deleted = /^[-].*$/m; - break; - - default: - prismLanguage = prism.languages[language]; - break; - } - - if (!prismLanguage) { - if (language) { - throw new Error(`unsupported language: "${language}", "${code}"`); - } else { - prismLanguage = prism.languages.jsx; - } - } - - return prism.highlight(code, prismLanguage); - }, - renderer, -}; - const styles = (theme) => ({ root: { ...theme.typography.body1, @@ -301,14 +204,90 @@ function MarkdownElement(props) { const userLanguage = useSelector((state) => state.options.userLanguage); - // eslint-disable-next-line no-underscore-dangle - global.__MARKED_USER_LANGUAGE__ = userLanguage; + const renderedMarkdown = React.useMemo(() => { + return renderMarkdown(text, { + highlight(code, language) { + let prismLanguage; + switch (language) { + case 'ts': + prismLanguage = prism.languages.tsx; + break; + + case 'js': + case 'sh': + prismLanguage = prism.languages.jsx; + break; + + case 'diff': + prismLanguage = { ...prism.languages.diff }; + // original `/^[-<].*$/m` matches lines starting with `<` which matches + // + // we will only use `-` as the deleted marker + prismLanguage.deleted = /^[-].*$/m; + break; + + default: + prismLanguage = prism.languages[language]; + break; + } + + if (!prismLanguage) { + if (language) { + throw new Error(`unsupported language: "${language}", "${code}"`); + } else { + prismLanguage = prism.languages.jsx; + } + } + + return prism.highlight(code, prismLanguage); + }, + heading: (headingText, 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 `${headingText}`; + } + + // eslint-disable-next-line no-underscore-dangle + const hash = textToHash(headingText, global.__MARKED_UNIQUE__); + + return [ + ``, + ``, + headingText, + `', + ``, + ].join(''); + }, + link: (href, title, linkText) => { + let more = ''; + + if (externs.some((domain) => href.indexOf(domain) !== -1)) { + more = ' target="_blank" rel="noopener nofollow"'; + } + + let finalHref = href; + + if ( + userLanguage !== 'en' && + finalHref.indexOf('/') === 0 && + finalHref !== '/size-snapshot' + ) { + finalHref = `/${userLanguage}${finalHref}`; + } + + return `${linkText}`; + }, + }); + }, [text, userLanguage]); /* eslint-disable react/no-danger */ return (
); diff --git a/docs/src/modules/utils/parseMarkdown.js b/docs/src/modules/utils/parseMarkdown.js index 2f75a72b9d4ea8..d1c0a8c9b279d2 100644 --- a/docs/src/modules/utils/parseMarkdown.js +++ b/docs/src/modules/utils/parseMarkdown.js @@ -1,3 +1,5 @@ +import marked from 'marked/lib/marked'; + const headerRegExp = /---[\r\n]([\s\S]*)[\r\n]---/; const titleRegExp = /# (.*)[\r\n]/; const descriptionRegExp = /

(.*)<\/p>[\r\n]/; @@ -63,3 +65,31 @@ export function getDescription(markdown) { return matches[1]; } + +/** + * Render markdown used in the Material-UI docs + * + * @param {string} markdown + * @param {object} [options] + * @param {function} [options.highlight] - https://marked.js.org/#/USING_ADVANCED.md#highlight + * @param {object} [options.rest] - properties from https://marked.js.org/#/USING_PRO.md#renderer + */ +export function render(markdown, options = {}) { + const { highlight, ...rendererOptions } = options; + + const renderer = Object.assign(new marked.Renderer(), rendererOptions); + + const markedOptions = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + smartLists: true, + smartypants: false, + highlight, + renderer, + }; + + return marked(markdown, markedOptions); +}