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.
Files changed (69) hide show
  1. package/README.md +187 -85
  2. package/adoption-check.mjs +149 -0
  3. package/check.mjs +21 -0
  4. package/classic.d.ts +4 -0
  5. package/delimiters.d.ts +8 -0
  6. package/delimiters.mjs +39 -0
  7. package/examples/README.md +25 -0
  8. package/examples/browser.html +10 -0
  9. package/examples/browser.mjs +24 -0
  10. package/examples/formula.mjs +12 -0
  11. package/examples/markdown.mjs +9 -0
  12. package/examples/react.tsx +19 -0
  13. package/examples/stream.mjs +19 -0
  14. package/examples/unified.mjs +26 -0
  15. package/examples/worker.mjs +7 -0
  16. package/index.d.ts +43 -0
  17. package/index.mjs +205 -108
  18. package/integration.d.ts +9 -0
  19. package/integration.mjs +28 -0
  20. package/markdown-it.d.ts +6 -0
  21. package/markdown-it.mjs +45 -0
  22. package/markdown-rules.mjs +65 -0
  23. package/math-components.json +6 -6
  24. package/package.json +78 -5
  25. package/rapier-math-1.1.0.js +2980 -0
  26. package/rehype.d.ts +6 -0
  27. package/rehype.mjs +37 -0
  28. package/src/math/FONT-SOURCE.md +32 -6
  29. package/src/math/README.md +210 -0
  30. package/src/math/adoption-verify.mjs +149 -0
  31. package/src/math/api.js +37 -14
  32. package/src/math/browser.js +67 -30
  33. package/src/math/build-font.py +237 -95
  34. package/src/math/classic.d.ts +4 -0
  35. package/src/math/delimiters.d.ts +8 -0
  36. package/src/math/delimiters.mjs +39 -0
  37. package/src/math/examples/README.md +25 -0
  38. package/src/math/examples/browser.html +10 -0
  39. package/src/math/examples/browser.mjs +24 -0
  40. package/src/math/examples/formula.mjs +12 -0
  41. package/src/math/examples/markdown.mjs +9 -0
  42. package/src/math/examples/react.tsx +19 -0
  43. package/src/math/examples/stream.mjs +19 -0
  44. package/src/math/examples/unified.mjs +26 -0
  45. package/src/math/examples/worker.mjs +7 -0
  46. package/src/math/fallbacks.js +5 -5
  47. package/src/math/font.js +59 -26
  48. package/src/math/index.d.ts +43 -0
  49. package/src/math/integration.d.ts +9 -0
  50. package/src/math/integration.mjs +28 -0
  51. package/src/math/layout.js +37 -33
  52. package/src/math/markdown-it.d.ts +6 -0
  53. package/src/math/markdown-it.mjs +45 -0
  54. package/src/math/markdown-rules.mjs +65 -0
  55. package/src/math/package-build.mjs +4 -4
  56. package/src/math/package-check.mjs +118 -0
  57. package/src/math/package-readme.mjs +8 -0
  58. package/src/math/package-verify.mjs +7 -2
  59. package/src/math/rehype.d.ts +6 -0
  60. package/src/math/rehype.mjs +37 -0
  61. package/src/math/stream.d.ts +14 -0
  62. package/src/math/stream.mjs +46 -0
  63. package/src/package.json +1 -1
  64. package/src/shell/math-resources.json +184 -25
  65. package/src/tools/build-math.mjs +23 -4
  66. package/stream.d.ts +14 -0
  67. package/stream.mjs +46 -0
  68. package/verify.mjs +7 -2
  69. package/rapier-math-1.0.1.js +0 -2883
@@ -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,view=doc?.defaultView;
121
- if(!view)return ()=>{};
122
- let state=mathBrowserDocuments.get(doc);
123
- if(!state) {
124
- const nodes=new Map(),columns=new Set();let frame=0,printing=false;
125
- const refresh=()=> {
126
- frame=0;
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 ()=>{for(const svg of nodes)state.nodes.delete(svg);state.schedule();};
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
  }
@@ -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
- def pack_paths(glyphs, profiles):
261
- """Store exact coordinate deltas, sharing matching outline structures."""
262
- output = bytearray()
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
- unsigned(value * 2 if value >= 0 else -value * 2 - 1, target)
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
- unsigned(unit, output)
284
- unsigned(len(glyphs), output)
285
- metric_unit = math.lcm(*(Fraction(value).denominator for glyph in glyphs for value in glyph[:7]))
286
- unsigned(metric_unit, output)
287
- previous_metrics = [0] * 7
288
- for glyph in glyphs:
289
- for index, value in enumerate(glyph[:7]):
290
- value = int(value * metric_unit)
291
- signed(value - previous_metrics[index], output)
292
- previous_metrics[index] = value
293
- structures = {}
294
- for glyph_index, (glyph, tokens) in enumerate(zip(glyphs, paths)):
295
- cursor = 0
296
- segments = []
297
- previous = None
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
- length = lengths[command]
307
- values = [int(Fraction(value) * unit) if "." in value else int(value) * unit for value in tokens[cursor:cursor + length]]
308
- if len(values) != length:
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 integer arithmetic")
312
- cursor += length
313
- segments.append((command, values))
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(segments) * 2, record)
317
- x = y = first_x = first_y = 0
318
- for command, values in segments:
319
- record.append(command)
320
- for index, value in enumerate(values):
321
- if command == 3 or (command not in (2, 3) and index % 2):
322
- signed(value - y, record)
323
- y = value
324
- else:
325
- signed(value - x, record)
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, previous in zip(coordinates, previous_coordinates):
338
- signed(value - previous, shared)
339
- if len(shared) >= len(record):
340
- break
341
- if len(shared) < len(record):
342
- record = shared
343
- candidates.append((glyph_index, coordinates))
344
- output.extend(record)
345
- glyph[7] = ""
346
- unsigned(len(profiles), output)
347
- previous_id = -1
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
- unsigned(glyph_id - previous_id - 1, output)
351
- previous_id = glyph_id
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
- profile_shapes[shape].append((glyph_id, profile))
381
- output.extend(record)
382
- packed = zlib.compress(output, level=9, wbits=-15)
383
- return base64.b64encode(packed).decode("ascii"), len(output)
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
- const source=Uint8Array.from(atob(__PATHS__),value=>value.charCodeAt(0)),bytes=new Uint8Array(__SIZE__);
388
- let position=0,held=0,available=0,written=0,cursor=0;
389
- const invalid=()=>{throw new Error('Invalid math font data');};
390
- const fill=count=>{while(available<count&&position<source.length){held|=source[position++]<<available;available+=8;}};
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
- const unsigned=()=>{let value=0,shift=0,byte;do{if(cursor>=bytes.length||shift>49)invalid();byte=bytes[cursor++];value+=(byte&127)*2**shift;shift+=7;}while(byte&128);return value;};
419
- const unit=unsigned();if(!unit)invalid();
420
- const signed=()=>{const value=unsigned();return value&1?-(value+1)/2:value/2;};
421
- const glyphCount=unsigned(),metricUnit=unsigned(),metrics=Array(7).fill(0);if(!metricUnit)invalid();
422
- for(let i=0;i<glyphCount;i++){const glyph=[];for(let j=0;j<7;j++){metrics[j]+=signed();glyph.push(metrics[j]/metricUnit);}glyph.push('');MATH_FONT.glyphs.push(glyph);}
423
- const commands='MLHVCZ',lengths=[2,2,1,1,6,0];
424
- for(const glyph of MATH_FONT.glyphs){const header=unsigned();
425
- if(header&1){glyph[7]=MATH_FONT.glyphs[header>>>1][7].replace(/-?\\d+(?:\\.\\d+)?/g,value=>(Math.round(Number(value)*unit)+signed())/unit);continue;}
426
- let x=0,y=0,firstX=0,firstY=0,path='';const count=header>>>1;
427
- for(let segment=0;segment<count;segment++){const command=bytes[cursor++];if(command>5)invalid();path+=commands[command];
428
- for(let index=0;index<lengths[command];index++){if(index)path+=' ';if(command===3||(command!==2&&command!==3&&index%2)){y+=signed();path+=y/unit;}else{x+=signed();path+=x/unit;}}
429
- if(command===0){firstX=x;firstY=y;}else if(command===5){x=firstX;y=firstY;}}
430
- glyph[7]=path;}
431
- const profileCount=unsigned();let id=-1;
432
- for(let i=0;i<profileCount;i++){id+=unsigned()+1;const header=unsigned();let profile,left=0,right=0;
433
- if(header&1){const parent=MATH_FONT.edgeProfiles[header>>>1];if(!parent)invalid();profile=[parent[0]];
434
- for(const strip of parent.slice(1)){if(!strip){profile.push(null);continue;}left+=signed();right+=signed();profile.push([strip[0]+left,strip[1]+right]);}}
435
- else{profile=[signed()];for(let j=0;j<header>>>1;j++){const token=unsigned();if(!token){profile.push(null);continue;}const value=token-1;left+=value&1?-(value+1)/2:value/2;right+=signed();profile.push([left,right]);}}
436
- MATH_FONT.edgeProfiles[id]=profile;}
437
- if(cursor!==bytes.length)invalid();
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
- paths, size = pack_paths(data["glyphs"], data["edgeProfiles"])
607
+ pages, index, size, packing = pack_font(data)
468
608
  glyph_count = len(data["glyphs"])
469
609
  profile_count = len(data["edgeProfiles"])
470
- data["glyphs"] = []
471
- data["edgeProfiles"] = {}
472
- output = (b"/*!\n" + notice + b"\n*/\nconst MATH_FONT=" + json.dumps(data, ensure_ascii=True, separators=(",", ":")).encode("utf-8") + b";\n" + PATH_DECODER.replace("__PATHS__", json.dumps(paths)).replace("__SIZE__", str(size)).encode("utf-8"))
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": len(data["chars"]), "edgeProfiles": profile_count, "bytes": len(output), "gzipBytes": len(gzip.compress(output, compresslevel=9, mtime=0))}, indent=2))
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,4 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import type API from './index.js';
3
+ declare global {var RapierMath: typeof API;}
4
+ export {};
@@ -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>