fix: preserve table math markdown

This commit is contained in:
lucaronin
2026-04-28 16:47:52 +02:00
parent a2843a0cf3
commit 8e66fb4c19
3 changed files with 131 additions and 6 deletions

View File

@@ -95,6 +95,58 @@ describe('math markdown round-trip', () => {
)
})
it('round-trips inline math inside table cells', () => {
const tableCellMath = preProcessMathMarkdown({ markdown: '$a+b$' })
const blocks = [{
type: 'table',
content: {
type: 'tableContent',
rows: [{
cells: [{
type: 'tableCell',
content: [{ type: 'text', text: tableCellMath, styles: {} }],
}],
}],
},
children: [],
}]
const [tableBlock] = injectMathInBlocks(blocks) as Array<{
content: { rows: Array<{ cells: Array<{ content: unknown[] }> }> }
}>
expect(tableBlock.content.rows[0].cells[0].content).toEqual([{
type: MATH_INLINE_TYPE,
props: { latex: 'a+b' },
content: undefined,
}])
const editor = {
blocksToMarkdownLossy: vi.fn(() => '| Formula |\n| --- |\n| $a+b$ |'),
}
expect(serializeMathAwareBlocks(editor, [tableBlock])).toBe('| Formula |\n| --- |\n| $a+b$ |')
expect(editor.blocksToMarkdownLossy).toHaveBeenCalledWith([{
type: 'table',
content: {
type: 'tableContent',
rows: [{
cells: [{
type: 'tableCell',
content: [{ type: 'text', text: '$a+b$' }],
}],
}],
},
children: [],
}])
})
it('leaves display-style math inside table cells as Markdown source', () => {
expect(preProcessMathMarkdown({ markdown: '| Formula |\n| --- |\n| $$c$$ |' })).toBe(
'| Formula |\n| --- |\n| $$c$$ |',
)
})
it('leaves inline code and fenced code math-looking text untouched', () => {
const markdown = [
'Keep `$not_math$` literal.',

View File

@@ -19,12 +19,33 @@ interface InlineItem {
interface BlockLike {
type?: string
content?: InlineItem[]
content?: BlockContent
props?: Record<string, string>
children?: BlockLike[]
[key: string]: unknown
}
type BlockContent = InlineItem[] | TableContentLike | unknown
interface TableContentLike {
type?: string
rows?: TableRowLike[]
[key: string]: unknown
}
interface TableRowLike {
cells?: TableCellValue[]
[key: string]: unknown
}
interface TableCellLike {
content?: InlineItem[]
[key: string]: unknown
}
type TableCellValue = TableCellLike | string
type InlineContentTransform = (content: InlineItem[]) => InlineItem[]
interface MarkdownSerializer {
blocksToMarkdownLossy: (blocks: unknown[]) => string
}
@@ -111,7 +132,7 @@ function isCodeFence({ text: line }: { text: string }): boolean {
}
function isSingleDollar({ text, index }: TextPosition): boolean {
return text[index] === '$' && text[index + 1] !== '$'
return text[index] === '$' && text[index - 1] !== '$' && text[index + 1] !== '$'
}
function isInlineMathEnd(position: TextPosition): boolean {
@@ -274,10 +295,47 @@ function restoreInlineMath(content: InlineItem[]): InlineItem[] {
})
}
function isTableContent(content: BlockContent): content is TableContentLike {
return Boolean(
content
&& typeof content === 'object'
&& !Array.isArray(content)
&& (content as TableContentLike).type === 'tableContent'
&& Array.isArray((content as TableContentLike).rows),
)
}
function transformTableCell(cell: TableCellValue, transform: InlineContentTransform): TableCellValue {
if (typeof cell === 'string' || !Array.isArray(cell.content)) return cell
return { ...cell, content: transform(cell.content) }
}
function transformTableContent(
content: TableContentLike,
transform: InlineContentTransform,
): TableContentLike {
return {
...content,
rows: content.rows?.map((row) => ({
...row,
cells: row.cells?.map((cell) => transformTableCell(cell, transform)),
})),
}
}
function transformBlockContent(
content: BlockContent,
transform: InlineContentTransform,
): BlockContent {
if (Array.isArray(content)) return transform(content)
if (isTableContent(content)) return transformTableContent(content, transform)
return content
}
function injectMathInBlock(block: BlockLike): BlockLike {
const content = Array.isArray(block.content) ? expandInlineMath(block.content) : block.content
const content = transformBlockContent(block.content, expandInlineMath)
const children = Array.isArray(block.children) ? block.children.map(injectMathInBlock) : block.children
const latex = readDisplayMathToken(content)
const latex = Array.isArray(content) ? readDisplayMathToken(content) : null
if (latex !== null) {
return buildMathBlock({ block, latex })
@@ -303,7 +361,7 @@ function buildMathBlock({ block, latex }: { block: BlockLike } & LatexPayload):
}
function restoreInlineMathInBlock(block: BlockLike): BlockLike {
const content = Array.isArray(block.content) ? restoreInlineMath(block.content) : block.content
const content = transformBlockContent(block.content, restoreInlineMath)
const children = Array.isArray(block.children) ? block.children.map(restoreInlineMathInBlock) : block.children
return { ...block, content, children }
}

View File

@@ -9,6 +9,8 @@ let tempVaultDir: string
const INLINE_LATEX = 'E=mc^2'
const DISPLAY_LATEX = '\\int_0^1 x^2 \\, dx = \\frac{1}{3}'
const MALFORMED_LATEX = '\\frac{'
const TABLE_INLINE_LATEX = '\\frac{a}{b}+c'
const TABLE_DISPLAY_STYLE_LATEX = '\\sum_{i=1}^{n} i'
test.beforeEach(async ({ page }, testInfo) => {
testInfo.setTimeout(90_000)
@@ -107,6 +109,11 @@ ${DISPLAY_LATEX}
$$
Malformed math $${MALFORMED_LATEX}$ stays visible.
| Kind | Formula |
| --- | --- |
| inline | $${TABLE_INLINE_LATEX}$ |
| display-style | $$${TABLE_DISPLAY_STYLE_LATEX}$$ |
`
await setRawEditorContent(page, nextContent)
@@ -117,15 +124,20 @@ Malformed math $${MALFORMED_LATEX}$ stays visible.
await toggleRawMode(page, '.bn-editor')
await expectMathNode(page, '.math--inline', INLINE_LATEX)
await expectMathNode(page, '.math--inline', TABLE_INLINE_LATEX)
await expectMathNode(page, '.math--block', DISPLAY_LATEX)
await expectMathNode(page, '.math--inline', MALFORMED_LATEX)
await expect(page.locator('.math .katex, .math .katex-error')).toHaveCount(3)
await expect(page.locator('table')).toHaveCount(1)
await expect(page.locator('.math .katex, .math .katex-error')).toHaveCount(4)
await toggleRawMode(page, '.cm-content')
const rawAfterRichMode = await getRawEditorContent(page)
expect(rawAfterRichMode).toContain(`$${INLINE_LATEX}$`)
expect(rawAfterRichMode).toContain(`$$\n${DISPLAY_LATEX}\n$$`)
expect(rawAfterRichMode).toContain(`$${MALFORMED_LATEX}$`)
expect(rawAfterRichMode).toContain(`$${TABLE_INLINE_LATEX}$`)
expect(rawAfterRichMode).toContain(`$$${TABLE_DISPLAY_STYLE_LATEX}$$`)
expect(rawAfterRichMode).not.toContain('@@TOLARIA_MATH')
await toggleRawMode(page, '.bn-editor')
await openNote(page, 'Note C')
@@ -136,4 +148,7 @@ Malformed math $${MALFORMED_LATEX}$ stays visible.
expect(reopenedRaw).toContain(`$${INLINE_LATEX}$`)
expect(reopenedRaw).toContain(`$$\n${DISPLAY_LATEX}\n$$`)
expect(reopenedRaw).toContain(`$${MALFORMED_LATEX}$`)
expect(reopenedRaw).toContain(`$${TABLE_INLINE_LATEX}$`)
expect(reopenedRaw).toContain(`$$${TABLE_DISPLAY_STYLE_LATEX}$$`)
expect(reopenedRaw).not.toContain('@@TOLARIA_MATH')
})