rapier-math 0.0.0-stage → 1.0.2

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.
@@ -0,0 +1,580 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Boxes use ems, a baseline at y=0 and a downward SVG axis. Font outlines stay in font units.
3
+ const mathNumber = n => {
4
+ if (!Number.isFinite(n)) throw new Error('Math geometry is not finite');
5
+ const scaled = n * 1e6;
6
+ return String((Number.isFinite(scaled) ? Math.round(scaled) / 1e6 : n) || 0);
7
+ };
8
+ const mathEscape = s => String(s).replace(/[&<>"'\t\n\r]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','\t':'&#9;','\n':'&#10;','\r':'&#13;'}[c]));
9
+ const mathFontRatio = (MATH_FONT.textXHeight / MATH_FONT.textUnits) / (MATH_FONT.xHeight / MATH_FONT.units);
10
+ function mathScale(c) {
11
+ if (!c.level) return c.size;
12
+ const scale = MATH_FONT.constants[c.level === 1 ? 'ScriptPercentScaleDown' : 'ScriptScriptPercentScaleDown'] / 100;
13
+ return c.size * (c.screen ? Math.max(scale, Math.min(1, (c.level === 1 ? 12 : 10) / c.screen.fontSize)) : scale);
14
+ }
15
+
16
+ function mathScreenStroke(c, scale) {
17
+ if (!c.screen) return 0;
18
+ const {fontSize, pixelRatio, dark} = c.screen, size = fontSize * (scale ?? mathScale(c));
19
+ if (!(size > 0)) return 0;
20
+ const small = Math.max(0, Math.min(1, (32 - size) / 16));
21
+ const hairline = MATH_FONT.constants.FractionRuleThickness / MATH_FONT.units * size;
22
+ return small * ((dark ? .18 : .09) / Math.sqrt(pixelRatio) + Math.max(0, (dark ? 1 : .85) / pixelRatio - hairline) * .35) / fontSize;
23
+ }
24
+ const mathConstant = (name, c, fallback) => (MATH_FONT.constants[name] ?? fallback * MATH_FONT.units) / MATH_FONT.units * mathScale(c);
25
+ const mathScriptStyle = (c, cramped = c.cramped) => ({...c, level: Math.min(2, c.level + 1), display: false, cramped});
26
+ const mathScreenRelayout = (n,c,ratio) => mathLayout(n,{...c,screen:{...c.screen,fontSize:c.screen.fontSize*ratio},cache:new WeakMap()});
27
+ const mathBox = (w = 0, h = 0, d = 0, svg = '', extra = {}) => ({w, h, d, svg, left: 0, right: w, inkTop: -h, inkBottom: d, class: 'ord', ...extra});
28
+ const mathMove = (b, x, y) => b.svg ? '<g transform="translate(' + mathNumber(x) + ' ' + mathNumber(y) + ')">' + b.svg + '</g>' : '';
29
+
30
+ function mathCompose(items, width, extra = {}) {
31
+ let h = 0, d = 0, left = 0, right = width, inkTop=Infinity, inkBottom=-Infinity, svg = '';
32
+ for (const [b, x, y] of items) {
33
+ if(svg.length+b.svg.length+128>8*1024*1024)throw Object.assign(new Error('Mathematical composition exceeds the SVG budget'),{code:'resource-limit'});
34
+ h = Math.max(h, b.h - y); d = Math.max(d, b.d + y);
35
+ left = Math.min(left, x + b.left); right = Math.max(right, x + b.right);
36
+ if(b.svg) {inkTop=Math.min(inkTop,b.inkTop+y);inkBottom=Math.max(inkBottom,b.inkBottom+y);}
37
+ svg += mathMove(b, x, y);
38
+ }
39
+ const single=items.length===1?items[0]:null,b=single?.[0];
40
+ const glyph=b?.glyph===undefined?{}:{glyph:b.glyph,unitScale:b.unitScale,glyphX:(b.glyphX || 0)+single[1],glyphY:(b.glyphY || 0)+single[2],extended:b.extended,scriptBase:b.scriptBase,stroke:b.stroke};
41
+ return mathBox(width, h, d, svg, {left, right, inkTop:Number.isFinite(inkTop)?inkTop:-h, inkBottom:Number.isFinite(inkBottom)?inkBottom:d,...glyph,...extra});
42
+ }
43
+
44
+ const MATH_STYLED_RANGES = {
45
+ italic: [0x1d434, 0x1d44e, null], bold: [0x1d400, 0x1d41a, 0x1d7ce],
46
+ 'bold-italic': [0x1d468, 0x1d482, null], script: [0x1d49c, 0x1d4b6, null],
47
+ 'bold-script': [0x1d4d0, 0x1d4ea, null], fraktur: [0x1d504, 0x1d51e, null],
48
+ 'bold-fraktur': [0x1d56c, 0x1d586, null], 'double-struck': [0x1d538, 0x1d552, 0x1d7d8],
49
+ 'sans-serif': [0x1d5a0, 0x1d5ba, 0x1d7e2], 'sans-serif-bold': [0x1d5d4, 0x1d5ee, 0x1d7ec],
50
+ 'sans-serif-italic': [0x1d608, 0x1d622, null], 'sans-serif-bold-italic': [0x1d63c, 0x1d656, null],
51
+ monospace: [0x1d670, 0x1d68a, 0x1d7f6],
52
+ };
53
+ const MATH_STYLED_LEGACY = {
54
+ italic: {104: 0x210e},
55
+ script: {66:0x212c,69:0x2130,70:0x2131,72:0x210b,73:0x2110,76:0x2112,77:0x2133,82:0x211b,101:0x212f,103:0x210a,111:0x2134},
56
+ fraktur: {67:0x212d,72:0x210c,73:0x2111,82:0x211c,90:0x2128},
57
+ 'double-struck': {67:0x2102,72:0x210d,78:0x2115,80:0x2119,81:0x211a,82:0x211d,90:0x2124},
58
+ };
59
+ const MATH_STYLED_GREEK = {bold:0x1d6a8, italic:0x1d6e2, 'bold-italic':0x1d71c, 'sans-serif-bold':0x1d756, 'sans-serif-bold-italic':0x1d790};
60
+ const MATH_STYLED_GREEK_SPECIAL = [0x2207,0x2202,0x3f5,0x3d1,0x3f0,0x3d5,0x3f1,0x3d6];
61
+ function mathStyledCodePoint(cp, variant) {
62
+ if (variant === 'normal') variant = 'italic';
63
+ if (MATH_STYLED_LEGACY[variant]?.[cp] !== undefined) return MATH_STYLED_LEGACY[variant][cp];
64
+ const r = MATH_STYLED_RANGES[variant];
65
+ if (r) {
66
+ if (cp >= 65 && cp <= 90) return r[0] + cp - 65;
67
+ if (cp >= 97 && cp <= 122) return r[1] + cp - 97;
68
+ if (r[2] && cp >= 48 && cp <= 57) return r[2] + cp - 48;
69
+ }
70
+ const greek = MATH_STYLED_GREEK[variant];
71
+ if (greek !== undefined) {
72
+ if (cp >= 0x391 && cp <= 0x3a9 && cp !== 0x3a2) return greek + cp - 0x391;
73
+ if (cp >= 0x3b1 && cp <= 0x3c9) return greek + 26 + cp - 0x3b1;
74
+ const special = MATH_STYLED_GREEK_SPECIAL.indexOf(cp);
75
+ if (special === 0) return greek + 25;
76
+ if (special > 0) return greek + 50 + special;
77
+ }
78
+ return cp;
79
+ }
80
+
81
+ function mathGlyphIndex(id, c, extra = {}) {
82
+ if(c.dotless)id=MATH_FONT.dotless?.[id] ?? id;
83
+ const alternates=MATH_FONT.scriptAlternates[id],scale=mathScale(c);
84
+ const pixels=c.screen?c.screen.fontSize*scale:Infinity,
85
+ optical=Math.max(c.level,pixels<10?2:pixels<15?1:0);
86
+ if(optical && alternates)id=alternates[Math.min(optical,alternates.length)-1];
87
+ if(c.flattenAccent)id=MATH_FONT.flatAccents[id] ?? id;
88
+ const g = MATH_FONT.glyph(id), s = scale / MATH_FONT.units, stroke = mathScreenStroke(c,scale), radius = stroke / 2;
89
+ if (!g) throw Object.assign(new Error('Unsupported mathematical glyph'), {code:'unsupported-glyph'});
90
+ if(c.budget) {
91
+ c.budget.outlineBytes=(c.budget.outlineBytes || 0)+g[7].length+96;
92
+ if(c.budget.outlineBytes>4*1024*1024)throw Object.assign(new Error('Mathematical outlines exceed the rendering budget'),{code:'resource-limit'});
93
+ }
94
+ const svg = g[7] ? '<path d="' + g[7] + '" transform="scale(' + mathNumber(s) + ' ' + mathNumber(-s) + ')"'+
95
+ (c.screen ? ' data-math-ink="' + mathNumber(scale) + '"' : '') +
96
+ (stroke ? ' stroke="currentColor" stroke-width="' + mathNumber(stroke / s) + '" stroke-linejoin="round" paint-order="stroke fill"' : '') + '/>' : '';
97
+ return mathBox(g[0] * s, Math.max(0, g[4] * s + radius), Math.max(0, -g[2] * s + radius), svg,
98
+ {left: Math.min(0,g[1]*s-radius), right: Math.max(g[0]*s,g[3]*s+radius), inkTop:-g[4]*s-radius, inkBottom:-g[2]*s+radius, italic: g[5]*s, stroke,
99
+ accent: g[6] == null ? g[0]*s/2 : g[6]*s,glyph:id,unitScale:s,glyphX:0,glyphY:0,extended:!!MATH_FONT.extendedShapes[id],...extra});
100
+ }
101
+
102
+ function mathGlyph(value, c, extra = {}) {
103
+ const cp = value.codePointAt(0);
104
+ if (value.length !== (cp > 0xffff ? 2 : 1)) return mathText(value, c, extra);
105
+ const mapped = mathStyledCodePoint(cp, extra.variant || c.variant);
106
+ const id = MATH_FONT.chars[mapped] ?? MATH_FONT.chars[cp];
107
+ if (id === undefined) {
108
+ const composed = mathFallbackGlyph(value, c, extra);
109
+ if (composed) return composed;
110
+ const e = new Error('Unsupported character U+' + cp.toString(16).toUpperCase());
111
+ e.code = 'unsupported-glyph'; e.command = value; throw e;
112
+ }
113
+ return mathGlyphIndex(id, c, extra);
114
+ }
115
+
116
+ function mathText(value, c, extra = {}) {
117
+ const items = []; let x = 0;
118
+ for (const ch of String(value)) {
119
+ if (/\s/u.test(ch)) { x += mathScale(c)*0.25; continue; }
120
+ const b = mathGlyph(ch, {...c, variant: extra.variant || 'roman'});
121
+ items.push([b,x,0]); x += b.w;
122
+ }
123
+ return mathCompose(items,x,{class:extra.class || 'ord',limits:extra.limits,...extra});
124
+ }
125
+
126
+ function mathRule(w, thickness, y = 0) {
127
+ return mathBox(w, Math.max(0, -y), Math.max(0,y+thickness),
128
+ '<path data-math-rule="" d="M0 ' + mathNumber(y) + 'H' + mathNumber(w) + 'v' + mathNumber(thickness) + 'H0Z"/>',{inkTop:y,inkBottom:y+thickness});
129
+ }
130
+
131
+ function mathStretch(value, target, c, horizontal = false) {
132
+ if (!value || value === '.') return mathBox();
133
+ const cp = value.codePointAt(0), id = MATH_FONT.chars[cp];
134
+ if (id === undefined) return mathFallbackGlyph(value,c,{variant:'roman',stretchTarget:target,
135
+ stretchAxis:horizontal?'horizontal':'vertical'}) || mathGlyph(value,c,{variant:'roman'});
136
+ // Combining accents have no text advance; stretching uses their actual font extent.
137
+ const shape = (glyph, advance) => {
138
+ const b = mathGlyphIndex(glyph,c);
139
+ if (!horizontal || b.w !== 0) return b;
140
+ const g = MATH_FONT.glyph(b.glyph), unit = b.unitScale, x = -g[1] * unit;
141
+ const width = (advance ?? g[3] - g[1]) * unit;
142
+ return mathCompose([[b,x,0]],width,{italic:b.italic,accent:b.accent+x});
143
+ };
144
+ const natural = shape(id);
145
+ let construction = (horizontal ? MATH_FONT.horizontal : MATH_FONT.vertical)[id];
146
+ const extent = b => horizontal ? b.w : b.inkBottom-b.inkTop;
147
+ if (extent(natural) >= target) return natural;
148
+ // Some donor arrowheads advertise a zero connector on the joining side.
149
+ // Keep their designed heads and extend the drawn shaft instead of joining outside those bounds.
150
+ if(horizontal && '→↔↦'.includes(value) && construction?.assembly?.parts.some((p,i,parts)=>i && Math.min(parts[i-1][2],p[1])<MATH_FONT.minConnectorOverlap))construction=null;
151
+ if (!construction) {
152
+ if(!horizontal || natural.w<=0)return natural;
153
+ // These font glyphs have straight central shafts but no MATH assembly.
154
+ // Preserve both end shapes and widen only a narrow central strip.
155
+ if('=⇌⇋↞↠→↔↦'.includes(value)) {
156
+ const g=MATH_FONT.glyph(natural.glyph),unit=natural.unitScale,added=target-natural.w,
157
+ split=g[0]*(value==='↞'?0.75:value==='↠'?0.25:0.5),half=g[0]*0.05,
158
+ left=split-half,right=split+half,delta=added/unit;
159
+ // The two-headed arrows have their plain shaft opposite both heads.
160
+ // All decoded font segments are absolute M/L/H/V/C/Z commands.
161
+ const shift=x=>x<=left?x:x>=right?x+delta:x+(x-left)*delta/(right-left);
162
+ let command='',coordinate=0;
163
+ const path=g[7].replace(/[MLHVCZ]|-?\d+(?:\.\d+)?/g,token=>{
164
+ if(/[MLHVCZ]/.test(token)){command=token;coordinate=0;return token;}
165
+ const isX=command!=='V' && (command==='H' || coordinate%2===0);coordinate++;
166
+ return isX?mathNumber(shift(Number(token))):token;
167
+ });
168
+ const svg='<path d="'+path+'" transform="scale('+mathNumber(unit)+' '+mathNumber(-unit)+')"'+
169
+ (c.screen?' data-math-ink="'+mathNumber(mathScale(c))+'"':'')+
170
+ (natural.stroke?' stroke="currentColor" stroke-width="'+mathNumber(natural.stroke/unit)+'" stroke-linejoin="round" paint-order="stroke fill"':'')+'/>';
171
+ return {...natural,w:target,right:natural.right+added,accent:target/2,svg,glyph:undefined};
172
+ }
173
+ const ratio=target/natural.w;
174
+ return {...natural,w:target,left:natural.left*ratio,right:natural.right*ratio,accent:(natural.accent ?? natural.w/2)*ratio,
175
+ svg:'<g transform="scale('+mathNumber(ratio)+' 1)">'+natural.svg+'</g>',glyph:undefined};
176
+ }
177
+ let last = natural;
178
+ for (const [glyph,advance] of construction.variants || []) {
179
+ last = shape(glyph,advance);
180
+ if (advance * mathScale(c) / MATH_FONT.units >= target) return last;
181
+ }
182
+ const a = construction.assembly;
183
+ if (!a?.parts?.length || !a.parts.some(p=>p[4])) {
184
+ // A wide accent may outgrow its final designed variant without an extender.
185
+ // Expand only its horizontal outline; retain the font's vertical stroke shape.
186
+ if (horizontal && last.w>0 && target>last.w) {
187
+ const ratio=target/last.w;
188
+ return {...last,w:target,left:last.left*ratio,right:last.right*ratio,accent:(last.accent ?? last.w/2)*ratio,
189
+ svg:'<g transform="scale('+mathNumber(ratio)+' 1)">'+last.svg+'</g>',glyph:undefined};
190
+ }
191
+ return last;
192
+ }
193
+ const unit = mathScale(c)/MATH_FONT.units, wanted = target/unit;
194
+ const minOverlap = MATH_FONT.minConnectorOverlap || 0;
195
+ const extenders=a.parts.filter(p=>p[4]),fixed=a.parts.filter(p=>!p[4]),
196
+ fixedAdvance=fixed.reduce((sum,p)=>sum+p[3],0),growth=extenders.reduce((sum,p)=>sum+p[3]-minOverlap,0);
197
+ if(growth<=0)return last;
198
+ const repeats=Math.max(0,Math.ceil((wanted-fixedAdvance+minOverlap*(fixed.length-1))/growth));
199
+ if (repeats >= 2048) throw Object.assign(new Error('Mathematical delimiter is too large'),{code:'resource-limit'});
200
+ const pieces=a.parts.flatMap(p=>p[4]?Array.from({length:repeats},()=>p):[p]),
201
+ full=pieces.reduce((sum,p)=>sum+p[3],0),connections=pieces.slice(1).map((p,i)=>Math.min(pieces[i][2],p[1]));
202
+ if(connections.some(overlap=>overlap<minOverlap))return last;
203
+ const overlap=connections.length?Math.max(minOverlap,Math.min((full-wanted)/connections.length,...connections)):0,
204
+ length=(full-overlap*connections.length)*unit;
205
+ const items=[]; let pos=0, width=0;
206
+ for (let i=0;i<pieces.length;i++) {
207
+ const p=pieces[i], b=mathGlyphIndex(p[0],c), g=MATH_FONT.glyph(b.glyph);
208
+ if (horizontal) items.push([b,pos,0]);
209
+ else items.push([b,0,-pos+g[2]*unit]);
210
+ width=Math.max(width,b.w);
211
+ pos+=(p[3]-overlap)*unit;
212
+ }
213
+ return mathCompose(items,horizontal?length:width,{italic:(a.italic || 0)*unit,
214
+ ...(horizontal?{accent:length/2,stretchAxis:'horizontal'}:{h:length,d:0,extended:true})});
215
+ }
216
+
217
+ function mathDelimiter(value, target, c) {
218
+ if(!value || value==='.')return mathBox();
219
+ const b=mathStretch(value,target,c), axis=mathConstant('AxisHeight',c,0.25);
220
+ return mathCompose([[b,0,-(b.inkTop+b.inkBottom)/2-axis]],b.w,{class:'inner',extended:true,italic:b.italic});
221
+ }
222
+
223
+ // The TeXbook's inter-atom spacing table (chapter 18). Rows: the left atom; columns: the right atom, in
224
+ // MATH_ATOM_CLASSES order. 0 = none, 1 = thin, a = thin, b = medium, c = thick. A letter spaces only in
225
+ // display and text styles; a digit spaces in every style. * cannot occur: a Bin beside these becomes Ord.
226
+ const MATH_ATOM_CLASSES = ['ord','op','bin','rel','open','close','punct','inner'];
227
+ const MATH_ATOM_SPACING = {
228
+ ord: '01bc000a', op: '11*c000a', bin: 'bb**b**b', rel: 'cc*0c00c',
229
+ open: '00000000', close: '01bc000a', punct: 'aa*aaaaa', inner: 'a1bca0aa'
230
+ };
231
+ function mathSpace(left, right, c) {
232
+ const column=MATH_ATOM_CLASSES.indexOf(right);
233
+ if (!left || !right || !MATH_ATOM_SPACING[left] || column<0) return 0;
234
+ const code=MATH_ATOM_SPACING[left][column], s=mathScale(c);
235
+ if (code==='0' || code==='*') return 0;
236
+ if (code==='1') return s*3/18;
237
+ if (c.level) return 0;
238
+ return s*({a:3,b:4,c:5})[code]/18;
239
+ }
240
+
241
+ function mathRow(children,c) {
242
+ const boxes=children.map(n=>{
243
+ const box=mathLayout(n,c);
244
+ if(c.screen)box.refit=ratio=>mathScreenRelayout(n,c,ratio);
245
+ if(n.type==='frac' && n.bar!==false && n.barThickness!==0 && !n.left && !n.right)box.flowWrap=width=>mathFraction(n,c,width);
246
+ if(n.type==='fence' && !n.size)box.flowWrap=width=>mathResponsiveLayout(n,c,width);
247
+ return box;
248
+ });
249
+ const significant=boxes.map((b,i)=>b.space?null:i).filter(i=>i!==null);
250
+ for (let k=0;k<significant.length;k++) {
251
+ const i=significant[k], b=boxes[i];
252
+ if (b.class!=='bin') continue;
253
+ const before=boxes[significant[k-1]]?.class, after=boxes[significant[k+1]]?.class;
254
+ if (!before || ['bin','op','rel','open','punct'].includes(before) || !after || ['rel','close','punct'].includes(after)) b.class='ord';
255
+ }
256
+ const items=[];let x=0,previous=null;
257
+ for (const b of boxes) {
258
+ if (!b.space) {
259
+ if(previous?.italic && previous.class!=='op' && !(b.italic && b.class!=='op'))x+=previous.italic;
260
+ x+=mathSpace(previous?.class,b.class,c);
261
+ }
262
+ items.push([b,x,0]);x+=b.w;
263
+ if (!b.space) previous=b;
264
+ }
265
+ const last=boxes.at(-1);
266
+ return mathCompose(items,x,{class:boxes.length===1?boxes[0].class:'ord',italic:boxes.length!==1 && last?.class==='op'?0:last?.italic || 0,rowItems:items,
267
+ ...(boxes.length===1 ? {accent:last.accent,limits:last.limits,large:last.large,scriptBase:last.scriptBase} : {})});
268
+ }
269
+
270
+ function mathKern(b,corner,height) {
271
+ const record=MATH_FONT.kern?.[b.glyph]?.[corner];
272
+ if (!record || !b.unitScale) return 0;
273
+ const [heights,values]=record; let i=0;
274
+ while (i<heights.length && (height+(b.glyphY || 0))/b.unitScale>=heights[i]) i++;
275
+ return (values[i] || 0)*b.unitScale;
276
+ }
277
+
278
+ function mathContourBands(box, dy = 0) {
279
+ if (!box.unitScale || box.extended) return null;
280
+ const profile = MATH_FONT.profile(box.glyph);
281
+ if (!profile) return null;
282
+ const bands = [], step = MATH_FONT.edgeStep, unit = box.unitScale;
283
+ const x = box.glyphX || 0, y = (box.glyphY || 0) + dy, radius = (box.stroke || 0) / 2;
284
+ for (let i = profile.length - 1; i > 0; i--) if (profile[i]) {
285
+ bands.push({top: y - (profile[0] + i * step) * unit - radius,
286
+ bottom: y - (profile[0] + (i - 1) * step) * unit + radius,
287
+ left: x + profile[i][0] * unit - radius, right: x + profile[i][1] * unit + radius});
288
+ }
289
+ return bands;
290
+ }
291
+
292
+ function mathContourScript(base, script, dy, nominal, c) {
293
+ const a = mathContourBands(base);
294
+ if (!a) return nominal;
295
+ const b = mathContourBands(script, dy) || [{top: script.inkTop + dy, bottom: script.inkBottom + dy,
296
+ left: script.left, right: script.right}];
297
+ const gap = c.screen ? Math.max(.035 * mathScale(c), .9 / (c.screen.fontSize * c.screen.pixelRatio)) : .025 * mathScale(c);
298
+ let j = 0, edge = -Infinity;
299
+ for (const left of a) {
300
+ while (j < b.length && b[j].bottom + gap < left.top) j++;
301
+ for (let k = j; k < b.length && b[k].top <= left.bottom + gap; k++) {
302
+ if (b[k].bottom + gap >= left.top) edge = Math.max(edge, left.right - b[k].left);
303
+ }
304
+ }
305
+ return Number.isFinite(edge) ? Math.max(base.glyphX || 0, edge + gap) : nominal;
306
+ }
307
+
308
+ function mathScripts(n,c) {
309
+ const b=mathLayout(n.base,c), up=n.sup?mathLayout(n.sup,mathScriptStyle(c)):null,
310
+ down=n.sub?mathLayout(n.sub,mathScriptStyle(c,true)):null;
311
+ const limits=n.limits || b.limits, stack=limits==='always' || (limits==='display' && c.display && c.level===0);
312
+ if (stack) {
313
+ const w=Math.max(b.w,up?.w || 0,down?.w || 0), items=[[b,(w-b.w)/2,0]], ic=b.italic || 0;
314
+ if (up) {
315
+ const y=-b.h-Math.max(mathConstant('UpperLimitBaselineRiseMin',c,0.6),mathConstant('UpperLimitGapMin',c,0.15)+up.d);
316
+ items.push([up,(w-up.w)/2+ic/2,y]);
317
+ }
318
+ if (down) {
319
+ const y=b.d+Math.max(mathConstant('LowerLimitBaselineDropMin',c,0.6),mathConstant('LowerLimitGapMin',c,0.15)+down.h);
320
+ items.push([down,(w-down.w)/2-ic/2,y]);
321
+ }
322
+ return mathCompose(items,w,{class:b.class});
323
+ }
324
+ const base=b.scriptBase || b,baseAscent=-base.inkTop,baseDescent=base.inkBottom,
325
+ upDescent=up?.inkBottom || 0,downAscent=-(down?.inkTop || 0),boxed=base.glyph===undefined || base.extended;
326
+ let rise=up?Math.max(mathConstant(c.cramped?'SuperscriptShiftUpCramped':'SuperscriptShiftUp',c,0.36),
327
+ upDescent+mathConstant('SuperscriptBottomMin',c,0.1),boxed?baseAscent-mathConstant('SuperscriptBaselineDropMax',c,0.25):0):0;
328
+ let drop=down?Math.max(mathConstant('SubscriptShiftDown',c,0.2),
329
+ downAscent-mathConstant('SubscriptTopMax',c,0.4),boxed?baseDescent+mathConstant('SubscriptBaselineDropMin',c,0.1):0):0;
330
+ if (up && down) {
331
+ const missing=mathConstant('SubSuperscriptGapMin',c,0.2)-(rise+drop-upDescent-downAscent);
332
+ if(missing>0) {
333
+ const shift=Math.min(missing,Math.max(0,mathConstant('SuperscriptBottomMaxWithSubscript',c,0.35)-(rise-upDescent)));
334
+ rise+=shift;drop+=missing-shift;
335
+ }
336
+ }
337
+ const items=[[b,0,0]]; let w=b.w;
338
+ if (up) {
339
+ const kern=Math.min(mathKern(base,0,rise-upDescent)+mathKern(up,3,-upDescent),mathKern(base,0,baseAscent)+mathKern(up,3,baseAscent-rise));
340
+ const nominal=b.w+(b.italic || 0)+kern;
341
+ const x=MATH_FONT.kern[base.glyph]?nominal:mathContourScript(base,up,-rise,nominal,c);
342
+ items.push([up,x,-rise]);w=Math.max(w,x+up.w);
343
+ }
344
+ if (down) {
345
+ const kern=Math.min(mathKern(base,2,downAscent-drop)+mathKern(down,1,downAscent),mathKern(base,2,-baseDescent)+mathKern(down,1,drop-baseDescent));
346
+ const nominal=b.w+kern;
347
+ const x=MATH_FONT.kern[base.glyph]?nominal:mathContourScript(base,down,drop,nominal,c);
348
+ items.push([down,x,drop]);w=Math.max(w,x+down.w);
349
+ }
350
+ return mathCompose(items,w+mathConstant('SpaceAfterScript',c,0.05),{class:b.class});
351
+ }
352
+
353
+ function mathFraction(n,c,width) {
354
+ if (n.style) c={...c,level:({display:0,text:0,script:1,scriptscript:2})[n.style],display:n.style==='display',cramped:false};
355
+ const display=c.display && c.level===0, child=display?{...c,display:false}:mathScriptStyle(c);
356
+ const top=width===undefined?mathLayout(n.num,child):mathResponsiveLayout(n.num,child,Math.max(.05,width-.2*mathScale(c))),
357
+ bottom=mathLayout(n.den,{...child,cramped:true}),axis=mathConstant('AxisHeight',c,0.25);
358
+ const line=n.bar===false?0:n.barThickness===undefined?mathConstant('FractionRuleThickness',c,0.04):n.barThickness*mathScale(c);
359
+ let rise=mathConstant(line?(display?'FractionNumeratorDisplayStyleShiftUp':'FractionNumeratorShiftUp'):(display?'StackTopDisplayStyleShiftUp':'StackTopShiftUp'),c,display?0.68:0.4),
360
+ drop=mathConstant(line?(display?'FractionDenominatorDisplayStyleShiftDown':'FractionDenominatorShiftDown'):(display?'StackBottomDisplayStyleShiftDown':'StackBottomShiftDown'),c,display?0.68:0.35);
361
+ if (line) {
362
+ rise=Math.max(rise,top.d+axis+line/2+mathConstant(display?'FractionNumDisplayStyleGapMin':'FractionNumeratorGapMin',c,display?0.12:0.06));
363
+ drop=Math.max(drop,bottom.h-axis+line/2+mathConstant(display?'FractionDenomDisplayStyleGapMin':'FractionDenominatorGapMin',c,display?0.12:0.06));
364
+ } else {
365
+ const min=mathConstant(display?'StackDisplayStyleGapMin':'StackGapMin',c,display?0.4:0.15),gap=rise+drop-top.d-bottom.h;
366
+ if (gap<min) {rise+=(min-gap)/2;drop+=(min-gap)/2;}
367
+ }
368
+ const pad=mathScale(c)*0.1,w=Math.max(top.w,bottom.w)+2*pad;
369
+ const numeratorX=n.align==='l'?pad:n.align==='r'?w-pad-top.w:(w-top.w)/2;
370
+ const items=[[top,numeratorX,-rise],[bottom,(w-bottom.w)/2,drop]];
371
+ if (line) items.push([mathRule(w,line,-axis-line/2),0,0]);
372
+ const b=mathCompose(items,w,{class:'inner',innerLines:top.lines || 1,fitted:!!top.fitted});
373
+ if(width!==undefined && c.screen)b.refit=ratio=>mathFraction(n,{...c,screen:{...c.screen,fontSize:c.screen.fontSize*ratio},cache:new WeakMap()},width);
374
+ if (n.left || n.right) return mathFenceBoxes(n.left || '',n.right || '',b,c);
375
+ return b;
376
+ }
377
+
378
+ function mathRoot(n,c) {
379
+ const body=mathLayout(n.body,{...c,cramped:true}),line=mathConstant('RadicalRuleThickness',c,0.04),
380
+ gap=mathConstant(c.display && !c.level?'RadicalDisplayStyleVerticalGap':'RadicalVerticalGap',c,0.1),extra=mathConstant('RadicalExtraAscender',c,0.05);
381
+ const sign=mathStretch('√',body.h+body.d+gap+line,c),rise=-sign.inkTop-body.h-gap-line,
382
+ index=n.index?mathLayout(n.index,{...c,level:2,display:false}):null;
383
+ let before=0,idxX=0;
384
+ if(index) {idxX=Math.max(0,mathConstant('RadicalKernBeforeDegree',c,0.2));before=idxX+index.w+Math.max(-index.w,mathConstant('RadicalKernAfterDegree',c,-0.3));}
385
+ const bodyX=before+sign.w,w=bodyX+body.w,items=[[sign,before,rise],[body,bodyX,0]];
386
+ items.push([mathRule(body.w,line,-body.h-gap-line),bodyX,0]);
387
+ if(index) {const ratio=(MATH_FONT.constants.RadicalDegreeBottomRaisePercent ?? 60)/100;items.push([index,idxX,sign.inkBottom+rise-(sign.inkBottom-sign.inkTop)*ratio-index.inkBottom]);}
388
+ const b=mathCompose(items,w);b.h+=extra;return b;
389
+ }
390
+
391
+ function mathFenceBoxes(left,right,body,c,size) {
392
+ const axis=mathConstant('AxisHeight',c,0.25),reach=Math.max(body.h-axis,body.d+axis),target=size?size*mathScale(c):Math.max(1*mathScale(c),2*reach*0.901,2*reach-0.5*mathScale(c));
393
+ const a=mathDelimiter(left,target,c),b=mathDelimiter(right,target,c);
394
+ return mathCompose([[a,0,0],[body,a.w,0],[b,a.w+body.w,0]],a.w+body.w+b.w,{class:'inner'});
395
+ }
396
+
397
+ function mathAccent(n,c) {
398
+ const position=n.position || 'over';let nucleus=n.base;
399
+ while(nucleus?.type==='style' || (nucleus?.type==='row' && nucleus.children.length===1))nucleus=nucleus.type==='style'?nucleus.body:nucleus.children[0];
400
+ const b=mathLayout(n.base,position==='under'?c:{...c,cramped:true,dotless:nucleus?.type==='glyph' && !n.overlay && /^[\u0300-\u036f\u20d0-\u20ff]$/u.test(n.value)}),s=mathScale(c);
401
+ const semantics={class:n.class || b.class,limits:n.limits || b.limits,italic:b.italic,accent:b.accent,scriptBase:b.scriptBase || b};
402
+ if(n.overlay) {
403
+ const w=Math.max(b.w,0.2*s),line=mathConstant('FractionRuleThickness',c,0.04);
404
+ const rising='M0 '+mathNumber(b.d)+'L'+mathNumber(w)+' '+mathNumber(-b.h),
405
+ falling='M0 '+mathNumber(-b.h)+'L'+mathNumber(w)+' '+mathNumber(b.d);
406
+ let path=n.value==='╲'?falling:n.value==='╳'?rising+falling:rising;
407
+ if(n.value==='↗') {
408
+ const tip=0.2*s;path+='M'+mathNumber(w-tip)+' '+mathNumber(-b.h)+'H'+mathNumber(w)+'v'+mathNumber(tip);
409
+ }
410
+ return {...b,...semantics,svg:b.svg+'<path d="'+path+'" fill="none" stroke="currentColor" stroke-width="'+mathNumber(line)+'"/>'};
411
+ }
412
+ if (n.value==='overline' || n.value==='underline' || n.value==='¯' || n.value==='_') {
413
+ const over=position!=='under',line=mathConstant(over?'OverbarRuleThickness':'UnderbarRuleThickness',c,0.04),gap=mathConstant(over?'OverbarVerticalGap':'UnderbarVerticalGap',c,0.1);
414
+ const y=over?-b.h-gap-line:b.d+gap;
415
+ const result=mathCompose([[b,0,0],[mathRule(b.w,line,y),0,0]],b.w,semantics);
416
+ result[over?'h':'d']+=mathConstant(over?'OverbarExtraAscender':'UnderbarExtraDescender',c,0.04);
417
+ return result;
418
+ }
419
+ const names={hat:'̂',widehat:'̂',tilde:'̃',widetilde:'̃',bar:'̄',vec:'⃗',dot:'̇',ddot:'̈',dddot:'⃛',ddddot:'⃜',acute:'́',grave:'̀',breve:'̆',check:'̌',overbrace:'⏞',underbrace:'⏟',overrightarrow:'→',overleftarrow:'←',overleftrightarrow:'↔'};
420
+ const value=names[n.value] || n.value,accentStyle={...c,flattenAccent:position!=='under' && b.h>mathConstant('FlattenedAccentBaseHeight',c,0.65)},
421
+ a=n.stretch?mathStretch(value,Math.max(b.w,s*0.3),accentStyle,true):mathGlyph(value,accentStyle,{variant:'roman'}),
422
+ x=(b.accent ?? b.w/2)-(a.accent ?? a.w/2),combining=/^[\u0300-\u036f\u20d0-\u20ff]$/u.test(value);
423
+ let y;
424
+ if(position==='under')y=b.d+mathConstant('UnderbarVerticalGap',c,0.1)-a.inkTop;
425
+ else if(combining)y=-Math.max(0,b.h-mathConstant('AccentBaseHeight',c,0.48));
426
+ else y=-b.h-mathConstant('OverbarVerticalGap',c,0.1)-a.inkBottom;
427
+ return mathCompose([[b,0,0],[a,x,y]],b.w,semantics);
428
+ }
429
+
430
+ function mathTable(n,c) {
431
+ const small=/smallmatrix$/.test(n.env) || n.env==='subarray' || n.env==='substack',
432
+ paired=/^(align(?:ed)?(?:at)?|flalign|split)$/.test(n.env),
433
+ display=paired || /^(gather(?:ed)?|eqnarray|multline|equation|drcases|dcases)$/.test(n.env),
434
+ cellStyle={...c,level:small?Math.max(1,c.level):c.level,display:small?false:display};
435
+ const rows=n.rows.map(row=>row.map((cell,i)=>{
436
+ // The second cell in each equation pair continues the first math list.
437
+ // An empty ordinary atom supplies the left neighbour of a leading relation.
438
+ if(paired && i%2) return mathRow([{type:'row',children:[]},...(cell.type==='row'?cell.children:[cell])],cellStyle);
439
+ return mathLayout(cell,cellStyle);
440
+ }));
441
+ const cols=Math.max(n.align?.length || 0,n.pairs?2*n.pairs:0,...rows.map(r=>r.length)),s=mathScale(c),cs=mathScale(cellStyle),
442
+ widths=Array.from({length:cols},(_,i)=>Math.max(0,...rows.map(row=>row[i]?.w || 0))),
443
+ align=n.env==='eqnarray'?Array.from({length:cols},(_,i)=>['r','c','l'][i%3]):n.align || Array(cols).fill('c');
444
+ if(rows.length*(cols+(n.columnInsertions?.length || 0))+(n.columnRules?.length || 0)+(n.rowRules?.length || 0)>100000)
445
+ throw Object.assign(new Error('Array composition exceeds the rendering budget'),{code:'resource-limit'});
446
+ const insertions=Array.from({length:cols+1},()=>[]);
447
+ for(const insertion of n.columnInsertions || []) {
448
+ if(insertion.before<0 || insertion.before>cols) throw Object.assign(new Error('Array insertion exceeds its columns'),{code:'invalid-alignment'});
449
+ insertions[insertion.before].push({...insertion,box:mathLayout(insertion.body,cellStyle)});
450
+ }
451
+ const gaps=Array.from({length:cols+1},(_,i)=>{
452
+ let gap=i===0 || i===cols?0:paired?(i%2?0:/^(alignat|alignedat)$/.test(n.env)?0:2*s):small?cs/3:s;
453
+ if(insertions[i].some(item=>item.replaceGap))gap=0;
454
+ return gap+insertions[i].reduce((sum,item)=>sum+item.box.w,0);
455
+ });
456
+ const starts=[],ends=[],boundaries=[],insertionItems=[];let w=0;
457
+ for(let i=0;i<=cols;i++) {
458
+ const contentWidth=insertions[i].reduce((sum,item)=>sum+item.box.w,0);
459
+ boundaries.push(w+gaps[i]/2);
460
+ let x=w+(gaps[i]-contentWidth)/2;
461
+ for(const item of insertions[i]) {insertionItems.push([item.box,x]);x+=item.box.w;}
462
+ w+=gaps[i];if(i<cols) {starts.push(w);w+=widths[i];ends.push(w);}
463
+ }
464
+ const insertionHeight=Math.max(0,...insertionItems.map(([b])=>b.h)),insertionDepth=Math.max(0,...insertionItems.map(([b])=>b.d)),
465
+ heights=rows.map(row=>Math.max(0.7*cs,insertionHeight,...row.map(b=>b.h))),
466
+ depths=rows.map(row=>Math.max(0.2*cs,insertionDepth,...row.map(b=>b.d))),baselines=[];
467
+ let total=0;
468
+ for(let r=0;r<rows.length;r++) {
469
+ total+=heights[r];baselines.push(total);total+=depths[r];
470
+ if(r+1<rows.length)total+=(small?0.2:0.3)*cs+(n.rowGaps?.[r] || 0)*s;
471
+ }
472
+ const offset=n.position==='t'?-(baselines[0] || 0):n.position==='b'?-(baselines.at(-1) || 0):-total/2-mathConstant('AxisHeight',c,0.25),items=[];
473
+ for(let r=0;r<rows.length;r++) {
474
+ for(let i=0;i<cols;i++) {
475
+ const b=rows[r][i] || mathBox(),a=n.env==='multline' && cols===1?(r===0?'l':r===rows.length-1?'r':'c'):align[i] || 'c',
476
+ dx=a==='r'?widths[i]-b.w:a==='l'?0:(widths[i]-b.w)/2;
477
+ items.push([b,starts[i]+dx,baselines[r]+offset]);
478
+ }
479
+ for(const [b,x] of insertionItems)items.push([b,x,baselines[r]+offset]);
480
+ }
481
+ const line=mathConstant('FractionRuleThickness',c,0.04),ruleCounts=new Map();
482
+ for(const boundary of n.columnRules || []) {
483
+ if(boundary<0 || boundary>cols)throw Object.assign(new Error('Array rule exceeds its columns'),{code:'invalid-rule'});
484
+ const count=ruleCounts.get(boundary) || 0;ruleCounts.set(boundary,count+1);
485
+ const x=boundaries[boundary]+count*0.15*s;
486
+ items.push([mathBox(line/2,0,total,'<path data-math-line="vertical" d="M0 0v'+mathNumber(total)+'" fill="none" stroke="currentColor" stroke-width="'+mathNumber(line)+'"/>',{left:-line/2}),x,offset]);
487
+ }
488
+ ruleCounts.clear();
489
+ for(const rule of n.rowRules || []) {
490
+ const boundary=rule.before,from=rule.from ?? 0,to=rule.to ?? cols;
491
+ if(boundary<0 || boundary>rows.length || from<0 || to>cols)throw Object.assign(new Error('Array rule exceeds its cells'),{code:'invalid-rule'});
492
+ const count=ruleCounts.get(boundary) || 0;ruleCounts.set(boundary,count+1);
493
+ const y=(boundary===0?0:boundary===rows.length?total:(baselines[boundary-1]+depths[boundary-1]+baselines[boundary]-heights[boundary])/2)+offset+count*0.15*s,
494
+ left=from===0?0:boundaries[from],right=to===cols?w:boundaries[to];
495
+ const stroke='<path data-math-line="horizontal" d="M0 0H'+mathNumber(right-left)+'" fill="none" stroke="currentColor" stroke-width="'+mathNumber(line)+'"'+(rule.dashed?' stroke-dasharray="'+mathNumber(.15*s)+' '+mathNumber(.1*s)+'"':'')+'/>';
496
+ items.push([mathBox(right-left,line/2,line/2,stroke),left,y]);
497
+ }
498
+ const b=mathCompose(items,w,{class:'inner',h:Math.max(0,-offset),d:Math.max(0,total+offset)});
499
+ const delimiters={pmatrix:['(',')'],bmatrix:['[',']'],Bmatrix:['{','}'],vmatrix:['|','|'],Vmatrix:['‖','‖'],
500
+ psmallmatrix:['(',')'],bsmallmatrix:['[',']'],Bsmallmatrix:['{','}'],vsmallmatrix:['|','|'],Vsmallmatrix:['‖','‖'],
501
+ cases:['{',''],dcases:['{',''],rcases:['','}'],drcases:['','}']};
502
+ const pair=delimiters[n.env];
503
+ return pair?mathFenceBoxes(pair[0],pair[1],b,c):b;
504
+ }
505
+
506
+ function mathLayout(n,c) {
507
+ if(!n)return mathBox();
508
+ if(c.budget && ++c.budget.nodes>100000)throw Object.assign(new Error('Mathematical layout exceeds the rendering budget'),{code:'resource-limit'});
509
+ switch(n.type) {
510
+ case 'row': return mathRow(n.children,c);
511
+ case 'glyph': {
512
+ if(n.delimiterSize || (n.delimiter && c.delimiterTarget))return {...mathDelimiter(n.value,n.delimiterSize?n.delimiterSize*mathScale(c):c.delimiterTarget,c),class:n.class || 'ord'};
513
+ if(n.stretch)return {...mathStretch(n.value,c.stretchTarget || mathScale(c),c,true),class:n.class || 'rel',limits:n.limits,stretchAxis:'horizontal'};
514
+ let b=mathGlyph(n.value,c,{class:n.class || 'ord',variant:n.variant || (c.variant==='normal'?n.defaultVariant || 'normal':c.variant),large:n.large,limits:n.limits});
515
+ if(n.large && c.display && !c.level) {
516
+ const target=mathConstant('DisplayOperatorMinHeight',c,1.3),a=mathStretch(n.value,target,c);
517
+ b=mathCompose([[a,0,(a.h-a.d)/2-mathConstant('AxisHeight',c,0.25)]],a.w,{class:n.class || 'op',italic:a.italic,limits:n.limits,large:true,glyph:a.glyph,unitScale:a.unitScale});
518
+ }
519
+ return b;
520
+ }
521
+ case 'text': return mathText(n.value,c,{variant:n.variant || (c.variant==='normal'?'roman':c.variant),class:n.class,limits:n.limits});
522
+ case 'space': return mathBox(n.width*mathScale(c),0,0,'',{space:true});
523
+ case 'scripts': return mathScripts(n,c);
524
+ case 'frac': return mathFraction(n,c);
525
+ case 'root': return mathRoot(n,c);
526
+ case 'fence': {
527
+ // A nested fence does not inherit an outer \middle target. Its cached box
528
+ // survives the outer fence's second pass, keeping nested layout linear.
529
+ const cache=c.cache || (c.cache=new WeakMap()),key=[c.size,c.level,c.variant,c.display,c.cramped,c.stretchTarget].join('|');
530
+ let entries=cache.get(n);if(entries?.has(key))return {...entries.get(key)};
531
+ const local={...c,delimiterTarget:undefined};let b=mathLayout(n.body,local);
532
+ if(n.body) {
533
+ const axis=mathConstant('AxisHeight',c,0.25),reach=Math.max(b.h-axis,b.d+axis),target=Math.max(mathScale(c),2*reach*0.901,2*reach-0.5*mathScale(c));
534
+ b=mathLayout(n.body,{...local,delimiterTarget:target});
535
+ }
536
+ b=mathFenceBoxes(n.left,n.right,b,local,n.size);
537
+ if(!entries)cache.set(n,entries=new Map());entries.set(key,b);return {...b};
538
+ }
539
+ case 'accent': return mathAccent(n,c);
540
+ case 'table': return mathTable(n,c);
541
+ case 'style': {
542
+ const style={...c};if(n.variant)style.variant=n.variant;if(n.absoluteSize!==undefined)style.size=n.absoluteSize;if(n.size!==undefined)style.size*=n.size;
543
+ if(n.mathStyle) {style.level=({display:0,text:0,script:1,scriptscript:2})[n.mathStyle];style.display=n.mathStyle==='display';style.cramped=false;}
544
+ const b=mathLayout(n.body,style);if(n.class)b.class=n.class;if(n.limits)b.limits=n.limits;
545
+ if(n.width!==undefined) {
546
+ const width=n.width*mathScale(style),shift=n.align==='right'?width-b.w:n.align==='center'?(width-b.w)/2:0;
547
+ Object.assign(b,mathCompose([[{...b},shift,0]],width,{class:b.class}));
548
+ }
549
+ if(n.border || n.background) {
550
+ const pad=(n.padding ?? 0.2)*mathScale(style),line=mathConstant('FractionRuleThickness',style,0.04),w=b.w+2*pad,h=b.h+b.d+2*pad;
551
+ const border='<path d="M0 '+mathNumber(-b.h-pad)+'H'+mathNumber(w)+'v'+mathNumber(h)+'H0Z" fill="'+(n.background?mathEscape(n.background):'none')+'" fill-opacity="1" stroke-opacity="1"'+(n.border?' stroke="'+mathEscape(n.border)+'" stroke-width="'+mathNumber(line)+'"':'')+'/>';
552
+ Object.assign(b,mathCompose([[{...b},pad,0]],w,{h:b.h+pad,d:b.d+pad,class:b.class}));b.svg=border+b.svg;
553
+ }
554
+ if(n.color)b.svg='<g fill="'+mathEscape(n.color)+'" color="'+mathEscape(n.color)+'" fill-opacity="1" stroke-opacity="1">'+b.svg+'</g>';
555
+ return b;
556
+ }
557
+ case 'overunder': {
558
+ const over=n.over?mathLayout(n.over,mathScriptStyle(c)):null,under=n.under?mathLayout(n.under,mathScriptStyle(c,true)):null,
559
+ b=mathLayout(n.base,{...c,stretchTarget:Math.max(mathScale(c),Math.max(over?.w || 0,under?.w || 0)+0.4*mathScale(c))});
560
+ const w=Math.max(b.w,over?.w || 0,under?.w || 0),items=[[b,(w-b.w)/2,0]],stretched=b.stretchAxis==='horizontal';
561
+ if(over) {
562
+ const rise=stretched?Math.max(mathConstant('StretchStackTopShiftUp',c,0.8),b.h+over.inkBottom+mathConstant('StretchStackGapAboveMin',c,0.1)):
563
+ b.h+over.inkBottom+mathConstant('OverbarVerticalGap',c,0.1);
564
+ items.push([over,(w-over.w)/2,-rise]);
565
+ }
566
+ if(under) {
567
+ const drop=stretched?Math.max(mathConstant('StretchStackBottomShiftDown',c,0.6),b.d-under.inkTop+mathConstant('StretchStackGapBelowMin',c,0.1)):
568
+ b.d-under.inkTop+mathConstant('UnderbarVerticalGap',c,0.1);
569
+ items.push([under,(w-under.w)/2,drop]);
570
+ }
571
+ return mathCompose(items,w,{class:n.class || b.class});
572
+ }
573
+ case 'phantom': {const b=mathLayout(n.body,c);return mathBox(n.mode==='v'?0:b.w,n.mode==='h'?0:b.h,n.mode==='h'?0:b.d);}
574
+ case 'smash': {const b=mathLayout(n.body,c);return {...b,h:n.mode==='b'?b.h:0,d:n.mode==='t'?b.d:0};}
575
+ case 'raise': {const b=mathLayout(n.body,c),s=mathScale(c);return mathCompose([[b,0,-n.amount*s]],b.w,{class:b.class,...(n.height===undefined?{}:{h:n.height*s}),...(n.depth===undefined?{}:{d:n.depth*s})});}
576
+ case 'rule': {const s=mathScale(c);return mathRule(n.width*s,(n.height+n.depth)*s,-n.height*s);}
577
+ case 'link': {const b=mathLayout(n.body,c);const url=new URL(n.href);if(!['http:','https:'].includes(url.protocol))throw Object.assign(new Error('Unsupported equation link'),{code:'unsafe-url'});b.svg='<a href="'+mathEscape(n.href)+'" target="_blank" rel="noopener noreferrer">'+b.svg+'</a>';return b;}
578
+ default: throw Object.assign(new Error('Unsupported maths construct: '+n.type),{code:'unsupported-construct'});
579
+ }
580
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // The module has the exact classic renderer body, with module-local exports.
3
+ import {readFile, writeFile} from 'node:fs/promises';
4
+ import {dirname, resolve} from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ export function mathModule(bundle) {
8
+ const text = String(bundle), opening = "(function(){\n'use strict';\n", ending = '\n})();\n';
9
+ const start = text.indexOf(opening), assignment = '\nglobalThis.RapierMath=Object.freeze({';
10
+ if (start < 0 || !text.endsWith(ending) || text.indexOf(opening, start + opening.length) !== -1 ||
11
+ text.split(assignment).length !== 2 || !text.startsWith('/*!\n') || !text.slice(0, start).endsWith('*/\n'))
12
+ throw new Error('The math payload has an unexpected module boundary');
13
+ const body = text.slice(start + opening.length, -ending.length).replace(assignment, '\nconst RapierMath=Object.freeze({');
14
+ return text.slice(0, start) + body + '\n\n' +
15
+ 'const {version, parse, render, fit, observe, renderToString} = RapierMath;\n' +
16
+ 'export {version, parse, render, fit, observe, renderToString};\nexport default RapierMath;\n';
17
+ }
18
+
19
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
20
+ if (!process.argv[2]) throw new Error('usage: node src/math/package-build.mjs <module-file>');
21
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
+ const {buildMath} = await import('../tools/build-math.mjs');
23
+ const result = await buildMath({root, updatePins: false});
24
+ const text = mathModule(await readFile(resolve(root, 'shell/vendor', result.bundle), 'utf8'));
25
+ await writeFile(resolve(process.argv[2]), text);
26
+ console.log(JSON.stringify({file: process.argv[2], bytes: Buffer.byteLength(text)}));
27
+ }