From 026c279ee8f705f0f2e56db9b50d4f2f6a69bc72 Mon Sep 17 00:00:00 2001 From: Maximilian Falco Widjaya Date: Mon, 1 Jun 2026 13:44:35 +1000 Subject: [PATCH 1/6] fix: prevent blank lines from fragmenting in fallback parser --- __tests__/parsers/tables.test.ts | 53 +++++++++++++++++++ .../transform/mdxish/tables/mdxish-tables.ts | 16 +++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/__tests__/parsers/tables.test.ts b/__tests__/parsers/tables.test.ts index bab7a7f28..35969b6ef 100644 --- a/__tests__/parsers/tables.test.ts +++ b/__tests__/parsers/tables.test.ts @@ -6,6 +6,7 @@ import { removePosition } from 'unist-util-remove-position'; import { mdast } from '../../lib'; import { mdxish } from '../../lib/mdxish'; +import { collapseBlankLines } from '../../processor/transform/mdxish/tables/mdxish-tables'; import { collectNodes, findAllElementsByTagName, parseMdxishWithSource } from '../helpers'; describe('table parser', () => { @@ -1170,4 +1171,56 @@ None of the following content will get rendered!`; expect(html).toContain('another_name_here'); }); }); + + describe('lowercase
fallback path', () => { + it('keeps a block-with-whitespace-only-line intact when the mdx-aware parser throws on a cell expression', () => { + const doc = ['
', ' ', ' ', ' ', ' ', '
{not valid jsx}
'].join('\n'); + + const hast = mdxish(doc); + const html = toHtml(hast); + + expect(html).not.toContain('
');
+      expect(html).toContain('{not valid jsx}');
+    });
+
+    it('keeps a block-with-empty-line intact when the mdx-aware parser throws on a cell expression', () => {
+      const doc = ['', '    ', '        ', '', '    ', '
{not valid jsx}
'].join('\n'); + + const hast = mdxish(doc); + const html = toHtml(hast); + + expect(html).not.toContain('
');
+      expect(html).toContain('{not valid jsx}');
+    });
+
+    it('collapseBlankLines: removes exactly one blank line per run, leaves every other line byte-identical', () => {
+      const input = [
+        '',
+        '    ',
+        '        ',
+        '    ',
+        '        ',
+        '',
+        '        ',
+        '    ',
+        '
row 1row 2row 3
', + ].join('\n'); + + const output = collapseBlankLines(input); + const inputLines = input.split('\n'); + const nonBlankInputLines = inputLines.filter(line => !/^[ \t]*$/.test(line)); + + expect(output.split('\n')).toStrictEqual(nonBlankInputLines); + }); + + it('collapseBlankLines: only consumes one blank line per run (consecutive blanks are mostly preserved)', () => { + expect(collapseBlankLines('a\n\n\nb')).toBe('a\n\nb'); + expect(collapseBlankLines('a\n \n \nb')).toBe('a\n \nb'); + }); + + it('collapseBlankLines: does not strip trailing whitespace on non-empty lines', () => { + const input = 'a \nb'; + expect(collapseBlankLines(input)).toBe(input); + }); + }); }); diff --git a/processor/transform/mdxish/tables/mdxish-tables.ts b/processor/transform/mdxish/tables/mdxish-tables.ts index 46e217ff7..5cc2057b0 100644 --- a/processor/transform/mdxish/tables/mdxish-tables.ts +++ b/processor/transform/mdxish/tables/mdxish-tables.ts @@ -61,6 +61,17 @@ const buildTableNodeProcessor = (withMdx: boolean) => const tableNodeProcessor = buildTableNodeProcessor(true); const fallbackTableNodeProcessor = buildTableNodeProcessor(false); +const BLANK_LINE_REGEX = /(\r?\n)[ \t]*\r?\n/g; + +/** + * Collapses a single blank line (empty or whitespace-only) per match so it + * doesn't terminate the CommonMark type-6 block when the fallback parser + * sees it. Non-greedy: only one blank line per run is consumed, so authorial + * sequences of multiple blank lines are mostly preserved. + */ +export const collapseBlankLines = (value: string): string => + value.replace(BLANK_LINE_REGEX, '$1'); + /** * Parse the HTML node that contains the full table substring * into the table parts (headers, rows, cells). @@ -355,8 +366,9 @@ const mdxishTables = (): Transform => tree => { } else if (node.value.startsWith(' Date: Mon, 1 Jun 2026 14:18:01 +1000 Subject: [PATCH 2/6] feat: show multiple markdown views for strip comments --- example/Doc.tsx | 58 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/example/Doc.tsx b/example/Doc.tsx index 7bcd9bffe..149afae40 100644 --- a/example/Doc.tsx +++ b/example/Doc.tsx @@ -53,6 +53,20 @@ const variables = { ], }; +interface StripState { + error: string | null; + stripped: string | null; +} + +type PipelineKey = 'legacy' | 'mdxish' | 'rmdx'; + +const EMPTY_STRIP_STATE: StripState = { error: null, stripped: null }; +const EMPTY_STRIP_STATE_MAP: Record = { + legacy: EMPTY_STRIP_STATE, + mdxish: EMPTY_STRIP_STATE, + rmdx: EMPTY_STRIP_STATE, +}; + const Doc = () => { const { fixture } = useParams(); const [searchParams] = useSearchParams(); @@ -61,7 +75,6 @@ const Doc = () => { const mdxish = searchParams.has('mdxish'); const showRmdx = searchParams.has('rmdx'); - type PipelineKey = 'legacy' | 'mdxish' | 'rmdx'; const selectedPipelines: PipelineKey[] = []; if (showRmdx) selectedPipelines.push('rmdx'); if (legacy) selectedPipelines.push('legacy'); @@ -88,8 +101,8 @@ const Doc = () => { const [rmdxHast, setRmdxHast] = useState(null); const [mdxishMdast, setMdxishMdast] = useState(null); const [mdxishHast, setMdxishHast] = useState(null); - const [strippedMarkdown, setStrippedMarkdown] = useState(null); - const [stripError, setStripError] = useState(null); + const [stripByPipeline, setStripByPipeline] = useState>(EMPTY_STRIP_STATE_MAP); + const hasAnyStripped = Object.values(stripByPipeline).some(s => s.stripped !== null); const [view, setView] = useState<'hast' | 'markdown' | 'mdast' | 'rendered'>('rendered'); const showToc = fixture === 'tableOfContentsTests'; @@ -101,8 +114,7 @@ const Doc = () => { useEffect(() => { const sanitize = async (mode: PipelineKey) => { if (!stripComments) { - setStrippedMarkdown(null); - setStripError(null); + setStripByPipeline(prev => ({ ...prev, [mode]: EMPTY_STRIP_STATE })); return doc; } try { @@ -110,15 +122,13 @@ const Doc = () => { mdx: mode === 'rmdx', mdxish: mode === 'mdxish', }); - setStrippedMarkdown(sanitized); - setStripError(null); + setStripByPipeline(prev => ({ ...prev, [mode]: { stripped: sanitized, error: null } })); return sanitized; } catch (e) { // eslint-disable-next-line no-console console.error(e); const message = e instanceof Error ? e.message : String(e); - setStripError(message); - setStrippedMarkdown(null); + setStripByPipeline(prev => ({ ...prev, [mode]: { stripped: null, error: message } })); return null; } }; @@ -261,7 +271,7 @@ const Doc = () => {
{!ci &&

{name}

} - {(strippedMarkdown !== null || showAst) && ( + {(hasAnyStripped || showAst) && (
- {strippedMarkdown !== null && ( + {hasAnyStripped && (
)} - {stripError && ( -
- stripComments error: {stripError} + {view === 'markdown' && hasAnyStripped ? ( +
+ {activePipelines.map(p => { + const { error, stripped } = stripByPipeline[p]; + return ( +
+
{pipelineLabels[p]}
+ {error && ( +
+ stripComments error: {error} +
+ )} + {stripped !== null ? ( +
{stripped}
+ ) : ( + !error &&
No stripped output for this pipeline
+ )} +
+ ); + })}
- )} - {view === 'markdown' && strippedMarkdown !== null ? ( -
{strippedMarkdown}
) : (view === 'mdast' || view === 'hast') && showAst ? (
{activePipelines.map(p => { From 421762c7541ea70e1249fbb91c97cd4df1c72951 Mon Sep 17 00:00:00 2001 From: Maximilian Falco Widjaya Date: Mon, 1 Jun 2026 14:32:10 +1000 Subject: [PATCH 3/6] fix(stripComments): remove indented whole-line HTML comments cleanly --- __tests__/lib/stripComments.test.ts | 45 +++++++++++++++++++ .../transform/mdxish/tables/mdxish-tables.ts | 8 ++-- processor/transform/stripComments.ts | 8 +++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/__tests__/lib/stripComments.test.ts b/__tests__/lib/stripComments.test.ts index 6794ab628..747f911fe 100644 --- a/__tests__/lib/stripComments.test.ts +++ b/__tests__/lib/stripComments.test.ts @@ -356,6 +356,51 @@ end"`); expect(output).toContain('A name field'); }); + it('does not break an HTML block when an inner comment occupies a whole line', async () => { + const input = ` + + + + + +
Row 1
Row 3
`; + + const output = await stripComments(input, { mdxish: true }); + expect(output).not.toContain(' + + row 2 + + + + row 3 + +`; + + const expected = ` + + + + + + + + +
row 1
row 2
row 3
`; + + const output = await stripComments(input, { mdxish: true }); + expect(output).toBe(expected); + }); + it('strips comments inside jsx tables in mdxish mode', async () => { const input = ` diff --git a/processor/transform/mdxish/tables/mdxish-tables.ts b/processor/transform/mdxish/tables/mdxish-tables.ts index 5cc2057b0..0fd0fd819 100644 --- a/processor/transform/mdxish/tables/mdxish-tables.ts +++ b/processor/transform/mdxish/tables/mdxish-tables.ts @@ -61,13 +61,11 @@ const buildTableNodeProcessor = (withMdx: boolean) => const tableNodeProcessor = buildTableNodeProcessor(true); const fallbackTableNodeProcessor = buildTableNodeProcessor(false); -const BLANK_LINE_REGEX = /(\r?\n)[ \t]*\r?\n/g; +const BLANK_LINE_REGEX = /(\r?\n)[ \t]*\r?\n(?![ \t]*\r?\n)/g; /** - * Collapses a single blank line (empty or whitespace-only) per match so it - * doesn't terminate the CommonMark type-6 block when the fallback parser - * sees it. Non-greedy: only one blank line per run is consumed, so authorial - * sequences of multiple blank lines are mostly preserved. + * Collapses one blank line per match so it doesn't terminate the CommonMark + * type-6 block. Non-greedy: runs of multiple blank lines lose just one. */ export const collapseBlankLines = (value: string): string => value.replace(BLANK_LINE_REGEX, '$1'); diff --git a/processor/transform/stripComments.ts b/processor/transform/stripComments.ts index 5bc5b83b4..973b8cf8c 100644 --- a/processor/transform/stripComments.ts +++ b/processor/transform/stripComments.ts @@ -3,6 +3,9 @@ import type { Root } from 'mdast'; import { visit, SKIP } from 'unist-util-visit'; const HTML_COMMENT_REGEX = //g; +// Indented whole-line comment plus trailing newline; removing the whole line +// avoids leaving a whitespace-only line that terminates the surrounding block. +const WHOLE_LINE_HTML_COMMENT_REGEX = /^[ \t]+[ \t]*(?:\r?\n|$)/gm; export const MDX_COMMENT_REGEX = /\/\*(?:(?!\*\/)[\s\S])*\*\//g; /** @@ -14,7 +17,10 @@ export const stripCommentsTransformer = () => { if (parent && typeof index === 'number') { // Remove HTML comments if (node.type === 'html' && HTML_COMMENT_REGEX.test(node.value)) { - const newValue = node.value.replace(HTML_COMMENT_REGEX, '').trim(); + const newValue = node.value + .replace(WHOLE_LINE_HTML_COMMENT_REGEX, '') + .replace(HTML_COMMENT_REGEX, '') + .trim(); if (newValue) { node.value = newValue; } else { From 1a96e937624657c88f89671c0d54617c71f7030f Mon Sep 17 00:00:00 2001 From: Maximilian Falco Widjaya Date: Mon, 1 Jun 2026 14:43:07 +1000 Subject: [PATCH 4/6] bundlewatch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa071476e..0b6fd595d 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ }, { "path": "dist/main.node.js", - "maxSize": "947KB" + "maxSize": "950KB" } ] }, From d171fdb177b2da7e50b3165a13aa9a7f43b173ed Mon Sep 17 00:00:00 2001 From: Maximilian Falco Widjaya Date: Mon, 1 Jun 2026 14:49:47 +1000 Subject: [PATCH 5/6] fix: collapse all consecutive blank lines in fallback --- __tests__/parsers/tables.test.ts | 31 ++++++++++++++++--- .../transform/mdxish/tables/mdxish-tables.ts | 6 ++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/__tests__/parsers/tables.test.ts b/__tests__/parsers/tables.test.ts index 35969b6ef..02f81fb5f 100644 --- a/__tests__/parsers/tables.test.ts +++ b/__tests__/parsers/tables.test.ts @@ -1193,7 +1193,29 @@ None of the following content will get rendered!`; expect(html).toContain('{not valid jsx}'); }); - it('collapseBlankLines: removes exactly one blank line per run, leaves every other line byte-identical', () => { + it('keeps a block-with-many-blank-lines intact', () => { + const doc = [ + '
', + ' ', + ' ', + '', + '', + '', + '', + '', + '', + ' ', + '
{not valid jsx}
', + ].join('\n'); + + const hast = mdxish(doc); + const html = toHtml(hast); + + expect(html).not.toContain('
');
+      expect(html).toContain('{not valid jsx}');
+    });
+
+    it('collapseBlankLines: removes every blank line, leaves every other line byte-identical', () => {
       const input = [
         '',
         '    ',
@@ -1213,9 +1235,10 @@ None of the following content will get rendered!`;
       expect(output.split('\n')).toStrictEqual(nonBlankInputLines);
     });
 
-    it('collapseBlankLines: only consumes one blank line per run (consecutive blanks are mostly preserved)', () => {
-      expect(collapseBlankLines('a\n\n\nb')).toBe('a\n\nb');
-      expect(collapseBlankLines('a\n   \n   \nb')).toBe('a\n   \nb');
+    it('collapseBlankLines: collapses any run of consecutive blank lines to a single newline', () => {
+      expect(collapseBlankLines('a\n\n\nb')).toBe('a\nb');
+      expect(collapseBlankLines('a\n\n\n\n\n\n\nb')).toBe('a\nb');
+      expect(collapseBlankLines('a\n   \n   \nb')).toBe('a\nb');
     });
 
     it('collapseBlankLines: does not strip trailing whitespace on non-empty lines', () => {
diff --git a/processor/transform/mdxish/tables/mdxish-tables.ts b/processor/transform/mdxish/tables/mdxish-tables.ts
index 0fd0fd819..45590179f 100644
--- a/processor/transform/mdxish/tables/mdxish-tables.ts
+++ b/processor/transform/mdxish/tables/mdxish-tables.ts
@@ -61,11 +61,11 @@ const buildTableNodeProcessor = (withMdx: boolean) =>
 const tableNodeProcessor = buildTableNodeProcessor(true);
 const fallbackTableNodeProcessor = buildTableNodeProcessor(false);
 
-const BLANK_LINE_REGEX = /(\r?\n)[ \t]*\r?\n(?![ \t]*\r?\n)/g;
+const BLANK_LINE_REGEX = /(\r?\n)(?:[ \t]*\r?\n)+/g;
 
 /**
- * Collapses one blank line per match so it doesn't terminate the CommonMark
- * type-6 block. Non-greedy: runs of multiple blank lines lose just one.
+ * Collapses any run of blank lines (empty or whitespace-only) to a single
+ * newline so the CommonMark type-6 block isn't terminated mid-table.
  */
 export const collapseBlankLines = (value: string): string =>
   value.replace(BLANK_LINE_REGEX, '$1');

From a6f87bafd78f5197e609caed1ae3a9a96ab2b8e8 Mon Sep 17 00:00:00 2001
From: Maximilian Falco Widjaya 
Date: Mon, 1 Jun 2026 15:48:01 +1000
Subject: [PATCH 6/6] fix: pr feedback on tests

---
 __tests__/lib/stripComments.test.ts | 46 ++++++++++++++++++++++++++---
 1 file changed, 42 insertions(+), 4 deletions(-)

diff --git a/__tests__/lib/stripComments.test.ts b/__tests__/lib/stripComments.test.ts
index 747f911fe..e1d4753a9 100644
--- a/__tests__/lib/stripComments.test.ts
+++ b/__tests__/lib/stripComments.test.ts
@@ -365,11 +365,15 @@ end"`);
   
`; + const expected = ` + + + + +
Row 1
Row 3
`; + const output = await stripComments(input, { mdxish: true }); - expect(output).not.toContain('