From f64771788f7abe015ffd828af9593ab72caf32db Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Tue, 8 Sep 2026 14:40:24 +0200 Subject: [PATCH 1/2] Validate inline code highlights in docs --- .../fixtures/src/content/missing-highlight.md | 3 + .../src/content/out-of-bounds-highlight.md | 3 + .../fixtures/src/content/valid-highlight.md | 3 + .../lint-markdown-code-blocks.test.js | 31 +++++++++ eslint-local-rules/rules/inline-highlights.js | 66 +++++++++++++++++++ .../rules/lint-markdown-code-blocks.js | 14 +++- package.json | 2 +- 7 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 eslint-local-rules/__tests__/fixtures/src/content/missing-highlight.md create mode 100644 eslint-local-rules/__tests__/fixtures/src/content/out-of-bounds-highlight.md create mode 100644 eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md create mode 100644 eslint-local-rules/rules/inline-highlights.js diff --git a/eslint-local-rules/__tests__/fixtures/src/content/missing-highlight.md b/eslint-local-rules/__tests__/fixtures/src/content/missing-highlight.md new file mode 100644 index 00000000000..f6dc1e734de --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/missing-highlight.md @@ -0,0 +1,3 @@ +```js [[1, 1, "submitAction"]] +function UpdateName() {} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/out-of-bounds-highlight.md b/eslint-local-rules/__tests__/fixtures/src/content/out-of-bounds-highlight.md new file mode 100644 index 00000000000..ac65974ad5d --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/out-of-bounds-highlight.md @@ -0,0 +1,3 @@ +```js [[1, 3, "submitAction"]] +function submitAction() {} +``` diff --git a/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md b/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md new file mode 100644 index 00000000000..1698274f9ca --- /dev/null +++ b/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md @@ -0,0 +1,3 @@ +```js [[1, 1, "submitAction"]] +function submitAction() {} +``` diff --git a/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js b/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js index 250e0a1e58f..f0b0e991ec1 100644 --- a/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js +++ b/eslint-local-rules/__tests__/lint-markdown-code-blocks.test.js @@ -123,6 +123,37 @@ async function run() { ), 'expected malformed metadata to be replaced with canonical form' ); + + const validHighlightResult = await lintFixture('valid-highlight.md'); + assert.strictEqual( + validHighlightResult.messages.length, + 0, + 'expected a valid inline highlight to pass' + ); + + const missingHighlightResult = await lintFixture('missing-highlight.md'); + assert.strictEqual( + missingHighlightResult.messages.length, + 1, + 'expected a highlight with text missing from its line to fail' + ); + assert.strictEqual( + missingHighlightResult.messages[0].message, + "Could not find 'submitAction' on highlighted line 1" + ); + + const outOfBoundsHighlightResult = await lintFixture( + 'out-of-bounds-highlight.md' + ); + assert.strictEqual( + outOfBoundsHighlightResult.messages.length, + 1, + 'expected an out-of-bounds highlight line to fail' + ); + assert.strictEqual( + outOfBoundsHighlightResult.messages[0].message, + 'Code highlight line 3 is outside this code block' + ); } run().catch(error => { diff --git a/eslint-local-rules/rules/inline-highlights.js b/eslint-local-rules/rules/inline-highlights.js new file mode 100644 index 00000000000..1e25fa6b9e9 --- /dev/null +++ b/eslint-local-rules/rules/inline-highlights.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const INLINE_HIGHLIGHT_REGEX = /(\[\[.*\]\])/; + +function validateInlineHighlights(meta, code) { + const match = INLINE_HIGHLIGHT_REGEX.exec(meta); + if (!match) { + return []; + } + + let highlights; + try { + highlights = JSON.parse(match[1]); + } catch (error) { + return ['Code highlight metadata must be valid JSON']; + } + + if (!Array.isArray(highlights)) { + return ['Code highlight metadata must be an array']; + } + + const lines = code.split('\n'); + const errors = []; + + for (const highlight of highlights) { + if (!Array.isArray(highlight) || highlight.length < 3) { + errors.push('Each code highlight must specify a step, line, and text'); + continue; + } + + const [, lineNo, text, fromIndex] = highlight; + if (!Number.isInteger(lineNo) || lineNo < 1 || lineNo > lines.length) { + errors.push(`Code highlight line ${lineNo} is outside this code block`); + continue; + } + if (typeof text !== 'string') { + errors.push(`Code highlight text on line ${lineNo} must be a string`); + continue; + } + + const line = lines[lineNo - 1]; + let index = line.indexOf(text); + const lastIndex = line.lastIndexOf(text); + if (index !== lastIndex) { + if (fromIndex === undefined) { + errors.push( + `Found '${text}' twice on highlighted line ${lineNo}; specify fromIndex` + ); + continue; + } + index = line.indexOf(text, fromIndex); + } + if (index === -1) { + errors.push(`Could not find '${text}' on highlighted line ${lineNo}`); + } + } + + return errors; +} + +module.exports = {validateInlineHighlights}; diff --git a/eslint-local-rules/rules/lint-markdown-code-blocks.js b/eslint-local-rules/rules/lint-markdown-code-blocks.js index 5ec327947b2..ce7e829a352 100644 --- a/eslint-local-rules/rules/lint-markdown-code-blocks.js +++ b/eslint-local-rules/rules/lint-markdown-code-blocks.js @@ -16,6 +16,7 @@ const { setCompilerExpectedLines, } = require('./metadata'); const {normalizeDiagnostics} = require('./diagnostics'); +const {validateInlineHighlights} = require('./inline-highlights'); const {parseMarkdownFile} = require('./markdown'); const {runReactCompiler} = require('./react-compiler'); @@ -23,7 +24,7 @@ module.exports = { meta: { type: 'problem', docs: { - description: 'Run React Compiler on markdown code blocks', + description: 'Validate and compile markdown code blocks', category: 'Possible Errors', }, fixable: 'code', @@ -43,6 +44,17 @@ module.exports = { const {blocks} = parseMarkdownFile(sourceCode.text, filename); // For each supported code block, run the compiler and reconcile metadata. for (const block of blocks) { + for (const message of validateInlineHighlights( + block.fence.metaText, + block.code + )) { + context.report({ + node, + loc: block.position, + message, + }); + } + const compilerResult = runReactCompiler( block.code, `${filename}#codeblock` diff --git a/package.json b/package.json index 567102e0440..4873e565751 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "prettier:diff": "yarn nit:source", "lint-heading-ids": "node scripts/headingIdLinter.js", "fix-headings": "node scripts/headingIdLinter.js --fix", - "ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks", + "ci-check": "npm-run-all prettier:diff --parallel lint test:eslint-local-rules tsc lint-heading-ids rss deadlinks", "tsc": "tsc --noEmit", "start": "next start", "postinstall": "yarn --cwd eslint-local-rules install && is-ci || husky install .husky", From fe8c3f7f462415dba48dd377f54f5d2bc48985d2 Mon Sep 17 00:00:00 2001 From: Aurora Scharff Date: Tue, 8 Sep 2026 14:49:27 +0200 Subject: [PATCH 2/2] Match MDX code highlight metadata parsing --- .../__tests__/fixtures/src/content/valid-highlight.md | 4 ++++ eslint-local-rules/rules/lint-markdown-code-blocks.js | 2 +- eslint-local-rules/rules/markdown.js | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md b/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md index 1698274f9ca..dd6074029d1 100644 --- a/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md +++ b/eslint-local-rules/__tests__/fixtures/src/content/valid-highlight.md @@ -1,3 +1,7 @@ ```js [[1, 1, "submitAction"]] function submitAction() {} ``` + +```js [[1, 1, "\\"hidden\\""]] +const mode = "hidden"; +``` diff --git a/eslint-local-rules/rules/lint-markdown-code-blocks.js b/eslint-local-rules/rules/lint-markdown-code-blocks.js index ce7e829a352..14bf10a1269 100644 --- a/eslint-local-rules/rules/lint-markdown-code-blocks.js +++ b/eslint-local-rules/rules/lint-markdown-code-blocks.js @@ -45,7 +45,7 @@ module.exports = { // For each supported code block, run the compiler and reconcile metadata. for (const block of blocks) { for (const message of validateInlineHighlights( - block.fence.metaText, + block.meta, block.code )) { context.report({ diff --git a/eslint-local-rules/rules/markdown.js b/eslint-local-rules/rules/markdown.js index d888d1311a1..5d3be73cfb9 100644 --- a/eslint-local-rules/rules/markdown.js +++ b/eslint-local-rules/rules/markdown.js @@ -16,6 +16,7 @@ const {parseFenceMetadata} = require('./metadata'); * @property {{lineIndex: number, rawText: string, metaText: string, range: [number, number]}} fence * @property {string} filePath * @property {string} lang + * @property {string} meta * @property {import('./metadata').FenceMetadata} metadata */ @@ -76,6 +77,7 @@ function parseMarkdownFile(content, filePath) { blocks.push({ lang: rawLang || normalizedLang, + meta: node.meta || metaText.trim(), metadata, filePath, code: node.value || '',