diff --git a/__tests__/lib/mdxish/mdxishMdastToMd.test.ts b/__tests__/lib/mdxish/mdxishMdastToMd.test.ts index f2c6f02cd..0ca26b5e0 100644 --- a/__tests__/lib/mdxish/mdxishMdastToMd.test.ts +++ b/__tests__/lib/mdxish/mdxishMdastToMd.test.ts @@ -1,4 +1,5 @@ -import type { Root as MdastRoot, RootContent, Table } from 'mdast'; +import type { MdxishMdastRoot } from '../../../types'; +import type { Root as MdastRoot, RootContent } from 'mdast'; import { NodeTypes } from '../../../enums'; import { mdxishMdastToMd } from '../../../lib'; @@ -144,7 +145,7 @@ describe('mdxishMdastToMd', () => { describe('tables with flow content', () => { it('should serialize a table with newlines in cells to JSX ', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -176,7 +177,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -216,7 +217,7 @@ describe('mdxishMdastToMd', () => { }); it('should serialize a table with newlines in cells to JSX
and separate the lines with an empty line between them', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -261,7 +262,7 @@ describe('mdxishMdastToMd', () => { ] } ] - } as Table + } ] }; @@ -287,7 +288,7 @@ describe('mdxishMdastToMd', () => { }); it('should serialize a table with list content in cells to JSX
', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -322,7 +323,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -358,7 +359,7 @@ describe('mdxishMdastToMd', () => { }); it('should include align attribute and per-column styles when columns have alignment', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -392,7 +393,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -438,7 +439,7 @@ describe('mdxishMdastToMd', () => { }); it('should omit align attribute when all alignments are null', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -470,7 +471,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -508,7 +509,7 @@ describe('mdxishMdastToMd', () => { }); it('should handle a table with multiple body rows', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -543,7 +544,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -594,7 +595,7 @@ describe('mdxishMdastToMd', () => { }); it('should handle an empty cell alongside flow content', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -619,7 +620,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -655,7 +656,7 @@ describe('mdxishMdastToMd', () => { }); it('should handle inline formatting in phrasing-only cells as markdown', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -689,7 +690,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -702,7 +703,7 @@ describe('mdxishMdastToMd', () => { }); it('should keep tables with raw html nodes as markdown to avoid breaking remarkMdx roundtrip', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -727,7 +728,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -740,7 +741,7 @@ describe('mdxishMdastToMd', () => { }); it('should convert tables with code-tabs content to JSX', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -766,13 +767,13 @@ describe('mdxishMdastToMd', () => { children: [ { type: 'code', lang: 'js', meta: null, value: 'console.log("hi")' }, ], - } as unknown as MdastRoot['children'][number], + }, ], }, ], }, ], - } as Table, + }, ], }; @@ -809,7 +810,7 @@ describe('mdxishMdastToMd', () => { }); it('should serialize a table with self-closing JSX component in cell to JSX
', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -839,7 +840,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -849,7 +850,7 @@ describe('mdxishMdastToMd', () => { }); it('should keep table with plain HTML in cell as GFM', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -879,7 +880,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -889,7 +890,7 @@ describe('mdxishMdastToMd', () => { }); it('should keep tables with readme-variable nodes as GFM markdown', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -909,7 +910,7 @@ describe('mdxishMdastToMd', () => { type: NodeTypes.variable, data: { hName: 'Variable', hProperties: { name: 'WHOA' } }, value: '{user.WHOA}', - } as unknown as MdastRoot['children'][number], + }, ], }, ], @@ -923,7 +924,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -933,7 +934,7 @@ describe('mdxishMdastToMd', () => { }); it('should keep tables with readme-variable alongside text as GFM markdown', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -954,7 +955,7 @@ describe('mdxishMdastToMd', () => { type: NodeTypes.variable, data: { hName: 'Variable', hProperties: { name: 'name' } }, value: '{user.name}', - } as unknown as MdastRoot['children'][number], + }, ], }, ], @@ -970,7 +971,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; @@ -980,7 +981,7 @@ describe('mdxishMdastToMd', () => { }); it('should keep phrasing-only tables as markdown tables', () => { - const mdast: MdastRoot = { + const mdast: MdxishMdastRoot = { type: 'root', children: [ { @@ -1002,7 +1003,7 @@ describe('mdxishMdastToMd', () => { ], }, ], - } as Table, + }, ], }; diff --git a/processor/transform/mdxish/mdxish-jsx-to-mdast.ts b/processor/transform/mdxish/mdxish-jsx-to-mdast.ts index ec630b06a..afbe6c312 100644 --- a/processor/transform/mdxish/mdxish-jsx-to-mdast.ts +++ b/processor/transform/mdxish/mdxish-jsx-to-mdast.ts @@ -1,7 +1,7 @@ import type { MagicBlockEmbed, MagicBlockImage } from './magic-blocks/types'; -import type { FigureNode } from './types'; +import type { FigureNode, MdxishTable, MdxishTableCell, MdxishTableRow } from './types'; import type { Anchor, Callout, EmbedBlock, ImageAlign, ImageBlock, Recipe } from '../../../types'; -import type { Html, Node, Paragraph, Parent, PhrasingContent, RootContent, Table, TableCell, TableRow } from 'mdast'; +import type { Html, Node, Paragraph, Parent, PhrasingContent, RootContent, Table } from 'mdast'; import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'; import type { Plugin } from 'unified'; @@ -531,7 +531,7 @@ const isTableCell = (node: Node): node is MdxJsxFlowElement & { name: 'td' | 'th * Converts a JSX
element to an MDAST table node with alignment. * Returns null for header-less tables since MDAST always promotes the first row to . */ -const transformTable = (jsx: MdxJsxFlowElement): Table | null => { +const transformTable = (jsx: MdxJsxFlowElement): MdxishTable | null => { let hasThead = false; visit(jsx as Node, isMDXElement, (child: MdxJsxFlowElement | MdxJsxTextElement) => { if (child.name === 'thead') hasThead = true; @@ -542,7 +542,7 @@ const transformTable = (jsx: MdxJsxFlowElement): Table | null => { const { align: alignAttr } = getAttrs>(jsx); const align = Array.isArray(alignAttr) ? alignAttr : null; - const rows: TableRow[] = []; + const rows: MdxishTableRow[] = []; visit(jsx as Node, isMDXElement, (child: MdxJsxFlowElement | MdxJsxTextElement) => { if (child.name !== 'thead' && child.name !== 'tbody') return; @@ -550,7 +550,7 @@ const transformTable = (jsx: MdxJsxFlowElement): Table | null => { visit(child as Node, isMDXElement, (row: MdxJsxFlowElement | MdxJsxTextElement) => { if (row.name !== 'tr') return; - const cells: TableCell[] = []; + const cells: MdxishTableCell[] = []; visit(row as Node, isTableCell, (cell: MdxJsxFlowElement & { name: 'td' | 'th' }) => { const parsedChildren = unwrapSoleParagraph(cell.children as Node[]); @@ -559,7 +559,7 @@ const transformTable = (jsx: MdxJsxFlowElement): Table | null => { type: 'tableCell', children: parsedChildren, position: cell.position, - } as TableCell); + }); }); rows.push({ @@ -576,12 +576,14 @@ const transformTable = (jsx: MdxJsxFlowElement): Table | null => { ? align.slice(0, columnCount).concat(Array.from({ length: Math.max(0, columnCount - align.length) }, () => null)) : Array.from({ length: columnCount }, () => null); - return { + const table: MdxishTable = { type: 'table', align: alignArray, position: jsx.position, children: rows, }; + + return table; }; /** diff --git a/processor/transform/mdxish/tables/mdxish-tables-to-jsx.ts b/processor/transform/mdxish/tables/mdxish-tables-to-jsx.ts index fbb42a3df..4f833141f 100644 --- a/processor/transform/mdxish/tables/mdxish-tables-to-jsx.ts +++ b/processor/transform/mdxish/tables/mdxish-tables-to-jsx.ts @@ -1,4 +1,5 @@ -import type { Literal, Node, Table, TableCell } from 'mdast'; +import type { MdxishTable, MdxishTableCell } from '../types'; +import type { Literal, Node } from 'mdast'; import type { Transform } from 'mdast-util-from-markdown'; import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'; @@ -38,10 +39,11 @@ const mdxishTablesToJsx = (): Transform => tree => { visit( tree, (node: Node) => ['table', 'tableau'].includes(node.type), - (table: Table, index, parent) => { + (tableNode, index, parent) => { + const table = tableNode as MdxishTable; let hasFlowContent = false; - visit(table, isTableCell, (cell: TableCell) => { + visit(table, isTableCell, (cell: MdxishTableCell) => { if (hasFlowContent || cell.children.length === 0) return; visit(cell, 'break', (_, breakIndex, breakParent) => { @@ -61,7 +63,7 @@ const mdxishTablesToJsx = (): Transform => tree => { // serialization instead of GFM. `phrasing()` from mdast-util-phrasing returns // true for inline node types (text, emphasis, strong, link, etc.) which are // safe to keep in GFM cells. - const hasFlowChild = (cell.children as Node[]).some(child => { + const hasFlowChild = cell.children.some(child => { if (child.type === 'paragraph' || child.type === 'plain' || child.type === 'escape') return false; if (child.type === NodeTypes.variable) return false; if (phrasing(child as Parameters[0])) return false; diff --git a/processor/transform/mdxish/tables/utils.ts b/processor/transform/mdxish/tables/utils.ts index 48c647b6f..82e10af66 100644 --- a/processor/transform/mdxish/tables/utils.ts +++ b/processor/transform/mdxish/tables/utils.ts @@ -7,13 +7,13 @@ import type { Node } from 'mdast'; * When there are multiple paragraphs, leave them intact — they represent distinct lines * of content that need to be preserved for JSX `
` serialization. */ -export const unwrapSoleParagraph = (children: Node[]): Node[] => { +export const unwrapSoleParagraph = (children: Node[]) => { const paragraphCount = children.filter(c => c.type === 'paragraph').length; if (paragraphCount !== 1) return children; return children.flatMap(child => { if (child.type === 'paragraph' && 'children' in child && Array.isArray(child.children)) { - return child.children as Node[]; + return child.children; } return [child]; }); diff --git a/processor/transform/mdxish/types.ts b/processor/transform/mdxish/types.ts index 8bd03cbc1..64b833d35 100644 --- a/processor/transform/mdxish/types.ts +++ b/processor/transform/mdxish/types.ts @@ -1,4 +1,15 @@ -import type { RootContent } from 'mdast'; +import type { + Code, + Html, + List, + Paragraph, + PhrasingContent, + RootContent, + Table, + TableCell, + TableRow, +} from 'mdast'; +import type { CodeTabs } from 'types'; import type { Position } from 'unist'; /** @@ -14,3 +25,36 @@ export interface FigureNode { position?: Position; type: 'figure'; } + +/** + * Table cell content for the mdxish pipeline. mdast’s `TableCell` is typed as + * `PhrasingContent[]` only, but GFM / remark output and our transforms can + * place block-level nodes (e.g. paragraphs, lists, code) inside cells. + * + * If there's more node types that should be allowed, add them here. + */ +export type MdxishTableCellContent = + | Code + | CodeTabs + | Html + | List + | Paragraph + | PhrasingContent; + +/** + * A `tableCell` that allows the same content the serializer accepts at runtime. + */ +export interface MdxishTableCell extends Omit { + children: MdxishTableCellContent[]; +} + +export interface MdxishTableRow extends Omit { + children: MdxishTableCell[]; +} + +export interface MdxishTable extends Omit { + children: MdxishTableRow[]; +} + +/** `Root` content when `table` is allowed to use the wider cell shape. */ +export type MdxishMdastRootContent = Exclude | MdxishTable; diff --git a/types.d.ts b/types.d.ts index ac691225d..13ef96417 100644 --- a/types.d.ts +++ b/types.d.ts @@ -17,8 +17,17 @@ import type { } from 'mdast'; import type { MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx'; import type { MDXModule } from 'mdx/types'; +import type { MdxishMdastRootContent } from 'processor/transform/mdxish/types'; import type { Position } from 'unist'; +/** + * Extension of MDAST's Root that has some custom extended content types + * E.g. Tables whose cells carry flow content beyond mdast’s built-in `TableCell` typing. + */ +export interface MdxishMdastRoot extends Omit { + children: MdxishMdastRootContent[]; +} + /** * Custom hast node emitted by `mdxJsxElementHandler` for every MDX JSX element. * Registered with hast below so the unified/unist tooling (remark-rehype handlers,