Skip to content

Commit 8185461

Browse files
authored
[docs] Refactor markdown parsing (mui#20549)
1 parent f593db9 commit 8185461

File tree

3 files changed

+145
-142
lines changed

3 files changed

+145
-142
lines changed

docs/src/modules/components/AppTableOfContents.js

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* eslint-disable react/no-danger */
22
import React from 'react';
33
import PropTypes from 'prop-types';
4-
import marked from 'marked/lib/marked';
54
import throttle from 'lodash/throttle';
65
import clsx from 'clsx';
76
import Box from '@material-ui/core/Box';
87
import { useSelector } from 'react-redux';
98
import { makeStyles } from '@material-ui/core/styles';
109
import Typography from '@material-ui/core/Typography';
10+
import { render as renderMarkdown } from 'docs/src/modules/utils/parseMarkdown';
1111
import textToHash from 'docs/src/modules/utils/textToHash';
1212
import DiamondSponsors from 'docs/src/modules/components/DiamondSponsors';
1313
import Link from 'docs/src/modules/components/Link';
@@ -60,43 +60,6 @@ const useStyles = makeStyles((theme) => ({
6060
active: {},
6161
}));
6262

63-
const renderer = new marked.Renderer();
64-
65-
function setRenderer(itemsCollector, unique) {
66-
renderer.heading = (text2, level) => {
67-
const text = text2
68-
.replace(
69-
/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
70-
'',
71-
) // remove emojis
72-
.replace(/<\/?[^>]+(>|$)/g, ''); // remove HTML
73-
74-
if (level === 2) {
75-
itemsCollector.current.push({
76-
text,
77-
level,
78-
hash: textToHash(text, unique),
79-
children: [],
80-
});
81-
} else if (level === 3) {
82-
if (!itemsCollector.current[itemsCollector.current.length - 1]) {
83-
throw new Error(`Missing parent level for: ${text}`);
84-
}
85-
86-
itemsCollector.current[itemsCollector.current.length - 1].children.push({
87-
text,
88-
level,
89-
hash: textToHash(text, unique),
90-
});
91-
}
92-
};
93-
}
94-
95-
function getItemsServer(contents, itemsCollector) {
96-
marked(contents.join(''), { renderer });
97-
return itemsCollector.current;
98-
}
99-
10063
function getItemsClient(items) {
10164
const itemsClient = [];
10265

@@ -145,9 +108,40 @@ export default function AppTableOfContents(props) {
145108
const t = useSelector((state) => state.options.t);
146109

147110
const itemsServer = React.useMemo(() => {
148-
const itemsCollectorRef = { current: [] };
149-
setRenderer(itemsCollectorRef, {});
150-
return getItemsServer(contents, itemsCollectorRef);
111+
const items = [];
112+
const unique = {};
113+
114+
renderMarkdown(contents.join(''), {
115+
heading: (text2, level) => {
116+
const text = text2
117+
.replace(
118+
/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
119+
'',
120+
) // remove emojis
121+
.replace(/<\/?[^>]+(>|$)/g, ''); // remove HTML
122+
123+
if (level === 2) {
124+
items.push({
125+
text,
126+
level,
127+
hash: textToHash(text, unique),
128+
children: [],
129+
});
130+
} else if (level === 3) {
131+
if (!items[items.length - 1]) {
132+
throw new Error(`Missing parent level for: ${text}`);
133+
}
134+
135+
items[items.length - 1].children.push({
136+
text,
137+
level,
138+
hash: textToHash(text, unique),
139+
});
140+
}
141+
},
142+
});
143+
144+
return items;
151145
}, [contents]);
152146

153147
const itemsClientRef = React.useRef([]);

docs/src/modules/components/MarkdownElement.js

Lines changed: 80 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,11 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import clsx from 'clsx';
44
import { useSelector } from 'react-redux';
5-
import marked from 'marked/lib/marked';
65
import { withStyles } from '@material-ui/core/styles';
76
import textToHash from 'docs/src/modules/utils/textToHash';
7+
import { render as renderMarkdown } from 'docs/src/modules/utils/parseMarkdown';
88
import prism from 'docs/src/modules/components/prism';
99

10-
// Monkey patch to preserve non-breaking spaces
11-
// https://github.com/chjj/marked/blob/6b0416d10910702f73da9cb6bb3d4c8dcb7dead7/lib/marked.js#L142-L150
12-
marked.Lexer.prototype.lex = function lex(src) {
13-
src = src
14-
.replace(/\r\n|\r/g, '\n')
15-
.replace(/\t/g, ' ')
16-
.replace(/\u2424/g, '\n');
17-
18-
return this.token(src, true);
19-
};
20-
21-
const renderer = new marked.Renderer();
22-
renderer.heading = (text, level) => {
23-
// Small title. No need for an anchor.
24-
// It's reducing the risk of duplicated id and it's fewer elements in the DOM.
25-
if (level >= 4) {
26-
return `<h${level}>${text}</h${level}>`;
27-
}
28-
29-
// eslint-disable-next-line no-underscore-dangle
30-
const hash = textToHash(text, global.__MARKED_UNIQUE__);
31-
32-
return [
33-
`<h${level}>`,
34-
`<a class="anchor-link" id="${hash}"></a>`,
35-
text,
36-
`<a class="anchor-link-style" aria-hidden="true" aria-label="anchor" href="#${hash}">`,
37-
'<svg><use xlink:href="#anchor-link-icon" /></svg>',
38-
'</a>',
39-
`</h${level}>`,
40-
].join('');
41-
};
42-
4310
const externs = [
4411
'https://material.io/',
4512
'https://getbootstrap.com/',
@@ -50,70 +17,6 @@ const externs = [
5017
'https://ui-kit.co/',
5118
];
5219

53-
renderer.link = (href, title, text) => {
54-
let more = '';
55-
56-
if (externs.some((domain) => href.indexOf(domain) !== -1)) {
57-
more = ' target="_blank" rel="noopener nofollow"';
58-
}
59-
60-
// eslint-disable-next-line no-underscore-dangle
61-
const userLanguage = global.__MARKED_USER_LANGUAGE__;
62-
let finalHref = href;
63-
64-
if (userLanguage !== 'en' && finalHref.indexOf('/') === 0 && finalHref !== '/size-snapshot') {
65-
finalHref = `/${userLanguage}${finalHref}`;
66-
}
67-
68-
return `<a href="${finalHref}"${more}>${text}</a>`;
69-
};
70-
71-
const markedOptions = {
72-
gfm: true,
73-
tables: true,
74-
breaks: false,
75-
pedantic: false,
76-
sanitize: false,
77-
smartLists: true,
78-
smartypants: false,
79-
highlight(code, language) {
80-
let prismLanguage;
81-
switch (language) {
82-
case 'ts':
83-
prismLanguage = prism.languages.tsx;
84-
break;
85-
86-
case 'js':
87-
case 'sh':
88-
prismLanguage = prism.languages.jsx;
89-
break;
90-
91-
case 'diff':
92-
prismLanguage = { ...prism.languages.diff };
93-
// original `/^[-<].*$/m` matches lines starting with `<` which matches
94-
// <SomeComponent />
95-
// we will only use `-` as the deleted marker
96-
prismLanguage.deleted = /^[-].*$/m;
97-
break;
98-
99-
default:
100-
prismLanguage = prism.languages[language];
101-
break;
102-
}
103-
104-
if (!prismLanguage) {
105-
if (language) {
106-
throw new Error(`unsupported language: "${language}", "${code}"`);
107-
} else {
108-
prismLanguage = prism.languages.jsx;
109-
}
110-
}
111-
112-
return prism.highlight(code, prismLanguage);
113-
},
114-
renderer,
115-
};
116-
11720
const styles = (theme) => ({
11821
root: {
11922
...theme.typography.body1,
@@ -301,14 +204,90 @@ function MarkdownElement(props) {
301204

302205
const userLanguage = useSelector((state) => state.options.userLanguage);
303206

304-
// eslint-disable-next-line no-underscore-dangle
305-
global.__MARKED_USER_LANGUAGE__ = userLanguage;
207+
const renderedMarkdown = React.useMemo(() => {
208+
return renderMarkdown(text, {
209+
highlight(code, language) {
210+
let prismLanguage;
211+
switch (language) {
212+
case 'ts':
213+
prismLanguage = prism.languages.tsx;
214+
break;
215+
216+
case 'js':
217+
case 'sh':
218+
prismLanguage = prism.languages.jsx;
219+
break;
220+
221+
case 'diff':
222+
prismLanguage = { ...prism.languages.diff };
223+
// original `/^[-<].*$/m` matches lines starting with `<` which matches
224+
// <SomeComponent />
225+
// we will only use `-` as the deleted marker
226+
prismLanguage.deleted = /^[-].*$/m;
227+
break;
228+
229+
default:
230+
prismLanguage = prism.languages[language];
231+
break;
232+
}
233+
234+
if (!prismLanguage) {
235+
if (language) {
236+
throw new Error(`unsupported language: "${language}", "${code}"`);
237+
} else {
238+
prismLanguage = prism.languages.jsx;
239+
}
240+
}
241+
242+
return prism.highlight(code, prismLanguage);
243+
},
244+
heading: (headingText, level) => {
245+
// Small title. No need for an anchor.
246+
// It's reducing the risk of duplicated id and it's fewer elements in the DOM.
247+
if (level >= 4) {
248+
return `<h${level}>${headingText}</h${level}>`;
249+
}
250+
251+
// eslint-disable-next-line no-underscore-dangle
252+
const hash = textToHash(headingText, global.__MARKED_UNIQUE__);
253+
254+
return [
255+
`<h${level}>`,
256+
`<a class="anchor-link" id="${hash}"></a>`,
257+
headingText,
258+
`<a class="anchor-link-style" aria-hidden="true" aria-label="anchor" href="#${hash}">`,
259+
'<svg><use xlink:href="#anchor-link-icon" /></svg>',
260+
'</a>',
261+
`</h${level}>`,
262+
].join('');
263+
},
264+
link: (href, title, linkText) => {
265+
let more = '';
266+
267+
if (externs.some((domain) => href.indexOf(domain) !== -1)) {
268+
more = ' target="_blank" rel="noopener nofollow"';
269+
}
270+
271+
let finalHref = href;
272+
273+
if (
274+
userLanguage !== 'en' &&
275+
finalHref.indexOf('/') === 0 &&
276+
finalHref !== '/size-snapshot'
277+
) {
278+
finalHref = `/${userLanguage}${finalHref}`;
279+
}
280+
281+
return `<a href="${finalHref}"${more}>${linkText}</a>`;
282+
},
283+
});
284+
}, [text, userLanguage]);
306285

307286
/* eslint-disable react/no-danger */
308287
return (
309288
<div
310289
className={clsx(classes.root, 'markdown-body', className)}
311-
dangerouslySetInnerHTML={{ __html: marked(text, markedOptions) }}
290+
dangerouslySetInnerHTML={{ __html: renderedMarkdown }}
312291
{...other}
313292
/>
314293
);

docs/src/modules/utils/parseMarkdown.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import marked from 'marked/lib/marked';
2+
13
const headerRegExp = /---[\r\n]([\s\S]*)[\r\n]---/;
24
const titleRegExp = /# (.*)[\r\n]/;
35
const descriptionRegExp = /<p class="description">(.*)<\/p>[\r\n]/;
@@ -63,3 +65,31 @@ export function getDescription(markdown) {
6365

6466
return matches[1];
6567
}
68+
69+
/**
70+
* Render markdown used in the Material-UI docs
71+
*
72+
* @param {string} markdown
73+
* @param {object} [options]
74+
* @param {function} [options.highlight] - https://marked.js.org/#/USING_ADVANCED.md#highlight
75+
* @param {object} [options.rest] - properties from https://marked.js.org/#/USING_PRO.md#renderer
76+
*/
77+
export function render(markdown, options = {}) {
78+
const { highlight, ...rendererOptions } = options;
79+
80+
const renderer = Object.assign(new marked.Renderer(), rendererOptions);
81+
82+
const markedOptions = {
83+
gfm: true,
84+
tables: true,
85+
breaks: false,
86+
pedantic: false,
87+
sanitize: false,
88+
smartLists: true,
89+
smartypants: false,
90+
highlight,
91+
renderer,
92+
};
93+
94+
return marked(markdown, markedOptions);
95+
}

0 commit comments

Comments
 (0)