ntk 7.7.0 → 8.0.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.
@@ -1,679 +0,0 @@
1
- // TeX math rendering on top of KaTeX (pure JS) and the ntk text stack.
2
- //
3
- // KaTeX parses TeX and produces a DOM-like tree (spans, vertical lists,
4
- // symbol nodes, rules, SVG surds) that browsers lay out with CSS. This
5
- // module reimplements the small CSS subset that tree actually uses —
6
- // inline flow with em margins, vlist absolute positioning, border-bottom
7
- // rules, font-size/font-family classes — directly in pixel space, using
8
- // the bundled KaTeX .ttf fonts for glyphs and metrics.
9
- //
10
- // Wire efficiency: adjacent symbols that resolve to the same font/size/color
11
- // are concatenated and shaped as single runs, and drawing routes every run
12
- // of a formula into ONE batched CompositeGlyphs request per color (via
13
- // drawGlyphRuns) — not one placement per character. Radical/stretchy SVG
14
- // paths are filled server-side as trapezoids through the same scratch mask
15
- // as the vector text path.
16
- //
17
- // const box = layoutTex('\\frac{a+b}{2}', { size: 24 });
18
- // box.draw(ctx, x, y); // x, y = top-left; box.width/height/baseline
19
- //
20
- // or as a widget: new TexView(wnd, { tex: '...', size: 32 }).
21
- import { parseSvgPath } from '../path.js';
22
- import { flatten } from '../rasterize.js';
23
- import { trapezoidize } from '../trapezoid.js';
24
- import { builtin, nodeRequire } from '../builtin.js';
25
- import Font from '../text/font.js';
26
-
27
- // node builtins are fetched lazily (nodeRequire/builtin) so browser bundles
28
- // never resolve them; there, inject the assets with configureTex() instead.
29
-
30
- // katex is sizable — load it on first formula, not on package import
31
- let katex = null;
32
- function loadKatex() {
33
- if (!katex) {
34
- const require = nodeRequire();
35
- if (!require) {
36
- throw new Error(
37
- 'TeX rendering: the katex package cannot be required here — inject it with configureTex({ katex })'
38
- );
39
- }
40
- katex = require('katex');
41
- }
42
- return katex;
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // KaTeX font files
47
-
48
- let fontDir = null; // node path to katex/dist/fonts (null when injected)
49
- let fontFiles = null; // Set of available .ttf file names
50
- let injectedFonts = null; // Map file name -> bytes | Font
51
-
52
- /**
53
- * Provide the KaTeX assets explicitly instead of resolving them from
54
- * node_modules — required in environments without node builtins (browser
55
- * bundles), harmless elsewhere.
56
- *
57
- * @param {object} opts { katex: the katex module, fonts: map/object of
58
- * 'KaTeX_Main-Regular.ttf' -> font bytes (or Font) for the .ttf files
59
- * shipped in katex/dist/fonts (a subset is fine) }
60
- */
61
- export function configureTex({ katex: katexModule, fonts } = {}) {
62
- if (katexModule) katex = katexModule;
63
- if (fonts) {
64
- injectedFonts = fonts instanceof Map ? fonts : new Map(Object.entries(fonts));
65
- fontDir = null;
66
- fontFiles = null;
67
- fontCache.clear();
68
- }
69
- }
70
-
71
- function katexFontFiles() {
72
- if (!fontFiles) {
73
- if (injectedFonts) {
74
- fontFiles = new Set(injectedFonts.keys());
75
- } else {
76
- const require = nodeRequire();
77
- if (!require) {
78
- throw new Error(
79
- 'TeX rendering: KaTeX fonts cannot be read here — inject them with configureTex({ fonts })'
80
- );
81
- }
82
- const { readdirSync } = builtin('node:fs');
83
- const { dirname, join } = builtin('node:path');
84
- fontDir = join(dirname(require.resolve('katex/package.json')), 'dist', 'fonts');
85
- fontFiles = new Set(readdirSync(fontDir).filter((f) => f.endsWith('.ttf')));
86
- }
87
- }
88
- return fontFiles;
89
- }
90
-
91
- const fontCache = new Map(); // file name -> Font
92
-
93
- /** resolve a KaTeX family/weight/style to a bundled Font, with graceful
94
- * degradation when the exact variant does not exist (e.g. Math-Regular) */
95
- function katexFont(family, bold, italic) {
96
- const files = katexFontFiles();
97
- const variants = [
98
- bold && italic ? 'BoldItalic' : null,
99
- bold ? 'Bold' : null,
100
- italic ? 'Italic' : null,
101
- 'Regular',
102
- 'Italic' // KaTeX_Math ships italic faces only
103
- ];
104
- for (const variant of variants) {
105
- if (!variant) continue;
106
- const file = `KaTeX_${family}-${variant}.ttf`;
107
- if (!files.has(file)) continue;
108
- let font = fontCache.get(file);
109
- if (!font) {
110
- if (injectedFonts) {
111
- const value = injectedFonts.get(file);
112
- font = value instanceof Font ? value : Font.fromData(value, { key: `katex:${file}` });
113
- } else {
114
- font = Font.loadSync(`${fontDir}/${file}`);
115
- }
116
- fontCache.set(file, font);
117
- }
118
- return font;
119
- }
120
- throw new Error(`no KaTeX font for family ${family}`);
121
- }
122
-
123
- // ---------------------------------------------------------------------------
124
- // class / style interpretation (mirrors katex.css)
125
-
126
- // KaTeX size multipliers, sizes 1..11 (src/Options.js)
127
- const SIZE_MULT = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488];
128
-
129
- const FONT_CLASSES = {
130
- mathnormal: { family: 'Math', italic: true },
131
- mathit: { family: 'Main', italic: true },
132
- mathrm: { italic: false },
133
- mathbf: { family: 'Main', bold: true },
134
- boldsymbol: { family: 'Math', bold: true, italic: true },
135
- amsrm: { family: 'AMS' },
136
- mathbb: { family: 'AMS' },
137
- textbb: { family: 'AMS' },
138
- mathcal: { family: 'Caligraphic' },
139
- mathfrak: { family: 'Fraktur' },
140
- textfrak: { family: 'Fraktur' },
141
- mathboldfrak: { family: 'Fraktur', bold: true },
142
- textboldfrak: { family: 'Fraktur', bold: true },
143
- mathtt: { family: 'Typewriter' },
144
- texttt: { family: 'Typewriter' },
145
- mathscr: { family: 'Script' },
146
- textscr: { family: 'Script' },
147
- mathsf: { family: 'SansSerif' },
148
- textsf: { family: 'SansSerif' },
149
- mathboldsf: { family: 'SansSerif', bold: true },
150
- textboldsf: { family: 'SansSerif', bold: true },
151
- mathsfit: { family: 'SansSerif', italic: true },
152
- mathitsf: { family: 'SansSerif', italic: true },
153
- textitsf: { family: 'SansSerif', italic: true },
154
- mainrm: { family: 'Main', italic: false },
155
- textrm: { family: 'Main' },
156
- textbf: { bold: true },
157
- textit: { italic: true }
158
- };
159
-
160
- // css lengths on domTree nodes are 'Xem' (occasionally 'Xpx'); px stays px
161
- function parseLen(value, em) {
162
- if (!value) return 0;
163
- const n = parseFloat(value);
164
- if (Number.isNaN(n)) return 0;
165
- return String(value).endsWith('px') ? n : n * em;
166
- }
167
-
168
- /** apply a node's classes and inline style to the inherited state */
169
- function applyState(node, st) {
170
- const classes = node.classes || [];
171
- let out = st;
172
- const own = () => (out === st ? (out = { ...st }) : out);
173
-
174
- const sizing =
175
- classes.includes('katex-sizing') ||
176
- classes.includes('sizing') ||
177
- classes.includes('fontsize-ensurer');
178
- const delimsizing = classes.includes('delimsizing');
179
-
180
- let resetSize = null;
181
- let toSize = null;
182
- for (const c of classes) {
183
- const font = FONT_CLASSES[c];
184
- if (font) Object.assign(own(), font);
185
- let m;
186
- if ((m = /^reset-size(\d+)$/.exec(c))) {
187
- resetSize = +m[1];
188
- } else if (sizing && (m = /^size(\d+)$/.exec(c))) {
189
- toSize = +m[1];
190
- } else if (delimsizing && (m = /^size(\d+)$/.exec(c))) {
191
- own().family = `Size${m[1]}`;
192
- own().italic = false;
193
- own().bold = false;
194
- } else if ((m = /^delim-size(\d+)$/.exec(c))) {
195
- own().family = `Size${m[1]}`;
196
- own().italic = false;
197
- own().bold = false;
198
- } else if (c === 'small-op') {
199
- own().family = 'Size1';
200
- } else if (c === 'large-op') {
201
- own().family = 'Size2';
202
- }
203
- }
204
- if (resetSize !== null && toSize !== null) {
205
- own().em = st.em * (SIZE_MULT[toSize - 1] / SIZE_MULT[resetSize - 1]);
206
- }
207
- if (node.style && node.style.color) {
208
- own().color = node.style.color;
209
- }
210
- return out;
211
- }
212
-
213
-
214
- // ---------------------------------------------------------------------------
215
- // layout walker
216
-
217
- const STRETCH = -1; // width placeholder resolved by the containing vlist
218
-
219
- function isSymbol(node) {
220
- return node.constructor && node.constructor.name === 'SymbolNode';
221
- }
222
-
223
- function isSvg(node) {
224
- return node.constructor && node.constructor.name === 'SvgNode';
225
- }
226
-
227
- function classSet(node) {
228
- return node.classes || [];
229
- }
230
-
231
- function has(node, cls) {
232
- return classSet(node).includes(cls);
233
- }
234
-
235
- class Walker {
236
- constructor() {
237
- this.warnings = [];
238
- }
239
-
240
- /** dispatch any child node -> box { width, items, stretch? } */
241
- layoutAny(node, st, align) {
242
- if (isSymbol(node)) {
243
- // bare symbol outside an hbox (e.g. directly inside a vlist wrap)
244
- return this.hbox([node], st, 0);
245
- }
246
- if (isSvg(node)) return this.svg(node, st, null);
247
- if (node.children) return this.span(node, st, align);
248
- return { width: 0, items: [] };
249
- }
250
-
251
- span(node, st0, align = 'left') {
252
- const st = applyState(node, st0);
253
- const em = st.em;
254
- const cls = classSet(node);
255
- const style = node.style || {};
256
-
257
- if (cls.includes('katex-strut') || cls.includes('pstrut') || cls.includes('vlist-s')) {
258
- return { width: 0, items: [] };
259
- }
260
-
261
- // rules drawn with borders: frac-line, overline/underline-line, hlines
262
- if (style.borderBottomWidth !== undefined) {
263
- const h = Math.max(parseLen(style.borderBottomWidth, em), 1);
264
- return {
265
- width: 0,
266
- stretch: true,
267
- items: [{ type: 'rect', x: 0, y: -h, w: STRETCH, h, color: st.color }]
268
- };
269
- }
270
- // \rule: content-less span sized by borders, lifted by style.bottom
271
- if (cls.includes('katex-rule') || cls.includes('rule')) {
272
- const w = parseLen(style.borderRightWidth ?? style.width, em);
273
- const h = Math.max(parseLen(style.borderTopWidth ?? style.height, em), 1);
274
- const shift = parseLen(style.bottom, em);
275
- return {
276
- width: w,
277
- items: w > 0 ? [{ type: 'rect', x: 0, y: -shift - h, w, h, color: st.color }] : []
278
- };
279
- }
280
-
281
- if (cls.includes('vlist-t')) {
282
- return this.vlist(node, st, align);
283
- }
284
-
285
- // spans hosting an absolutely-positioned svg (sqrt surds, stretchy fills)
286
- const svgChild = (node.children || []).find(isSvg);
287
- if (svgChild) {
288
- const cssH = parseLen(style.height, em) || parseLen(svgChild.attributes?.height, em);
289
- const box = this.svg(svgChild, st, cssH);
290
- const minW = parseLen(style.minWidth, em);
291
- return {
292
- width: Math.max(parseLen(style.width, em), minW),
293
- stretch: true, // hide-tail spans are width:100% of their column
294
- stretchMin: minW,
295
- items: box.items
296
- };
297
- }
298
-
299
- // generic inline flow
300
- const box = this.hbox(node.children || [], st, parseLen(style.paddingLeft, em), align);
301
-
302
- // explicit width overrides (mspace \hspace etc.) and width:0 hacks
303
- if (style.width !== undefined) box.width = parseLen(style.width, em);
304
- if (cls.includes('nulldelimiter')) box.width = 0.12 * em;
305
- if (cls.includes('llap') || cls.includes('clap')) {
306
- const shift = cls.includes('llap') ? -box.width : -box.width / 2;
307
- for (const item of box.items) item.x += shift;
308
- box.width = 0;
309
- } else if (cls.includes('rlap')) {
310
- box.width = 0;
311
- } else if (cls.includes('accent-body') && !cls.includes('accent-full')) {
312
- box.width = 0;
313
- }
314
- return box;
315
- }
316
-
317
- /** children in inline flow; merges adjacent same-style symbols into runs */
318
- hbox(children, st, x0 = 0, align = 'left') {
319
- const items = [];
320
- let x = x0;
321
- let group = null;
322
-
323
- const flush = () => {
324
- if (!group) return;
325
- const run = group.font.shape(group.text, group.size);
326
- items.push({ type: 'run', run, x: group.x0, y: 0, color: group.color });
327
- x = group.x0 + run.width + group.italic;
328
- group = null;
329
- };
330
-
331
- for (const child of children) {
332
- if (isSymbol(child)) {
333
- const cst = applyState(child, st);
334
- // strip zero-width space / BOM (vlist alignment hacks)
335
- const text = child.text.replace(/[\u200b\ufeff]/g, '');
336
- if (!text) continue;
337
- const style = child.style || {};
338
- const ml = parseLen(style.marginLeft, cst.em);
339
- const mr = parseLen(style.marginRight, cst.em);
340
- if (ml) {
341
- flush();
342
- x += ml;
343
- }
344
- const font = katexFont(cst.family, cst.bold, cst.italic);
345
- if (
346
- group &&
347
- (group.font !== font || group.size !== cst.em || group.color !== cst.color)
348
- ) {
349
- flush();
350
- }
351
- if (!group) group = { font, size: cst.em, color: cst.color, text: '', x0: x, italic: 0 };
352
- group.text += text;
353
- group.italic = (child.italic || 0) * cst.em;
354
- // italic correction / explicit margin ends the mergeable sequence
355
- if (child.italic || mr) {
356
- flush();
357
- x += mr;
358
- }
359
- continue;
360
- }
361
- if (isSvg(child)) {
362
- // absolutely positioned inside its span: draws but does not advance
363
- flush();
364
- const box = this.svg(child, st, null);
365
- for (const item of box.items) {
366
- item.x += x;
367
- items.push(item);
368
- }
369
- continue;
370
- }
371
- if (!child.children && !child.style) continue; // text/doc nodes
372
- flush();
373
- const style = child.style || {};
374
- const cem = applyState(child, st).em;
375
- x += parseLen(style.marginLeft, cem);
376
- const box = this.span(child, st, this.childAlign(child, align));
377
- const dy = parseLen(style.top, cem) - parseLen(style.verticalAlign, cem);
378
- for (const item of box.items) {
379
- item.x += x;
380
- item.y += dy;
381
- items.push(item);
382
- }
383
- x += box.width + parseLen(style.marginRight, cem);
384
- }
385
- flush();
386
- return { width: x, items };
387
- }
388
-
389
- /** text-align context for a span's children (inherits; a few classes set it) */
390
- childAlign(node, inherited) {
391
- const cls = classSet(node);
392
- if (cls.includes('mfrac') || cls.includes('op-limits') || cls.includes('katex-accent')) {
393
- return 'center';
394
- }
395
- if (cls.includes('col-align-c')) return 'center';
396
- if (cls.includes('col-align-r')) return 'right';
397
- if (cls.includes('col-align-l') || cls.includes('msupsub') || cls.includes('svg-align')) {
398
- return 'left';
399
- }
400
- return inherited;
401
- }
402
-
403
- /** KaTeX vertical list: rows absolutely positioned via pstrut + top */
404
- vlist(vtable, st, align) {
405
- const em = st.em;
406
- const rows = (vtable.children || []).filter((c) => has(c, 'vlist-r'));
407
- const column = rows.length ? (rows[0].children || []).find((c) => has(c, 'vlist')) : null;
408
- if (!column) return { width: 0, items: [] };
409
-
410
- const entries = [];
411
- for (const wrap of column.children || []) {
412
- const kids = wrap.children || [];
413
- const pstrut = kids.find((k) => has(k, 'pstrut'));
414
- const elem = kids.find((k) => k !== pstrut);
415
- if (!elem) continue;
416
- const style = wrap.style || {};
417
- const P = parseLen(pstrut?.style?.height, em);
418
- const T = parseLen(style.top, em);
419
- const wrapAlign = this.childAlign(wrap, align);
420
- // wrapper classes carry state for stacked delimiters (delim-sizeN)
421
- const box = this.layoutAny(elem, applyState(wrap, st), wrapAlign);
422
- entries.push({
423
- box,
424
- dy: P + T,
425
- ml: parseLen(style.marginLeft, em),
426
- mr: parseLen(style.marginRight, em),
427
- align: has(wrap, 'svg-align') ? 'left' : wrapAlign
428
- });
429
- }
430
-
431
- let colWidth = 0;
432
- for (const e of entries) {
433
- if (e.box.stretch) {
434
- if (e.box.stretchMin) colWidth = Math.max(colWidth, e.box.stretchMin);
435
- continue;
436
- }
437
- colWidth = Math.max(colWidth, e.ml + e.box.width + e.mr);
438
- }
439
-
440
- const items = [];
441
- for (const e of entries) {
442
- const w = e.box.stretch ? colWidth : e.box.width;
443
- let ox = e.ml;
444
- if (!e.box.stretch) {
445
- if (e.align === 'center') ox += (colWidth - e.ml - e.box.width - e.mr) / 2;
446
- else if (e.align === 'right') ox += colWidth - e.ml - e.box.width - e.mr;
447
- }
448
- for (const item of e.box.items) {
449
- if (item.w === STRETCH) item.w = w;
450
- if (item.clipW === STRETCH) item.clipW = w;
451
- item.x += ox;
452
- item.y += e.dy;
453
- items.push(item);
454
- }
455
- }
456
- return { width: colWidth, items };
457
- }
458
-
459
- /** SvgNode -> filled path item; box is bottom-aligned to the baseline */
460
- svg(node, st, cssH) {
461
- const attrs = node.attributes || {};
462
- const viewBox = String(attrs.viewBox || '0 0 1 1')
463
- .split(/[\s,]+/)
464
- .map(Number);
465
- const vbH = viewBox[3] || 1;
466
- const heightPx = cssH ?? parseLen(attrs.height, st.em);
467
- const scale = vbH ? heightPx / vbH : 0;
468
-
469
- const polys = [];
470
- for (const child of node.children || []) {
471
- let d = child.alternate;
472
- if (!d && typeof child.toMarkup === 'function') {
473
- const m = / d="([^"]*)"/.exec(child.toMarkup());
474
- if (m) d = m[1];
475
- }
476
- if (!d) continue;
477
- for (const poly of flatten(parseSvgPath(d))) {
478
- const scaled = new Array(poly.length);
479
- for (let i = 0; i < poly.length; i += 2) {
480
- scaled[i] = poly[i] * scale;
481
- scaled[i + 1] = poly[i + 1] * scale - heightPx; // bottom on the baseline
482
- }
483
- polys.push(scaled);
484
- }
485
- }
486
- return {
487
- width: 0,
488
- items: polys.length
489
- ? [{ type: 'path', polys, x: 0, y: 0, clipW: STRETCH, color: st.color }]
490
- : []
491
- };
492
- }
493
- }
494
-
495
- // ---------------------------------------------------------------------------
496
- // public API
497
-
498
- /**
499
- * Parse and lay out a TeX formula with KaTeX. Fully headless (no X
500
- * connection needed) — the result can be measured, then drawn any number of
501
- * times onto 2d contexts.
502
- *
503
- * @param {string} tex
504
- * @param {object} [options]
505
- * - size: base font size in px (the em of the formula), default 16
506
- * - displayMode: KaTeX display mode (block, \displaystyle), default false
507
- * - color: default ink color (spans with explicit \color override it)
508
- * - katex: extra options passed through to KaTeX (macros, strict, ...)
509
- * @returns {TexBox}
510
- */
511
- export function layoutTex(tex, options = {}) {
512
- const em = options.size ?? 16;
513
- const tree = loadKatex().__renderToDomTree(String(tex ?? ''), {
514
- output: 'html',
515
- displayMode: !!options.displayMode,
516
- ...options.katex
517
- });
518
-
519
- // find the .katex span ( __renderToDomTree may wrap it in .katex-display )
520
- let root = tree;
521
- if (has(root, 'katex-display')) root = root.children.find((c) => has(c, 'katex'));
522
- const html = (root.children || []).find((c) => has(c, 'katex-html')) ?? root;
523
-
524
- const st = {
525
- em,
526
- family: 'Main',
527
- bold: false,
528
- italic: false,
529
- color: options.color ?? null
530
- };
531
- const walker = new Walker();
532
- const box = walker.hbox(html.children || [], st);
533
-
534
- const height = (root.height || 0) * em;
535
- const depth = (root.depth || 0) * em;
536
- return new TexBox(box.items, box.width, height, depth, em);
537
- }
538
-
539
- /**
540
- * A laid-out formula. Coordinates are relative to the top-left corner;
541
- * `baseline` px below the top is the alphabetic baseline.
542
- */
543
- export class TexBox {
544
- constructor(items, width, height, depth, em) {
545
- this.items = items;
546
- this.width = width;
547
- this.baseline = height;
548
- this.height = height + depth;
549
- this.depth = depth;
550
- this.em = em;
551
- }
552
-
553
- /**
554
- * Draw at (x, y) = top-left. Text is batched into one CompositeGlyphs
555
- * request per color; rules become FillRectangles; radical strokes become
556
- * trapezoids. The context fillStyle is used for items without an explicit
557
- * color and is left untouched.
558
- */
559
- draw(ctx, x = 0, y = 0) {
560
- const app = ctx.window.app;
561
- const Render = app.display.Render;
562
- const base = y + this.baseline;
563
-
564
- for (const item of this.items) {
565
- if (item.type !== 'rect') continue;
566
- const src = item.color ? ctx._stylePicture(item.color) : ctx._backgroundPicture;
567
- Render.Composite(
568
- Render.PictOp.Over,
569
- src.id,
570
- ctx._compositeMask(),
571
- ctx.picture.id,
572
- Math.round(x + item.x),
573
- Math.round(base + item.y),
574
- Math.round(x + item.x),
575
- Math.round(base + item.y),
576
- Math.round(x + item.x),
577
- Math.round(base + item.y),
578
- Math.max(1, Math.round(item.w)),
579
- Math.max(1, Math.round(item.h))
580
- );
581
- }
582
-
583
- for (const item of this.items) {
584
- if (item.type !== 'path') continue;
585
- const src = item.color ? ctx._stylePicture(item.color) : ctx._backgroundPicture;
586
- const traps = [];
587
- for (const poly of item.polys) {
588
- let clipped = poly;
589
- if (item.clipW !== STRETCH && item.clipW !== undefined) {
590
- clipped = poly.slice();
591
- for (let i = 0; i < clipped.length; i += 2) {
592
- if (clipped[i] > item.clipW) clipped[i] = item.clipW;
593
- }
594
- }
595
- trapezoidize([clipped], x + item.x, base + item.y, traps);
596
- }
597
- // through the context, so the clip applies — compositing straight to
598
- // ctx.picture spills out of scrollviews and clipped boxes
599
- ctx.drawTraps(Render.PictOp.Over, src, traps);
600
- }
601
-
602
- // batch every run of the same color into a single CompositeGlyphs
603
- const byColor = new Map();
604
- for (const item of this.items) {
605
- if (item.type !== 'run') continue;
606
- const key = item.color ?? '';
607
- let list = byColor.get(key);
608
- if (!list) byColor.set(key, (list = { color: item.color, runs: [] }));
609
- list.runs.push({ run: item.run, x: x + item.x, y: base + item.y });
610
- }
611
- for (const { color, runs } of byColor.values()) {
612
- const src = color ? ctx._stylePicture(color) : ctx._backgroundPicture;
613
- ctx.drawGlyphs(Render.PictOp.Over, src, runs);
614
- }
615
-
616
- ctx._markDirty();
617
- return this;
618
- }
619
- }
620
-
621
- /**
622
- * A widget that renders one TeX formula into a window, KaTeX-style.
623
- *
624
- * const view = new TexView(wnd, { tex: 'e^{i\\pi} + 1 = 0', size: 40 });
625
- * wnd.map();
626
- *
627
- * Options: tex, size, color, background, displayMode, padding, katex
628
- * (pass-through options). `setTex(tex)` updates and re-renders.
629
- */
630
- export default class TexView {
631
- constructor(window, opts = {}) {
632
- this.window = window ?? null;
633
- this.size = opts.size ?? 24;
634
- this.color = opts.color ?? '#222222';
635
- this.background = opts.background ?? 'white';
636
- this.displayMode = opts.displayMode ?? true;
637
- this.padding = opts.padding ?? 16;
638
- this.katexOptions = opts.katex;
639
- this._box = null;
640
- this._tex = '';
641
- if (opts.tex) this.setTex(opts.tex);
642
-
643
- if (this.window) {
644
- this._ctx = this.window.getContext('2d');
645
- this.window.on('expose', () => this.render());
646
- }
647
- }
648
-
649
- setTex(tex) {
650
- this._tex = String(tex ?? '');
651
- this._box = layoutTex(this._tex, {
652
- size: this.size,
653
- color: this.color,
654
- displayMode: this.displayMode,
655
- katex: this.katexOptions
656
- });
657
- if (this.window && this.window._mapped) this.render();
658
- return this;
659
- }
660
-
661
- /** the laid-out TexBox (width/height/baseline) for the current tex */
662
- get box() {
663
- return this._box;
664
- }
665
-
666
- render() {
667
- if (!this.window) throw new Error('TexView.render() needs a window');
668
- const ctx = this._ctx;
669
- ctx.fillStyle = this.background;
670
- ctx.fillRect(0, 0, this.window.width, this.window.height);
671
- if (!this._box) return this;
672
- // centered, like KaTeX display mode
673
- const x = Math.max(this.padding, (this.window.width - this._box.width) / 2);
674
- const y = Math.max(this.padding, (this.window.height - this._box.height) / 2);
675
- ctx.fillStyle = this.color;
676
- this._box.draw(ctx, x, y);
677
- return this;
678
- }
679
- }