diff --git a/__tests__/transformers/mdxish-tables.test.ts b/__tests__/transformers/mdxish-tables.test.ts
index 3d7295f93..ff5f2d2fe 100644
--- a/__tests__/transformers/mdxish-tables.test.ts
+++ b/__tests__/transformers/mdxish-tables.test.ts
@@ -203,6 +203,181 @@ describe('mdxish tables transformation', () => {
});
});
+ describe('given a leading list marker inside a JSX cell', () => {
+ it('renders an unescaped leading dash as a bullet list (default markdown behaviour)', () => {
+ const doc = `
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).toContain('');
+ expect(html).toContain('- foo
');
+ });
+
+ it('renders an escaped leading dash as literal text (raw HTML )', () => {
+ const doc = `
+| \\- | \\- |
+| \\- fqefeq | \\- hello |
+
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('');
+ expect(html).not.toContain('- ');
+ expect(html).toContain('- fqefeq');
+ expect(html).toContain('- hello');
+ });
+
+ it('renders an escaped leading dash as literal text (
component)', () => {
+ const doc = `
+
+ | \\- | \\- |
+
+
+ | \\- fqefeq | \\- hello |
+
+
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('');
+ expect(html).not.toContain('- ');
+ expect(html).toContain('- fqefeq');
+ expect(html).toContain('- hello');
+ });
+
+ it('renders an escaped lone marker (no following content) as literal text', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('
');
+ expect(html).toContain('-');
+ });
+
+ it('still parses emphasis (asterisk hugging text) inside a JSX cell', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toContain('italic');
+ });
+
+ it('preserves a real inside a JSX cell', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toContain('');
+ expect(html).toContain('- real item
');
+ });
+
+ it('renders an unescaped leading hash as a heading (default markdown behaviour)', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toMatch(/]*>heading<\/h1>/);
+ });
+
+ it('renders an escaped leading hash as literal text', () => {
+ const doc = `
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toMatch(/]*>/);
+ expect(html).toContain('# heading');
+ });
+
+ it('renders an escaped leading hash with multiple hashes as literal text', () => {
+ const doc = `
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toMatch(/]*>/);
+ expect(html).toContain('## subheading');
+ });
+
+ it.each([
+ ['-', '-'],
+ ['*', '*'],
+ ['+', '+'],
+ ['#', '#'],
+ ['##', '##'],
+ ['###', '###'],
+ ])('flattens a standalone "%s" cell to literal text instead of an empty list/heading', (marker, expected) => {
+ const doc = `
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('');
+ expect(html).not.toMatch(/]*><\/h[1-6]>/);
+ expect(html).toContain(`| ${expected} | `);
+ });
+
+ it('keeps a list when the cell has actual content after the marker', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toContain('');
+ expect(html).toContain('- foo
');
+ });
+
+ it('keeps a heading when the cell has actual content after the hash', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toMatch(/]*>heading<\/h1>/);
+ });
+
+ it('flattens a standalone `>` blockquote marker to literal text', () => {
+ const doc = `
`;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toMatch(/(>|>|>)<\/th>/);
+ });
+
+ it('flattens a standalone `---` thematic break to literal text', () => {
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('
{
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).not.toContain('
{
+ const doc = ``;
+ const html = toHtml(mdxish(doc));
+ expect(html).toContain('', () => {
// Stray duplicated makes mdxjs reject the captured value;
// the non-MDX fallback should still split the html node so blank-line
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"
}
]
},
diff --git a/processor/transform/mdxish/tables/mdxish-tables.ts b/processor/transform/mdxish/tables/mdxish-tables.ts
index 46e217ff7..59544bddd 100644
--- a/processor/transform/mdxish/tables/mdxish-tables.ts
+++ b/processor/transform/mdxish/tables/mdxish-tables.ts
@@ -1,4 +1,4 @@
-import type { Html, Node, Parents, Root, Table, TableCell, TableRow } from 'mdast';
+import type { Html, List, ListItem, Node, Parent, Parents, Root, Table, TableCell, TableRow, Text } from 'mdast';
import type { Transform } from 'mdast-util-from-markdown';
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx';
@@ -38,6 +38,48 @@ const tableTypes = {
td: 'tableCell',
};
+const CELL_OPEN_TAG_RE = /^<(td|th)(?:\s[^>]*)?>/i;
+const LEADING_ESCAPED_MARKER_RE = /^\s*\\(?:[-*+](?=[ \t]|<|$|\n)|#)/;
+
+/**
+ * Cell starts with `\-`/`\*`/`\+`/`\#`; restore `\` before re-parse.
+ */
+const cellSourceHasEscapedMarker = (cellSrc: string): boolean => {
+ const open = cellSrc.match(CELL_OPEN_TAG_RE);
+ return open != null && LEADING_ESCAPED_MARKER_RE.test(cellSrc.slice(open[0].length));
+};
+
+/**
+ * Re-parsed children are a phantom empty block from a lone marker.
+ */
+const isLonePhantomBlock = (children: Node[]): boolean => {
+ if (children.length !== 1) return false;
+ const child = children[0];
+ switch (child.type) {
+ case 'list': {
+ const items = (child as List).children;
+ return items.length === 1 && ((items[0] as ListItem).children?.length ?? 0) === 0;
+ }
+ case 'heading':
+ case 'blockquote':
+ return ((child as Parent).children?.length ?? 0) === 0;
+ case 'thematicBreak':
+ return true;
+ default:
+ return false;
+ }
+};
+
+/** Slice the cell's substring via its outer-document offsets. */
+const sliceCellSource = (
+ tableSource: string | undefined,
+ cellPosition: Node['position'] | undefined,
+ baseOffset: number,
+): string | undefined => {
+ if (!tableSource || cellPosition?.start?.offset == null || cellPosition?.end?.offset == null) return undefined;
+ return tableSource.slice(cellPosition.start.offset - baseOffset, cellPosition.end.offset - baseOffset);
+};
+
// `mdxjs` + `mdxFromMarkdown` is what `remarkMdx` registers internally; we
// register them manually so we control ordering against our other tokenizers.
// The fallback omits these so blank-line-separated markdown inside cells still
@@ -142,6 +184,7 @@ const processTableNode = (
index: number,
parent: Parents,
documentPosition?: Node['position'],
+ tableSource?: string,
): void => {
if (node.name !== 'Table' && node.name !== 'table') return;
@@ -150,24 +193,36 @@ const processTableNode = (
const align = Array.isArray(alignAttr) ? alignAttr : null;
let tableHasFlowContent = false;
+ const tableBaseOffset = position?.start?.offset ?? 0;
// Re-parse text-only cells through markdown and detect flow content
visit(node as Node, isTableCell, (cell: MdxJsxTableCell) => {
if (!isTextOnly(cell.children as unknown[])) return;
- const textContent = extractTextFromChildren(cell.children as unknown[]);
- if (!textContent.trim()) return;
+ const originalText = extractTextFromChildren(cell.children as unknown[]);
+ if (!originalText.trim()) return;
+
+ // Restore the `\` so `\- foo` re-parses as text.
+ const cellSrc = sliceCellSource(tableSource, cell.position, tableBaseOffset);
+ const textContent = cellSrc && cellSourceHasEscapedMarker(cellSrc) ? `\\${originalText}` : originalText;
// Since now we are using remarkMdx, which can fail and error, we need to
// gate this behind a try/catch to ensure that malformed syntaxes do not
// crash the page
try {
const parsed = tableNodeProcessor.runSync(tableNodeProcessor.parse(textContent)) as Root;
- if (parsed.children.length > 0) {
- cell.children = parsed.children as MdxJsxTableCell['children'];
- if (hasFlowContent(parsed.children as Node[])) {
- tableHasFlowContent = true;
- }
+ if (parsed.children.length === 0) return;
+
+ // Lone marker → empty block; render as literal char.
+ if (isLonePhantomBlock(parsed.children as Node[])) {
+ const textNode: Text = { type: 'text', value: originalText.trim() };
+ cell.children = [textNode] as unknown as MdxJsxTableCell['children'];
+ return;
+ }
+
+ cell.children = parsed.children as MdxJsxTableCell['children'];
+ if (hasFlowContent(parsed.children as Node[])) {
+ tableHasFlowContent = true;
}
} catch {
// If parsing fails, keep original children
@@ -349,7 +404,7 @@ const mdxishTables = (): Transform => tree => {
// to build on the markdown / JSX table
visit(parsed as Node, isMDXElement, (tableNode: MdxJsxFlowElement | MdxJsxTextElement) => {
if (tableNode.name !== 'Table' && tableNode.name !== 'table') return undefined;
- processTableNode(tableNode, index, parent as Parents, node.position);
+ processTableNode(tableNode, index, parent as Parents, node.position, node.value);
return EXIT;
});
} else if (node.value.startsWith('