import fs from 'fs'; import path from 'path'; import { kebabCase } from 'es-toolkit/string'; import { createRender, getContents, getDescription, getCodeblock, getFeatureList, getHeaders, getTitle, } from './parseMarkdown.mjs'; /** * @type {string | string[]} */ const BaseUIReexportedComponents = []; /** * @param {string} productId * @example 'material' * @param {string} componentPkg * @example 'mui-base' * @param {string} component * @example 'Button' * @returns {string} */ function resolveComponentApiUrl(productId, componentPkg, component) { if (!productId) { return `/api/${kebabCase(component)}/`; } if (productId === 'x-date-pickers') { return `/x/api/date-pickers/${kebabCase(component)}/`; } if (productId === 'x-charts') { return `/x/api/charts/${kebabCase(component)}/`; } if (productId === 'x-tree-view') { return `/x/api/tree-view/${kebabCase(component)}/`; } if (productId === 'x-data-grid') { return `/x/api/data-grid/${kebabCase(component)}/`; } if (componentPkg === 'mui-base' || BaseUIReexportedComponents.includes(component)) { return `/base-ui/react-${kebabCase(component)}/components-api/#${kebabCase(component)}`; } if (productId === 'toolpad-core') { return `/toolpad/core/api/${kebabCase(component)}/`; } return `/${productId}/api/${kebabCase(component)}/`; } /** * @typedef {{ component: string, demo?: undefined }} ComponentEntry * @typedef {{ component?: undefined, demo: string, hideToolbar?: boolean }} DemoEntry */ /** * @typedef {{ rendered: Array }} TranslatedDoc */ /** * @param {object} config * @param {Array<{ markdown: string, filename: string, userLanguage: string }>} config.translations - Mapping of locale to its markdown * @param {string} config.fileRelativeContext - posix filename relative to repository root directory * @param {object} config.options - provided to the webpack loader * @param {string} config.options.workspaceRoot - The absolute path of the repository root directory * @param {object} [config.componentPackageMapping] - Mapping of productId to mapping of component name to package name * @example { 'material': { 'Button': 'mui-material' } } * @returns {{ docs: Record }} - Mapping of locale to its prepared markdown */ function prepareMarkdown(config) { const { fileRelativeContext, translations, componentPackageMapping = {}, options } = config; /** * @type {Record} */ const docs = {}; const headingHashes = {}; translations // Process the English markdown before the other locales. // English ToC anchor links are used in all languages .sort((a) => (a.userLanguage === 'en' ? -1 : 1)) .forEach((translation) => { const { filename, markdown, userLanguage } = translation; const headers = getHeaders(markdown); const location = headers.filename || `/${fileRelativeContext}/${filename}`; const markdownH1 = getTitle(markdown); const title = headers.title || markdownH1; const description = headers.description || getDescription(markdown); if (title == null || title === '') { throw new Error(`docs-infra: Missing title in the page: ${location}\n`); } if (title.length > 70) { throw new Error( [ `docs-infra: The title "${title}" is too long (${title.length} characters).`, 'It needs to have fewer than 70 characters—ideally less than 60. For more details, see:', 'https://developers.google.com/search/docs/advanced/appearance/title-link', '', ].join('\n'), ); } if (description == null || description === '') { throw new Error(`docs-infra: Missing description in the page: ${location}\n`); } if (description.length > 160) { throw new Error( [ `docs-infra: The description "${description}" is too long (${description.length} characters).`, 'It needs to have fewer than 170 characters—ideally less than 160. For more details, see:', 'https://ahrefs.com/blog/meta-description/#4-be-concise', '', ].join('\n'), ); } if (description.slice(-1) !== '.' && description.slice(-1) !== '!') { throw new Error( `docs-infra: The description "${description}" should end with a "." or "!", those are sentences.`, ); } const contents = getContents(markdown); if (headers.components.length > 0 && headers.productId !== 'base-ui') { contents.push(` ## API See the documentation below for a complete reference to all of the props and classes available to the components mentioned here. ${headers.components .map((component) => { const componentPkgMap = componentPackageMapping[headers.productId]; const componentPkg = componentPkgMap ? componentPkgMap[component] : null; return `- [\`<${component} />\`](${resolveComponentApiUrl( headers.productId, componentPkg, component, )})`; }) .join('\n')} ${headers.hooks .map((hook) => { const componentPkgMap = componentPackageMapping[headers.productId]; const componentPkg = componentPkgMap ? componentPkgMap[hook] : null; return `- [\`${hook}\`](${resolveComponentApiUrl(headers.productId, componentPkg, hook)})`; }) .join('\n')} `); } const toc = []; const render = createRender({ headingHashes, toc, userLanguage, location, options, }); const rendered = contents.map((content) => { if (/^"(demo|component)": "(.*)"/.test(content)) { try { return JSON.parse(`{${content}}`); } catch (err) { console.error('JSON.parse fails with: ', `{${content}}`); console.error(err); return null; } } const codeblock = getCodeblock(content); if (codeblock) { return codeblock; } const featureList = getFeatureList(content); if (featureList) { return featureList; } return render(content); }); // fragment link symbol rendered.unshift(` `); rendered.unshift(` `); rendered.unshift(` + `); rendered.unshift(` `); // icons for callout (info, success, warning, error) rendered.unshift( ` `, ); rendered.unshift( ` `, ); rendered.unshift( ` `, ); rendered.unshift( ` `, ); docs[userLanguage] = { description, location, rendered, toc, title, headers, }; }); if (docs.en.headers.card === 'true') { const slug = docs.en.location.replace(/(.*)\/(.*)\.md/, '$2'); const exists = fs.existsSync( path.resolve(config.options.workspaceRoot, `docs/public/static/blog/${slug}/card.png`), ); if (!exists) { throw new Error( [ `MUI: the card image for the blog post "${slug}" is missing.`, `Add a docs/public/static/blog/${slug}/card.png file and then restart Next.js or else remove card: true from the headers.`, ].join('\n'), ); } } return { docs }; } export default prepareMarkdown;