@fnix/lexxy-mathjax 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,15 @@ Equations are stored in the document (and in what ActionText persists) as:
16
16
  <div class="lexxy-math" data-latex="\int_0^1 x\,dx">...</div> <!-- display/block -->
17
17
  ```
18
18
 
19
- Inside the editor the extension typesets these with the page's MathJax. **MathJax is host-provided, not bundled**: load MathJax v3 (`tex-chtml`) on any page that uses the editor or displays saved content.
19
+ Inside the editor the extension typesets these with the page's MathJax. On a display page, `typesetMath` (below) does the same thing without an editor. **MathJax is host-provided, not bundled**: load MathJax v4 (`tex-chtml` or `tex-svg`) on any page that uses the editor or displays saved content. The extension also works against MathJax v3, so hosts already loading v3 can upgrade on their own schedule.
20
+
21
+ ## The persisted format
22
+
23
+ This is the whole contract between the editor and anything else that needs to render or process saved equations — MathJax, but also any other script or server-side tool:
24
+
25
+ - `<span data-latex="...">` is an inline equation, `<div data-latex="...">` is display/block. Tag name is the only signal for inline vs. display; treat it as authoritative.
26
+ - Text content is the raw LaTeX, with no delimiters (`\(...\)`, `$$...$$`, `\[...\]`).
27
+ - `data-latex` is the one attribute this package relies on surviving sanitization (see the ActionText step below). `class="lexxy-math"` / `lexxy-math--display` are presentation only, applied defensively by `typesetMath`, and are not guaranteed to be present or to survive a sanitizer.
20
28
 
21
29
  ## Installation
22
30
 
@@ -55,13 +63,22 @@ Include the stylesheet (`@fnix/lexxy-mathjax/styles`, or copy `styles/lexxy-math
55
63
 
56
64
  ```html
57
65
  <script>
58
- window.MathJax = { chtml: { displayAlign: "center" } }
66
+ window.MathJax = { output: { displayAlign: "center" } }
59
67
  </script>
60
- <script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
68
+ <script defer src="https://cdn.jsdelivr.net/npm/mathjax@4/tex-chtml.js"></script>
61
69
  ```
62
70
 
63
71
  If MathJax is missing, the editor still works and equations show their raw LaTeX.
64
72
 
73
+ #### MathJax v4 notes
74
+
75
+ This package persists raw LaTeX (`data-latex`) and re-typesets it on every load, so a few v4 changes are worth knowing about before upgrading a host that already has saved content:
76
+
77
+ - **Different default font.** v4 defaults to `mathjax-newcm` (New Computer Modern), noticeably lighter than v3's TeX font. Set `output: { font: "mathjax-tex" }` to keep the old look.
78
+ - **`\text{...}` is now macro-parsed.** The `textmacros` extension ships in all v4 combined components, so a backslash or brace inside `\text{}` that was inert in v3 can now raise "undefined control sequence" for existing saved equations.
79
+ - **Font-size macros changed in MathJax 4.1.2** (`\tiny`/`\Tiny` swapped, `\large`…`\Huge` shifted by one), so existing content can render at a different size. Opt out with the `fontsizev3` TeX package (`loader: { load: ["[tex]/fontsizev3"] }, tex: { packages: { "[+]": ["fontsizev3"] } }`) if you pin a floating `@4` range.
80
+ - **Accessibility moved, but only for math MathJax typesets itself.** v4 turns assistive MathML off by default and turns the expression explorer (speech/braille) on instead. MathJax's own docs warn that math inserted via a direct conversion call — `tex2chtmlPromise`/`tex2svgPromise`, which is what this package uses, both in the editor and on display pages — "will not become part of the list of math expressions that MathJax knows about in the page", so the explorer never attaches to it. Both `createDOM` (editor) and `typesetMath` (display pages) compensate by setting `role="math"` / `aria-label="Equation: ..."` on every equation directly, rather than relying on the explorer.
81
+
65
82
  ## Rails / ActionText integration
66
83
 
67
84
  **1. Allow `data-latex` through the server-side sanitizer** (ActionText strips unknown attributes when rendering):
@@ -73,22 +90,97 @@ ActiveSupport.on_load(:action_text_content) do
73
90
  end
74
91
  ```
75
92
 
76
- **2. Typeset saved content on display pages.** Load MathJax (as above) plus a small script that renders `[data-latex]` elements, Turbo-compatible:
93
+ **2. Typeset saved content on display pages.** Load MathJax (as above), include the stylesheet, and start the auto-typesetter:
94
+
95
+ ```js
96
+ // app/javascript/application.js
97
+ import "@fnix/lexxy-mathjax/styles"
98
+ import { startMathjaxAutoTypeset } from "@fnix/lexxy-mathjax/typeset"
99
+
100
+ startMathjaxAutoTypeset()
101
+ ```
102
+
103
+ `@fnix/lexxy-mathjax/typeset` is a separate entry point from the package's default export: it pulls in neither lexxy nor Lexical, so a display-only page doesn't load the editor. It re-typesets on `DOMContentLoaded`, `turbo:load`, `turbo:frame-load` and `turbo:render`, skips equations it has already rendered (so repeated Turbo navigations are cheap), and leaves any equation inside a live `<lexxy-editor>` to the extension.
104
+
105
+ For manual control — a specific container, a one-off re-render, or Stimulus — call `typesetMath` directly. It never throws and resolves with a summary:
106
+
107
+ ```js
108
+ import { typesetMath } from "@fnix/lexxy-mathjax/typeset"
109
+
110
+ const { total, typeset, failed, skipped, mathjax } = await typesetMath(document.querySelector(".post-body"))
111
+ ```
112
+
113
+ Options: `output` (`"auto" | "chtml" | "svg"`, default `"auto"` — detects whether the host loaded `tex-chtml` or `tex-svg`), `selector` (default `"[data-latex]"`), `force` (re-typeset elements already rendered), `accessible` (default `true`, adds `role="math"` / `aria-label`).
114
+
115
+ With Stimulus, for per-element control instead of a page-wide listener:
116
+
117
+ ```js
118
+ // app/javascript/controllers/math_controller.js
119
+ import { Controller } from "@hotwired/stimulus"
120
+ import { typesetMath } from "@fnix/lexxy-mathjax/typeset"
121
+
122
+ export default class extends Controller {
123
+ connect() {
124
+ typesetMath(this.element)
125
+ }
126
+ }
127
+ ```
128
+ ```erb
129
+ <div data-controller="math"><%= @post.body %></div>
130
+ ```
131
+
132
+ **Without importing this package's JS at all**, wrap the stored LaTeX in MathJax's own delimiters and let `typesetPromise` scan for it normally:
77
133
 
78
134
  ```js
79
- function typesetEquations() {
80
- const elements = document.querySelectorAll(".trix-content [data-latex], .lexxy-content [data-latex]")
81
- if (elements.length === 0 || !window.MathJax?.typesetPromise) return
135
+ document.querySelectorAll("[data-latex]").forEach((el) => {
136
+ const latex = el.getAttribute("data-latex")
137
+ el.textContent = el.tagName === "DIV" ? `\\[${latex}\\]` : `\\(${latex}\\)`
138
+ })
139
+ window.MathJax.typesetPromise()
140
+ ```
141
+
142
+ This is the only approach that keeps MathJax's contextual menu and the v4 expression explorer, because the math becomes part of MathJax's own document list (see the accessibility note above). The trade-offs: it depends on the host's `tex.inlineMath` / `tex.displayMath` configuration matching those delimiters, and invalid LaTeX renders as MathJax's own `merror` markup rather than this package's `.lexxy-math--error` styling.
143
+
144
+ > An earlier version of this README suggested setting `el.textContent` to the bare, undelimited `data-latex` value and then calling `MathJax.typesetPromise([...elements])`. That never worked: `typesetPromise` scans for delimiters, and bare LaTeX has none — passing an element list narrows *where* it scans, not *whether* it requires delimiters. If you copied that snippet, switch to one of the two approaches above.
145
+
146
+ ## Rendering outside a browser (PDF export)
147
+
148
+ Both approaches above need an actual MathJax running in a DOM — the editor's, or a display page's. Some PDF pipelines have neither: [sghtmltopdf](https://github.com/waka/sghtmltopdf), for one, runs no JavaScript at all and does not render inline `<svg>`, so MathJax can never execute there, and CHTML output needs WOFF/WOFF2 web fonts, which such engines typically don't support either. The persisted format above is designed for exactly this case: `data-latex` carries everything a separate rendering step needs, without this package's help.
82
149
 
83
- elements.forEach((el) => { el.textContent = el.getAttribute("data-latex") })
84
- window.MathJax.typesetPromise([ ...elements ])
150
+ This isn't something this package ships — it's server-side, has no browser to run in, and every detail (page size, body font, cache store, job queue, PDF engine) belongs to the app, not the editor extension. The recipe:
151
+
152
+ **1. Render LaTeX to SVG in Node, using MathJax's server-side v4 package**, `@mathjax/src@4` (`mathjax-full` is the v3 name, `mathjax-node` is v2-era and abandoned — most recipes found online are stale):
153
+
154
+ ```js
155
+ global.MathJax = {
156
+ loader: { paths: { mathjax: "@mathjax/src/bundle" }, load: [ "adaptors/liteDOM" ], require: (f) => import(f) },
157
+ svg: { fontCache: "none" }, // no shared <use> references — safer for a standalone image
158
+ options: { enableSpeech: false, enableBraille: false, enableEnrichment: false } // skip the speech-rule-engine; it's the dominant startup cost
85
159
  }
160
+ await import("@mathjax/src/bundle/tex-svg.js")
161
+ await MathJax.startup.promise
162
+
163
+ const svg = await MathJax.tex2svgPromise(latex, { display, em, ex, containerWidth })
164
+ // pick em/containerWidth from the PDF's body font and page width — 16/780 above are editor-viewport defaults, not PDF ones
165
+
166
+ MathJax.done() // mandatory: shuts down the speech-rule-engine's worker threads, or the process never exits
167
+ ```
168
+
169
+ **2. Transfer the geometry, then embed as an image.** MathJax puts sizing on the `<svg>` element itself — `output/svg.ts`'s `createSVG` sets `width="Wex"`, `height="Hex"` and `style="vertical-align:-Dex"` — so wrapping it in an `<img>` (as most non-JS PDF engines require) discards all three unless you copy them onto the wrapper:
86
170
 
87
- document.addEventListener("turbo:load", typesetEquations)
88
- document.addEventListener("DOMContentLoaded", typesetEquations)
171
+ ```html
172
+ <img src="data:image/svg+xml;base64,…" alt="\frac{a}{b}"
173
+ style="width:2.62em;height:1.29em;vertical-align:-0.40em">
89
174
  ```
90
175
 
91
- Note: `typesetPromise` scans for `\(...\)` delimiters by default; the snippet above instead sets each element's content from `data-latex` and typesets just those elements. If you prefer delimiter scanning, configure `tex.inlineMath` accordingly.
176
+ **3. Run this at export time, on a throwaway copy of the HTML not at save time.** Keep `data-latex` as the only thing that's ever persisted:
177
+ - stored content stays canonical and round-trips through the editor unchanged (an `<img>` in a saved record wouldn't be recognized by `importDOM`);
178
+ - the sanitizer never has to accept `data:` URIs on `img[src]` app-wide, which would be a real XSS surface (`data:image/svg+xml` can carry `<script>`);
179
+ - there's no staleness or backfill migration when MathJax's output changes — bump a version in your cache key and the next export re-renders.
180
+
181
+ If your PDF engine has no accessibility tagging (sghtmltopdf doesn't), `alt` never reaches the PDF's own text layer either — still worth setting for tools that read it another way, but don't rely on it for accessibility.
182
+
183
+ Since the transform depends on nothing this package controls, and the format contract is exactly the `data-latex` spec above, this is intentionally left as a recipe rather than shipped code.
92
184
 
93
185
  ## Demo
94
186
 
@@ -115,4 +207,7 @@ The code follows lexxy's own conventions: plain ES modules, vanilla JS, no build
115
207
  - `src/nodes/math_node.js` — `DecoratorNode` storing `{ latex, display }`; `createDOM` typesets with MathJax, `exportDOM`/`importDOM` handle the `data-latex` HTML format
116
208
  - `src/extensions/mathjax_extension.js` — the `Lexxy.Extension`: registers the node, the `insertMath` command, the toolbar button, and click-to-edit
117
209
  - `src/elements/math_editor_dialog.js` — `<lexxy-math-editor>` dialog with live preview
118
- - `src/helpers/mathjax_helper.js` — serialized MathJax typesetting with graceful degradation
210
+ - `src/helpers/mathjax_helper.js` — serialized MathJax typesetting with graceful degradation; shared by the editor and by `typeset_math.js`
211
+ - `src/typeset_math.js` — `typesetMath(root)`, the display-page renderer; batches conversions and refreshes MathJax's stylesheet once per call instead of once per equation
212
+ - `src/auto_typeset.js` — `startMathjaxAutoTypeset()`, wires `typesetMath` to `DOMContentLoaded`/Turbo events
213
+ - `src/typeset.js` — the `@fnix/lexxy-mathjax/typeset` entry point (re-exports the two above without pulling in lexxy/Lexical)
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@fnix/lexxy-mathjax",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MathJax LaTeX equation extension for Lexxy, the rich text editor for Rails.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js",
9
+ "./typeset": "./src/typeset.js",
9
10
  "./styles": "./styles/lexxy-mathjax.css"
10
11
  },
11
12
  "files": [
@@ -0,0 +1,21 @@
1
+ import { typesetMath } from "./typeset_math.js"
2
+
3
+ const DEFAULT_EVENTS = [ "DOMContentLoaded", "turbo:load", "turbo:frame-load", "turbo:render" ]
4
+
5
+ // Wires typesetMath up to a page's navigation lifecycle. Covers full-page
6
+ // loads, Turbo Drive visits, and Turbo Frames/Streams: on turbo:frame-load
7
+ // the event target is the frame itself, so only its own subtree is
8
+ // retypeset instead of walking the whole document again.
9
+ //
10
+ // Returns a teardown function that removes the listeners.
11
+ export function startMathjaxAutoTypeset({ events = DEFAULT_EVENTS, ...options } = {}) {
12
+ const handler = (event) => {
13
+ const root = event.target?.nodeType === 1 ? event.target : document
14
+ return typesetMath(root, options)
15
+ }
16
+
17
+ events.forEach((name) => document.addEventListener(name, handler))
18
+ if (document.readyState !== "loading") handler({})
19
+
20
+ return () => events.forEach((name) => document.removeEventListener(name, handler))
21
+ }
@@ -1,12 +1,32 @@
1
1
  const MATHJAX_WAIT_ATTEMPTS = 40
2
2
  const MATHJAX_WAIT_INTERVAL = 250
3
3
 
4
+ const CONVERTERS = {
5
+ chtml: "tex2chtmlPromise",
6
+ svg: "tex2svgPromise"
7
+ }
8
+
4
9
  let warnedAboutMissingMathJax = false
10
+ let warnedAboutMissingConverter = false
5
11
  let renderQueue = Promise.resolve()
12
+ let mathjaxPromise = null
6
13
 
7
14
  // Resolves with window.MathJax once its startup has finished, or null if
8
15
  // MathJax never shows up (the host page is responsible for loading it).
9
- export async function findMathJax() {
16
+ // The poll is memoized so concurrent callers share one wait instead of each
17
+ // burning the full ~10s timeout on its own.
18
+ export function findMathJax() {
19
+ if (!mathjaxPromise) {
20
+ mathjaxPromise = pollForMathJax().then((mathjax) => {
21
+ if (!mathjax) mathjaxPromise = null // let a later call retry if MathJax arrives late
22
+ return mathjax
23
+ })
24
+ }
25
+
26
+ return mathjaxPromise
27
+ }
28
+
29
+ async function pollForMathJax() {
10
30
  for (let attempt = 0; attempt < MATHJAX_WAIT_ATTEMPTS; attempt++) {
11
31
  if (window.MathJax?.startup?.promise) {
12
32
  await window.MathJax.startup.promise
@@ -18,43 +38,116 @@ export async function findMathJax() {
18
38
  if (!warnedAboutMissingMathJax) {
19
39
  warnedAboutMissingMathJax = true
20
40
  console.warn("@fnix/lexxy-mathjax: window.MathJax not found. Equations will show raw LaTeX. " +
21
- "Load MathJax v3 (tex-chtml) on pages that use the editor.")
41
+ "Load MathJax v4 (tex-chtml) on pages that use the editor.")
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ // Runs task once every previously enqueued task has settled, without ever
48
+ // leaving the shared queue itself rejected. Returns a promise for THIS task
49
+ // specifically (not the shared chain), so a caller can observe its own
50
+ // outcome instead of waiting on unrelated work queued after it.
51
+ export function enqueue(task) {
52
+ const result = renderQueue.then(task)
53
+ renderQueue = result.catch(() => {})
54
+ return result
55
+ }
56
+
57
+ // Picks which TeX conversion method to use. Consults the document's live
58
+ // output jax first (so a renderer switched via MathJax's own contextual
59
+ // menu is respected), then falls back to whichever of chtml/svg is loaded.
60
+ // MathJax only creates a tex2*Promise method for jax it was actually asked
61
+ // to load, so a host that loaded tex-svg.js has no tex2chtmlPromise at all.
62
+ export function resolveConverter(mathjax, output = "auto") {
63
+ const candidates = output === "auto"
64
+ ? [ mathjax.startup?.document?.outputJax?.name?.toLowerCase(), "chtml", "svg" ]
65
+ : [ output ]
66
+
67
+ for (const name of candidates) {
68
+ const method = CONVERTERS[name]
69
+ if (method && typeof mathjax[method] === "function") {
70
+ return { name, convert: (latex, options) => mathjax[method](latex, options) }
71
+ }
72
+ }
73
+
74
+ if (!warnedAboutMissingConverter) {
75
+ warnedAboutMissingConverter = true
76
+ console.warn(`@fnix/lexxy-mathjax: no TeX conversion method found on window.MathJax for output "${output}".`)
22
77
  }
23
78
 
24
79
  return null
25
80
  }
26
81
 
82
+ // clientWidth is 0 on a non-replaced inline element (e.g. a <span> before
83
+ // the package stylesheet has loaded), so fall back to the parent's width.
84
+ export function measureContainerWidth(element) {
85
+ return element.clientWidth || element.parentElement?.clientWidth || 780
86
+ }
87
+
88
+ // Converts latex into element using converter, replacing its content. Falls
89
+ // back to showing the raw LaTeX with an error class when conversion fails.
90
+ // Returns whether conversion succeeded, so callers can decide whether a
91
+ // stylesheet refresh is worth doing.
92
+ export async function convertInto(converter, element, latex, { display = false, containerWidth } = {}) {
93
+ try {
94
+ const rendered = await converter.convert(latex, {
95
+ display,
96
+ em: 16,
97
+ ex: 8,
98
+ containerWidth: containerWidth ?? measureContainerWidth(element)
99
+ })
100
+ element.replaceChildren(rendered)
101
+ element.classList.remove("lexxy-math--error")
102
+ return true
103
+ } catch (error) {
104
+ element.textContent = latex
105
+ element.classList.add("lexxy-math--error")
106
+ element.title = `${error}`
107
+ return false
108
+ }
109
+ }
110
+
111
+ // Refreshes MathJax's global stylesheet so glyphs used by newly-converted
112
+ // equations render. reset(), not clear(): clear() also wipes MathJax's
113
+ // record of every expression the host page itself has typeset, which isn't
114
+ // ours to clear. This matters for SVG output too, not just CHTML: with the
115
+ // default global font cache, updateDocument() is what inserts the shared
116
+ // glyph cache into the page, and skipping it can leave SVG <use> references
117
+ // pointing at nothing.
118
+ export function refreshDocumentStyles(mathjax) {
119
+ mathjax.startup.document.reset()
120
+ mathjax.startup.document.updateDocument()
121
+ }
122
+
27
123
  // Typesets LaTeX into the given element, replacing its content. Falls back to
28
124
  // showing the raw LaTeX when MathJax is unavailable or the LaTeX is invalid.
29
- // Calls are serialized because MathJax's conversion pipeline is not reentrant.
125
+ // Calls are serialized because MathJax's own promise chaining only covers
126
+ // conversion, not our DOM insertion and stylesheet refresh below.
30
127
  export function typesetInto(element, latex, { display = false } = {}) {
31
128
  element.textContent = latex
32
129
 
33
- renderQueue = renderQueue.then(async () => {
130
+ return enqueue(async () => {
34
131
  const mathjax = await findMathJax()
35
132
  if (!mathjax) return
36
133
 
37
- try {
38
- const rendered = await mathjax.tex2chtmlPromise(latex, {
39
- display,
40
- em: 16,
41
- ex: 8,
42
- containerWidth: element.clientWidth || 780
43
- })
44
- element.replaceChildren(rendered)
45
- element.classList.remove("lexxy-math--error")
46
-
47
- // Refresh MathJax's global stylesheet so glyphs used by this equation render.
48
- mathjax.startup.document.clear()
49
- mathjax.startup.document.updateDocument()
50
- } catch (error) {
51
- element.textContent = latex
52
- element.classList.add("lexxy-math--error")
53
- element.title = `${error}`
134
+ const converter = resolveConverter(mathjax)
135
+ if (!converter) return
136
+
137
+ if (await convertInto(converter, element, latex, { display })) {
138
+ refreshDocumentStyles(mathjax)
54
139
  }
55
140
  })
141
+ }
56
142
 
57
- return renderQueue
143
+ // Test-only: clears the module-level caches (memoized MathJax lookup, render
144
+ // queue, warning flags) between test cases. Not part of the public API and
145
+ // never re-exported from src/index.js.
146
+ export function __resetMathjaxHelperForTests() {
147
+ warnedAboutMissingMathJax = false
148
+ warnedAboutMissingConverter = false
149
+ renderQueue = Promise.resolve()
150
+ mathjaxPromise = null
58
151
  }
59
152
 
60
153
  function sleep(milliseconds) {
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { MathjaxExtension, INSERT_MATH_COMMAND } from "./extensions/mathjax_extension.js"
2
2
  export { MathNode, $createMathNode, $isMathNode } from "./nodes/math_node.js"
3
+ export { typesetMath, startMathjaxAutoTypeset } from "./typeset.js"
3
4
  export { MathjaxExtension as default } from "./extensions/mathjax_extension.js"
@@ -75,7 +75,7 @@ export class MathNode extends DecoratorNode {
75
75
 
76
76
  exportDOM() {
77
77
  const element = document.createElement(this.__display ? "div" : "span")
78
- element.className = "lexxy-math"
78
+ element.className = this.__display ? "lexxy-math lexxy-math--display" : "lexxy-math"
79
79
  element.setAttribute("data-latex", this.__latex)
80
80
  element.textContent = this.__latex
81
81
  return { element }
package/src/typeset.js ADDED
@@ -0,0 +1,5 @@
1
+ // Entry point for display-only pages: @fnix/lexxy-mathjax/typeset pulls in
2
+ // neither lexxy nor Lexical, unlike the "." entry which loads the whole
3
+ // editor extension.
4
+ export { typesetMath } from "./typeset_math.js"
5
+ export { startMathjaxAutoTypeset } from "./auto_typeset.js"
@@ -0,0 +1,94 @@
1
+ import {
2
+ convertInto,
3
+ findMathJax,
4
+ measureContainerWidth,
5
+ refreshDocumentStyles,
6
+ resolveConverter
7
+ } from "./helpers/mathjax_helper.js"
8
+
9
+ const DEFAULT_SELECTOR = "[data-latex]"
10
+ const EDITOR_SELECTOR = "lexxy-editor, [contenteditable='true']"
11
+ const OUTPUT_ATTRIBUTE = "data-lexxy-math-output"
12
+
13
+ // Typesets every [data-latex] element under root (root itself included, so
14
+ // typesetMath(element) works as well as typesetMath(document)). This is the
15
+ // display-page counterpart to MathNode#createDOM: it turns saved
16
+ // `<span data-latex="...">`/`<div data-latex="...">` markup back into
17
+ // rendered math, for pages that have no editor at all.
18
+ //
19
+ // Never rejects: a single bad equation is reported in the result, not thrown.
20
+ export async function typesetMath(root = document, {
21
+ output = "auto",
22
+ selector = DEFAULT_SELECTOR,
23
+ force = false,
24
+ accessible = true
25
+ } = {}) {
26
+ const elements = collectElements(root, selector)
27
+ if (elements.length === 0) return { total: 0, typeset: 0, failed: 0, skipped: 0, mathjax: true }
28
+
29
+ const mathjax = await findMathJax()
30
+ if (!mathjax) return { total: elements.length, typeset: 0, failed: 0, skipped: 0, mathjax: false }
31
+
32
+ const converter = resolveConverter(mathjax, output)
33
+ if (!converter) return { total: elements.length, typeset: 0, failed: 0, skipped: 0, mathjax: true }
34
+
35
+ const result = { total: elements.length, typeset: 0, failed: 0, skipped: 0, mathjax: true }
36
+
37
+ try {
38
+ const pending = []
39
+
40
+ for (const element of elements) {
41
+ if (shouldSkip(element, converter, force)) {
42
+ result.skipped++
43
+ continue
44
+ }
45
+
46
+ pending.push({
47
+ element,
48
+ latex: element.getAttribute("data-latex"),
49
+ display: element.tagName === "DIV",
50
+ containerWidth: measureContainerWidth(element)
51
+ })
52
+ }
53
+
54
+ for (const { element, latex, display, containerWidth } of pending) {
55
+ applyPresentation(element, display, accessible, latex)
56
+
57
+ const succeeded = await convertInto(converter, element, latex, { display, containerWidth })
58
+ element.setAttribute(OUTPUT_ATTRIBUTE, succeeded ? converter.name : "error")
59
+
60
+ if (succeeded) result.typeset++
61
+ else result.failed++
62
+ }
63
+
64
+ if (result.typeset > 0) refreshDocumentStyles(mathjax)
65
+ } catch {
66
+ // typesetMath never rejects: any surprise here is reported via the
67
+ // per-element failed/typeset counts already recorded above.
68
+ }
69
+
70
+ return result
71
+ }
72
+
73
+ function collectElements(root, selector) {
74
+ const own = root.matches?.(selector) ? [ root ] : []
75
+ return own.concat([ ...root.querySelectorAll(selector) ])
76
+ }
77
+
78
+ function shouldSkip(element, converter, force) {
79
+ if (element.closest(EDITOR_SELECTOR)) return true
80
+ if (force) return false
81
+
82
+ const rendered = element.getAttribute(OUTPUT_ATTRIBUTE)
83
+ return rendered === converter.name || rendered === "error"
84
+ }
85
+
86
+ function applyPresentation(element, display, accessible, latex) {
87
+ element.classList.add("lexxy-math")
88
+ element.classList.toggle("lexxy-math--display", display)
89
+
90
+ if (accessible) {
91
+ element.setAttribute("role", "math")
92
+ element.setAttribute("aria-label", `Equation: ${latex}`)
93
+ }
94
+ }
@@ -4,6 +4,9 @@
4
4
  display: inline-block;
5
5
  padding: 0 0.1em;
6
6
  border-radius: 0.2em;
7
+ }
8
+
9
+ lexxy-editor .lexxy-math {
7
10
  cursor: pointer;
8
11
  }
9
12