From ef55891cc27b4ebd2caace0d7646ee9a6ac88da1 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 29 May 2026 11:50:09 +0200 Subject: [PATCH] fix: preserve katex square root radicals --- src/components/SafeMarkup.test.tsx | 22 ++++++++++++++++++++++ src/components/SafeMarkup.tsx | 9 ++++++--- tests/smoke/latex-math-notes.spec.ts | 18 +++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/components/SafeMarkup.test.tsx diff --git a/src/components/SafeMarkup.test.tsx b/src/components/SafeMarkup.test.tsx new file mode 100644 index 00000000..8d2ca488 --- /dev/null +++ b/src/components/SafeMarkup.test.tsx @@ -0,0 +1,22 @@ +import { render, waitFor } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { renderMathToHtml } from '../utils/mathMarkdown' +import { SafeHtmlSpan } from './SafeMarkup' + +describe('SafeHtmlSpan', () => { + it('preserves KaTeX SVG radicals used by square root notation', async () => { + const markup = renderMathToHtml({ latex: '\\sqrt{x}', displayMode: false }) + const { container } = render() + + await waitFor(() => expect(container.querySelector('.katex svg path')).toBeInTheDocument()) + expect(container.querySelector('.katex svg')).toHaveAttribute('viewBox') + expect(container.querySelector('.katex svg')).toHaveAttribute('preserveAspectRatio') + }) + + it('continues to remove script elements from sanitized markup', async () => { + const { container } = render() + + await waitFor(() => expect(container.querySelector('span')).toHaveTextContent('safe')) + expect(container.querySelector('script')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/SafeMarkup.tsx b/src/components/SafeMarkup.tsx index 27fcc587..2c554f44 100644 --- a/src/components/SafeMarkup.tsx +++ b/src/components/SafeMarkup.tsx @@ -17,10 +17,13 @@ const MERMAID_SVG_SANITIZE_CONFIG = { HTML_INTEGRATION_POINTS: { foreignobject: true }, } +const KATEX_MARKUP_SANITIZE_CONFIG = { + // KaTeX uses inline SVG paths for stretched radicals such as \sqrt{x}. + USE_PROFILES: { html: true, mathMl: true, svg: true }, +} + function importSanitizedMarkupNodes(markup: string): Node[] { - const sanitized = DOMPurify.sanitize(markup, { - USE_PROFILES: { html: true, mathMl: true }, - }) + const sanitized = DOMPurify.sanitize(markup, KATEX_MARKUP_SANITIZE_CONFIG) const parsed = new DOMParser().parseFromString(sanitized, 'text/html') return Array.from(parsed.body.childNodes, (node) => document.importNode(node, true)) } diff --git a/tests/smoke/latex-math-notes.spec.ts b/tests/smoke/latex-math-notes.spec.ts index 5a721f57..69955097 100644 --- a/tests/smoke/latex-math-notes.spec.ts +++ b/tests/smoke/latex-math-notes.spec.ts @@ -7,6 +7,7 @@ import { executeCommand, openCommandPalette } from './helpers' let tempVaultDir: string const INLINE_LATEX = 'E=mc^2' +const SQRT_LATEX = '\\sqrt{x}' const DISPLAY_LATEX = '\\int_0^1 x^2 \\, dx = \\frac{1}{3}' const MALFORMED_LATEX = '\\frac{' const TABLE_INLINE_LATEX = '\\frac{a}{b}+c' @@ -92,6 +93,14 @@ async function expectMathNode(page: Page, selector: string, latex: string): Prom ).toBe(true) } +async function expectMathSvgPath(page: Page, latex: string): Promise { + await expect.poll(async () => + page.locator('.math').evaluateAll((nodes, expectedLatex) => + nodes.some((node) => node.getAttribute('data-latex') === expectedLatex && Boolean(node.querySelector('svg path'))), + latex), + ).toBe(true) +} + function readNoteBFile(): string { return fs.readFileSync(path.join(tempVaultDir, 'note', 'note-b.md'), 'utf8') } @@ -105,6 +114,8 @@ test('LaTeX math source round-trips through rich mode, save, and reopen @smoke', Inline math $${INLINE_LATEX}$ stays in prose. +Square root $${SQRT_LATEX}$ should draw a stretched radical. + $$ ${DISPLAY_LATEX} $$ @@ -120,20 +131,24 @@ Malformed math $${MALFORMED_LATEX}$ stays visible. await setRawEditorContent(page, nextContent) await expect.poll(readNoteBFile).toContain(`$${INLINE_LATEX}$`) + await expect.poll(readNoteBFile).toContain(`$${SQRT_LATEX}$`) await expect.poll(readNoteBFile).toContain(`$$\n${DISPLAY_LATEX}\n$$`) await toggleRawMode(page, '.bn-editor') await expectMathNode(page, '.math--inline', INLINE_LATEX) + await expectMathNode(page, '.math--inline', SQRT_LATEX) + await expectMathSvgPath(page, SQRT_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('table')).toHaveCount(1) - await expect(page.locator('.math .katex, .math .katex-error')).toHaveCount(4) + await expect(page.locator('.math .katex, .math .katex-error')).toHaveCount(5) await toggleRawMode(page, '.cm-content') const rawAfterRichMode = await getRawEditorContent(page) expect(rawAfterRichMode).toContain(`$${INLINE_LATEX}$`) + expect(rawAfterRichMode).toContain(`$${SQRT_LATEX}$`) expect(rawAfterRichMode).toContain(`$$\n${DISPLAY_LATEX}\n$$`) expect(rawAfterRichMode).toContain(`$${MALFORMED_LATEX}$`) expect(rawAfterRichMode).toContain(`$${TABLE_INLINE_LATEX}$`) @@ -147,6 +162,7 @@ Malformed math $${MALFORMED_LATEX}$ stays visible. const reopenedRaw = await getRawEditorContent(page) expect(reopenedRaw).toContain(`$${INLINE_LATEX}$`) + expect(reopenedRaw).toContain(`$${SQRT_LATEX}$`) expect(reopenedRaw).toContain(`$$\n${DISPLAY_LATEX}\n$$`) expect(reopenedRaw).toContain(`$${MALFORMED_LATEX}$`) expect(reopenedRaw).toContain(`$${TABLE_INLINE_LATEX}$`)