From e9c6f6af096f557df303382db55e7879e60303bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:27:02 +0000 Subject: [PATCH 1/4] docs: add remarkInlineMarkdownInHtml plugin to fix links in GFM alert HTML blocks Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- Makefile | 6 + docs/astro.config.mjs | 3 +- docs/src/lib/remark/inlineMarkdownInHtml.js | 106 ++++++++++++ .../lib/remark/inlineMarkdownInHtml.test.js | 162 ++++++++++++++++++ 4 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 docs/src/lib/remark/inlineMarkdownInHtml.js create mode 100644 docs/src/lib/remark/inlineMarkdownInHtml.test.js diff --git a/Makefile b/Makefile index 77023c2f193..a195fe99380 100644 --- a/Makefile +++ b/Makefile @@ -1036,6 +1036,12 @@ clean-docs: @rm -rf docs/dist docs/node_modules docs/.astro @echo "✓ Documentation artifacts cleaned" +.PHONY: test-docs-remark +test-docs-remark: + @echo "Running docs remark plugin unit tests..." + @node docs/src/lib/remark/inlineMarkdownInHtml.test.js + @echo "✓ Docs remark plugin unit tests passed" + # Sync templates from .github to pkg/cli/templates # Sync action pins from .github/aw to pkg/actionpins/data and pkg/workflow/data .PHONY: sync-action-pins diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 932cc1952ee..5e694823001 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url'; import { unified } from '@astrojs/markdown-remark'; import remarkStripEmojis from './src/lib/remark/stripEmojis.js'; import remarkTableDataLabels from './src/lib/remark/tableDataLabels.js'; +import remarkInlineMarkdownInHtml from './src/lib/remark/inlineMarkdownInHtml.js'; import rehypeTableWrapper from './src/lib/rehype/tableWrapper.js'; /** @@ -39,7 +40,7 @@ export default defineConfig({ trailingSlash: 'always', markdown: { processor: unified({ - remarkPlugins: [remarkStripEmojis, remarkTableDataLabels], + remarkPlugins: [remarkStripEmojis, remarkTableDataLabels, remarkInlineMarkdownInHtml], rehypePlugins: [rehypeTableWrapper], }), }, diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.js b/docs/src/lib/remark/inlineMarkdownInHtml.js new file mode 100644 index 00000000000..1239c6c628b --- /dev/null +++ b/docs/src/lib/remark/inlineMarkdownInHtml.js @@ -0,0 +1,106 @@ +// @ts-check + +/** + * Remark plugin that converts markdown link syntax (`[text](url)`) to HTML + * anchor tags inside raw HTML nodes. + * + * CommonMark block-level HTML elements such as `
` and `` + * are treated as opaque HTML blocks by remark-parse, so any markdown syntax + * they contain is left as raw text rather than being processed. This plugin + * runs after remark-parse to fill that gap: it visits every `html` MDAST node + * and rewrites markdown links found inside `` (and similar + * inline-content elements) to proper `` tags. + * + * This is the correct, AST-level fix for GFM alerts that contain + * `
/` with markdown links — replacing the previous + * approach of manipulating rendered HTML strings after compilation. + */ + +/** + * @returns {(tree: import('unist').Node) => void} + */ +export default function remarkInlineMarkdownInHtml() { + return function transform(tree) { + visit(tree); + }; +} + +/** + * @param {any} node + */ +function visit(node) { + if (!node || typeof node !== 'object') return; + + if (node.type === 'html' && typeof node.value === 'string') { + node.value = processMarkdownLinksInHtml(node.value); + } + + const { children } = node; + if (Array.isArray(children)) { + for (const child of children) visit(child); + } +} + +/** + * Tags whose text content should have markdown link syntax converted to HTML. + * These are inline-text contexts that appear as block-level HTML in markdown + * and therefore bypass normal remark inline processing. + * + * @type {string[]} + */ +const INLINE_TEXT_TAGS = ['summary', 'figcaption', 'caption', 'dt', 'dd', 'th', 'td', 'li']; + +/** + * Replace markdown link syntax `[text](url)` with `text` + * inside the content of specific HTML tags that are expected to carry inline + * text with markdown links. + * + * The replacement is intentionally narrow: + * - Only targets known inline-text tags listed in INLINE_TEXT_TAGS. + * - Does not process content inside `` or `
` elements.
+ * - Text that already contains an `]*)?>)([\\s\\S]*?)(<\\/(?:${tagPattern})>)`,
+		'gi',
+	);
+
+	return html.replace(tagRe, (_match, openTag, content, closeTag) => {
+		// Skip content that already contains an anchor tag to avoid double-processing.
+		if (/]/i.test(content)) return _match;
+
+		const processed = convertMarkdownLinks(content);
+		return openTag + processed + closeTag;
+	});
+}
+
+/**
+ * Convert `[text](url)` markdown link syntax to `text`.
+ * Backtick-enclosed spans are preserved as-is so code snippets are not
+ * accidentally rewritten.
+ *
+ * @param {string} text
+ * @returns {string}
+ */
+function convertMarkdownLinks(text) {
+	// Tokenise the input so that backtick code spans are excluded from link
+	// replacement while the rest of the text is processed normally.
+	const parts = text.split(/(`.+?`)/gs);
+	return parts
+		.map((part, index) => {
+			// Even-indexed parts are plain text; odd-indexed parts are code spans.
+			if (index % 2 !== 0) return part;
+			return part.replace(
+				/\[([^\]]+)\]\(([^)]+)\)/g,
+				'$1',
+			);
+		})
+		.join('');
+}
diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.test.js b/docs/src/lib/remark/inlineMarkdownInHtml.test.js
new file mode 100644
index 00000000000..8037efbb467
--- /dev/null
+++ b/docs/src/lib/remark/inlineMarkdownInHtml.test.js
@@ -0,0 +1,162 @@
+#!/usr/bin/env node
+// @ts-check
+
+/**
+ * Unit tests for remarkInlineMarkdownInHtml.
+ *
+ * Run with: node docs/src/lib/remark/inlineMarkdownInHtml.test.js
+ */
+
+import remarkInlineMarkdownInHtml from './inlineMarkdownInHtml.js';
+
+let passed = 0;
+let failed = 0;
+
+function assertEqual(actual, expected, label) {
+	if (actual === expected) {
+		console.log(`  ✓ ${label}`);
+		passed++;
+	} else {
+		console.error(`  ✗ ${label}`);
+		console.error(`    expected: ${JSON.stringify(expected)}`);
+		console.error(`    actual:   ${JSON.stringify(actual)}`);
+		failed++;
+	}
+}
+
+function makeHtmlNode(value) {
+	return { type: 'html', value };
+}
+
+function makeTree(...htmlNodes) {
+	return { type: 'root', children: htmlNodes };
+}
+
+function runTransform(tree) {
+	const transform = remarkInlineMarkdownInHtml();
+	transform(tree);
+	return tree;
+}
+
+// -------------------------------------------------------------------
+// Test: markdown link inside  is converted to 
+// -------------------------------------------------------------------
+console.log('\nmarkdown link inside :');
+{
+	const tree = makeTree(
+		makeHtmlNode('Open a Codespace terminal in [Step 6](06-install-gh-aw.md) to install'),
+	);
+	runTransform(tree);
+	assertEqual(
+		tree.children[0].value,
+		'Open a Codespace terminal in Step 6 to install',
+		'single link converted',
+	);
+}
+
+// -------------------------------------------------------------------
+// Test: multiple links inside 
+// -------------------------------------------------------------------
+console.log('\nmultiple links inside :');
+{
+	const tree = makeTree(
+		makeHtmlNode('See [Step 6](step6.md) and [Step 7](step7.md) for details'),
+	);
+	runTransform(tree);
+	assertEqual(
+		tree.children[0].value,
+		'See Step 6 and Step 7 for details',
+		'both links converted',
+	);
+}
+
+// -------------------------------------------------------------------
+// Test: backtick code spans inside  are preserved
+// -------------------------------------------------------------------
+console.log('\nbacktick code in :');
+{
+	const tree = makeTree(
+		makeHtmlNode('Install `gh-aw` via [Step 6](step6.md)'),
+	);
+	runTransform(tree);
+	assertEqual(
+		tree.children[0].value,
+		'Install `gh-aw` via Step 6',
+		'code span preserved, link converted',
+	);
+}
+
+// -------------------------------------------------------------------
+// Test: link-like syntax inside backtick code span is NOT converted
+// -------------------------------------------------------------------
+console.log('\nlink syntax inside code span is not converted:');
+{
+	const tree = makeTree(
+		makeHtmlNode('Use `[var](param)` to configure'),
+	);
+	runTransform(tree);
+	assertEqual(
+		tree.children[0].value,
+		'Use `[var](param)` to configure',
+		'code span link syntax preserved',
+	);
+}
+
+// -------------------------------------------------------------------
+// Test:  that already contains  tags is left untouched
+// -------------------------------------------------------------------
+console.log('\n with existing  tag not double-processed:');
+{
+	const input = 'See Step 6 for details';
+	const tree = makeTree(makeHtmlNode(input));
+	runTransform(tree);
+	assertEqual(tree.children[0].value, input, 'existing anchor tag not rewritten');
+}
+
+// -------------------------------------------------------------------
+// Test: html nodes without recognised tags are left unchanged
+// -------------------------------------------------------------------
+console.log('\nhtml nodes without target tags:');
+{
+	const input = '
[link text](url) text
'; + const tree = makeTree(makeHtmlNode(input)); + runTransform(tree); + assertEqual(tree.children[0].value, input, 'non-target tag left unchanged'); +} + +// ------------------------------------------------------------------- +// Test: link inside GFM alert + (the full real-world case) +// ------------------------------------------------------------------- +console.log('\nfull GFM alert with
/:'); +{ + const tree = makeTree( + makeHtmlNode( + '
\nUsing the CCA? Open a terminal in [Step 6](06-install-gh-aw.md) to install `gh-aw`.\n\n**More info:** See [Step 1](01-prerequisites.md) for the checklist.\n\n
', + ), + ); + runTransform(tree); + const expected = + '
\nUsing the CCA? Open a terminal in Step 6 to install `gh-aw`.\n\n**More info:** See [Step 1](01-prerequisites.md) for the checklist.\n\n
'; + assertEqual(tree.children[0].value, expected, 'link in converted, body outside tag unchanged'); +} + +// ------------------------------------------------------------------- +// Test: nested transform (html nodes inside paragraph children) +// ------------------------------------------------------------------- +console.log('\nhtml node nested inside paragraph:'); +{ + const htmlNode = makeHtmlNode('See [Step 6](step6.md)'); + const tree = { type: 'root', children: [{ type: 'paragraph', children: [htmlNode] }] }; + runTransform(tree); + assertEqual( + htmlNode.value, + 'See Step 6', + 'nested html node processed', + ); +} + +// ------------------------------------------------------------------- +// Summary +// ------------------------------------------------------------------- +console.log(`\n${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); From 18eca063d15d381da3b36d60fff9a3d5d9da6bc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:27:58 +0000 Subject: [PATCH 2/4] docs: fix regex patterns in remarkInlineMarkdownInHtml per code review Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/lib/remark/inlineMarkdownInHtml.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.js b/docs/src/lib/remark/inlineMarkdownInHtml.js index 1239c6c628b..40b4fdf85f6 100644 --- a/docs/src/lib/remark/inlineMarkdownInHtml.js +++ b/docs/src/lib/remark/inlineMarkdownInHtml.js @@ -92,13 +92,17 @@ function processMarkdownLinksInHtml(html) { function convertMarkdownLinks(text) { // Tokenise the input so that backtick code spans are excluded from link // replacement while the rest of the text is processed normally. - const parts = text.split(/(`.+?`)/gs); + // Use [^`]+ (no dotAll) so a single code span cannot span multiple lines. + const parts = text.split(/(`[^`]+`)/g); return parts .map((part, index) => { // Even-indexed parts are plain text; odd-indexed parts are code spans. if (index % 2 !== 0) return part; + // Match standard markdown links. The link text pattern [^\]\[]+ rejects + // unescaped `[` and `]` characters so we don't accidentally match nested + // brackets or partial link syntax. return part.replace( - /\[([^\]]+)\]\(([^)]+)\)/g, + /\[([^\]\[]+)\]\(([^)]+)\)/g, '$1', ); }) From 6dc2330312b00186c87924714c176ab957f6d9d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:50:47 +0000 Subject: [PATCH 3/4] docs: apply unified markdown transformation in remarkInlineMarkdownInHtml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/lib/remark/inlineMarkdownInHtml.js | 84 +++++++++---------- .../lib/remark/inlineMarkdownInHtml.test.js | 12 +-- 2 files changed, 46 insertions(+), 50 deletions(-) diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.js b/docs/src/lib/remark/inlineMarkdownInHtml.js index 40b4fdf85f6..c3b6bf4c015 100644 --- a/docs/src/lib/remark/inlineMarkdownInHtml.js +++ b/docs/src/lib/remark/inlineMarkdownInHtml.js @@ -1,21 +1,50 @@ // @ts-check +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeStringify from 'rehype-stringify'; + /** - * Remark plugin that converts markdown link syntax (`[text](url)`) to HTML - * anchor tags inside raw HTML nodes. + * Remark plugin that applies markdown transformation to the content of + * specific HTML tags that are treated as opaque HTML blocks by remark-parse. * * CommonMark block-level HTML elements such as `
` and `` * are treated as opaque HTML blocks by remark-parse, so any markdown syntax * they contain is left as raw text rather than being processed. This plugin * runs after remark-parse to fill that gap: it visits every `html` MDAST node - * and rewrites markdown links found inside `` (and similar - * inline-content elements) to proper `` tags. + * and applies a full markdown transformation to the content of `` + * (and similar inline-content elements), producing proper HTML. * * This is the correct, AST-level fix for GFM alerts that contain * `
/` with markdown links — replacing the previous * approach of manipulating rendered HTML strings after compilation. */ +/** + * Reusable processor for converting markdown inline content to HTML. + * allowDangerousHtml is required to preserve inline HTML (e.g. ``, ``) + * that authors place inside target tags alongside their markdown. + */ +const inlineProcessor = unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeStringify, { allowDangerousHtml: true }); + +/** + * Apply markdown transformation to text, producing HTML. + * remark-parse wraps inline content in a `

` element; that wrapper is + * stripped so the result can be placed back inside the original HTML tag. + * + * @param {string} text + * @returns {string} + */ +function applyMarkdownTransformation(text) { + const result = String(inlineProcessor.processSync(text)); + // Strip the outer

wrapper added by remark for a single paragraph. + return result.replace(/^

([\s\S]*?)<\/p>\n?$/, '$1'); +} + /** * @returns {(tree: import('unist').Node) => void} */ @@ -32,7 +61,7 @@ function visit(node) { if (!node || typeof node !== 'object') return; if (node.type === 'html' && typeof node.value === 'string') { - node.value = processMarkdownLinksInHtml(node.value); + node.value = processMarkdownInHtml(node.value); } const { children } = node; @@ -42,7 +71,7 @@ function visit(node) { } /** - * Tags whose text content should have markdown link syntax converted to HTML. + * Tags whose text content should have markdown transformed to HTML. * These are inline-text contexts that appear as block-level HTML in markdown * and therefore bypass normal remark inline processing. * @@ -51,21 +80,16 @@ function visit(node) { const INLINE_TEXT_TAGS = ['summary', 'figcaption', 'caption', 'dt', 'dd', 'th', 'td', 'li']; /** - * Replace markdown link syntax `[text](url)` with `text` - * inside the content of specific HTML tags that are expected to carry inline - * text with markdown links. + * Apply markdown transformation inside the content of specific HTML tags. * - * The replacement is intentionally narrow: * - Only targets known inline-text tags listed in INLINE_TEXT_TAGS. - * - Does not process content inside `` or `

` elements.
- * - Text that already contains an `]*)?>)([\\s\\S]*?)(<\\/(?:${tagPattern})>)`,
@@ -76,35 +100,7 @@ function processMarkdownLinksInHtml(html) {
 		// Skip content that already contains an anchor tag to avoid double-processing.
 		if (/]/i.test(content)) return _match;
 
-		const processed = convertMarkdownLinks(content);
+		const processed = applyMarkdownTransformation(content);
 		return openTag + processed + closeTag;
 	});
 }
-
-/**
- * Convert `[text](url)` markdown link syntax to `text`.
- * Backtick-enclosed spans are preserved as-is so code snippets are not
- * accidentally rewritten.
- *
- * @param {string} text
- * @returns {string}
- */
-function convertMarkdownLinks(text) {
-	// Tokenise the input so that backtick code spans are excluded from link
-	// replacement while the rest of the text is processed normally.
-	// Use [^`]+ (no dotAll) so a single code span cannot span multiple lines.
-	const parts = text.split(/(`[^`]+`)/g);
-	return parts
-		.map((part, index) => {
-			// Even-indexed parts are plain text; odd-indexed parts are code spans.
-			if (index % 2 !== 0) return part;
-			// Match standard markdown links.  The link text pattern [^\]\[]+ rejects
-			// unescaped `[` and `]` characters so we don't accidentally match nested
-			// brackets or partial link syntax.
-			return part.replace(
-				/\[([^\]\[]+)\]\(([^)]+)\)/g,
-				'$1',
-			);
-		})
-		.join('');
-}
diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.test.js b/docs/src/lib/remark/inlineMarkdownInHtml.test.js
index 8037efbb467..c980e7e76bf 100644
--- a/docs/src/lib/remark/inlineMarkdownInHtml.test.js
+++ b/docs/src/lib/remark/inlineMarkdownInHtml.test.js
@@ -71,7 +71,7 @@ console.log('\nmultiple links inside :');
 }
 
 // -------------------------------------------------------------------
-// Test: backtick code spans inside  are preserved
+// Test: backtick code spans inside  are converted to 
 // -------------------------------------------------------------------
 console.log('\nbacktick code in :');
 {
@@ -81,8 +81,8 @@ console.log('\nbacktick code in :');
 	runTransform(tree);
 	assertEqual(
 		tree.children[0].value,
-		'Install `gh-aw` via Step 6',
-		'code span preserved, link converted',
+		'Install gh-aw via Step 6',
+		'backtick converted to , link converted',
 	);
 }
 
@@ -97,8 +97,8 @@ console.log('\nlink syntax inside code span is not converted:');
 	runTransform(tree);
 	assertEqual(
 		tree.children[0].value,
-		'Use `[var](param)` to configure',
-		'code span link syntax preserved',
+		'Use [var](param) to configure',
+		'code span link syntax not converted, backtick rendered as ',
 	);
 }
 
@@ -136,7 +136,7 @@ console.log('\nfull GFM alert with 
/:'); ); runTransform(tree); const expected = - '
\nUsing the CCA? Open a terminal in Step 6 to install `gh-aw`.\n\n**More info:** See [Step 1](01-prerequisites.md) for the checklist.\n\n
'; + '
\nUsing the CCA? Open a terminal in Step 6 to install gh-aw.\n\n**More info:** See [Step 1](01-prerequisites.md) for the checklist.\n\n
'; assertEqual(tree.children[0].value, expected, 'link in converted, body outside tag unchanged'); } From 8771dde16e2acc763465cb6b3c581f8652cd87cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 07:51:26 +0000 Subject: [PATCH 4/4] docs: document allowDangerousHtml trust model in remarkInlineMarkdownInHtml Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/lib/remark/inlineMarkdownInHtml.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/src/lib/remark/inlineMarkdownInHtml.js b/docs/src/lib/remark/inlineMarkdownInHtml.js index c3b6bf4c015..9e678836851 100644 --- a/docs/src/lib/remark/inlineMarkdownInHtml.js +++ b/docs/src/lib/remark/inlineMarkdownInHtml.js @@ -23,8 +23,14 @@ import rehypeStringify from 'rehype-stringify'; /** * Reusable processor for converting markdown inline content to HTML. - * allowDangerousHtml is required to preserve inline HTML (e.g. ``, ``) - * that authors place inside target tags alongside their markdown. + * + * `allowDangerousHtml` is required to preserve inline HTML (e.g. ``, + * ``) that authors place inside target tags alongside their markdown. + * This is safe here because: + * - the processor runs **at build time**, never in a browser; + * - inputs are documentation source files committed by repository maintainers + * and are not derived from end-user input; + * - no output is injected into an HTTP response or an untrusted context. */ const inlineProcessor = unified() .use(remarkParse)