@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 +108 -13
- package/package.json +2 -1
- package/src/auto_typeset.js +21 -0
- package/src/helpers/mathjax_helper.js +115 -22
- package/src/index.js +1 -0
- package/src/nodes/math_node.js +1 -1
- package/src/typeset.js +5 -0
- package/src/typeset_math.js +94 -0
- package/styles/lexxy-mathjax.css +3 -0
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
|
|
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 = {
|
|
66
|
+
window.MathJax = { output: { displayAlign: "center" } }
|
|
59
67
|
</script>
|
|
60
|
-
<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)
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
130
|
+
return enqueue(async () => {
|
|
34
131
|
const mathjax = await findMathJax()
|
|
35
132
|
if (!mathjax) return
|
|
36
133
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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"
|
package/src/nodes/math_node.js
CHANGED
|
@@ -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
|
+
}
|