diff --git a/docs/pages/blog/discord-announcement.md b/docs/pages/blog/discord-announcement.md index 9e4883d581c28c..ab636c916be01f 100644 --- a/docs/pages/blog/discord-announcement.md +++ b/docs/pages/blog/discord-announcement.md @@ -7,7 +7,7 @@ tags: ['News'] card: true --- -Discord banner with link +Discord banner with link This year, one of our highest priorities has been to create a central hub for the MUI community. Up until now, our community has been scattered across discussions and interactions on [Twitter](https://twitter.com/MUI_hq), [LinkedIn](https://linkedin.com/company/mui), [GitHub](https://github.com/mui), and [Stack Overflow](https://stackoverflow.com/questions/tagged/material-ui). diff --git a/docs/pages/blog/lab-tree-view-to-mui-x.md b/docs/pages/blog/lab-tree-view-to-mui-x.md index d7a83cd477f2a9..11ebd91d5d9090 100644 --- a/docs/pages/blog/lab-tree-view-to-mui-x.md +++ b/docs/pages/blog/lab-tree-view-to-mui-x.md @@ -4,7 +4,7 @@ description: Migrate to the new package to start building with our powerful Tree date: 2023-08-21T00:00:00.000Z authors: ['flaviendelangle'] tags: ['MUI X', 'News'] -card: true +card: false --- After more than 4 years in the lab, the [Tree View](https://mui.com/x/react-tree-view/) components have found a new home as part of MUI X. diff --git a/docs/pages/experiments/blog/blog.md b/docs/pages/experiments/blog/blog.md index 7d7497d9610a8d..09c041c7df713e 100644 --- a/docs/pages/experiments/blog/blog.md +++ b/docs/pages/experiments/blog/blog.md @@ -3,7 +3,7 @@ title: Blog post title description: Our internationally distributed startup gathered on a remote island to get to know each other better. Here's what happened! date: 2022-07-28T00:00:00.000Z authors: ['samuelsycamore'] -card: true +card: false tags: ['Company'] --- diff --git a/docs/public/static/blog/2023-discord-announcement/card.png b/docs/public/static/blog/2023-discord-announcement/card.png deleted file mode 100644 index 63c97d0cf810e9..00000000000000 Binary files a/docs/public/static/blog/2023-discord-announcement/card.png and /dev/null differ diff --git a/docs/public/static/blog/discord-announcement/card.png b/docs/public/static/blog/discord-announcement/card.png new file mode 100644 index 00000000000000..bb36a43a9e2dfd Binary files /dev/null and b/docs/public/static/blog/discord-announcement/card.png differ diff --git a/docs/public/static/blog/2023-discord-announcement/discord.png b/docs/public/static/blog/discord-announcement/discord.png similarity index 100% rename from docs/public/static/blog/2023-discord-announcement/discord.png rename to docs/public/static/blog/discord-announcement/discord.png diff --git a/docs/src/modules/components/TopLayoutBlog.js b/docs/src/modules/components/TopLayoutBlog.js index 30101956eaaaf2..46667eea7cffdb 100644 --- a/docs/src/modules/components/TopLayoutBlog.js +++ b/docs/src/modules/components/TopLayoutBlog.js @@ -247,7 +247,7 @@ function TopLayoutBlog(props) { const { description, rendered, title, headers } = docs.en; const finalTitle = title || headers.title; const router = useRouter(); - const slug = router.pathname.replace(/\/blog\//, ''); + const slug = router.pathname.replace(/(.*)\/(.*)/, '$2'); const { canonicalAsServer } = pathnameToLanguage(router.asPath); const card = headers.card === 'true' diff --git a/packages/markdown/loader.js b/packages/markdown/loader.js index 22ad0dc398bc75..99da3ae6403e1c 100644 --- a/packages/markdown/loader.js +++ b/packages/markdown/loader.js @@ -1,6 +1,6 @@ const { promises: fs, readdirSync } = require('fs'); const path = require('path'); -const { prepareMarkdown } = require('./parseMarkdown'); +const prepareMarkdown = require('./prepareMarkdown'); const extractImports = require('./extractImports'); const notEnglishMarkdownRegExp = /-([a-z]{2})\.md$/; diff --git a/packages/markdown/parseMarkdown.js b/packages/markdown/parseMarkdown.js index b76cd29f941902..1a920d842d3884 100644 --- a/packages/markdown/parseMarkdown.js +++ b/packages/markdown/parseMarkdown.js @@ -1,5 +1,4 @@ const { marked } = require('marked'); -const kebabCase = require('lodash/kebabCase'); const textToHash = require('./textToHash'); const prism = require('./prism'); @@ -448,236 +447,6 @@ function createRender(context) { return render; } -const BaseUIReexportedComponents = ['ClickAwayListener', 'NoSsr', 'Portal', 'TextareaAutosize']; - -/** - * @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 (componentPkg === 'mui-base' || BaseUIReexportedComponents.indexOf(component) >= 0) { - return `/base-ui/react-${kebabCase(component)}/components-api/#${kebabCase(component)}`; - } - return `/${productId}/api/${kebabCase(component)}/`; -} - -/** - * @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 - */ -function prepareMarkdown(config) { - const { fileRelativeContext, translations, componentPackageMapping = {}, options } = config; - - const demos = {}; - /** - * @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 title = headers.title || getTitle(markdown); - 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 > 170) { - 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'), - ); - } - - const contents = getContents(markdown); - - if (headers.unstyled) { - contents.push(` -## Unstyled - -:::success -[Base UI](/base-ui/getting-started/) provides a headless ("unstyled") version of this [${title}](${headers.unstyled}). Try it if you need more flexibility in customization and a smaller bundle size. -::: - `); - } - - if (headers.components.length > 0) { - 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; - } - - 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, - }; - }); - - return { demos, docs }; -} - module.exports = { createRender, getContents, @@ -685,6 +454,5 @@ module.exports = { getCodeblock, getHeaders, getTitle, - prepareMarkdown, renderInline, }; diff --git a/packages/markdown/parseMarkdown.test.js b/packages/markdown/parseMarkdown.test.js index 5deabd7115bc72..7221ac2cfb6966 100644 --- a/packages/markdown/parseMarkdown.test.js +++ b/packages/markdown/parseMarkdown.test.js @@ -1,21 +1,7 @@ import { expect } from 'chai'; -import { - getContents, - getDescription, - getTitle, - getHeaders, - prepareMarkdown, - getCodeblock, -} from './parseMarkdown'; +import { getContents, getDescription, getTitle, getHeaders, getCodeblock } from './parseMarkdown'; describe('parseMarkdown', () => { - const defaultParams = { - fileRelativeContext: 'test/bar', - options: { - env: {}, - }, - }; - describe('getTitle', () => { it('remove backticks', () => { expect( @@ -200,369 +186,6 @@ authors: }); }); - describe('prepareMarkdown', () => { - it('returns the table of contents with html and emojis preserved and tags stripped', () => { - const markdown = ` -# Support - -

Foo

- -## Community help (free) -### GitHub GitHub logo -### Unofficial 👍 -### Warning ⚠️ -### Header with Pro plan
-### Header with \`code\` -`; - - const { - docs: { - en: { toc }, - }, - } = prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - - expect(toc).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'github', - level: 3, - text: 'GitHub GitHub logo', - }, - { hash: 'unofficial', level: 3, text: 'Unofficial 👍' }, - { hash: 'warning', level: 3, text: 'Warning ⚠️' }, - { - hash: 'header-with-pro-plan', - level: 3, - text: 'Header with Pro plan ', - }, - { - hash: 'header-with-code', - level: 3, - text: 'Header with code', - }, - ], - hash: 'community-help-free', - level: 2, - text: 'Community help (free)', - }, - ]); - }); - - it('enables word-break for function signatures', () => { - const markdown = ` -# Theming - -

Foo

- -## API -### responsiveFontSizes(theme, options) => theme -### createTheme(options, ...args) => theme -`; - - const { - docs: { - en: { toc }, - }, - } = prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - - expect(toc).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'responsivefontsizes-theme-options-theme', - level: 3, - text: 'responsiveFontSizes(​theme, options) => theme', - }, - { - hash: 'createtheme-options-args-theme', - level: 3, - text: 'createTheme(​options, ...args) => theme', - }, - ], - hash: 'api', - level: 2, - text: 'API', - }, - ]); - }); - - it('use english hash for different locales', () => { - const markdownEn = ` -# Localization - -

Foo

- -## Locales -### Example -### Use same hash -`; - - const markdownPt = ` -# Localização - -

Foo

- -## Idiomas -### Exemplo -### Usar o mesmo hash -`; - - const markdownZh = ` -# 所在位置 - -

Foo

- -## 语言环境 -### 例 -### 使用相同的哈希 -`; - const { - docs: { - en: { toc: tocEn }, - pt: { toc: tocPt }, - zh: { toc: tocZh }, - }, - } = prepareMarkdown({ - pageFilename: '/same-hash-test', - translations: [ - { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' }, - { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' }, - { filename: 'localization-zh.md', markdown: markdownZh, userLanguage: 'zh' }, - ], - }); - - expect(tocZh).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'example', - level: 3, - text: '例', - }, - { - hash: 'use-same-hash', - level: 3, - text: '使用相同的哈希', - }, - ], - hash: 'locales', - level: 2, - text: '语言环境', - }, - ]); - - expect(tocPt).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'example', - level: 3, - text: 'Exemplo', - }, - { - hash: 'use-same-hash', - level: 3, - text: 'Usar o mesmo hash', - }, - ], - hash: 'locales', - level: 2, - text: 'Idiomas', - }, - ]); - - expect(tocEn).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'example', - level: 3, - text: 'Example', - }, - { - hash: 'use-same-hash', - level: 3, - text: 'Use same hash', - }, - ], - hash: 'locales', - level: 2, - text: 'Locales', - }, - ]); - }); - - it('use translated hash for translations are not synced', () => { - const markdownEn = ` -# Localization - -

Foo

- -## Locales -### Example -### Use same hash -`; - - const markdownPt = ` -# Localização - -

Foo

- -## Idiomas -### Exemplo -### Usar o mesmo hash -### Usar traduzido -`; - - const { - docs: { - en: { toc: tocEn }, - pt: { toc: tocPt }, - }, - } = prepareMarkdown({ - pageFilename: '/same-hash-test', - translations: [ - { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' }, - { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' }, - ], - }); - - expect(tocPt).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'example', - level: 3, - text: 'Exemplo', - }, - { - hash: 'use-same-hash', - level: 3, - text: 'Usar o mesmo hash', - }, - { - hash: 'usar-traduzido', - level: 3, - text: 'Usar traduzido', - }, - ], - hash: 'locales', - level: 2, - text: 'Idiomas', - }, - ]); - - expect(tocEn).to.have.deep.ordered.members([ - { - children: [ - { - hash: 'example', - level: 3, - text: 'Example', - }, - { - hash: 'use-same-hash', - level: 3, - text: 'Use same hash', - }, - ], - hash: 'locales', - level: 2, - text: 'Locales', - }, - ]); - }); - - it('should report missing trailing splashes', () => { - const markdown = ` -# Localization - -

Foo

- -[bar](/bar/) -[foo](/foo) -`; - - expect(() => { - prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - }).to.throw(`docs-infra: Missing trailing slash. The following link: -[foo](/foo) in /test/bar/index.md is missing a trailing slash, please add it. - -See https://ahrefs.com/blog/trailing-slash/ for more details. -`); - }); - - it('should report missing leading splashes', () => { - const markdown = ` -# Localization - -

Foo

- -[bar](/bar/) -[foo](foo/) -`; - - expect(() => { - prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - }).to.throw(`docs-infra: Missing leading slash. The following link: -[foo](foo/) in /test/bar/index.md is missing a leading slash, please add it. -`); - }); - - it('should report title too long', () => { - const markdown = ` -# Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo - -

Foo

- -`; - - expect(() => { - prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - }).to - .throw(`docs-infra: The title "Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (117 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 -`); - }); - - it('should report description too long', () => { - const markdown = ` -# Foo - -

Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo

- -`; - - expect(() => { - prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - }).to - .throw(`docs-infra: The description "Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (188 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 -`); - }); - }); - describe('getCodeblock', () => { it('should return undefined if no codeblock found', () => { const codeblock = getCodeblock('## Tabs'); @@ -611,31 +234,4 @@ https://ahrefs.com/blog/meta-description/#4-be-concise }); }); }); - - it('should not accept sh', () => { - const markdown = ` -# Foo - -

Fo

- -\`\`\`sh -npm install @mui/material -\`\`\` - -`; - - expect(() => { - prepareMarkdown({ - ...defaultParams, - translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], - }); - }).to.throw(`docs-infra: Unsupported language: "sh" in: - -\`\`\`sh -npm install @mui/material -\`\`\` - -Use "bash" instead. -`); - }); }); diff --git a/packages/markdown/prepareMarkdown.js b/packages/markdown/prepareMarkdown.js new file mode 100644 index 00000000000000..6447e88dfdfaa9 --- /dev/null +++ b/packages/markdown/prepareMarkdown.js @@ -0,0 +1,259 @@ +const fs = require('fs'); +const path = require('path'); +const kebabCase = require('lodash/kebabCase'); +const { + createRender, + getContents, + getDescription, + getCodeblock, + getHeaders, + getTitle, +} = require('./parseMarkdown'); + +const BaseUIReexportedComponents = ['ClickAwayListener', 'NoSsr', 'Portal', 'TextareaAutosize']; + +/** + * @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 (componentPkg === 'mui-base' || BaseUIReexportedComponents.indexOf(component) >= 0) { + return `/base-ui/react-${kebabCase(component)}/components-api/#${kebabCase(component)}`; + } + return `/${productId}/api/${kebabCase(component)}/`; +} + +/** + * @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 + */ +function prepareMarkdown(config) { + const { fileRelativeContext, translations, componentPackageMapping = {}, options } = config; + + const demos = {}; + /** + * @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 title = headers.title || getTitle(markdown); + 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 > 170) { + 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'), + ); + } + + const contents = getContents(markdown); + + if (headers.unstyled) { + contents.push(` +## Unstyled + +:::success +[Base UI](/base-ui/getting-started/) provides a headless ("unstyled") version of this [${title}](${headers.unstyled}). Try it if you need more flexibility in customization and a smaller bundle size. +::: + `); + } + + if (headers.components.length > 0) { + 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; + } + + 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(__dirname, `../../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 { demos, docs }; +} + +module.exports = prepareMarkdown; diff --git a/packages/markdown/prepareMarkdown.test.js b/packages/markdown/prepareMarkdown.test.js new file mode 100644 index 00000000000000..00abbd5faa9d4a --- /dev/null +++ b/packages/markdown/prepareMarkdown.test.js @@ -0,0 +1,399 @@ +import { expect } from 'chai'; +import prepareMarkdown from './prepareMarkdown'; + +describe('parseMarkdown', () => { + const defaultParams = { + fileRelativeContext: 'test/bar', + options: { + env: {}, + }, + }; + + it('returns the table of contents with html and emojis preserved and tags stripped', () => { + const markdown = ` +# Support + +

Foo

+ +## Community help (free) +### GitHub GitHub logo +### Unofficial 👍 +### Warning ⚠️ +### Header with Pro plan
+### Header with \`code\` +`; + + const { + docs: { + en: { toc }, + }, + } = prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + + expect(toc).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'github', + level: 3, + text: 'GitHub GitHub logo', + }, + { hash: 'unofficial', level: 3, text: 'Unofficial 👍' }, + { hash: 'warning', level: 3, text: 'Warning ⚠️' }, + { + hash: 'header-with-pro-plan', + level: 3, + text: 'Header with Pro plan ', + }, + { + hash: 'header-with-code', + level: 3, + text: 'Header with code', + }, + ], + hash: 'community-help-free', + level: 2, + text: 'Community help (free)', + }, + ]); + }); + + it('enables word-break for function signatures', () => { + const markdown = ` +# Theming + +

Foo

+ +## API +### responsiveFontSizes(theme, options) => theme +### createTheme(options, ...args) => theme +`; + + const { + docs: { + en: { toc }, + }, + } = prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + + expect(toc).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'responsivefontsizes-theme-options-theme', + level: 3, + text: 'responsiveFontSizes(​theme, options) => theme', + }, + { + hash: 'createtheme-options-args-theme', + level: 3, + text: 'createTheme(​options, ...args) => theme', + }, + ], + hash: 'api', + level: 2, + text: 'API', + }, + ]); + }); + + it('use english hash for different locales', () => { + const markdownEn = ` +# Localization + +

Foo

+ +## Locales +### Example +### Use same hash +`; + + const markdownPt = ` +# Localização + +

Foo

+ +## Idiomas +### Exemplo +### Usar o mesmo hash +`; + + const markdownZh = ` +# 所在位置 + +

Foo

+ +## 语言环境 +### 例 +### 使用相同的哈希 +`; + const { + docs: { + en: { toc: tocEn }, + pt: { toc: tocPt }, + zh: { toc: tocZh }, + }, + } = prepareMarkdown({ + pageFilename: '/same-hash-test', + translations: [ + { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' }, + { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' }, + { filename: 'localization-zh.md', markdown: markdownZh, userLanguage: 'zh' }, + ], + }); + + expect(tocZh).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'example', + level: 3, + text: '例', + }, + { + hash: 'use-same-hash', + level: 3, + text: '使用相同的哈希', + }, + ], + hash: 'locales', + level: 2, + text: '语言环境', + }, + ]); + + expect(tocPt).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'example', + level: 3, + text: 'Exemplo', + }, + { + hash: 'use-same-hash', + level: 3, + text: 'Usar o mesmo hash', + }, + ], + hash: 'locales', + level: 2, + text: 'Idiomas', + }, + ]); + + expect(tocEn).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'example', + level: 3, + text: 'Example', + }, + { + hash: 'use-same-hash', + level: 3, + text: 'Use same hash', + }, + ], + hash: 'locales', + level: 2, + text: 'Locales', + }, + ]); + }); + + it('use translated hash for translations are not synced', () => { + const markdownEn = ` +# Localization + +

Foo

+ +## Locales +### Example +### Use same hash +`; + + const markdownPt = ` +# Localização + +

Foo

+ +## Idiomas +### Exemplo +### Usar o mesmo hash +### Usar traduzido +`; + + const { + docs: { + en: { toc: tocEn }, + pt: { toc: tocPt }, + }, + } = prepareMarkdown({ + pageFilename: '/same-hash-test', + translations: [ + { filename: 'localization.md', markdown: markdownEn, userLanguage: 'en' }, + { filename: 'localization-pt.md', markdown: markdownPt, userLanguage: 'pt' }, + ], + }); + + expect(tocPt).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'example', + level: 3, + text: 'Exemplo', + }, + { + hash: 'use-same-hash', + level: 3, + text: 'Usar o mesmo hash', + }, + { + hash: 'usar-traduzido', + level: 3, + text: 'Usar traduzido', + }, + ], + hash: 'locales', + level: 2, + text: 'Idiomas', + }, + ]); + + expect(tocEn).to.have.deep.ordered.members([ + { + children: [ + { + hash: 'example', + level: 3, + text: 'Example', + }, + { + hash: 'use-same-hash', + level: 3, + text: 'Use same hash', + }, + ], + hash: 'locales', + level: 2, + text: 'Locales', + }, + ]); + }); + + it('should report missing trailing splashes', () => { + const markdown = ` +# Localization + +

Foo

+ +[bar](/bar/) +[foo](/foo) +`; + + expect(() => { + prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + }).to.throw(`docs-infra: Missing trailing slash. The following link: +[foo](/foo) in /test/bar/index.md is missing a trailing slash, please add it. + +See https://ahrefs.com/blog/trailing-slash/ for more details. +`); + }); + + it('should report missing leading splashes', () => { + const markdown = ` +# Localization + +

Foo

+ +[bar](/bar/) +[foo](foo/) +`; + + expect(() => { + prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + }).to.throw(`docs-infra: Missing leading slash. The following link: +[foo](foo/) in /test/bar/index.md is missing a leading slash, please add it. +`); + }); + + it('should report title too long', () => { + const markdown = ` +# Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + +

Foo

+ +`; + + expect(() => { + prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + }).to + .throw(`docs-infra: The title "Foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (117 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 +`); + }); + + it('should report description too long', () => { + const markdown = ` +# Foo + +

Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo

+ +`; + + expect(() => { + prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + }).to + .throw(`docs-infra: The description "Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" is too long (188 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 +`); + }); + + it('should not accept sh', () => { + const markdown = ` +# Foo + +

Fo

+ +\`\`\`sh +npm install @mui/material +\`\`\` + +`; + + expect(() => { + prepareMarkdown({ + ...defaultParams, + translations: [{ filename: 'index.md', markdown, userLanguage: 'en' }], + }); + }).to.throw(`docs-infra: Unsupported language: "sh" in: + +\`\`\`sh +npm install @mui/material +\`\`\` + +Use "bash" instead. +`); + }); +});