Skip to content

Commit

Permalink
[docs] Refactor markdown parsing (#20549)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Apr 14, 2020
1 parent f593db9 commit 8185461
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 142 deletions.
76 changes: 35 additions & 41 deletions docs/src/modules/components/AppTableOfContents.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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([]);
Expand Down
181 changes: 80 additions & 101 deletions docs/src/modules/components/MarkdownElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<h${level}>${text}</h${level}>`;
}

// eslint-disable-next-line no-underscore-dangle
const hash = textToHash(text, global.__MARKED_UNIQUE__);

return [
`<h${level}>`,
`<a class="anchor-link" id="${hash}"></a>`,
text,
`<a class="anchor-link-style" aria-hidden="true" aria-label="anchor" href="#${hash}">`,
'<svg><use xlink:href="#anchor-link-icon" /></svg>',
'</a>',
`</h${level}>`,
].join('');
};

const externs = [
'https://material.io/',
'https://getbootstrap.com/',
Expand All @@ -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 `<a href="${finalHref}"${more}>${text}</a>`;
};

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
// <SomeComponent />
// 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,
Expand Down Expand Up @@ -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
// <SomeComponent />
// 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 `<h${level}>${headingText}</h${level}>`;
}

// eslint-disable-next-line no-underscore-dangle
const hash = textToHash(headingText, global.__MARKED_UNIQUE__);

return [
`<h${level}>`,
`<a class="anchor-link" id="${hash}"></a>`,
headingText,
`<a class="anchor-link-style" aria-hidden="true" aria-label="anchor" href="#${hash}">`,
'<svg><use xlink:href="#anchor-link-icon" /></svg>',
'</a>',
`</h${level}>`,
].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 `<a href="${finalHref}"${more}>${linkText}</a>`;
},
});
}, [text, userLanguage]);

/* eslint-disable react/no-danger */
return (
<div
className={clsx(classes.root, 'markdown-body', className)}
dangerouslySetInnerHTML={{ __html: marked(text, markedOptions) }}
dangerouslySetInnerHTML={{ __html: renderedMarkdown }}
{...other}
/>
);
Expand Down
30 changes: 30 additions & 0 deletions docs/src/modules/utils/parseMarkdown.js
Original file line number Diff line number Diff line change
@@ -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 class="description">(.*)<\/p>[\r\n]/;
Expand Down Expand Up @@ -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);
}

0 comments on commit 8185461

Please sign in to comment.