ntk 7.7.0 → 8.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.
- package/README.md +9 -10
- package/lib/app.js +4 -0
- package/lib/fontconfig.js +185 -61
- package/lib/index.js +2 -29
- package/lib/renderingcontext_2d.js +583 -19
- package/lib/shadow.js +199 -0
- package/lib/text/fontsource.js +47 -3
- package/lib/text/layout.js +4 -4
- package/lib/widgets/svgview.js +14 -16
- package/package.json +4 -9
- package/lib/widgets/css.js +0 -673
- package/lib/widgets/highlight.js +0 -97
- package/lib/widgets/htmlview.js +0 -893
- package/lib/widgets/markdown.js +0 -148
- package/lib/widgets/markdownview.js +0 -492
- package/lib/widgets/tex.js +0 -679
- package/lib/yoga.js +0 -119
package/lib/widgets/tex.js
DELETED
|
@@ -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
|
-
}
|