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,
+ );
+ }
+}