@mathzetta/inlinemath 0.0.0-stage → 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +282 -2
  3. package/dist/cas.external.js +2 -0
  4. package/dist/cas.js +2 -0
  5. package/dist/chunks/calculate-BX9lJ7Ff.js +2 -0
  6. package/dist/chunks/draw-Cm4exbg7.js +2 -0
  7. package/dist/chunks/html-CvuSyhzk.js +2 -0
  8. package/dist/chunks/html-DWtfytjA.js +2 -0
  9. package/dist/chunks/lite-Bj_L6w2g.js +2 -0
  10. package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
  11. package/dist/fonts/fonts.css +17 -0
  12. package/dist/fonts/lmroman12-italic.woff2 +0 -0
  13. package/dist/fonts/lmroman12-regular.woff2 +0 -0
  14. package/dist/inlinemath-cas.external.global.min.js +2 -0
  15. package/dist/inlinemath-cas.global.min.js +2 -0
  16. package/dist/inlinemath-lite-calc.global.min.js +2 -0
  17. package/dist/inlinemath-lite.external.global.min.js +2 -0
  18. package/dist/inlinemath-lite.global.min.js +2 -0
  19. package/dist/inlinemath.css +127 -0
  20. package/dist/inlinemath.external.global.min.js +2 -0
  21. package/dist/inlinemath.external.js +2 -0
  22. package/dist/inlinemath.global.min.js +2 -0
  23. package/dist/inlinemath.js +2 -0
  24. package/dist/lite-calc.js +2 -0
  25. package/dist/lite.external.js +2 -0
  26. package/dist/lite.js +2 -0
  27. package/dist/markdown.external.js +2 -0
  28. package/dist/markdown.js +2 -0
  29. package/fonts/GUST-FONT-LICENSE.txt +30 -0
  30. package/fonts/fonts.css +17 -0
  31. package/fonts/lmroman12-italic.woff2 +0 -0
  32. package/fonts/lmroman12-regular.woff2 +0 -0
  33. package/inlinemath.css +127 -0
  34. package/package.json +95 -4
  35. package/src/calculator.js +6 -0
  36. package/src/cas.d.ts +3 -0
  37. package/src/cas.js +139 -0
  38. package/src/draw.js +72 -0
  39. package/src/glyphs.js +38 -0
  40. package/src/html.js +32 -0
  41. package/src/index.d.ts +3 -0
  42. package/src/index.js +17 -0
  43. package/src/layout.js +605 -0
  44. package/src/lite-calc.d.ts +3 -0
  45. package/src/lite-calc.js +15 -0
  46. package/src/lite.d.ts +3 -0
  47. package/src/lite.js +18 -0
  48. package/src/markdown.d.ts +3 -0
  49. package/src/markdown.js +361 -0
  50. package/src/mathzetta.js +17 -0
  51. package/src/select.js +108 -0
  52. package/src/types.d.ts +133 -0
  53. package/src/view.js +162 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Robert Wilcox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,283 @@
1
- # Temporary Holding Version
1
+ # Inline Math
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Draws calculus-level maths as plain HTML and CSS (the `@mathzetta/inlinemath` package, at inlinemath.com). The input is anything
4
+ [MathZetta](https://github.com/triplelog/mathzetta/tree/main/packages/mathzetta) reads (`sin^2 x`, `\frac{d}{dx}(x^2)`, `x**2/2`, `√x`).
5
+ Every part of the drawing can be found, coloured, clicked and animated.
6
+
7
+ Inline Math is not a LaTeX engine. It covers what a Calculus course writes:
8
+ - fractions, exponents and subscripts, radicals, brackets that grow;
9
+ - `d/dx` and `dy/dx`, integrals with limits, `lim` with its subscript, `∑`/`∏`;
10
+ - absolute value, floor and ceiling, sets and intervals;
11
+ - piecewise functions, `\boxed`, `\cancel`, `\vec`, `\hat`, `\overline`.
12
+
13
+ It needs nothing besides MathZetta: no KaTeX, no fonts (Latin Modern is
14
+ optional), and no build step.
15
+
16
+ ```bash
17
+ npm install @mathzetta/inlinemath mathzetta # mathzetta 1.x is a peer; add @mathzetta/cas for @mathzetta/inlinemath/cas
18
+ ```
19
+
20
+ Inline Math is ES modules (`import`). TypeScript declarations come with it. `require('@mathzetta/inlinemath')`
21
+ works where Node can require ES modules (Node 20.19+ and 22.12+); in the browser, use the bundles
22
+ in `dist/` (below).
23
+
24
+ ```js
25
+ import { toHTML, render } from '@mathzetta/inlinemath';
26
+
27
+ toHTML('\\int_0^1 x^2\\,dx'); // HTML string; works in Node too
28
+
29
+ const view = render(div, 'x^2 + 2x'); // a live view
30
+ view.highlight('2x'); // name a part by writing it
31
+ view.color('x^2', 'crimson');
32
+ view.on('click', ({ node, id, path }) => …);
33
+ view.set('2x + x^2', { animate: true }); // the next step: matching parts glide over
34
+ view.segments(); // [{ text: 'x', id: 3 }, { text: 'squared', id: 5 }, …]
35
+ ```
36
+
37
+ Inline Math needs its stylesheet on the page (the fonts are optional):
38
+
39
+ ```js
40
+ import '@mathzetta/inlinemath/inlinemath.css'; // with a bundler
41
+ import '@mathzetta/inlinemath/fonts/fonts.css'; // optional: Latin Modern, the TeX look
42
+ ```
43
+
44
+ ```html
45
+ <!-- or from the CDN -->
46
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/inlinemath.css">
47
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/fonts/fonts.css"> <!-- optional -->
48
+ ```
49
+
50
+ ### Lite: drawing only
51
+
52
+ `@mathzetta/inlinemath/lite` is `toHTML()` and nothing live: the same layout, glyphs, colours, highlights and
53
+ spoken labels for screen readers, without the live view (`render`, step animation, events), Markdown
54
+ or the CAS — about a third smaller. `{{…}}` blocks are worked out only if MathZetta's calculator is
55
+ loaded too (`import 'mathzetta/calculate'`, or `@mathzetta/inlinemath/lite-calc`, which brings it); otherwise
56
+ they are drawn as written.
57
+
58
+ ```js
59
+ import { toHTML } from '@mathzetta/inlinemath/lite';
60
+ el.innerHTML = toHTML('\\frac{d}{dx} x^2 = 2x', { colors: { '2x': 'crimson' } });
61
+ ```
62
+
63
+ ## Try it
64
+
65
+ ```bash
66
+ npm test # 253 checks; every input in mathzetta/test/cases/calculus.tsv
67
+ ```
68
+
69
+ [inlinemath.com](https://inlinemath.com) has the live editor, steps and the gallery; its
70
+ [Markdown](https://inlinemath.com/markdown.html) and [CAS](https://inlinemath.com/cas.html) pages are playgrounds.
71
+
72
+ ## Files
73
+
74
+ | File | What |
75
+ | --- | --- |
76
+ | `src/mathzetta.js`, `src/calculator.js` | The **only** imports from MathZetta (a peer dependency; in this repository, the workspace's `packages/mathzetta`): the core and speech, and the calculator, which lite leaves out. |
77
+ | `src/draw.js`, `src/html.js`, `src/lite.js` | `toHTML` without the calculator (shared), the full build's `toHTML` (with it), and the lite entry. |
78
+ | `src/layout.js` | Turns a tree into HTML. All notation choices live here. |
79
+ | `src/glyphs.js` | SVG paths for `( ) { } √ ∫ ∑ ∏`. Change a shape here. |
80
+ | `src/select.js` | Picks parts by id, expression, path or test. Also `numberTree`. |
81
+ | `src/view.js` | The live view: highlight, colour, events, speech segments, step animation. |
82
+ | `src/html.js` | `toHTML`, `treeOf`, and the `colors` / `highlight` options. |
83
+ | `inlinemath.css` | All the styling. The numbers marked *metric* must match the constants in layout.js. |
84
+
85
+ ## How it lays maths out
86
+
87
+ - **Estimated, not measured.** Each piece knows roughly how far it reaches above and below the baseline, in em.
88
+ - From that, `layout.js` writes `vertical-align` values that put fraction bars on the maths axis, lift exponents above tall bases, and size `∫` to its integrand.
89
+ - The same string comes out in Node, in a browser or in a video frame, with no layout pass after rendering.
90
+ - **Self-stretching delimiters.** Brackets, bars, floors and braces are drawn at `height: 100%` of what they enclose (SVG or CSS borders), so they need no estimate at all.
91
+ - This is the best idea from inline-math's imcss renderer. Inline Math fixes its aspect-ratio bug: shapes use `preserveAspectRatio="none"` with a fixed width.
92
+ - **Stable ids.** Every node's element has `data-node` = MathZetta's postfix index. MathML ids and `toSpeechSegments()` ids use the same number, so a spoken phrase can light up the part it describes.
93
+ - Decoration (signs, brackets, bars) has no id. A click on it belongs to the node that drew it.
94
+
95
+ ## Options
96
+
97
+ These go to `toHTML(input, options)` and `render(el, input, options)`.
98
+
99
+ | Option | Default | |
100
+ | --- | --- | --- |
101
+ | `display` | `true` | Limits above and below `∑`. `false` gives running-text style (smaller fractions, limits as scripts). |
102
+ | `fnParens` | `'auto'` | `sin x`, `sin 2x`, `ln|x|`, but `sin(x + 1)`. `'always'` gives `sin(x)`. |
103
+ | `trigPowers` | `true` | `sin(x)^2` → sin²x |
104
+ | `inverseTrig` | `'arc'` | `'power'` gives sin⁻¹x |
105
+ | `derivativeBrackets` | `'['` | `'('` for d/dx (x² + 1) |
106
+ | `scriptFractions` | `'slash'` | x^{3/2} written with a slash. `'stack'` stacks it. |
107
+ | `cases` | `true` | `c ? a : b` drawn as a piecewise brace |
108
+ | `colors` | | `{ 'x^2': 'red', 5: 'blue' }` (number keys are ids) or `[[target, colour]]` |
109
+ | `highlight` | | target(s) that get the `mv-hl` class |
110
+ | `attributes`, `style` | | `(node) => ({…})` / `(node) => 'css'` for each node's element |
111
+ | `label` | `true` | the accessible name of the expression (`role="math" aria-label`): `true` = its spoken form from MathZetta, or a string, `(tree) => string`, or `false` |
112
+ | `prefix` | `'mv'` | class prefix |
113
+
114
+ A *target* can be any of these:
115
+ - an id, e.g. `4`;
116
+ - an expression, e.g. `'x^2'`, which matches every part that is exactly that subtree;
117
+ - `{ path: [0, 1] }`;
118
+ - a function `(node) => bool`;
119
+ - an array of any of the above.
120
+
121
+ ## A static build: `npm run build`
122
+
123
+ ```bash
124
+ npm install # at the repository root: links mathzetta and MathZetta CAS, installs rollup
125
+ npm run build # dist/ (a few seconds)
126
+ ```
127
+
128
+ - **`dist/`** — Inline Math with everything inside, no import map or `node_modules` needed:
129
+ - `inlinemath.js`, `markdown.js`, `cas.js` — ES modules (shared code in `dist/chunks/`);
130
+ - `inlinemath.global.min.js` (136 KB, 46 KB gzipped) — a classic `<script>`: `window.InlineMath`, with Inline Math and the Markdown;
131
+ - `inlinemath-cas.global.min.js` (270 KB, 87 KB gzipped) — the same plus the CAS (`InlineMath.mathCAS`, `casBlocks`, `stepsHTML`);
132
+ - `inlinemath.css` and `fonts/` — put both on the page.
133
+
134
+ ```html
135
+ <!-- dist/ copied beside the page as inlinemath/ -->
136
+ <link rel="stylesheet" href="inlinemath/fonts/fonts.css">
137
+ <link rel="stylesheet" href="inlinemath/inlinemath.css">
138
+ <script src="inlinemath/inlinemath.global.min.js"></script>
139
+ <script>
140
+ el.innerHTML = InlineMath.markdownToHTML('The area is $\\pi r^2$; for $r = 3$ it is ${{pi r^2}}$.');
141
+ </script>
142
+ <!-- or as a module from the same copy: import { toHTML } from './inlinemath/inlinemath.js'; -->
143
+ ```
144
+
145
+ Both folders are build output (git-ignored); run the build again after changing `src/`.
146
+
147
+ ## From a CDN
148
+
149
+ Every npm release's `dist/` is on jsDelivr (and unpkg); the bundles carry MathZetta inside, so a
150
+ page needs nothing else:
151
+
152
+ ```html
153
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/fonts/fonts.css">
154
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/inlinemath.css">
155
+
156
+ <!-- a script tag: window.InlineMath -->
157
+ <script src="https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/inlinemath.global.min.js"></script>
158
+ <script>document.getElementById('out').innerHTML = InlineMath.toHTML('\\frac{d}{dx} x^2');</script>
159
+
160
+ <!-- or modules: inlinemath.js, markdown.js, cas.js (the CAS one holds MathZetta CAS too) -->
161
+ <script type="module">
162
+ import { toHTML } from 'https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/inlinemath.js';
163
+ import { markdownToHTML } from 'https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/markdown.js';
164
+ </script>
165
+ ```
166
+
167
+ `@0.1` follows the latest 0.1.x; `@0.1.0` pins a release.
168
+
169
+ **The stylesheet is required** — the drawing is HTML laid out by `inlinemath.css` (6.9 KB, 2.1 KB
170
+ gzipped): without it fractions, radicals and brackets don't line up. The classic builds
171
+ (`*.global.min.js`) carry it and add it to the page themselves (`<style id="inlinemath-css">`; a
172
+ `<link>` to `dist/inlinemath.css` is still fine, it is only added when absent); the ES modules
173
+ don't, so a page using them links `dist/inlinemath.css`. **The fonts are optional**: `fonts/fonts.css`
174
+ declares Latin Modern Roman (the TeX look) in two files, regular 45 KB and italic 47 KB (woff2), each
175
+ downloaded only when text uses it; without the link the maths is drawn in the system serif.
176
+
177
+ Every build comes **with MathZetta inside, or without it** (`*.external.*`), with or without the
178
+ CAS (MathZetta CAS). Sizes, gzipped:
179
+
180
+ | | ES module | classic `<script>` (`window.InlineMath`) |
181
+ |---|---|---|
182
+ | Lite (drawing only), MathZetta inside | `lite.js` 32 KB | `inlinemath-lite.global.min.js` 33 KB |
183
+ | Lite with the calculator (`{{…}}` worked out) | `lite-calc.js` 41 KB | `inlinemath-lite-calc.global.min.js` 43 KB |
184
+ | Lite without MathZetta | `lite.external.js` 7 KB | `inlinemath-lite.external.global.min.js` 9 KB |
185
+ | Inline Math (+ Markdown in the classic build), MathZetta inside | `inlinemath.js` 42 KB, `markdown.js` | `inlinemath.global.min.js` 46 KB |
186
+ | …without MathZetta | `inlinemath.external.js` **8 KB**, `markdown.external.js` | `inlinemath.external.global.min.js` 13 KB |
187
+ | with the CAS, MathZetta inside | `cas.js` 82 KB | `inlinemath-cas.global.min.js` 87 KB |
188
+ | with the CAS, without MathZetta | `cas.external.js` 40 KB | `inlinemath-cas.external.global.min.js` 52 KB |
189
+
190
+ Without MathZetta, the page loads it once itself: a classic page puts a MathZetta `<script>` first —
191
+ `mathzetta.min.js`, or the `maths-display` flavour, since speech, text and calculate are used (for
192
+ lite, `display`, or `maths-display` to work out `{{…}}`) — and
193
+ the external classic build reads `window.MathZetta`; a module page maps the names with an import
194
+ map (every name can point at the one MathZetta bundle). The fonts are 94 KB more, only if linked.
195
+
196
+ ```html
197
+ <script type="importmap">
198
+ { "imports": { "mathzetta": "https://cdn.jsdelivr.net/npm/mathzetta@1/build/mathzetta.esm.min.js",
199
+ "mathzetta/speech": "https://cdn.jsdelivr.net/npm/mathzetta@1/build/mathzetta.esm.min.js",
200
+ "mathzetta/calculate": "https://cdn.jsdelivr.net/npm/mathzetta@1/build/mathzetta.esm.min.js" } }
201
+ </script>
202
+ <script type="module">
203
+ import { toHTML } from 'https://cdn.jsdelivr.net/npm/@mathzetta/inlinemath@0.1/dist/inlinemath.external.js';
204
+ </script>
205
+ ```
206
+
207
+ Publishing is `npm publish` (`prepublishOnly` runs the tests and the build).
208
+
209
+ ## Markdown with maths: `@mathzetta/inlinemath/markdown`
210
+
211
+ ```js
212
+ import { markdownToHTML } from '@mathzetta/inlinemath/markdown';
213
+ el.innerHTML = markdownToHTML(text); // needs inlinemath.css
214
+ el.innerHTML = markdownToHTML(text, { blocks: casBlocks() }); // {{solve(…)}}, {{diff(…)}} too
215
+ ```
216
+
217
+ - **Maths:** `$…$` inline, `$$…$$` or a ` ```math ` fence on its own (centred), and
218
+ `\(…\)` / `\[…\]`. MathZetta reads it, so LaTeX, plain text (`$sqrt(x)/2$`) and
219
+ code all work.
220
+ - **Working out:** `{{…}}` inside maths is worked out (the calculator, or the CAS with
221
+ `blocks`). A definition like `$r = 3$` or `$g := 9.81$` is remembered, exactly, for
222
+ the `{{…}}` after it (`define: false` turns that off; `scope: { r: 3 }` gives values).
223
+ - **Dollars:** a `$` is maths only when it looks like it (Pandoc's rule): "$5 and $10"
224
+ stays text, and `\$` is a dollar sign. Code spans and fences are left alone.
225
+ - **Bad maths:** maths that doesn't read is shown as written, marked, with the reason as
226
+ its tooltip.
227
+ - **The Markdown:** a small built-in renderer — headings, emphasis, links, images, lists,
228
+ quotes, code, tables, rules. Raw HTML is shown as text unless `html: true`, and
229
+ `javascript:` links are dropped. `render: (md) => html` uses another renderer
230
+ (marked, markdown-it); the maths is taken out first and put back into its output.
231
+ - **Options:** `math` passes options to every drawing (`fnParens`, `colors` …);
232
+ `delimiters` picks which marks count.
233
+ - **Playground:** [inlinemath.com/markdown.html](https://inlinemath.com/markdown.html), an editor with a live preview.
234
+
235
+ ## The CAS: `@mathzetta/inlinemath/cas`
236
+
237
+ MathZetta CAS (exact simplify, factor, solve, d/dx, ∫ …; @mathzetta/cas's docs/cas.md)
238
+ in drawn maths:
239
+
240
+ ```js
241
+ import { toHTML } from '@mathzetta/inlinemath';
242
+ import { casBlocks, stepsHTML, mathCAS } from '@mathzetta/inlinemath/cas';
243
+
244
+ toHTML("f'(x) = {{diff(x^3 sin(x), x)}}", { blocks: casBlocks() })
245
+ toHTML('{{solve(x^2 - 5x + 6 = 0, x)}}', { blocks: casBlocks() }) // x = 2 ∨ x = 3
246
+ el.innerHTML = stepsHTML(mathCAS().solve('sqrt(x + 1) = x - 1', 'x', { steps: 'brief' }).steps)
247
+ ```
248
+
249
+ Block functions: `simplify`, `expand`, `factor`, `cancel`, `diff(f, x[, n])`,
250
+ `integrate(f, x[, a, b])`, `solve(eq, x)`, `approx(x[, digits])`; `d/dx …` and
251
+ `∫ … dx` go to the CAS too; every other block is the calculator, as before.
252
+ `stepsHTML` draws each step with the rewritten part highlighted and its
253
+ explanation. `mathCAS({ rules, names })` uses rule files of your own;
254
+ `mathCAS({ complex: true })` makes i the imaginary unit (and reads Re, Im, conj,
255
+ arg; the conjugate is drawn with an overline). Pages map
256
+ `@mathzetta/cas/engine` and `@mathzetta/cas` in their import map ([inlinemath.com/cas.html](https://inlinemath.com/cas.html) is a
257
+ playground: any input, any operation, detail level, and a live rule editor).
258
+
259
+ ## What still needs doing
260
+
261
+ **Notation MathZetta doesn't read yet** (Inline Math can only draw what parses):
262
+ - **Evaluation bars:** `[F(x)]_0^1` reads as a power of a subscript, and `\left. F \right|_0^1` is rejected.
263
+ - **Matrices:** `\begin{pmatrix}` becomes nested lists.
264
+ - **Partial derivatives:** `\partial f / \partial x` becomes products.
265
+ - **Derivative orders:** `f^{(4)}` becomes a power.
266
+ - **Lists of answers:** `x = 2, x = -3` is rejected.
267
+ - **Accents:** `\dot x` becomes a product.
268
+
269
+ **Inline Math features worth adding next:**
270
+ 1. **Exit animation.** Parts that disappear in a step should fade out where they were (ghost copies). Today they vanish, and new signs and bars just appear.
271
+ 2. **Step-diff helper.** Given `step.before`, `step.after` and `step.path`, return what changed, then highlight → animate → un-highlight in one call. That would be the video's main move.
272
+ 3. **Narration sync.** Something like `view.follow(segments, timings)`: drive highlights from a text-to-speech engine's word timings (`words: [{start, end, i, j}]`).
273
+ 4. **Click-to-edit.** Swap a clicked node for an input box showing its text (`toText` / LaTeX), re-parse, and splice it back in. This was a good inline-math idea.
274
+ 5. **Copy as LaTeX or text** from a selection.
275
+ 6. **Line breaking and alignment.** Multi-line derivations with the `=` signs lined up (an `align` helper), and breaking long sums.
276
+ 7. **A DOM refine pass, optional.** Measure once after fonts load, to nudge fraction bars and script heights where the em estimates are off with unusual fonts.
277
+ 8. **Spacing table.** Move the per-operator spacing (bin, rel, thin) into one table so a house style is one edit.
278
+
279
+ ## Known rough edges
280
+
281
+ - **Fonts.** The placement estimates are tuned for Latin Modern and Times-like serifs. Fonts with unusual ascent and descent will sit a little off.
282
+ - **Accents.** `\vec` and `\hat` use font glyphs and are only approximately centred.
283
+ - **Unbalanced fractions.** Taller-than-usual numerators on fractions are placed from estimates. Very deep nesting can drift by a few hundredths of an em.