Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ apm_modules/

/.worktrees
/.do-results.json
/.claude/scheduled_tasks.lock
3 changes: 0 additions & 3 deletions docs/guide/html-template/external-links.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
---
slug: external-links
page:
headHtml: |
<snippet var="js.mathjax" />
---

# External links
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/orgmode.org
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions docs/guide/orgmode.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
page:
headHtml: |
<snippet var="js.mathjax" />
slug: orgmode

46 changes: 30 additions & 16 deletions docs/tips/js/math.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,53 @@
---
slug: math
page:
headHtml: |
<snippet var="js.mathjax" />
---

# 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: |
<snippet var="js.mathjax" />
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: |
<snippet var="js.mathjax" />
```

## 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: |
<snippet var="js.katex" />
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
crossorigin="anonymous">
<script defer
src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"
crossorigin="anonymous"></script>
<script defer
src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
```

1 change: 1 addition & 0 deletions emanote/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 6 additions & 14 deletions emanote/default/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,6 @@ js:
};
</script>
<script async="" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
katex: |
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV"
crossorigin="anonymous">
<script defer
src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"
integrity="sha384-XjKyOOlGwcjNTAIQHIpgOno0Hl1YQqzUOEleOLALmuqehneUG+vnGctmUb0ZY0l8"
crossorigin="anonymous"></script>
<script defer
src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"
crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>

emanote:
# Whether to automatically treat folder notes as a folgezettel parent of its contents
Expand All @@ -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
4 changes: 2 additions & 2 deletions emanote/src/Emanote/Pandoc/Renderer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 12 additions & 5 deletions emanote/src/Emanote/View/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions tests/features/smoke.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 <math> 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 <math> element in the MathML namespace

Scenario: KaTeX is not loaded by default
When I open "/math.html"
Then no KaTeX stylesheet is referenced
14 changes: 14 additions & 0 deletions tests/fixtures/notebook/math.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
page:
siteTitle: Math Fixture
---

# Math

Inline math probe: $x^2 + y^2 = z^2$ should become a `<math>` element
with `display="inline"` (texmath's default for InlineMath).

Display math probe: the quadratic formula below should become a `<math>`
element with `display="block"`.

$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$
64 changes: 64 additions & 0 deletions tests/step_definitions/smoke_steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,70 @@ Then(
},
);

// texmath emits `<math xmlns="…" display="…">` for every expression. The HTML
// parser places `<math>` 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 <math> 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 <math> 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) {
Expand Down
Loading