import { marked } from 'marked'; import textToHash from './textToHash.mjs'; import prism from './prism.mjs'; /** * Option used by `marked` the library parsing markdown. */ const markedOptions = { gfm: true, breaks: false, pedantic: false, }; const headerRegExp = /---[\r\n]([\s\S]*)[\r\n]---/; const titleRegExp = /# (.*)[\r\n]/; const descriptionRegExp = /
(.*?)<\/p>/s;
const headerKeyValueRegExp = /(.*?):[\r\n]?\s+(\[[^\]]+\]|.*)/g;
const emptyRegExp = /^\s*$/;
/**
* Same as https://github.com/markedjs/marked/blob/master/src/helpers.js
* Need to duplicate because `marked` does not export `escape` function
*/
const escapeTest = /[&<>"']/;
const escapeReplace = /[&<>"']/g;
const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
const escapeReplacements = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
const getEscapeReplacement = (ch) => escapeReplacements[ch];
function escape(html, encode) {
if (encode) {
if (escapeTest.test(html)) {
return html.replace(escapeReplace, getEscapeReplacement);
}
} else if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
return html;
}
function checkUrlHealth(href, linkText, context) {
const url = new URL(href, 'https://mui.com/');
if (/\/{2,}$/.test(url.pathname)) {
throw new Error(
[
'docs-infra: Duplicated trailing slashes. The following link:',
`[${linkText}](${href}) in ${context.location} has duplicated trailing slashes, please only add one.`,
'',
'See https://ahrefs.com/blog/trailing-slash/ for more details.',
'',
].join('\n'),
);
}
// External links to MUI, ignore
if (url.host !== 'mui.com') {
return;
}
/**
* Break for links like:
* /material-ui/customization/theming
*
* It needs to be:
* /material-ui/customization/theming/
*/
if (!url.pathname.endsWith('/')) {
throw new Error(
[
'docs-infra: Missing trailing slash. The following link:',
`[${linkText}](${href}) in ${context.location} is missing a trailing slash, please add it.`,
'',
'See https://ahrefs.com/blog/trailing-slash/ for more details.',
'',
].join('\n'),
);
}
// Relative links
if (href[0] !== '#' && !(href.startsWith('https://') || href.startsWith('http://'))) {
/**
* Break for links like:
* material-ui/customization/theming/
*
* It needs to be:
* /material-ui/customization/theming/
*/
if (href[0] !== '/') {
throw new Error(
[
'docs-infra: Missing leading slash. The following link:',
`[${linkText}](${href}) in ${context.location} is missing a leading slash, please add it.`,
'',
].join('\n'),
);
}
}
}
/**
* Extract information from the top of the markdown.
* For instance, the following input:
*
* ---
* title: Backdrop React Component
* components: Backdrop
* ---
*
* # Backdrop
*
* should output:
* { title: 'Backdrop React Component', components: ['Backdrop'] }
*/
function getHeaders(markdown) {
let header = markdown.match(headerRegExp);
if (!header) {
return {
components: [],
};
}
header = header[1];
try {
let regexMatches;
const headers = {};
// eslint-disable-next-line no-cond-assign
while ((regexMatches = headerKeyValueRegExp.exec(header)) !== null) {
const key = regexMatches[1];
let value = regexMatches[2].replace(/(.*)/, '$1');
if (value.startsWith('[')) {
// Need double quotes to JSON parse.
value = value.replace(/'/g, '"');
// Remove the comma after the last value e.g. ["foo", "bar",] -> ["foo", "bar"].
value = value.replace(/,\s+\]$/g, ']');
headers[key] = JSON.parse(value);
} else {
// Remove quote YAML escaping.
headers[key] = value.replace(/^"|"$|^'|'$/g, '');
}
}
if (headers.components) {
headers.components = headers.components
.split(',')
.map((x) => x.trim())
.sort();
} else {
headers.components = [];
}
if (headers.hooks) {
headers.hooks = headers.hooks
.split(',')
.map((x) => x.trim())
.sort();
} else {
headers.hooks = [];
}
return headers;
} catch (err) {
throw new Error(
`docs-infra: ${err.message} in getHeader(markdown) with markdown: \n\n${header}\n`,
);
}
}
function getContents(markdown) {
const rep = markdown
.replace(headerRegExp, '') // Remove header information
.split(/^{{("(?:demo|component)":.*)}}$/gm) // Split markdown into an array, separating demos
.flatMap((text) => text.split(/^(', ...lines.map((line) => `
'].join(
'',
);
}
/**
* @param {string} markdown
*/
function renderMarkdown(markdown) {
// Check if the markdown contains an inline list. Unordered lists are block elements and cannot be parsed inline.
if (/[-*+] `([A-Za-z]+)`/g.test(markdown)) {
return marked.parse(markdown, markedOptions);
}
// Two new lines result in a newline in the table.
// All other new lines must be eliminated to prevent markdown mayhem.
return marked
.parseInline(markdown, markedOptions)
.replace(/(\r?\n){2}/g, '
')
.replace(/\r?\n/g, ' ');
}
// Help rank mui.com on component searches first.
const noSEOadvantage = [
'https://m2.material.io/',
'https://m3.material.io/',
'https://material.io/',
'https://getbootstrap.com/',
'https://icons.getbootstrap.com/',
'https://pictogrammers.com/',
'https://www.w3.org/',
'https://tailwindcss.com/',
'https://heroicons.com/',
'https://react-icons.github.io/',
'https://fontawesome.com/',
'https://react-spectrum.adobe.com/',
'https://headlessui.com/',
'https://refine.dev/',
'https://scaffoldhub.io/',
'https://marmelab.com/',
'https://framesxdesign.com/',
];
/**
* Creates a function that MUST be used to render non-inline markdown.
* It keeps track of a table of contents and hashes of its items.
* This is important to create anchors that are invariant between languages.
*
* @typedef {object} TableOfContentsEntry
* @property {TableOfContentsEntry[]} children
* @property {string} hash
* @property {number} level
* @property {string} text
* @param {object} context
* @param {Record]*>/gi, '').replace(/<\/code>/gi, '');
// Standardizes the hash from the default location (en) to different locations
// Need english.md file parsed first
let hash;
if (userLanguage === 'en') {
hash = textToHash(headingText, headingHashes);
} else {
headingIndex += 1;
hash = Object.keys(headingHashes)[headingIndex];
if (!hash) {
hash = textToHash(headingText, headingHashesFallbackTranslated);
}
}
// enable splitting of long words from function name + first arg name
// Closing parens are less interesting since this would only allow breaking one character earlier.
// Applying the same mechanism would also allow breaking of non-function signatures like "Community help (free)".
// To detect that we enabled breaking of open/closing parens we'd need a context-sensitive parser.
const displayText = headingText.replace(/([^\s]\()/g, '$1');
// create a nested structure with 2 levels starting with level 2 e.g.
// [{...level2, children: [level3, level3, level3]}, level2]
if (level === 2) {
toc.push({
text: displayText,
level,
hash,
children: [],
});
} else if (level === 3) {
if (!toc[toc.length - 1]) {
throw new Error(`docs-infra: Missing parent level for: ${headingText}\n`);
}
toc[toc.length - 1].children.push({
text: displayText,
level,
hash,
});
}
return [
headingHtml.includes('${headingHtml}`,
``,
].join('')
: `
\n`;
}
return `${escaped ? code : escape(code, true)}
${[
'${
escaped ? code : escape(code, true)
}