rapier-math 1.0.1 → 1.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 +187 -85
- package/adoption-check.mjs +149 -0
- package/check.mjs +21 -0
- package/classic.d.ts +4 -0
- package/delimiters.d.ts +8 -0
- package/delimiters.mjs +39 -0
- package/examples/README.md +25 -0
- package/examples/browser.html +10 -0
- package/examples/browser.mjs +24 -0
- package/examples/formula.mjs +12 -0
- package/examples/markdown.mjs +9 -0
- package/examples/react.tsx +19 -0
- package/examples/stream.mjs +19 -0
- package/examples/unified.mjs +26 -0
- package/examples/worker.mjs +7 -0
- package/index.d.ts +43 -0
- package/index.mjs +205 -108
- package/integration.d.ts +9 -0
- package/integration.mjs +28 -0
- package/markdown-it.d.ts +6 -0
- package/markdown-it.mjs +45 -0
- package/markdown-rules.mjs +65 -0
- package/math-components.json +6 -6
- package/package.json +78 -5
- package/rapier-math-1.1.0.js +2980 -0
- package/rehype.d.ts +6 -0
- package/rehype.mjs +37 -0
- package/src/math/FONT-SOURCE.md +32 -6
- package/src/math/README.md +210 -0
- package/src/math/adoption-verify.mjs +149 -0
- package/src/math/api.js +37 -14
- package/src/math/browser.js +67 -30
- package/src/math/build-font.py +237 -95
- package/src/math/classic.d.ts +4 -0
- package/src/math/delimiters.d.ts +8 -0
- package/src/math/delimiters.mjs +39 -0
- package/src/math/examples/README.md +25 -0
- package/src/math/examples/browser.html +10 -0
- package/src/math/examples/browser.mjs +24 -0
- package/src/math/examples/formula.mjs +12 -0
- package/src/math/examples/markdown.mjs +9 -0
- package/src/math/examples/react.tsx +19 -0
- package/src/math/examples/stream.mjs +19 -0
- package/src/math/examples/unified.mjs +26 -0
- package/src/math/examples/worker.mjs +7 -0
- package/src/math/fallbacks.js +5 -5
- package/src/math/font.js +59 -26
- package/src/math/index.d.ts +43 -0
- package/src/math/integration.d.ts +9 -0
- package/src/math/integration.mjs +28 -0
- package/src/math/layout.js +37 -33
- package/src/math/markdown-it.d.ts +6 -0
- package/src/math/markdown-it.mjs +45 -0
- package/src/math/markdown-rules.mjs +65 -0
- package/src/math/package-build.mjs +4 -4
- package/src/math/package-check.mjs +118 -0
- package/src/math/package-readme.mjs +8 -0
- package/src/math/package-verify.mjs +7 -2
- package/src/math/rehype.d.ts +6 -0
- package/src/math/rehype.mjs +37 -0
- package/src/math/stream.d.ts +14 -0
- package/src/math/stream.mjs +46 -0
- package/src/package.json +1 -1
- package/src/shell/math-resources.json +184 -25
- package/src/tools/build-math.mjs +23 -4
- package/stream.d.ts +14 -0
- package/stream.mjs +46 -0
- package/verify.mjs +7 -2
- package/rapier-math-1.0.1.js +0 -2883
package/src/math/browser.js
CHANGED
|
@@ -88,12 +88,18 @@ function mathFitElement(svg, printing = false) {
|
|
|
88
88
|
if(screenKey!==state.screen || (target===null)!==(state.width===null) || target!==null && Math.abs(target-state.width)>.005) {
|
|
89
89
|
const rendered=renderMath(source,{displayMode:display,maxWidth:(screenKey!==state.screen?width:target) || undefined,screen});
|
|
90
90
|
if(!rendered.errors.length) {
|
|
91
|
+
const active=doc.activeElement,links=active && svg.contains(active)?[...svg.querySelectorAll('a')]:[];
|
|
92
|
+
const focusIndex=links.indexOf(active),focusHref=focusIndex<0?null:active.getAttribute('href');
|
|
91
93
|
const replacement=new view.DOMParser().parseFromString(rendered.svg,'image/svg+xml').documentElement;
|
|
92
|
-
for(const name of ['width','height','viewBox','data-math-natural-width','data-math-max-width','data-math-lines','data-math-screen']) {
|
|
94
|
+
for(const name of ['role','width','height','viewBox','data-math-natural-width','data-math-max-width','data-math-lines','data-math-screen']) {
|
|
93
95
|
if(replacement.hasAttribute(name))svg.setAttribute(name,replacement.getAttribute(name));else svg.removeAttribute(name);
|
|
94
96
|
}
|
|
95
97
|
svg.style.verticalAlign=replacement.style.verticalAlign;
|
|
96
98
|
svg.replaceChildren(...Array.from(replacement.childNodes,node=>doc.importNode(node,true)));
|
|
99
|
+
if(focusIndex>=0 && (!doc.activeElement || doc.activeElement===doc.body)) {
|
|
100
|
+
const link=svg.querySelectorAll('a')[focusIndex];
|
|
101
|
+
if(link?.getAttribute('href')===focusHref)link.focus({preventScroll:true});
|
|
102
|
+
}
|
|
97
103
|
state.natural=Number(svg.getAttribute('data-math-natural-width'));state.screen=screenKey;
|
|
98
104
|
state.width=state.natural>width?width:null;
|
|
99
105
|
if(state.width===null)svg.removeAttribute('data-math-max-width');
|
|
@@ -116,35 +122,66 @@ function mathFit(root) {
|
|
|
116
122
|
return nodes.length;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
function mathObserveDocument(doc) {
|
|
126
|
+
const view=doc.defaultView,nodes=new Map(),columns=new Set();
|
|
127
|
+
let frame=null,active=true,printing=view.matchMedia('print').matches;
|
|
128
|
+
const refresh=()=> {
|
|
129
|
+
if(!active)return;
|
|
130
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
131
|
+
frame=null;
|
|
132
|
+
const wanted=new Set();
|
|
133
|
+
for(const svg of nodes.keys()) {
|
|
134
|
+
if(!svg.isConnected || svg.ownerDocument!==doc || !svg.matches(mathSvgSelector))continue;
|
|
135
|
+
const column=mathFitElement(svg,printing);if(column)wanted.add(column);
|
|
136
|
+
}
|
|
137
|
+
for(const column of columns)if(!wanted.has(column)) {resize.unobserve(column);columns.delete(column);}
|
|
138
|
+
for(const column of wanted)if(!columns.has(column)) {resize.observe(column);columns.add(column);}
|
|
139
|
+
};
|
|
140
|
+
const schedule=()=>{if(active && frame===null)frame=view.requestAnimationFrame(refresh);};
|
|
141
|
+
const resize=new view.ResizeObserver(schedule),theme=new view.MutationObserver(records=>{
|
|
142
|
+
for(const {target} of records)for(const svg of nodes.keys())
|
|
143
|
+
if(target!==svg && target.contains(svg)) {schedule();return;}
|
|
144
|
+
});
|
|
145
|
+
const beforePrint=()=>{printing=true;refresh();},afterPrint=()=>{printing=false;schedule();};
|
|
146
|
+
const media=['(forced-colors: active)','(prefers-contrast: more)','(prefers-color-scheme: dark)'].map(query=>view.matchMedia(query));
|
|
147
|
+
const fonts=doc.fonts;
|
|
148
|
+
theme.observe(doc.documentElement,{subtree:true,attributes:true,attributeFilter:['class','style','data-rapier-theme','data-md-theme']});
|
|
149
|
+
view.addEventListener('resize',schedule,{passive:true});
|
|
150
|
+
view.addEventListener('beforeprint',beforePrint);view.addEventListener('afterprint',afterPrint);
|
|
151
|
+
for(const query of media)query.addEventListener('change',schedule);
|
|
152
|
+
fonts?.addEventListener('loadingdone',schedule);
|
|
153
|
+
const state={nodes,schedule,dispose:()=>{
|
|
154
|
+
active=false;
|
|
155
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
156
|
+
frame=null;
|
|
157
|
+
resize.disconnect();theme.disconnect();columns.clear();nodes.clear();
|
|
158
|
+
view.removeEventListener('resize',schedule);
|
|
159
|
+
view.removeEventListener('beforeprint',beforePrint);view.removeEventListener('afterprint',afterPrint);
|
|
160
|
+
for(const query of media)query.removeEventListener('change',schedule);
|
|
161
|
+
fonts?.removeEventListener('loadingdone',schedule);
|
|
162
|
+
mathBrowserDocuments.delete(doc);
|
|
163
|
+
}};
|
|
164
|
+
mathBrowserDocuments.set(doc,state);
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
|
|
119
168
|
function mathObserve(root) {
|
|
120
|
-
const doc=root?.ownerDocument || root
|
|
121
|
-
if(!
|
|
122
|
-
let
|
|
123
|
-
if(!
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for(const [svg,column] of nodes) {
|
|
128
|
-
if(!svg.isConnected) {nodes.delete(svg);continue;}
|
|
129
|
-
const next=mathFitElement(svg,printing);if(next)nodes.set(svg,next);
|
|
130
|
-
}
|
|
131
|
-
const wanted=new Set(nodes.values());
|
|
132
|
-
for(const column of columns)if(!wanted.has(column)) {resize.unobserve(column);columns.delete(column);}
|
|
133
|
-
for(const column of wanted)if(!columns.has(column)) {resize.observe(column);columns.add(column);}
|
|
134
|
-
};
|
|
135
|
-
const schedule=()=>{if(!frame)frame=view.requestAnimationFrame(refresh);};
|
|
136
|
-
const resize=new view.ResizeObserver(schedule),theme=new view.MutationObserver(records=>{
|
|
137
|
-
if(records.some(({target})=>[...nodes.keys()].some(svg=>target!==svg && target.contains(svg))))schedule();
|
|
138
|
-
});
|
|
139
|
-
theme.observe(doc.documentElement,{subtree:true,attributes:true,attributeFilter:['class','style','data-rapier-theme','data-md-theme']});
|
|
140
|
-
view.addEventListener('resize',schedule,{passive:true});
|
|
141
|
-
view.addEventListener('beforeprint',()=>{printing=true;refresh();});view.addEventListener('afterprint',()=>{printing=false;schedule();});
|
|
142
|
-
for(const query of ['(forced-colors: active)','(prefers-contrast: more)','(prefers-color-scheme: dark)'])view.matchMedia(query).addEventListener('change',schedule);
|
|
143
|
-
doc.fonts?.addEventListener('loadingdone',schedule);
|
|
144
|
-
state={nodes,schedule};mathBrowserDocuments.set(doc,state);
|
|
145
|
-
}
|
|
146
|
-
const nodes=mathElements(root);
|
|
147
|
-
for(const svg of nodes)state.nodes.set(svg,mathColumn(svg));
|
|
169
|
+
const doc=root?.ownerDocument || root;
|
|
170
|
+
if(!doc?.defaultView)return ()=>{};
|
|
171
|
+
let owned=mathElements(root);
|
|
172
|
+
if(!owned.length)return ()=>{};
|
|
173
|
+
let state=mathBrowserDocuments.get(doc) || mathObserveDocument(doc);
|
|
174
|
+
// Each returned cleanup owns its subscription, including overlap and detached nodes.
|
|
175
|
+
for(const svg of owned)state.nodes.set(svg,(state.nodes.get(svg) || 0)+1);
|
|
148
176
|
state.schedule();
|
|
149
|
-
return ()=>{
|
|
177
|
+
return ()=>{
|
|
178
|
+
if(!owned)return;
|
|
179
|
+
const current=state;state=null;
|
|
180
|
+
for(const svg of owned) {
|
|
181
|
+
const remaining=current.nodes.get(svg)-1;
|
|
182
|
+
if(remaining)current.nodes.set(svg,remaining);else current.nodes.delete(svg);
|
|
183
|
+
}
|
|
184
|
+
owned=null;
|
|
185
|
+
if(current.nodes.size)current.schedule();else current.dispose();
|
|
186
|
+
};
|
|
150
187
|
}
|
package/src/math/build-font.py
CHANGED
|
@@ -257,21 +257,39 @@ def edge_profile(glyph_set, name):
|
|
|
257
257
|
return [first, *([math.floor(min(strip) - 1e-7), math.ceil(max(strip) + 1e-7)] if strip else None for strip in strips)]
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
PAGE_BYTES = 16384
|
|
261
|
+
PAGE_STRIDE = 32768
|
|
262
|
+
PARENT_DEPTH = 4
|
|
263
|
+
SCALAR_MAPS = ("chars", "dotless", "flatAccents", "extendedShapes")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def pack_font(data):
|
|
267
|
+
"""Share translated contours and retain independently inflatable record pages."""
|
|
268
|
+
glyphs, profiles = data["glyphs"], data["edgeProfiles"]
|
|
269
|
+
commands = "MLHVCZ"
|
|
270
|
+
lengths = (2, 2, 1, 1, 6, 0)
|
|
263
271
|
|
|
264
272
|
def unsigned(value, target):
|
|
273
|
+
if value < 0 or value > 2**53 - 1:
|
|
274
|
+
raise ValueError("Font integer exceeds exact JavaScript arithmetic")
|
|
265
275
|
while value >= 128:
|
|
266
276
|
target.append((value & 127) | 128)
|
|
267
277
|
value >>= 7
|
|
268
278
|
target.append(value)
|
|
269
279
|
|
|
270
280
|
def signed(value, target):
|
|
271
|
-
|
|
281
|
+
value = value * 2 if value >= 0 else -value * 2 - 1
|
|
282
|
+
if value > 2**53 - 1:
|
|
283
|
+
raise ValueError("Font integer exceeds exact JavaScript arithmetic")
|
|
284
|
+
if value < 240:
|
|
285
|
+
target.append(value)
|
|
286
|
+
elif value < 4080:
|
|
287
|
+
value -= 240
|
|
288
|
+
target.extend((240 + (value >> 8), value & 255))
|
|
289
|
+
else:
|
|
290
|
+
target.append(255)
|
|
291
|
+
unsigned(value - 4080, target)
|
|
272
292
|
|
|
273
|
-
commands = "MLHVCZ"
|
|
274
|
-
lengths = (2, 2, 1, 1, 6, 0)
|
|
275
293
|
paths = [re.findall(r"[MLHVCZ]|-?\d+(?:\.\d+)?", glyph[7]) for glyph in glyphs]
|
|
276
294
|
unit = 1
|
|
277
295
|
for glyph, tokens in zip(glyphs, paths):
|
|
@@ -280,21 +298,27 @@ def pack_paths(glyphs, profiles):
|
|
|
280
298
|
for token in tokens:
|
|
281
299
|
if "." in token:
|
|
282
300
|
unit = math.lcm(unit, Fraction(token).denominator)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
for
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
metric_unit = math.lcm(2, *(Fraction(value).denominator for glyph in glyphs for value in glyph[:7]))
|
|
302
|
+
metrics = [[int(value * metric_unit) for value in glyph[:7]] for glyph in glyphs]
|
|
303
|
+
for values in metrics:
|
|
304
|
+
values[3] -= values[0] - values[1]
|
|
305
|
+
values[6] -= values[0] // 2
|
|
306
|
+
metric_bases = [sorted(values[column] for values in metrics)[len(metrics) // 2] for column in range(7)]
|
|
307
|
+
contours, contour_ids, references = [], {}, []
|
|
308
|
+
for tokens in paths:
|
|
309
|
+
cursor, previous = 0, None
|
|
310
|
+
structure, coordinates, refs = [], [], []
|
|
311
|
+
first_x = first_y = x = y = 0
|
|
312
|
+
|
|
313
|
+
def keep_contour():
|
|
314
|
+
if not structure:
|
|
315
|
+
return
|
|
316
|
+
key = bytes(structure), tuple(coordinates)
|
|
317
|
+
if key not in contour_ids:
|
|
318
|
+
contour_ids[key] = len(contours)
|
|
319
|
+
contours.append(key)
|
|
320
|
+
refs.append((contour_ids[key], first_x, first_y))
|
|
321
|
+
|
|
298
322
|
while cursor < len(tokens):
|
|
299
323
|
if tokens[cursor] in commands:
|
|
300
324
|
command = commands.index(tokens[cursor])
|
|
@@ -303,52 +327,70 @@ def pack_paths(glyphs, profiles):
|
|
|
303
327
|
command = 1 if previous == 0 else previous
|
|
304
328
|
else:
|
|
305
329
|
raise ValueError("Font path has an invalid repeated command")
|
|
306
|
-
|
|
307
|
-
values = [int(Fraction(value) * unit)
|
|
308
|
-
if len(values) !=
|
|
330
|
+
count = lengths[command]
|
|
331
|
+
values = [int(Fraction(value) * unit) for value in tokens[cursor:cursor + count]]
|
|
332
|
+
if len(values) != count:
|
|
309
333
|
raise ValueError("Font path has an incomplete command")
|
|
310
334
|
if any(abs(value) * 4 > 2**53 - 1 for value in values):
|
|
311
|
-
raise ValueError("Font coordinates exceed exact JavaScript
|
|
312
|
-
cursor +=
|
|
313
|
-
|
|
335
|
+
raise ValueError("Font coordinates exceed exact JavaScript arithmetic")
|
|
336
|
+
cursor += count
|
|
337
|
+
if command == 0:
|
|
338
|
+
keep_contour()
|
|
339
|
+
structure, coordinates = [], []
|
|
340
|
+
first_x, first_y = x, y = values
|
|
341
|
+
else:
|
|
342
|
+
if previous is None:
|
|
343
|
+
raise ValueError("Font contour does not begin with a move")
|
|
344
|
+
structure.append(command)
|
|
345
|
+
for index, value in enumerate(values):
|
|
346
|
+
if command == 3 or (command not in (2, 3) and index % 2):
|
|
347
|
+
coordinates.append(value - y)
|
|
348
|
+
y = value
|
|
349
|
+
else:
|
|
350
|
+
coordinates.append(value - x)
|
|
351
|
+
x = value
|
|
352
|
+
if command == 5:
|
|
353
|
+
x, y = first_x, first_y
|
|
314
354
|
previous = command
|
|
355
|
+
keep_contour()
|
|
356
|
+
references.append(refs)
|
|
357
|
+
|
|
358
|
+
contour_records, shapes, depths = [], {}, []
|
|
359
|
+
for contour_id, (structure, coordinates) in enumerate(contours):
|
|
315
360
|
record = bytearray()
|
|
316
|
-
unsigned(len(
|
|
317
|
-
|
|
318
|
-
for
|
|
319
|
-
record
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
x = value
|
|
327
|
-
if command == 0:
|
|
328
|
-
first_x, first_y = x, y
|
|
329
|
-
elif command == 5:
|
|
330
|
-
x, y = first_x, first_y
|
|
331
|
-
structure = bytes(command for command, _ in segments)
|
|
332
|
-
coordinates = tuple(value for _, values in segments for value in values)
|
|
333
|
-
candidates = structures.setdefault(structure, [])
|
|
334
|
-
for parent, previous_coordinates in candidates:
|
|
361
|
+
unsigned(len(structure) * 2, record)
|
|
362
|
+
record.extend(structure)
|
|
363
|
+
for value in coordinates:
|
|
364
|
+
signed(value, record)
|
|
365
|
+
# Small residuals compress better even when their first encoding costs another byte.
|
|
366
|
+
best = (len(record) + sum(abs(value).bit_length() for value in coordinates), len(record))
|
|
367
|
+
parent_depth = -1
|
|
368
|
+
for parent, previous_coordinates in shapes.setdefault(structure, []):
|
|
369
|
+
if depths[parent] >= PARENT_DEPTH:
|
|
370
|
+
continue
|
|
335
371
|
shared = bytearray()
|
|
336
372
|
unsigned(parent * 2 + 1, shared)
|
|
337
|
-
for value,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
profile_shapes = {}
|
|
373
|
+
residuals = [value - previous_value for value, previous_value in zip(coordinates, previous_coordinates)]
|
|
374
|
+
for value in residuals:
|
|
375
|
+
signed(value, shared)
|
|
376
|
+
score = (len(shared) + sum(abs(value).bit_length() for value in residuals), len(shared))
|
|
377
|
+
if score < best:
|
|
378
|
+
best = score
|
|
379
|
+
record, parent_depth = shared, depths[parent]
|
|
380
|
+
depths.append(parent_depth + 1)
|
|
381
|
+
shapes[structure].append((contour_id, coordinates))
|
|
382
|
+
contour_records.append(record)
|
|
383
|
+
|
|
384
|
+
profile_records, profile_ids, profile_shapes, profile_depths = [], {}, {}, []
|
|
385
|
+
profile_values = {}
|
|
349
386
|
for glyph_id, profile in sorted(profiles.items()):
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
key = profile[0], tuple(None if strip is None else tuple(strip) for strip in profile[1:])
|
|
388
|
+
if key in profile_values:
|
|
389
|
+
profile_ids[glyph_id] = profile_values[key]
|
|
390
|
+
continue
|
|
391
|
+
profile_id = len(profile_records)
|
|
392
|
+
profile_ids[glyph_id] = profile_id
|
|
393
|
+
profile_values[key] = profile_id
|
|
352
394
|
record = bytearray()
|
|
353
395
|
unsigned((len(profile) - 1) * 2, record)
|
|
354
396
|
signed(profile[0], record)
|
|
@@ -362,7 +404,10 @@ def pack_paths(glyphs, profiles):
|
|
|
362
404
|
signed(strip[1] - right, record)
|
|
363
405
|
left, right = strip
|
|
364
406
|
shape = profile[0], tuple(strip is not None for strip in profile[1:])
|
|
407
|
+
parent_depth = -1
|
|
365
408
|
for parent_id, parent in profile_shapes.setdefault(shape, []):
|
|
409
|
+
if profile_depths[parent_id] >= PARENT_DEPTH:
|
|
410
|
+
continue
|
|
366
411
|
shared = bytearray()
|
|
367
412
|
unsigned(parent_id * 2 + 1, shared)
|
|
368
413
|
left = right = 0
|
|
@@ -376,18 +421,80 @@ def pack_paths(glyphs, profiles):
|
|
|
376
421
|
if len(shared) >= len(record):
|
|
377
422
|
break
|
|
378
423
|
if len(shared) < len(record):
|
|
379
|
-
record = shared
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
424
|
+
record, parent_depth = shared, profile_depths[parent_id]
|
|
425
|
+
profile_depths.append(parent_depth + 1)
|
|
426
|
+
profile_shapes[shape].append((profile_id, profile))
|
|
427
|
+
profile_records.append(record)
|
|
428
|
+
|
|
429
|
+
glyph_records = []
|
|
430
|
+
for glyph_id, (values, refs) in enumerate(zip(metrics, references)):
|
|
431
|
+
record = bytearray()
|
|
432
|
+
for value, base in zip(values, metric_bases):
|
|
433
|
+
signed(value - base, record)
|
|
434
|
+
unsigned(profile_ids.get(glyph_id, -1) + 1, record)
|
|
435
|
+
unsigned(len(refs), record)
|
|
436
|
+
for contour_id, x, y in refs:
|
|
437
|
+
unsigned(contour_id, record)
|
|
438
|
+
signed(x, record)
|
|
439
|
+
signed(y, record)
|
|
440
|
+
glyph_records.append(record)
|
|
441
|
+
|
|
442
|
+
pages, indices = [], bytearray()
|
|
443
|
+
unsigned(unit, indices)
|
|
444
|
+
unsigned(metric_unit, indices)
|
|
445
|
+
for value in metric_bases:
|
|
446
|
+
signed(value, indices)
|
|
447
|
+
for name in SCALAR_MAPS:
|
|
448
|
+
unsigned(len(data[name]), indices)
|
|
449
|
+
previous_key = previous_value = 0
|
|
450
|
+
for key, value in sorted(data[name].items()):
|
|
451
|
+
unsigned(key - previous_key, indices)
|
|
452
|
+
signed(value - previous_value, indices)
|
|
453
|
+
previous_key, previous_value = key, value
|
|
454
|
+
unsigned(len(data["scriptAlternates"]), indices)
|
|
455
|
+
previous_key, previous_values = 0, [0, 0]
|
|
456
|
+
for key, values in sorted(data["scriptAlternates"].items()):
|
|
457
|
+
unsigned(key - previous_key, indices)
|
|
458
|
+
previous_key = key
|
|
459
|
+
unsigned(len(values), indices)
|
|
460
|
+
for index, value in enumerate(values):
|
|
461
|
+
signed(value - previous_values[index], indices)
|
|
462
|
+
previous_values[index] = value
|
|
463
|
+
for records in (glyph_records, contour_records, profile_records):
|
|
464
|
+
page, offsets = bytearray(), []
|
|
465
|
+
for record in records:
|
|
466
|
+
if len(record) > PAGE_BYTES:
|
|
467
|
+
raise ValueError("Font record exceeds its independent page bound")
|
|
468
|
+
if len(page) + len(record) > PAGE_BYTES:
|
|
469
|
+
pages.append(bytes(page))
|
|
470
|
+
page = bytearray()
|
|
471
|
+
offsets.append(len(pages) * PAGE_STRIDE + len(page))
|
|
472
|
+
page.extend(record)
|
|
473
|
+
offsets.append(len(pages) * PAGE_STRIDE + len(page))
|
|
474
|
+
pages.append(bytes(page))
|
|
475
|
+
unsigned(len(offsets), indices)
|
|
476
|
+
previous = 0
|
|
477
|
+
for offset in offsets:
|
|
478
|
+
unsigned(offset - previous, indices)
|
|
479
|
+
previous = offset
|
|
480
|
+
|
|
481
|
+
def compressed(data):
|
|
482
|
+
return base64.b64encode(zlib.compress(data, level=9, wbits=-15)).decode("ascii")
|
|
483
|
+
|
|
484
|
+
packed = [(len(page), compressed(page)) for page in pages]
|
|
485
|
+
return packed, compressed(indices), len(indices), {
|
|
486
|
+
"contours": len(contours), "contourReferences": sum(map(len, references)), "profileRecords": len(profile_records),
|
|
487
|
+
"pages": len(pages), "pageBytes": PAGE_BYTES, "indexBytes": len(indices),
|
|
488
|
+
"recordBytes": sum(map(len, pages)), "maxParentDepth": PARENT_DEPTH,
|
|
489
|
+
}
|
|
384
490
|
|
|
385
491
|
|
|
386
492
|
PATH_DECODER = """{
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
const
|
|
493
|
+
// The font owns immutable pages and bounded hot records. No document data enters these caches.
|
|
494
|
+
const pages=__PAGES__,invalid=()=>{throw new Error('Invalid math font data');};
|
|
495
|
+
const inflate=(encoded,size)=>{
|
|
496
|
+
const source=atob(encoded),bytes=new Uint8Array(size);let position=0,held=0,available=0,written=0;
|
|
497
|
+
const fill=count=>{while(available<count&&position<source.length){held|=source.charCodeAt(position++)<<available;available+=8;}};
|
|
391
498
|
const bits=count=>{fill(count);if(available<count)invalid();const value=held&((1<<count)-1);held>>>=count;available-=count;return value;};
|
|
392
499
|
const tree=lengths=>{const width=Math.max(...lengths),counts=new Uint16Array(width+1),next=new Uint16Array(width+1),table=new Uint16Array(1<<width);
|
|
393
500
|
for(const length of lengths)if(length)counts[length]++;let code=0;
|
|
@@ -414,31 +521,64 @@ const code=value-257,extra=code<8?0:(code>>>2)-1,length=code===28?258:code<8?cod
|
|
|
414
521
|
const offset=symbol(distance);if(offset>29)invalid();const shift=offset<4?0:(offset>>>1)-1,back=offset<4?offset+1:((offset&1)+2)*2**shift+1+bits(shift);
|
|
415
522
|
if(back>written||written+length>bytes.length)invalid();for(let i=0;i<length;i++){bytes[written]=bytes[written-back];written++;}}
|
|
416
523
|
}
|
|
417
|
-
if(written!==bytes.length)invalid();
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
const signed=()
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
for(const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
for(let
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if(
|
|
524
|
+
if(written!==bytes.length||position-Math.floor(available/8)!==source.length)invalid();return bytes;
|
|
525
|
+
};
|
|
526
|
+
const unsigned=r=>{let value=0,shift=0,byte;do{if(r[1]>=r[2]||shift>49)invalid();byte=r[0][r[1]++];value+=(byte&127)*2**shift;shift+=7;}while(byte&128);if(!Number.isSafeInteger(value))invalid();return value;};
|
|
527
|
+
const signed=r=>{if(r[1]>=r[2])invalid();let value=r[0][r[1]++];if(value>=240){if(value===255){value=4080+unsigned(r);if(!Number.isSafeInteger(value))invalid();}else{if(r[1]>=r[2])invalid();value=240+(value-240)*256+r[0][r[1]++];}}return value%2?-(value+1)/2:value/2;};
|
|
528
|
+
const complete=r=>{if(r[1]!==r[2])invalid();};
|
|
529
|
+
const [unit,metricUnit,metricBases,glyphs,contours,profiles]=(()=>{
|
|
530
|
+
const bytes=inflate(__INDEX__,__SIZE__),r=[bytes,0,bytes.length],unit=unsigned(r),metricUnit=unsigned(r);if(!unit||!metricUnit)invalid();const metricBases=Array.from({length:7},()=>signed(r));
|
|
531
|
+
for(const name of __MAP_FIELDS__){const count=unsigned(r),map=MATH_FONT[name]={};if(count>bytes.length)invalid();let key=0,value=0;
|
|
532
|
+
for(let i=0;i<count;i++){key+=unsigned(r);value+=signed(r);map[key]=value;}}
|
|
533
|
+
const scripts=MATH_FONT.scriptAlternates={},scriptCount=unsigned(r),previous=[0,0];let key=0;if(scriptCount>bytes.length)invalid();
|
|
534
|
+
for(let i=0;i<scriptCount;i++){key+=unsigned(r);const count=unsigned(r);if(count<1||count>2)invalid();const values=[];for(let j=0;j<count;j++){previous[j]+=signed(r);values.push(previous[j]);}scripts[key]=values;}
|
|
535
|
+
const tables=Array.from({length:3},()=>{const count=unsigned(r);if(count<1||count>bytes.length)invalid();const offsets=new Uint32Array(count);let previous=0;
|
|
536
|
+
for(let i=0;i<count;i++){previous+=unsigned(r);if(previous>=pages.length*32768)invalid();offsets[i]=previous;}return offsets;});complete(r);return [unit,metricUnit,metricBases,...tables];
|
|
537
|
+
})();
|
|
538
|
+
const pageCache=new Map(),glyphCache=new Map(),profileCache=new Map();
|
|
539
|
+
const remember=(cache,id,value,limit)=>{if(cache.size>=limit)cache.delete(cache.keys().next().value);cache.set(id,value);return value;};
|
|
540
|
+
const touch=(cache,id)=>{const value=cache.get(id);if(value!==undefined){cache.delete(id);cache.set(id,value);}return value;};
|
|
541
|
+
const record=(offsets,id)=>{
|
|
542
|
+
if(!Number.isInteger(id)||id<0||id+1>=offsets.length)return;
|
|
543
|
+
const offset=offsets[id],page=offset>>>15,next=offsets[id+1];let bytes=touch(pageCache,page);
|
|
544
|
+
if(!bytes){const source=pages[page];if(!source||source[0]>16384)invalid();bytes=remember(pageCache,page,inflate(source[1],source[0]),8);}
|
|
545
|
+
const begin=offset&32767,end=next>>>15===page?next&32767:bytes.length;if(begin>=end||end>bytes.length)invalid();return [bytes,begin,end];
|
|
546
|
+
};
|
|
547
|
+
const commandLengths=[2,2,1,1,6,0],commandNames='MLHVCZ';
|
|
548
|
+
const contour=(id,local,depth=0)=>{
|
|
549
|
+
if(local.has(id))return local.get(id);if(depth>4)invalid();const r=record(contours,id);if(!r)invalid();const header=unsigned(r);let commands,coordinates;
|
|
550
|
+
if(header%2){const parentId=Math.floor(header/2);if(parentId>=id)invalid();const parent=contour(parentId,local,depth+1);commands=parent[0];coordinates=new Float64Array(parent[1].length);
|
|
551
|
+
for(let i=0;i<coordinates.length;i++)coordinates[i]=parent[1][i]+signed(r);}
|
|
552
|
+
else{const count=header/2;if(r[1]+count>r[2])invalid();commands=r[0].subarray(r[1],r[1]+count);r[1]+=count;let values=0;
|
|
553
|
+
for(const command of commands){if(command<1||command>5)invalid();values+=commandLengths[command];}if(values>r[2]-r[1])invalid();coordinates=new Float64Array(values);
|
|
554
|
+
for(let i=0;i<values;i++)coordinates[i]=signed(r);}
|
|
555
|
+
complete(r);const value=[commands,coordinates];local.set(id,value);return value;
|
|
556
|
+
};
|
|
557
|
+
const glyphRecord=id=>{
|
|
558
|
+
let cached=touch(glyphCache,id);if(cached)return cached;const r=record(glyphs,id);if(!r)return;
|
|
559
|
+
const glyph=[];for(let i=0;i<7;i++)glyph.push(signed(r)+metricBases[i]);glyph[3]+=glyph[0]-glyph[1];glyph[6]+=glyph[0]/2;for(let i=0;i<7;i++)glyph[i]/=metricUnit;const profile=unsigned(r)-1,count=unsigned(r),local=new Map();let path='';
|
|
560
|
+
if(profile>=profiles.length-1||count>r[2]-r[1])invalid();
|
|
561
|
+
for(let part=0;part<count;part++){const id=unsigned(r),firstX=signed(r),firstY=signed(r),[commands,coordinates]=contour(id,local);let x=firstX,y=firstY,cursor=0;
|
|
562
|
+
path+='M'+x/unit+' '+y/unit;
|
|
563
|
+
for(const command of commands){path+=commandNames[command];for(let index=0;index<commandLengths[command];index++){if(index)path+=' ';
|
|
564
|
+
if(command===3||(command!==2&&command!==3&&index%2)){y+=coordinates[cursor++];path+=y/unit;}else{x+=coordinates[cursor++];path+=x/unit;}}
|
|
565
|
+
if(command===5){x=firstX;y=firstY;}}
|
|
566
|
+
}complete(r);glyph.push(path);cached={glyph,profile};return remember(glyphCache,id,cached,256);
|
|
567
|
+
};
|
|
568
|
+
const profileRecord=(id,depth=0)=>{
|
|
569
|
+
const cached=touch(profileCache,id);if(cached)return cached;if(depth>4)invalid();const r=record(profiles,id);if(!r)invalid();const header=unsigned(r);let profile,left=0,right=0;
|
|
570
|
+
if(header%2){const parentId=Math.floor(header/2);if(parentId>=id)invalid();const parent=profileRecord(parentId,depth+1);profile=[parent[0]];
|
|
571
|
+
for(let i=1;i<parent.length;i++){const strip=parent[i];if(!strip){profile.push(null);continue;}left+=signed(r);right+=signed(r);profile.push([strip[0]+left,strip[1]+right]);}}
|
|
572
|
+
else{profile=[signed(r)];const count=header/2;if(count>r[2]-r[1])invalid();for(let i=0;i<count;i++){const token=unsigned(r);if(!token){profile.push(null);continue;}const value=token-1;left+=value%2?-(value+1)/2:value/2;right+=signed(r);profile.push([left,right]);}}
|
|
573
|
+
complete(r);return remember(profileCache,id,profile,128);
|
|
574
|
+
};
|
|
575
|
+
MATH_FONT.glyph=id=>glyphRecord(id)?.glyph;
|
|
576
|
+
MATH_FONT.profile=id=>{const glyph=glyphRecord(id);return glyph&&glyph.profile>=0?profileRecord(glyph.profile):undefined;};
|
|
438
577
|
}
|
|
439
578
|
"""
|
|
440
579
|
|
|
441
580
|
|
|
581
|
+
|
|
442
582
|
def main():
|
|
443
583
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
444
584
|
parser.add_argument("--font", type=Path, help="Optional pinned source font; otherwise fetch it")
|
|
@@ -464,16 +604,18 @@ def main():
|
|
|
464
604
|
"The font authors do not maintain this conversion.\n"
|
|
465
605
|
"Complete original work: https://mirrors.ctan.org/fonts/lm-math.zip\n"
|
|
466
606
|
"Conversion details: FONT-SOURCE.md.\n\n").encode("utf-8") + gust + b"\n" + lppl
|
|
467
|
-
|
|
607
|
+
pages, index, size, packing = pack_font(data)
|
|
468
608
|
glyph_count = len(data["glyphs"])
|
|
469
609
|
profile_count = len(data["edgeProfiles"])
|
|
470
|
-
data["
|
|
471
|
-
data["edgeProfiles"]
|
|
472
|
-
|
|
610
|
+
character_count = len(data["chars"])
|
|
611
|
+
del data["glyphs"], data["edgeProfiles"]
|
|
612
|
+
for name in (*SCALAR_MAPS, "scriptAlternates"):
|
|
613
|
+
del data[name]
|
|
614
|
+
output = (b"/*!\n" + notice + b"\n*/\nconst MATH_FONT=" + json.dumps(data, ensure_ascii=True, separators=(",", ":")).encode("utf-8") + b";\n" + PATH_DECODER.replace("__PAGES__", json.dumps(pages, separators=(",", ":"))).replace("__INDEX__", json.dumps(index)).replace("__SIZE__", str(size)).replace("__MAP_FIELDS__", json.dumps(SCALAR_MAPS, separators=(",", ":"))).encode("utf-8"))
|
|
473
615
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
474
616
|
args.out.write_bytes(output)
|
|
475
617
|
args.out.with_name("FONT-LICENSE.txt").write_bytes(notice)
|
|
476
|
-
print(json.dumps({"sourceSha256": hashlib.sha256(font_bytes).hexdigest(), "licenseSha256": hashlib.sha256(notice).hexdigest(), "glyphs": glyph_count, "characters":
|
|
618
|
+
print(json.dumps({"sourceSha256": hashlib.sha256(font_bytes).hexdigest(), "licenseSha256": hashlib.sha256(notice).hexdigest(), "glyphs": glyph_count, "characters": character_count, "edgeProfiles": profile_count, "packing": packing, "bytes": len(output), "gzipBytes": len(gzip.compress(output, compresslevel=9, mtime=0))}, indent=2))
|
|
477
619
|
|
|
478
620
|
|
|
479
621
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export interface DelimiterOptions {singleDollar?: boolean; brackets?: boolean}
|
|
3
|
+
export interface MathSpan {
|
|
4
|
+
start: number; end: number; bodyStart: number; bodyEnd: number;
|
|
5
|
+
open: string; close: string; displayMode: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Index a raw, host-owned text region. Does not parse Markdown or unescape source. */
|
|
8
|
+
export function createMathMatcher(source: string, options?: DelimiterOptions): (start: number, limit?: number) => MathSpan | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// A boundary index for a host-owned text region, not a Markdown parser.
|
|
3
|
+
export function createMathMatcher(source, {singleDollar = false, brackets = true} = {}) {
|
|
4
|
+
if (typeof source !== 'string') throw new TypeError('Math boundaries require source text.');
|
|
5
|
+
const markers = new Map(), next = Object.create(null);
|
|
6
|
+
let slashes = 0, line = 0;
|
|
7
|
+
for (let at = 0; at < source.length;) {
|
|
8
|
+
const char = source[at];
|
|
9
|
+
if (char === '\\') { slashes++; at++; continue; }
|
|
10
|
+
if (char === '\n' || char === '\r') line++;
|
|
11
|
+
if (char === '$') {
|
|
12
|
+
let end = at + 1;
|
|
13
|
+
while (source[end] === '$') end++;
|
|
14
|
+
const count = end - at;
|
|
15
|
+
if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar))
|
|
16
|
+
markers.set(at, {at, marker: count === 2 ? '$$' : '$', line});
|
|
17
|
+
at = end;
|
|
18
|
+
} else {
|
|
19
|
+
if (brackets && slashes & 1 && '()[]'.includes(char))
|
|
20
|
+
markers.set(at - 1, {at: at - 1, marker: '\\' + char, line});
|
|
21
|
+
at++;
|
|
22
|
+
}
|
|
23
|
+
slashes = 0;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of [...markers.values()].reverse()) {
|
|
26
|
+
const closing = entry.marker === '\\(' ? '\\)' : entry.marker === '\\[' ? '\\]' : entry.marker;
|
|
27
|
+
entry.close = next[closing];
|
|
28
|
+
next[entry.marker] = entry;
|
|
29
|
+
}
|
|
30
|
+
return function match(start, limit = source.length) {
|
|
31
|
+
const open = markers.get(start), close = open?.close;
|
|
32
|
+
if (!close || open.marker === '\\)' || open.marker === '\\]' || close.at + close.marker.length > limit) return null;
|
|
33
|
+
const bodyStart = start + open.marker.length, bodyEnd = close.at;
|
|
34
|
+
if (open.marker === '$' && (open.line !== close.line || bodyStart === bodyEnd ||
|
|
35
|
+
/\s/.test(source[bodyStart]) || /\s/.test(source[bodyEnd - 1]) || /[0-9]/.test(source[bodyEnd + 1] || ''))) return null;
|
|
36
|
+
return {start, end: bodyEnd + close.marker.length, bodyStart, bodyEnd,
|
|
37
|
+
open: open.marker, close: close.marker, displayMode: open.marker === '$$' || open.marker === '\\['};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Run the installed examples
|
|
2
|
+
|
|
3
|
+
From a consumer directory, install `rapier-math`, then run
|
|
4
|
+
`node node_modules/rapier-math/examples/formula.mjs`.
|
|
5
|
+
|
|
6
|
+
For Markdown and streaming, also install `markdown-it` and run `markdown.mjs` or `stream.mjs`.
|
|
7
|
+
For unified, install `unified remark-parse remark-math remark-rehype rehype-sanitize rehype-stringify hast-util-from-html`
|
|
8
|
+
and run `unified.mjs`. The remark example uses remark's syntax; it does not recognize backslash delimiters.
|
|
9
|
+
|
|
10
|
+
Serve the package directory over HTTP and open `examples/browser.html`. Its external module imports the
|
|
11
|
+
installed ESM renderer, fits the output and releases observation before replacement and on page exit.
|
|
12
|
+
Start `new Worker('./worker.mjs', {type: 'module'})` from this directory and post `{id, source}`.
|
|
13
|
+
Use an id owned by the current message and ignore stale replies. Workers render SVG; they do not call `observe`.
|
|
14
|
+
|
|
15
|
+
`react.tsx` is a small React 19/SSR component to copy into an existing TypeScript application. Install
|
|
16
|
+
`react react-dom` and the development types `@types/react @types/react-dom`. No framework is required by the core.
|
|
17
|
+
|
|
18
|
+
For streamed UTF-8, use `TextDecoderStream` or `decoder.decode(bytes, {stream: true})` and flush the decoder
|
|
19
|
+
at EOF. Feed decoded strings into one stream per message. Append incoming chunks to a text node for immediate
|
|
20
|
+
literal progress, and replace the whole preview when `write` returns a snapshot. Replace it with `finish()` at EOF.
|
|
21
|
+
Do not concatenate transport data into HTML. On cancellation call `abort`; on replacement cancel the old producer
|
|
22
|
+
before `reset`. A producer that still runs needs its own generation check before calling `write`.
|
|
23
|
+
|
|
24
|
+
The examples retain the original source as the editable/copyable authority. Markdown hosts can normalize
|
|
25
|
+
newlines or escapes in their syntax nodes; an HTML serialization is not an exact source archive.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<meta name="viewport" content="width=device-width">
|
|
4
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src-attr 'unsafe-inline'; style-src-elem 'none'; worker-src 'self'; connect-src 'none'; base-uri 'none'">
|
|
5
|
+
<title>Rapier Math example</title>
|
|
6
|
+
<label>TeX <textarea id="source">\frac{-b\pm\sqrt{b^2-4ac}}{2a}</textarea></label>
|
|
7
|
+
<button id="render" type="button">Render</button>
|
|
8
|
+
<main id="equation"></main>
|
|
9
|
+
<pre id="errors"></pre>
|
|
10
|
+
<script type="module" src="./browser.mjs"></script>
|