diff --git a/.gitignore b/.gitignore index 1a413c509..056f5e00b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ apm_modules/ /.worktrees /.do-results.json +/.claude/scheduled_tasks.lock diff --git a/docs/guide/html-template/external-links.md b/docs/guide/html-template/external-links.md index 0db3c745d..64b855538 100644 --- a/docs/guide/html-template/external-links.md +++ b/docs/guide/html-template/external-links.md @@ -1,8 +1,5 @@ --- slug: external-links -page: - headHtml: | - --- # External links diff --git a/docs/guide/orgmode.org b/docs/guide/orgmode.org index e24e6cdf1..5bf395a5a 100644 --- a/docs/guide/orgmode.org +++ b/docs/guide/orgmode.org @@ -15,7 +15,7 @@ Here is a handpicked selection of syntatic features of Org Mode as particularly *** Code blocks -See [[file:../tips/js/syntax-highlighting.md][Syntax Highlighting]] for general information. +See [[file:../tips/syntax-highlighting.md][Syntax Highlighting]] for general information. #+NAME: factorial #+BEGIN_SRC haskell :results silent :exports code :var n=0 diff --git a/docs/guide/orgmode.yaml b/docs/guide/orgmode.yaml index c03ee751f..7128739f6 100644 --- a/docs/guide/orgmode.yaml +++ b/docs/guide/orgmode.yaml @@ -1,3 +1,2 @@ -page: - headHtml: | - +slug: orgmode + diff --git a/docs/tips/js/math.md b/docs/tips/js/math.md index a0ec091c6..577f42d74 100644 --- a/docs/tips/js/math.md +++ b/docs/tips/js/math.md @@ -1,39 +1,53 @@ --- slug: math -page: - headHtml: | - --- # Math -## MathJax +Emanote renders `$...$` and `$$...$$` to **MathML at build time** by default via [`texmath`](https://hackage.haskell.org/package/texmath). Modern browsers (Firefox, Safari, Chrome ≥109) render MathML natively, so the page ships no math JS bundle. + +## Demo -[MathJax](https://www.mathjax.org) can be used to render Math formulas. For example, $a^2 + b ^ 2 = c$. +When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are +$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}.$$ + +## Opting out -To enable it, add the following to `page.headHtml` of [[yaml-config|YAML configuration]] or Markdown frontmatter. +If you prefer KaTeX's typography or need to support a very old browser, disable static rendering in your site's `index.yaml`: ```yaml -page: - headHtml: | - +emanote: + staticMath: false ``` -### Demo +Then enable a client-side renderer per page (or globally via `page.headHtml` in your root `index.yaml`). -When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are -$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ +### MathJax +```yaml +page: + headHtml: | + +``` -## KaTeX +The `js.mathjax` snippet is shipped in the default config. -[KaTeX](https://katex.org/) can be used as an alternative to MathJax. Just like MathJax, it renders math specified between dollar signs. +### KaTeX -To enable it: +Paste the KaTeX loader directly into `page.headHtml` — Emanote's default config no longer defines a `js.katex` snippet: ```yaml page: headHtml: | - + + + ``` diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md index e7e2e2bea..b5ddf2e24 100644 --- a/emanote/CHANGELOG.md +++ b/emanote/CHANGELOG.md @@ -6,6 +6,7 @@ - **Tailwind v3 → v4 migration** with CSS-variable design tokens ([#633](https://github.com/srid/emanote/pull/633)) - Built-in static syntax highlighting using skylighting, replacing client-side JS highlighters ([#624](https://github.com/srid/emanote/pull/624)) +- Built-in static math rendering (LaTeX → MathML at build time via `texmath`) ([#639](https://github.com/srid/emanote/pull/639)) - UI revamp (#622, [#636](https://github.com/srid/emanote/pull/636)) - New self-hosted typography: Lora + Space Grotesk + Space Mono. - Manual dark/light theme toggle (#605, #617) with `localStorage` persistence. diff --git a/emanote/default/index.yaml b/emanote/default/index.yaml index 47dd24bcd..52eddf1b7 100644 --- a/emanote/default/index.yaml +++ b/emanote/default/index.yaml @@ -137,20 +137,6 @@ js: }; - katex: | - - - emanote: # Whether to automatically treat folder notes as a folgezettel parent of its contents @@ -160,3 +146,9 @@ emanote: # Enable server-side syntax highlighting using skylighting (default: true) # Set to false to use client-side highlighters like highlight.js instead syntaxHighlighting: true + + # Render `$...$` / `$$...$$` to MathML at build time via texmath (default: true). + # Modern browsers render MathML natively (Firefox, Safari, Chrome >=109), so + # no runtime JS is needed. Set to false to fall back to client-side JS + # (MathJax snippet, or a custom KaTeX snippet — see docs/tips/js/math). + staticMath: true diff --git a/emanote/src/Emanote/Pandoc/Renderer.hs b/emanote/src/Emanote/Pandoc/Renderer.hs index b17d623a1..6bd28fca3 100644 --- a/emanote/src/Emanote/Pandoc/Renderer.hs +++ b/emanote/src/Emanote/Pandoc/Renderer.hs @@ -50,8 +50,8 @@ mkRenderCtxWithPandocRenderers :: Map Text Text -> model -> route -> - -- | Enable syntax highlighting for code blocks - Bool -> + -- | Rendering feature selection (code highlighting, static math, …) + Splices.RenderFeatures -> HeistT Identity m Splices.RenderCtx mkRenderCtxWithPandocRenderers nr@PandocRenderers {..} classRules model x = Splices.mkRenderCtx diff --git a/emanote/src/Emanote/View/Common.hs b/emanote/src/Emanote/View/Common.hs index fae6e55b1..07af5c474 100644 --- a/emanote/src/Emanote/View/Common.hs +++ b/emanote/src/Emanote/View/Common.hs @@ -31,7 +31,7 @@ import Emanote.View.LiveServerFiles qualified as LiveServerFiles import Emanote.View.Tailwind (generatedCssFile, tailwindBrowserConfig, themeRemapStyle) import Heist qualified as H import Heist.Extra.Splices.List qualified as Splices -import Heist.Extra.Splices.Pandoc.Ctx (RenderCtx) +import Heist.Extra.Splices.Pandoc.Ctx (CodeBackend (..), MathBackend (..), RenderCtx, RenderFeatures (..)) import Heist.Extra.TemplateState qualified as Tmpl import Heist.Interpreted qualified as HI import Heist.Splices.Apply qualified as HA @@ -90,13 +90,20 @@ mkTemplateRenderCtx model r meta = classRules model r - enableSyntaxHighlighting + renderFeatures classRules :: Map Text Text classRules = SData.lookupAeson mempty ("pandoc" :| ["rewriteClass"]) meta - enableSyntaxHighlighting :: Bool - enableSyntaxHighlighting = - SData.lookupAeson True ("emanote" :| ["syntaxHighlighting"]) meta + renderFeatures :: RenderFeatures + renderFeatures = + RenderFeatures + { codeHighlighting = + bool NoHighlighting Skylighting + $ SData.lookupAeson True ("emanote" :| ["syntaxHighlighting"]) meta + , mathRendering = + bool NoStaticMath StaticMathML + $ SData.lookupAeson True ("emanote" :| ["staticMath"]) meta + } defaultRouteMeta :: Model -> (LMLRoute, Aeson.Value) defaultRouteMeta model = diff --git a/flake.lock b/flake.lock index 1295aef92..92da794d2 100644 --- a/flake.lock +++ b/flake.lock @@ -136,11 +136,11 @@ "heist-extra": { "flake": false, "locked": { - "lastModified": 1766109391, - "narHash": "sha256-ytHgIoRlkI5K0SDq33znlY0wjlqcwoQCe1z9JfHT/Fw=", + "lastModified": 1776811520, + "narHash": "sha256-FCWJVrXjIu/du69h/rfkqXYcjA2+1CpJcxNVyDR6eyA=", "owner": "srid", "repo": "heist-extra", - "rev": "81f1ea0cf1226215430171dbe613a2988c6cc46a", + "rev": "13c70e98621740fda1d93f4a5bd766b357535377", "type": "github" }, "original": { diff --git a/tests/features/smoke.feature b/tests/features/smoke.feature index 12bc47731..75d405db2 100644 --- a/tests/features/smoke.feature +++ b/tests/features/smoke.feature @@ -11,3 +11,15 @@ Feature: Smoke Given I note the resolved primary palette at "/" When I open "/themed.html" Then the resolved primary palette differs from the noted value + + Scenario: Inline math renders to MathML at build time + When I open "/math.html" + Then the page contains an inline element in the MathML namespace + + Scenario: Display math renders to block MathML at build time + When I open "/math.html" + Then the page contains a block element in the MathML namespace + + Scenario: KaTeX is not loaded by default + When I open "/math.html" + Then no KaTeX stylesheet is referenced diff --git a/tests/fixtures/notebook/math.md b/tests/fixtures/notebook/math.md new file mode 100644 index 000000000..a4c65bea7 --- /dev/null +++ b/tests/fixtures/notebook/math.md @@ -0,0 +1,14 @@ +--- +page: + siteTitle: Math Fixture +--- + +# Math + +Inline math probe: $x^2 + y^2 = z^2$ should become a `` element +with `display="inline"` (texmath's default for InlineMath). + +Display math probe: the quadratic formula below should become a `` +element with `display="block"`. + +$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$ diff --git a/tests/step_definitions/smoke_steps.ts b/tests/step_definitions/smoke_steps.ts index 46ca514be..8cd5ef5fa 100644 --- a/tests/step_definitions/smoke_steps.ts +++ b/tests/step_definitions/smoke_steps.ts @@ -27,6 +27,70 @@ Then( }, ); +// texmath emits `` for every expression. The HTML +// parser places `` in the MathML namespace automatically, so +// `element.namespaceURI` is the reliable check — the `xmlns=` attribute is a +// namespace declaration that CSS `[xmlns=…]` selectors don't see. We assert +// the `display` attribute via a CSS selector and the namespace via JS. +const MATHML_NS = "http://www.w3.org/1998/Math/MathML"; + +async function assertMathMLCount( + page: EmanoteWorld["page"], + display: "inline" | "block", + msgSuffix: string, +) { + const count = await page.evaluate( + ({ display, ns }) => + [...document.querySelectorAll(`math[display="${display}"]`)].filter( + (el) => el.namespaceURI === ns, + ).length, + { display, ns: MATHML_NS }, + ); + assert.ok( + count > 0, + `Expected at least one ${display} MathML element; found ${count}. ${msgSuffix}`, + ); +} + +Then( + "the page contains an inline element in the MathML namespace", + async function (this: EmanoteWorld) { + await assertMathMLCount( + this.page, + "inline", + "emanote.staticMath default may have regressed, or texmath's InlineMath → display=\"inline\" mapping changed.", + ); + }, +); + +Then( + "the page contains a block element in the MathML namespace", + async function (this: EmanoteWorld) { + await assertMathMLCount( + this.page, + "block", + "The $$…$$ display-math path is broken.", + ); + }, +); + +Then( + "no KaTeX stylesheet is referenced", + async function (this: EmanoteWorld) { + // The js.katex snippet was removed from the default config in the same PR + // that flipped staticMath on. Default-config sites should therefore never + // emit a katex stylesheet link. + const count = await this.page + .locator('link[href*="katex"], script[src*="katex"]') + .count(); + assert.strictEqual( + count, + 0, + `Default config should not pull in KaTeX; found ${count} katex asset reference(s). Did the js.katex snippet or a default \`page.headHtml\` leak back in?`, + ); + }, +); + Then( "the resolved primary palette differs from the noted value", async function (this: EmanoteWorld) {