From a28631517c073da182f7bced64e4631d73430d94 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 13 Jun 2026 13:05:50 +0300 Subject: [PATCH] Add KaTeX math rendering assets --- benchmarks/LatexMathProcessorBench.php | 30 +++++++ config/common/di/content-pipeline.php | 2 + docs/configuration.md | 2 +- docs/plugins.md | 16 ++++ roadmap.md | 1 + .../LatexMath/LatexMathProcessor.php | 39 +++++++++ src/Processor/LatexMath/assets/latex-math.js | 30 +++++++ .../Unit/Processor/LatexMathProcessorTest.php | 84 +++++++++++++++++++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 benchmarks/LatexMathProcessorBench.php create mode 100644 src/Processor/LatexMath/LatexMathProcessor.php create mode 100644 src/Processor/LatexMath/assets/latex-math.js create mode 100644 tests/Unit/Processor/LatexMathProcessorTest.php diff --git a/benchmarks/LatexMathProcessorBench.php b/benchmarks/LatexMathProcessorBench.php new file mode 100644 index 0000000..5b43ae9 --- /dev/null +++ b/benchmarks/LatexMathProcessorBench.php @@ -0,0 +1,30 @@ +processor = new LatexMathProcessor(); + $this->content = str_repeat('

Inline x + y.

', 100); + } + + #[Revs(1_000)] + #[Iterations(3)] + #[Warmup(1)] + public function benchHeadAssetDetection(): void + { + $this->processor->headAssets($this->content); + } +} diff --git a/config/common/di/content-pipeline.php b/config/common/di/content-pipeline.php index 0a1ca6d..0adbf91 100644 --- a/config/common/di/content-pipeline.php +++ b/config/common/di/content-pipeline.php @@ -9,6 +9,7 @@ use YiiPress\Console\InitCommand; use YiiPress\Console\NewCommand; use YiiPress\Processor\ContentProcessorPipeline; +use YiiPress\Processor\LatexMath\LatexMathProcessor; use YiiPress\Processor\Mermaid\MermaidProcessor; use YiiPress\Processor\OEmbed\OEmbedProcessor; use YiiPress\Processor\Shortcode\TweetProcessor; @@ -40,6 +41,7 @@ Reference::to(TweetProcessor::class), Reference::to(OEmbedProcessor::class), Reference::to(MarkdownProcessor::class), + Reference::to(LatexMathProcessor::class), Reference::to(TagLinkProcessor::class), Reference::to(MermaidProcessor::class), Reference::to(SyntaxHighlightProcessor::class), diff --git a/docs/configuration.md b/docs/configuration.md index bdce7f4..285db2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -309,7 +309,7 @@ markdown: - **email_autolinks** — recognize e-mails as auto-links even without `<>` and `mailto:` (default: `true`) - **www_autolinks** — enable WWW auto-links (even without any scheme prefix, if they begin with 'www.') (default: `true`) - **collapse_whitespace** — collapse non-trivial whitespace into single space (default: `true`) -- **latex_math** — enable LaTeX math spans `$...$` and `$$...$$` (default: `false`) +- **latex_math** — enable LaTeX math spans `$...$` and `$$...$$` (default: `false`). Pages that contain math automatically include KaTeX CSS/JS and the shipped YiiPress math enhancer script. - **wikilinks** — enable wiki-style links `[[link]]` (default: `false`) - **underline** — underscore `_` denotes underline instead of emphasis (default: `false`) - **no_html_blocks** — disable raw HTML blocks (default: `true`) diff --git a/docs/plugins.md b/docs/plugins.md index 63b7dbc..b22f89d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -64,6 +64,22 @@ inline-styled highlighted output. The bundled `minimal` theme adds a client-side **Copy** button to rendered code blocks. +## LaTeX math + +When `markdown.latex_math` is enabled, md4c emits math spans as `` elements. YiiPress detects those elements and injects KaTeX rendering assets only on pages that contain math. + +Inline math uses `$...$`, and display math uses `$$...$$`: + +```markdown +Euler: $e^{i\pi} + 1 = 0$ + +$$ +\int_0^1 x^2\,dx +$$ +``` + +YiiPress ships a small browser enhancer at `assets/plugins/latex-math.js` and loads fixed-version KaTeX CSS/JS from jsDelivr for the renderer itself. + The native extension passes explicit input and output lengths so repeated highlighting calls avoid extra C-string scans at the PHP/Rust boundary. diff --git a/roadmap.md b/roadmap.md index f36c641..3130e27 100644 --- a/roadmap.md +++ b/roadmap.md @@ -31,6 +31,7 @@ - [x] Date-based archive pages (yearly, monthly) - [x] Cross-references / internal linking helpers (shorthand syntax to link between entries, prevents broken links on permalink changes) - [x] Markdown extensions configuration (enable/disable footnotes, definition lists, strikethrough, tables, etc.) +- [x] KaTeX rendering assets for LaTeX math output ## Priority 2: Documentation diff --git a/src/Processor/LatexMath/LatexMathProcessor.php b/src/Processor/LatexMath/LatexMathProcessor.php new file mode 100644 index 0000000..3456710 --- /dev/null +++ b/src/Processor/LatexMath/LatexMathProcessor.php @@ -0,0 +1,39 @@ + + + +HTML; + } + + public function assetFiles(): array + { + return [ + __DIR__ . '/assets/latex-math.js' => 'assets/plugins/latex-math.js', + ]; + } +} diff --git a/src/Processor/LatexMath/assets/latex-math.js b/src/Processor/LatexMath/assets/latex-math.js new file mode 100644 index 0000000..4400cfd --- /dev/null +++ b/src/Processor/LatexMath/assets/latex-math.js @@ -0,0 +1,30 @@ +(function () { + function renderEquation(element) { + var displayMode = element.getAttribute('type') === 'display'; + var replacement = document.createElement(displayMode ? 'div' : 'span'); + var source = element.textContent || ''; + + replacement.className = displayMode ? 'math math-display' : 'math math-inline'; + + if (!window.katex || typeof window.katex.render !== 'function') { + replacement.textContent = source; + element.replaceWith(replacement); + return; + } + + try { + window.katex.render(source, replacement, { + displayMode: displayMode, + throwOnError: false + }); + } catch (e) { + replacement.textContent = source; + } + + element.replaceWith(replacement); + } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('x-equation').forEach(renderEquation); + }); +})(); diff --git a/tests/Unit/Processor/LatexMathProcessorTest.php b/tests/Unit/Processor/LatexMathProcessorTest.php new file mode 100644 index 0000000..5104c15 --- /dev/null +++ b/tests/Unit/Processor/LatexMathProcessorTest.php @@ -0,0 +1,84 @@ +processor = new LatexMathProcessor(); + } + + public function testProcessLeavesRenderedContentUnchanged(): void + { + $content = '

Inline x.

'; + + assertSame($content, $this->processor->process($content, $this->createEntry())); + } + + public function testHeadAssetsReturnsKaTexAssetsWhenMathIsPresent(): void + { + $assets = $this->processor->headAssets('

x

'); + + $this->assertStringContainsString('katex.min.css', $assets); + $this->assertStringContainsString('katex.min.js', $assets); + $this->assertStringContainsString('assets/plugins/latex-math.js', $assets); + } + + public function testHeadAssetsReturnsEmptyStringWhenMathIsAbsent(): void + { + assertSame('', $this->processor->headAssets('

No math.

')); + } + + public function testAssetFilesReturnsBrowserEnhancer(): void + { + $files = $this->processor->assetFiles(); + + $this->assertCount(1, $files); + $sourceFile = array_key_first($files); + $this->assertStringEndsWith('/Processor/LatexMath/assets/latex-math.js', $sourceFile); + assertSame('assets/plugins/latex-math.js', $files[$sourceFile]); + + $script = file_get_contents($sourceFile); + $this->assertNotFalse($script); + $this->assertStringContainsString("document.querySelectorAll('x-equation')", $script); + $this->assertStringContainsString('window.katex.render', $script); + $this->assertStringContainsString('throwOnError: false', $script); + } + + private function createEntry(): Entry + { + return new Entry( + filePath: __FILE__, + collection: 'blog', + slug: 'test', + title: 'Test', + date: new DateTimeImmutable('2024-01-01'), + draft: false, + tags: [], + categories: [], + authors: [], + summary: '', + permalink: '', + layout: '', + theme: '', + weight: 0, + language: '', + redirectTo: '', + extra: [], + bodyOffset: 0, + bodyLength: 0, + ); + } +}