From d16bdecab6149b6fa0af054d87e89bf70f00e822 Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Mon, 27 Jul 2026 18:02:54 +0200 Subject: [PATCH] fix(toc): include raw KaTeX from headings in toc The KaTeX renderer usually renders the raw KaTeX into MathML. This can not be properly displayed inside the toc and further makes problems when trying to render a fallback, because then parts could become duplicated. This commit fixes this by stripping MathML code completely for the title processing, leaving the raw KaTeX code intact. The only downside of this approach is that the surrounding '$' get lost, but this seems still better than nothing or duplicated content. Signed-off-by: Erik Michelson --- .../extract-first-heading.spec.ts | 11 +++++++ .../title-extraction/extract-first-heading.ts | 2 ++ markdown-it-plugins/src/toc/index.test.ts | 31 +++++++++++++++++++ markdown-it-plugins/src/toc/toc-options.ts | 2 +- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/commons/src/title-extraction/extract-first-heading.spec.ts b/commons/src/title-extraction/extract-first-heading.spec.ts index 74348221f..438f04316 100644 --- a/commons/src/title-extraction/extract-first-heading.spec.ts +++ b/commons/src/title-extraction/extract-first-heading.spec.ts @@ -46,6 +46,17 @@ describe('extract first heading', () => { expect(extractFirstHeading(document)).toBe('Image Alt') }) + it('ignores MathML duplicated by KaTeX', () => { + const headline = new Element(`h${headlineIndex}`, {}, [ + new Element('span', { class: 'katex-mathml' }, [ + new Element('math', {}, [new Element('mi', {}, [new Text('alpha')])]), + ]), + new Element('span', { class: 'katex-html', 'aria-hidden': 'true' }, [new Text('alpha')]), + ]) + const document = new Document([headline]) + expect(extractFirstHeading(document)).toBe('alpha') + }) + it('extracts only the first found headline', () => { const headline1 = new Element(`h${headlineIndex}`, {}, [new Text(`headline${headlineIndex}`)]) const headline2 = new Element(`h${headlineIndex}`, {}, [new Text('headline1')]) diff --git a/commons/src/title-extraction/extract-first-heading.ts b/commons/src/title-extraction/extract-first-heading.ts index ddd77a1f5..e5a99d101 100644 --- a/commons/src/title-extraction/extract-first-heading.ts +++ b/commons/src/title-extraction/extract-first-heading.ts @@ -43,6 +43,8 @@ function extractInnerTextFromTag(node: Element): string { return '' } else if (node.name === 'img') { return findAttribute(node, 'alt')?.value ?? '' + } else if (node.name === 'math') { + return '' } else { return node.children.reduce((state, child) => { return state + extractInnerTextFromNode(child) diff --git a/markdown-it-plugins/src/toc/index.test.ts b/markdown-it-plugins/src/toc/index.test.ts index 5a2df7df1..b68cd6383 100644 --- a/markdown-it-plugins/src/toc/index.test.ts +++ b/markdown-it-plugins/src/toc/index.test.ts @@ -183,4 +183,35 @@ describe('toc', () => { `), ).toMatchSnapshot() }) + + it('includes inline math in heading names', () => { + const callback = jest.fn() + const markdownIt = new MarkdownIt().use(toc, { callback }) + markdownIt.inline.ruler.before('text', 'inlineMath', (state, silent) => { + if (state.src.slice(state.pos, state.pos + 8) !== '$\\alpha$') { + return false + } + + if (!silent) { + const token = state.push('inline_math', '', 0) + token.content = '\\alpha' + } + state.pos += 8 + return true + }) + + markdownIt.render('# $\\alpha$-foo') + + expect(callback).toHaveBeenCalledWith({ + children: [ + { + children: [], + level: 1, + name: '\\alpha-foo', + }, + ], + level: 0, + name: '', + }) + }) }) diff --git a/markdown-it-plugins/src/toc/toc-options.ts b/markdown-it-plugins/src/toc/toc-options.ts index 5564f4921..817e16908 100644 --- a/markdown-it-plugins/src/toc/toc-options.ts +++ b/markdown-it-plugins/src/toc/toc-options.ts @@ -37,6 +37,6 @@ export const defaultOptions: TocOptions = { linkClass: '', level: 1, listType: 'ol', - allowedTokenTypes: ['text', 'code_inline'], + allowedTokenTypes: ['text', 'code_inline', 'math', 'inline_math', 'display_math'], slugify: defaultSlugify, }