From cb641e81f7897fca8be6b7ab383302f06d1652b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Mon, 24 Aug 2026 16:38:33 +0100 Subject: [PATCH 1/2] chore(ci): format website build before uploading Argos text snapshots (#12386) --- .github/workflows/argos.yml | 12 ++++++------ .oxfmtrc.website.build.json | 5 +++++ argos/README.md | 23 +++++++++++++++++------ package.json | 5 ++++- project-words.txt | 1 + 5 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 .oxfmtrc.website.build.json diff --git a/.github/workflows/argos.yml b/.github/workflows/argos.yml index 616f2a5f6a5a..fcd68f66cb25 100644 --- a/.github/workflows/argos.yml +++ b/.github/workflows/argos.yml @@ -58,11 +58,11 @@ jobs: - name: Take and upload Argos screenshots run: pnpm argos:screenshot + # We rebuild the site with unminified assets + # This makes it easier to review text snapshots on Argos - name: Build website for text snapshots - run: pnpm argos:build - env: - # Easier HTML diffs to review on Argos - # an alternative could be to use Prettier/Oxfmt on build output - SKIP_HTML_MINIFICATION: true + run: pnpm argos:build:text-snapshots + - name: Format HTML for easier Argos diffs + run: pnpm argos:format:text-snapshots - name: Upload Argos text snapshots - run: pnpm argos:upload-text-snapshots + run: pnpm argos:upload:text-snapshots diff --git a/.oxfmtrc.website.build.json b/.oxfmtrc.website.build.json new file mode 100644 index 000000000000..377dd77c4d40 --- /dev/null +++ b/.oxfmtrc.website.build.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "ignorePatterns": ["**/badSyntax.css", "**/badSyntax.js"] +} diff --git a/argos/README.md b/argos/README.md index ae6204e0f50a..e3028b7204f8 100644 --- a/argos/README.md +++ b/argos/README.md @@ -6,27 +6,38 @@ This workspace can be run manually, but is generally run through the [Argos GitH ## Workflow overview -The workflow execute those following steps: +This workflow runs for `main` and PR branches, and add a commit status to each PR with a visual diff that we can easily inspect. + +The workflow execute the 2 main steps below: + +### Screenshots - Build the website locally with `pnpm argos:build` (a variant of `pnpm build:website:fast`) - Start the website server with `pnpm serve:website` on [http://localhost:3000](http://localhost:3000) - Take screenshots of all pages found in `sitemap.xml` with Playwright - Upload all screenshots to [Argos CI](https://argos-ci.com) using the [Playwright reporter](https://argos-ci.com/docs/reference/playwright) -- Upload [HTML/CSS/JS files to also diff them on Argos](https://argos-ci.com/docs/learn/how-to-guides/visual-coverage/compare-non-image-files) using `pnpm argos:upload-text-snapshots` -This workflow runs for `main` and PR branches, and add a commit status to each PR with a visual diff that we can easily inspect. +### Text snapshots + +- Rebuild - this time with HTML unminified/formatted. +- Upload [HTML/CSS/JS files to also diff them on Argos](https://argos-ci.com/docs/learn/how-to-guides/visual-coverage/compare-non-image-files) using `pnpm argos:upload-text-snapshots` ## Run manually -To run this manually, use: +To run the screenshot step manually: ```bash pnpm argos:build pnpm argos:screenshot -pnpm argos:upload-text-snapshots ``` -The Playwright reporter will upload the screenshots to Argos. +To run the text-snapshots step manually: + +```bash +pnpm argos:build:text-snapshots +pnpm argos:format:text-snapshots +pnpm argos:upload:text-snapshots +``` ## How to modify diff --git a/package.json b/package.json index 17a8b41ce526..b2d7f28b13c5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "argos": "pnpm argos:build && pnpm argos:screenshot && argos:upload-text-snapshots", "argos:build": "cross-env DOCUSAURUS_ARGOS_BUILD=true pnpm build:website:fast --dev", "argos:screenshot": "pnpm --filter argos screenshot", - "argos:upload-text-snapshots": "pnpm --filter argos upload-text-snapshots", + "argos:build:text-snapshots": "cross-env DOCUSAURUS_ARGOS_BUILD=true SKIP_HTML_MINIFICATION=true pnpm build:website:fast --dev --no-minify", + "argos:format:text-snapshots": "pnpm format:website:build", + "argos:upload:text-snapshots": "pnpm --filter argos upload-text-snapshots", "canary": "pnpm canary:bumpVersion && pnpm canary:publish", "canary:getCoreVersion": "node -p \"require('./packages/docusaurus/package.json').version.split('-')[0]\"", "canary:version": "echo `pnpm --silent canary:getCoreVersion`-canary-`git rev-list --count HEAD`+`git rev-parse --short HEAD`", @@ -48,6 +50,7 @@ "postinstall": "pnpm build:packages", "prepare": "husky", "format": "oxfmt .", + "format:website:build": "oxfmt --config .oxfmtrc.website.build.json website/build", "format:diff": "oxfmt --list-different .", "lint": "pnpm lint:js && pnpm lint:style && pnpm lint:SPELLING && pnpm lint:syncpack", "lint:ci": "pnpm lint:js --quiet && pnpm lint:style && pnpm lint:spelling && pnpm lint:syncpack", diff --git a/project-words.txt b/project-words.txt index 569565a8e5dc..ae60db4a7b99 100644 --- a/project-words.txt +++ b/project-words.txt @@ -312,6 +312,7 @@ unlinkable Unlisteds unlisteds unlocalized +unminified unstub unswizzle upvotes From 27de99dac0ae76dd7bd2f1430dd999b328d911f7 Mon Sep 17 00:00:00 2001 From: Seth Falco Date: Mon, 24 Aug 2026 18:03:28 +0100 Subject: [PATCH 2/2] fix(metadata): preserve MDX special symbols in inline code syntax (#11821) Co-authored-by: sebastien --- .cspell.json | 2 +- .github/workflows/tests.yml | 2 +- .../website/build-snap/blog/atom.xsl | 6 ++ .../website/build-snap/blog/rss.xsl | 6 ++ .../src/__tests__/markdownUtils.test.ts | 65 +++++++++++++++++++ .../docusaurus-utils/src/markdownUtils.ts | 63 ++++++++++++++++-- .../doc-with-complex-description.mdx | 3 + website/_dogfooding/docs-tests-sidebars.js | 1 + 8 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 website/_dogfooding/_docs tests/doc-with-complex-description.mdx diff --git a/.cspell.json b/.cspell.json index c7bea3bd6aa7..3e9dd8c3257a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -53,6 +53,6 @@ "*.min.*", "jest/vendor" ], - "ignoreRegExpList": ["Email", "Urls", "#[\\w-]*"], + "ignoreRegExpList": ["Email", "Urls", "#[\\w-]*", "\\u\\{[0-9A-F]{4}\\}"], "enableFiletypes": ["mdx"] } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 49f446944aa0..353baa22bf91 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,7 +52,7 @@ jobs: env: # Our website should build even with limited memory # See https://github.com/facebook/docusaurus/pull/10590 - NODE_OPTIONS: '--max-old-space-size=250' + NODE_OPTIONS: '--max-old-space-size=300' DOCUSAURUS_PERF_LOGGER: 'true' - name: Docusaurus site CSS order run: pnpm --filter website test:css-order diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl index b4b2d9619c0d..6480de3ff658 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/__fixtures__/website/build-snap/blog/atom.xsl @@ -1,4 +1,10 @@ + + { `), ).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); }); + + it('creates excerpt with XML tag inside inline code', () => { + expect( + createExcerpt(dedent` + # Markdown Regular Title + + This paragraph includes a link to the \`\` documentation. + `), + ).toBe('This paragraph includes a link to the documentation.'); + }); + + it('creates excerpt with XML tag inside inline code with hyperlink', () => { + expect( + createExcerpt(dedent` + # Markdown Regular Title + + This paragraph includes a link to the [\`\`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata) documentation. + `), + ).toBe('This paragraph includes a link to the documentation.'); + }); + + describe('complex case', () => { + const complexString = + '_aa_ *bb* **cc** ~~dd~~ ff ![gg](hh.png) [ii](jj) {#kk}'; + + it('creates excerpt for complex string', () => { + expect( + createExcerpt(dedent` + # Markdown Regular Title + + Escaping ${complexString} is hard. + `), + ).toBe('Escaping aa bb cc dd ff gg ii is hard.'); + }); + + it('creates excerpt for complex string in inline code block', () => { + expect( + createExcerpt(dedent` + # Markdown Regular Title + + Escaping \`${complexString}\` is hard. + `), + ).toBe( + 'Escaping _aa_ *bb* **cc** ~~dd~~ ff ![gg](hh.png) [ii](jj) {#kk} is hard.', + ); + }); + }); + + describe('internal markers', () => { + // Internal markers are used to escape/unescape MDX special chars + // within inline code blocks. We want to make sure the provided input + // doesn't conflict with the internal usage we have. + + it('creates excerpt without letting the input forge internal markers', () => { + expect(createExcerpt('A \u{FFFE}60\u{FFFF} B')).toBe('A 60 B'); + }); + + it('creates excerpt without letting inline code forge internal markers', () => { + expect(createExcerpt('A `\u{FFFE}60\u{FFFF}` B')).toBe('A 60 B'); + }); + + it('creates no excerpt for a line of Unicode non-characters', () => { + expect(createExcerpt('\u{FFFE}\u{FFFF}')).toBeUndefined(); + }); + }); }); describe('parseMarkdownContentTitle', () => { diff --git a/packages/docusaurus-utils/src/markdownUtils.ts b/packages/docusaurus-utils/src/markdownUtils.ts index f1ad5fdc009f..bea7f83031ad 100644 --- a/packages/docusaurus-utils/src/markdownUtils.ts +++ b/packages/docusaurus-utils/src/markdownUtils.ts @@ -7,6 +7,7 @@ import logger from '@docusaurus/logger'; import matter from '@11ty/gray-matter'; + import type { ParseFrontMatter, DefaultParseFrontMatter, @@ -16,6 +17,41 @@ import type { // server-side when we infer metadata like `title` and `description` from the // content. Most parsing is still done in MDX through the mdx-loader. +const MDXEscapingUtils = (function () { + const MARKUP_CHARS = ['_', ':', '*', '<', '>', '~', '!', '[', ']', '{', '}']; + + const markerOf = (char: string) => `\u{FFFE}${char.codePointAt(0)}\u{FFFF}`; + + const MARKUP_CHARS_ESCAPE_MAP = new Map( + MARKUP_CHARS.map((char) => [char, markerOf(char)]), + ); + const MARKUP_CHARS_UNESCAPE_MAP = new Map( + MARKUP_CHARS.map((char) => [markerOf(char), char]), + ); + + const MARKUP_CHARS_REGEX = new RegExp( + `[${RegExp.escape(MARKUP_CHARS.join(''))}]`, + 'g', + ); + const MARKUP_MARKERS_REGEX = /\u{FFFE}\d+\u{FFFF}/gu; + + function escapeMDX(str: string) { + return str.replace( + MARKUP_CHARS_REGEX, + (char) => MARKUP_CHARS_ESCAPE_MAP.get(char)!, + ); + } + + function unescapeMDX(str: string) { + return str.replace( + MARKUP_MARKERS_REGEX, + (marker) => MARKUP_CHARS_UNESCAPE_MAP.get(marker) ?? marker, + ); + } + + return {escapeMDX, unescapeMDX}; +})(); + /** * Hacky temporary escape hatch for Crowdin bad MDX support * See https://docusaurus.io/docs/i18n/crowdin#mdx @@ -142,7 +178,26 @@ export function createExcerpt(fileString: string): string | undefined { continue; } - const cleanedLine = fileLine + // Pre/postprocessing to handle MDX special chars within inline code blocks + // See https://github.com/facebook/docusaurus/pull/11821 + function preprocessLine(str: string) { + return ( + str + // Ignore internal Unicode markers found in input + // This ensures no possible conflict with our MDX escaping logic + .replace(/[\u{FFFE}\u{FFFF}]/gu, '') + // Unwrap inline code and escape special MDX chars within it + .replace(/`(?.+?)`/g, (_, text) => { + return MDXEscapingUtils.escapeMDX(text); + }) + ); + } + function postProcessLine(str: string) { + // Restore escaped MDX chars that have been previously escaped + return MDXEscapingUtils.unescapeMDX(str); + } + + const cleanedLine = preprocessLine(fileLine) // Remove HTML tags. .replace(/<[^>]*>/g, '') // Remove Title headers @@ -159,8 +214,6 @@ export function createExcerpt(fileString: string): string | undefined { .replace(/\[\^.+?\](?:: .*$)?/g, '') // Remove inline links. .replace(/\[(?.*?)\][[(].*?[\])]/g, '$1') - // Remove inline code. - .replace(/`(?.+?)`/g, '$1') // Remove blockquotes. .replace(/^\s{0,3}>\s?/g, '') // Remove admonition definition. @@ -169,10 +222,12 @@ export function createExcerpt(fileString: string): string | undefined { .replace(/\s?:(?:::|[^:\n])+:/g, '') // Remove custom Markdown heading id. .replace(/\{#*[\w-]+\}/, '') + // Collapse whitespace left behind by the removals above. + .replace(/\s+/g, ' ') .trim(); if (cleanedLine) { - return cleanedLine; + return postProcessLine(cleanedLine); } } diff --git a/website/_dogfooding/_docs tests/doc-with-complex-description.mdx b/website/_dogfooding/_docs tests/doc-with-complex-description.mdx new file mode 100644 index 000000000000..ebb5e46e85c6 --- /dev/null +++ b/website/_dogfooding/_docs tests/doc-with-complex-description.mdx @@ -0,0 +1,3 @@ +# Doc with Complex Description + +One Two _Three_ `` `:Five:` Six ![Seven](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII) diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index d8dac0c10e50..da20f4e6a9b7 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -22,6 +22,7 @@ const sidebars = { }, 'doc-without-sidebar', 'doc-with-another-sidebar', + 'doc-with-complex-description', 'doc-with-last-update', { type: 'category',