rapier-math 0.0.0-stage → 1.0.1
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/LICENSE +484 -0
- package/LICENSE-MIT +21 -0
- package/README.md +110 -2
- package/check.mjs +97 -0
- package/index.mjs +2884 -0
- package/math-LICENSE.txt +21 -0
- package/math-components.json +71 -0
- package/math-font-LICENSE.txt +457 -0
- package/package.json +44 -4
- package/rapier-math-1.0.1.js +2883 -0
- package/src/math/FONT-LICENSE.txt +457 -0
- package/src/math/FONT-SOURCE.md +117 -0
- package/src/math/LICENSE-MIT +21 -0
- package/src/math/api.js +86 -0
- package/src/math/browser.js +150 -0
- package/src/math/build-font.py +480 -0
- package/src/math/fallbacks.js +111 -0
- package/src/math/font.js +514 -0
- package/src/math/layout.js +576 -0
- package/src/math/package-build.mjs +27 -0
- package/src/math/package-verify.mjs +40 -0
- package/src/math/parser.js +1064 -0
- package/src/math/responsive.js +251 -0
- package/src/math/symbols.js +98 -0
- package/src/package.json +9 -0
- package/src/shell/math-resources.json +105 -0
- package/src/tools/build-math.mjs +92 -0
- package/verify.mjs +40 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Break only between mathematical atoms; each line keeps its complete vector ink.
|
|
3
|
+
const mathInkWidth = box => Math.max(box.w,box.right)-Math.min(0,box.left);
|
|
4
|
+
|
|
5
|
+
function mathFittedInk(svg, ratio, c) {
|
|
6
|
+
let growth=0;
|
|
7
|
+
if(c.screen)svg=svg.replace(/<path\b[^>]*\bdata-math-ink="([^"]+)"[^>]*\/>/g,(tag,original)=>{
|
|
8
|
+
const scale=Number(original)*ratio,stroke=mathScreenStroke({...c,level:0,size:scale});
|
|
9
|
+
const previous=Number(tag.match(/\bstroke-width="([^"]+)"/)?.[1] || 0)*scale/MATH_FONT.units;
|
|
10
|
+
growth=Math.max(growth,(stroke-previous)/2);
|
|
11
|
+
tag=tag.replace(/data-math-ink="[^"]+"/,'data-math-ink="'+mathNumber(scale)+'"');
|
|
12
|
+
const weight='stroke-width="'+mathNumber(stroke*MATH_FONT.units/scale)+'"';
|
|
13
|
+
return tag.includes('stroke-width=')?tag.replace(/stroke-width="[^"]+"/,weight):
|
|
14
|
+
tag.replace('/>',' stroke="currentColor" '+weight+' stroke-linejoin="round" paint-order="stroke fill"/>');
|
|
15
|
+
});
|
|
16
|
+
return {svg,growth};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mathFitBox(box,width,c) {
|
|
20
|
+
let extent=mathInkWidth(box);
|
|
21
|
+
if(extent<=width || !extent)return box;
|
|
22
|
+
if(c.screen && box.refit) {
|
|
23
|
+
let ratio=width/extent,next;
|
|
24
|
+
for(let pass=0;pass<2;pass++) {
|
|
25
|
+
next=box.refit(ratio);
|
|
26
|
+
ratio=Math.min(ratio,width/mathInkWidth(next));
|
|
27
|
+
}
|
|
28
|
+
box=next;extent=mathInkWidth(box);
|
|
29
|
+
}
|
|
30
|
+
let ratio=width/extent,ink;
|
|
31
|
+
// Fitting changes the final pixel size. Regrade the outlines and reserve their new ink.
|
|
32
|
+
for(let pass=0;pass<3;pass++) {
|
|
33
|
+
ink=mathFittedInk(box.svg,ratio,c);
|
|
34
|
+
if(extent*ratio+2*ink.growth<=width)break;
|
|
35
|
+
ratio=Math.max(Number.EPSILON,(width-2*ink.growth)/extent);
|
|
36
|
+
}
|
|
37
|
+
ink=mathFittedInk(box.svg,ratio,c);
|
|
38
|
+
const scaled={...box,svg:'<g transform="scale('+mathNumber(ratio)+')">'+ink.svg+'</g>',fitted:true};
|
|
39
|
+
for(const key of ['w','h','d','left','right','inkTop','inkBottom'])scaled[key]*=ratio;
|
|
40
|
+
scaled.left-=ink.growth;scaled.right+=ink.growth;scaled.inkTop-=ink.growth;scaled.inkBottom+=ink.growth;
|
|
41
|
+
scaled.h+=ink.growth;scaled.d+=ink.growth;
|
|
42
|
+
delete scaled.rowItems;
|
|
43
|
+
delete scaled.refit;
|
|
44
|
+
return scaled;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mathLineStack(lines,c) {
|
|
48
|
+
const items=[],gap=Math.max(mathConstant('MathLeading',c,0.15),0.3*mathScale(c));
|
|
49
|
+
let y=0,width=0;
|
|
50
|
+
for(let i=0;i<lines.length;i++) {
|
|
51
|
+
const {box,indent=0,gapBefore=0}=lines[i];
|
|
52
|
+
if(i)y+=lines[i-1].box.d+gap+box.h+gapBefore;
|
|
53
|
+
const x=indent-Math.min(0,box.left);
|
|
54
|
+
items.push([box,x,y]);width=Math.max(width,x+Math.max(box.w,box.right));
|
|
55
|
+
}
|
|
56
|
+
return mathCompose(items,width,{lines:lines.reduce((sum,line)=>sum+(line.box.lines || 1),0),
|
|
57
|
+
lastBaseline:y+(lines.at(-1)?.box.lastBaseline || 0),fitted:lines.some(line=>line.box.fitted)});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mathPositionStack(lines,c,n) {
|
|
61
|
+
const box=mathLineStack(lines,c),shift=n.position==='t'?0:n.position==='b'?-box.lastBaseline:
|
|
62
|
+
(box.h-box.d)/2-mathConstant('AxisHeight',c,.25);
|
|
63
|
+
return mathCompose([[box,0,shift]],box.w,{class:'inner',lines:box.lines,lastBaseline:box.lastBaseline+shift,fitted:box.fitted});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mathFlowAtoms(n,box,x=0,y=0) {
|
|
67
|
+
if(n.type==='row' && n.children.length && box.rowItems?.length===n.children.length)
|
|
68
|
+
return n.children.flatMap((child,i)=>{const [b,dx,dy]=box.rowItems[i];return mathFlowAtoms(child,b,x+dx,y+dy);});
|
|
69
|
+
if(n.type==='style' && n.width===undefined && !n.border && !n.background && !n.class && !n.limits) {
|
|
70
|
+
const atoms=mathFlowAtoms(n.body,box,x,y);
|
|
71
|
+
if(n.color) {
|
|
72
|
+
const paint=b=>({...b,svg:'<g fill="'+mathEscape(n.color)+'" color="'+mathEscape(n.color)+'" fill-opacity="1" stroke-opacity="1">'+b.svg+'</g>',
|
|
73
|
+
...(b.refit?{refit:ratio=>paint(b.refit(ratio))}:{}),...(b.flowWrap?{flowWrap:width=>paint(b.flowWrap(width))}:{})});
|
|
74
|
+
return atoms.map(([b,dx,dy])=>[paint(b),dx,dy]);
|
|
75
|
+
}
|
|
76
|
+
return atoms;
|
|
77
|
+
}
|
|
78
|
+
if(n.type==='frac' && n.bar!==false && n.barThickness!==0)box={...box,flowFactor:true};
|
|
79
|
+
if(n.type==='glyph' && n.value==='⋯')box={...box,flowDots:true};
|
|
80
|
+
if(n.type==='glyph' && '([{⌈⌊⟨⦅⟦⟪⦃)]}⌉⌋⟩⦆⟧⟫⦄'.includes(n.value))
|
|
81
|
+
box={...box,flowFence:')]}⌉⌋⟩⦆⟧⟫⦄'.includes(n.value)?'close':'open'};
|
|
82
|
+
return [[box,x,y]];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mathRefitRow(items,advance,ratio) {
|
|
86
|
+
let delta=0;
|
|
87
|
+
const fresh=items.map(([b,x,y])=>{
|
|
88
|
+
const next=b.refit?b.refit(ratio):b,placed=[next,x+delta,y];
|
|
89
|
+
delta+=next.w-b.w;return placed;
|
|
90
|
+
});
|
|
91
|
+
return mathCompose(fresh,advance+delta,{fitted:fresh.some(([b])=>b.fitted)});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mathWrapRow(box,c,width,{relationIndent,binaryIndent}={}) {
|
|
95
|
+
if(mathInkWidth(box)<=width || !box.rowItems?.length)return mathFitBox(box,width,c);
|
|
96
|
+
const atoms=box.rowItems.map(item=>[...item]),breaks=[0];let ink=false,depth=0,previous=null;
|
|
97
|
+
for(let i=0;i<atoms.length;i++) {
|
|
98
|
+
const b=atoms[i][0],next=atoms[i+1]?.[0],fence=b.flowFence || b.class;
|
|
99
|
+
if(fence==='close')depth=Math.max(0,depth-1);
|
|
100
|
+
if(ink && !depth && (b.class==='rel' || b.class==='bin' || b.flowDots && (previous?.flowFactor && next?.flowFactor || (previous?.flowFence || previous?.class)==='close' && (next?.flowFence || next?.class)==='open')))breaks.push(i);
|
|
101
|
+
if(fence==='open')depth++;
|
|
102
|
+
if(b.svg){ink=true;previous=b;}
|
|
103
|
+
}
|
|
104
|
+
breaks.push(atoms.length);
|
|
105
|
+
// Reuse structural wrapping before shrinking an oversized, otherwise indivisible atom.
|
|
106
|
+
const deltas=Array(atoms.length).fill(0);let changed=false;
|
|
107
|
+
for(let segment=0;segment<breaks.length-1;segment++) {
|
|
108
|
+
const start=breaks[segment],end=breaks[segment+1],origin=atoms[start]?.[1] || 0;
|
|
109
|
+
let right=0,left=0;
|
|
110
|
+
for(let i=start;i<end;i++){const [b,x]=atoms[i];left=Math.min(left,x-origin+b.left);right=Math.max(right,x-origin+b.right,x-origin+b.w);}
|
|
111
|
+
let extent=right-left;
|
|
112
|
+
if(extent<=width)continue;
|
|
113
|
+
for(let i=start;i<end;i++) {
|
|
114
|
+
const b=atoms[i][0];if(!b.flowWrap)continue;
|
|
115
|
+
const allowance=width-(extent-mathInkWidth(b));
|
|
116
|
+
if(allowance<=.2*mathScale(c))continue;
|
|
117
|
+
const next=b.flowWrap(allowance),delta=next.w-b.w;
|
|
118
|
+
if(Math.max(next.innerLines || 1,next.lines || 1)<2 || delta>=0)continue;
|
|
119
|
+
atoms[i][0]=next;deltas[i]=delta;extent+=delta;changed=true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if(changed) {
|
|
123
|
+
let shift=0,advance=0;
|
|
124
|
+
for(let i=0;i<atoms.length;i++){atoms[i][1]+=shift;shift+=deltas[i];advance=Math.max(advance,atoms[i][1]+atoms[i][0].w);}
|
|
125
|
+
box=mathCompose(atoms,advance,{rowItems:atoms,fitted:atoms.some(([b])=>b.fitted)});
|
|
126
|
+
if(c.screen)box.refit=ratio=>mathRefitRow(atoms,advance,ratio);
|
|
127
|
+
}
|
|
128
|
+
if(breaks.length===2)return mathFitBox(box,width,c);
|
|
129
|
+
const firstRelation=atoms.findIndex(([b])=>b.class==='rel');
|
|
130
|
+
const anchor=relationIndent ?? (firstRelation>=0 && atoms[firstRelation][1]<width*0.35?atoms[firstRelation][1]:0);
|
|
131
|
+
const continuation=binaryIndent ?? Math.min(anchor+mathScale(c),width*0.3);
|
|
132
|
+
const minimum=breaks.slice(0,-1).map((start,i)=>{
|
|
133
|
+
const origin=atoms[start][1];let left=0,right=0;
|
|
134
|
+
for(let j=start;j<breaks[i+1];j++){const [b,x]=atoms[j];left=Math.min(left,x-origin+b.left);right=Math.max(right,x-origin+b.right,x-origin+b.w);}
|
|
135
|
+
return right-left;
|
|
136
|
+
});
|
|
137
|
+
const inset=i=>{
|
|
138
|
+
const wanted=i===0?0:atoms[breaks[i]][0].class==='rel'?anchor:continuation;
|
|
139
|
+
return relationIndent===undefined?Math.min(wanted,Math.max(0,width-minimum[i])):wanted;
|
|
140
|
+
};
|
|
141
|
+
const costs=Array(breaks.length).fill(Infinity),next=Array(breaks.length).fill(0);
|
|
142
|
+
costs[breaks.length-1]=0;
|
|
143
|
+
// A bounded lookahead makes even thousands of break opportunities linear in input size.
|
|
144
|
+
for(let start=breaks.length-2;start>=0;start--) {
|
|
145
|
+
const first=breaks[start],origin=atoms[first][1],available=Math.max(.05,width-inset(start));
|
|
146
|
+
let left=0,right=0,cursor=first;
|
|
147
|
+
for(let end=start+1;end<Math.min(breaks.length,start+65);end++) {
|
|
148
|
+
while(cursor<breaks[end]) {
|
|
149
|
+
const [b,x]=atoms[cursor++];
|
|
150
|
+
left=Math.min(left,x-origin+b.left);right=Math.max(right,x-origin+b.right,x-origin+b.w);
|
|
151
|
+
}
|
|
152
|
+
const extent=right-left,over=Math.max(0,extent-available)/available,slack=Math.max(0,available-extent)/available;
|
|
153
|
+
const last=end===breaks.length-1,penalty=last?0:atoms[breaks[end]][0].class==='rel'?0:18;
|
|
154
|
+
const cost=costs[end]+12+penalty+(last?8:60)*slack*slack+(over?100000*(1+over*over):0);
|
|
155
|
+
if(cost<costs[start]) {costs[start]=cost;next[start]=end;}
|
|
156
|
+
if(extent>available && end>start+1)break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const lines=[];
|
|
160
|
+
for(let start=0;start<breaks.length-1;) {
|
|
161
|
+
const end=next[start] || start+1,from=breaks[start],to=breaks[end],origin=atoms[from][1],items=[];
|
|
162
|
+
let advance=0;
|
|
163
|
+
for(let i=from;i<to;i++) {
|
|
164
|
+
const [b,x,y]=atoms[i];items.push([b,x-origin,y]);advance=Math.max(advance,x-origin+b.w);
|
|
165
|
+
}
|
|
166
|
+
const indent=inset(start),line=mathCompose(items,advance,{fitted:items.some(([b])=>b.fitted)});
|
|
167
|
+
if(c.screen)line.refit=ratio=>mathRefitRow(items,advance,ratio);
|
|
168
|
+
lines.push({box:mathFitBox(line,Math.max(.05,width-indent),c),indent});start=end;
|
|
169
|
+
}
|
|
170
|
+
return mathLineStack(lines,c);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function mathWrapAligned(n,c,width,natural) {
|
|
174
|
+
if(!/^(align(?:ed)?(?:at)?|flalign|split|eqnarray|gather(?:ed)?|multline)$/.test(n.env) ||
|
|
175
|
+
n.columnRules?.length || n.rowRules?.length || n.columnInsertions?.length)return mathFitBox(natural,width,c);
|
|
176
|
+
if(n.env==='eqnarray')n={...n,env:'aligned',rows:n.rows.map(row=>[row[0],{type:'row',children:row.slice(1).flatMap(cell=>cell.type==='row'?cell.children:[cell])}])};
|
|
177
|
+
const paired=/^(align(?:ed)?(?:at)?|flalign|split)$/.test(n.env),cellStyle={...c,display:true};
|
|
178
|
+
if(!paired) {
|
|
179
|
+
const lines=[];
|
|
180
|
+
for(let r=0;r<n.rows.length;r++) {
|
|
181
|
+
const row=n.rows[r];
|
|
182
|
+
const body=row.length===1?row[0]:{type:'row',children:row};
|
|
183
|
+
lines.push({box:mathResponsiveLayout(body,cellStyle,width),gapBefore:(n.rowGaps?.[r-1] || 0)*mathScale(c)});
|
|
184
|
+
}
|
|
185
|
+
return mathPositionStack(lines,c,n);
|
|
186
|
+
}
|
|
187
|
+
const rows=n.rows.map(row=>row.map(cell=>mathLayout(cell,cellStyle)));
|
|
188
|
+
const leftWidth=Math.max(0,...rows.flatMap(row=>row.filter((_,i)=>i%2===0).map(mathInkWidth)));
|
|
189
|
+
const separate=leftWidth>width*0.4,anchor=separate?0:leftWidth;
|
|
190
|
+
const lines=[],s=mathScale(c);
|
|
191
|
+
for(let r=0;r<n.rows.length;r++) {
|
|
192
|
+
const row=n.rows[r],start=lines.length;
|
|
193
|
+
for(let pair=0;pair<row.length;pair+=2) {
|
|
194
|
+
const left=rows[r][pair],rightNode=row[pair+1] || {type:'row',children:[]};
|
|
195
|
+
const rightTree={type:'row',children:[{type:'row',children:[]},...(rightNode.type==='row'?rightNode.children:[rightNode])]},right=mathRow(rightTree.children,cellStyle);
|
|
196
|
+
if(c.screen)right.refit=ratio=>mathScreenRelayout(rightTree,cellStyle,ratio);
|
|
197
|
+
right.rowItems=mathFlowAtoms(rightTree,right);
|
|
198
|
+
const relationGap=mathSpace('ord','rel',cellStyle);
|
|
199
|
+
const wrapped=mathWrapRow(right,cellStyle,Math.max(.05,width-anchor),{relationIndent:relationGap,binaryIndent:Math.min(relationGap+s,width*0.2)});
|
|
200
|
+
if(separate && left.svg)lines.push({box:mathResponsiveLayout(row[pair],cellStyle,width)});
|
|
201
|
+
const parts=separate?[[wrapped,0,0]]:[[left,anchor-left.w,0],[wrapped,anchor,0]];
|
|
202
|
+
lines.push({box:mathCompose(parts,anchor+mathInkWidth(wrapped),{lines:wrapped.lines || 1,lastBaseline:wrapped.lastBaseline || 0,fitted:wrapped.fitted})});
|
|
203
|
+
}
|
|
204
|
+
if(lines[start])lines[start].gapBefore=(n.rowGaps?.[r-1] || 0)*s;
|
|
205
|
+
}
|
|
206
|
+
return mathPositionStack(lines,c,n);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function mathHasMiddle(n) {
|
|
210
|
+
if(!n || typeof n!=='object' || n.type==='fence')return false;
|
|
211
|
+
if(n.type==='glyph' && n.delimiter)return true;
|
|
212
|
+
return Object.values(n).some(value=>Array.isArray(value)?value.some(mathHasMiddle):mathHasMiddle(value));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function mathResponsiveLayout(n,c,width,natural=mathLayout(n,c)) {
|
|
216
|
+
if(!Number.isFinite(width) || width<=0 || mathInkWidth(natural)<=width)return natural;
|
|
217
|
+
if(c.screen && !natural.refit)natural={...natural,refit:ratio=>mathScreenRelayout(n,c,ratio)};
|
|
218
|
+
if(n.type==='row') {
|
|
219
|
+
if(n.children.length===1)return mathResponsiveLayout(n.children[0],c,width,natural.rowItems?.[0]?.[0]);
|
|
220
|
+
return mathWrapRow({...natural,rowItems:mathFlowAtoms(n,natural)},c,width);
|
|
221
|
+
}
|
|
222
|
+
if(n.type==='style' && n.width===undefined && !n.border && !n.background && !n.class && !n.limits) {
|
|
223
|
+
const style={...c};
|
|
224
|
+
if(n.variant)style.variant=n.variant;
|
|
225
|
+
if(n.absoluteSize!==undefined)style.size=n.absoluteSize;
|
|
226
|
+
if(n.size!==undefined)style.size*=n.size;
|
|
227
|
+
if(n.mathStyle) {style.level=({display:0,text:0,script:1,scriptscript:2})[n.mathStyle];style.display=n.mathStyle==='display';style.cramped=false;}
|
|
228
|
+
const box=mathResponsiveLayout(n.body,style,width);
|
|
229
|
+
if(n.color)box.svg='<g fill="'+mathEscape(n.color)+'" color="'+mathEscape(n.color)+'" fill-opacity="1" stroke-opacity="1">'+box.svg+'</g>';
|
|
230
|
+
return box;
|
|
231
|
+
}
|
|
232
|
+
if(n.type==='frac' && n.bar!==false && n.barThickness!==0 && !n.left && !n.right) {
|
|
233
|
+
const wrapped=mathFraction(n,c,width);if(wrapped.innerLines>1)return mathFitBox(wrapped,width,c);
|
|
234
|
+
}
|
|
235
|
+
if(n.type==='table')return mathWrapAligned(n,c,width,natural);
|
|
236
|
+
if(n.type==='fence' && !n.size) {
|
|
237
|
+
if(mathHasMiddle(n.body))return mathFitBox(natural,width,c);
|
|
238
|
+
const local={...c,delimiterTarget:undefined};
|
|
239
|
+
let available=width,box=natural;
|
|
240
|
+
for(let pass=0;pass<4;pass++) {
|
|
241
|
+
let body=mathResponsiveLayout(n.body,local,Math.max(.05,available));
|
|
242
|
+
if(body.lines>1)body=mathCompose([[body,0,(body.h-body.d)/2-mathConstant('AxisHeight',c,.25)]],body.w,{lines:body.lines,fitted:body.fitted});
|
|
243
|
+
box=mathFenceBoxes(n.left,n.right,body,local);
|
|
244
|
+
box.lines=body.lines;box.fitted=body.fitted;
|
|
245
|
+
if(mathInkWidth(box)<=width)return box;
|
|
246
|
+
available=width-(box.w-body.w)-.05;
|
|
247
|
+
}
|
|
248
|
+
return mathFitBox(box,width,c);
|
|
249
|
+
}
|
|
250
|
+
return mathFitBox(natural,width,c);
|
|
251
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Command spellings name Unicode characters; glyph outlines belong to the font.
|
|
3
|
+
const MATH_SYMBOLS = Object.create(null);
|
|
4
|
+
function mathSymbolGroup(kind, entries, extra) {
|
|
5
|
+
for (const entry of entries.trim().split(/\s+/)) {
|
|
6
|
+
const split = entry.indexOf('=');
|
|
7
|
+
const value = entry.slice(split + 1);
|
|
8
|
+
for (const name of entry.slice(0, split).split('/'))
|
|
9
|
+
MATH_SYMBOLS[name] = Object.freeze({type: 'glyph', value, class: kind, ...extra});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
mathSymbolGroup('ord', `
|
|
13
|
+
alpha=α beta=β gamma=γ delta=δ epsilon=ϵ varepsilon=ε zeta=ζ eta=η theta=θ vartheta=ϑ
|
|
14
|
+
iota=ι kappa=κ varkappa=ϰ lambda=λ mu=μ nu=ν xi=ξ omicron=ο pi=π varpi=ϖ rho=ρ varrho=ϱ
|
|
15
|
+
sigma=σ varsigma=ς tau=τ upsilon=υ phi=ϕ varphi=φ chi=χ psi=ψ omega=ω digamma=ϝ
|
|
16
|
+
Gamma=Γ Delta=Δ Theta=Θ Lambda=Λ Xi=Ξ Pi=Π Sigma=Σ Upsilon=Υ Phi=Φ Psi=Ψ Omega=Ω
|
|
17
|
+
varGamma=Γ varDelta=Δ varTheta=Θ varLambda=Λ varXi=Ξ varPi=Π varSigma=Σ varUpsilon=Υ varPhi=Φ varPsi=Ψ varOmega=Ω
|
|
18
|
+
aleph=ℵ beth=ℶ gimel=ℷ daleth=ℸ ell=ℓ hbar=ℏ hslash=ℏ imath=𝚤 jmath=𝚥
|
|
19
|
+
Re=ℜ Im=ℑ wp=℘ partial=∂ nabla=∇ infty=∞ emptyset=∅ varnothing=∅ complement=∁
|
|
20
|
+
top=⊤ bot=⊥ forall=∀ exists=∃ nexists=∄ neg/lnot=¬ angle=∠ measuredangle=∡ sphericalangle=∢
|
|
21
|
+
triangle=△ blacktriangle=▴ triangledown=▽ blacktriangledown=▾ square/Box=□ blacksquare=■ Diamond=◇ blacklozenge=◆ lozenge=◊
|
|
22
|
+
clubsuit=♣ diamondsuit=♢ heartsuit=♡ spadesuit=♠ checkmark=✓ maltese=✠ yen=¥ pounds=£ euro=€
|
|
23
|
+
S=§ P=¶ copyright=© circledR=® circledS=Ⓢ mho=℧ Finv=Ⅎ Game=⅁ eth=ð
|
|
24
|
+
prime=′ backprime=‵ doubleprime=″ tripleprime=‴ degree=° flat=♭ natural=♮ sharp=♯
|
|
25
|
+
surd=√ diagup=╱ diagdown=╲ Bbbk=𝕜
|
|
26
|
+
`, {variant: 'roman'});
|
|
27
|
+
// Lowercase Greek retains the ordinary mathematical italic convention.
|
|
28
|
+
for (const name of 'alpha beta gamma delta epsilon varepsilon zeta eta theta vartheta iota kappa varkappa lambda mu nu xi omicron pi varpi rho varrho sigma varsigma tau upsilon phi varphi chi psi omega digamma'.split(' '))
|
|
29
|
+
MATH_SYMBOLS[name] = Object.freeze({...MATH_SYMBOLS[name], variant: 'normal'});
|
|
30
|
+
for (const name of Object.keys(MATH_SYMBOLS).filter(name => /^var[A-Z]/.test(name)))
|
|
31
|
+
MATH_SYMBOLS[name] = Object.freeze({...MATH_SYMBOLS[name], variant: 'italic'});
|
|
32
|
+
mathSymbolGroup('bin', `
|
|
33
|
+
pm=± mp=∓ times=× div=÷ cdot=⋅ cdotp=⋅ ast=∗ star=⋆ circ=∘ bullet=∙ diamond=⋄
|
|
34
|
+
cap=∩ cup=∪ uplus=⊎ sqcap=⊓ sqcup=⊔ vee/lor=∨ wedge/land=∧ setminus=∖ smallsetminus=∖
|
|
35
|
+
oplus=⊕ ominus=⊖ otimes=⊗ oslash=⊘ odot=⊙ bigcirc=◯ dag/dagger=† ddag/ddagger=‡ amalg=⨿
|
|
36
|
+
triangleleft=◁ triangleright=▷ lhd=⊲ rhd=⊳ unlhd=⊴ unrhd=⊵ wr=≀
|
|
37
|
+
barwedge=⊼ veebar=⊻ doublebarwedge=⩞ curlywedge=⋏ curlyvee=⋎ boxplus=⊞ boxminus=⊟ boxtimes=⊠ boxdot=⊡
|
|
38
|
+
ltimes=⋉ rtimes=⋊ leftthreetimes=⋋ rightthreetimes=⋌ dotplus=∔ divideontimes=⋇
|
|
39
|
+
circledast=⊛ circledcirc=⊚ circleddash=⊝ intercal=⊺ centerdot=· land=∧ lor=∨ bigtriangleup=△ bigtriangledown=▽
|
|
40
|
+
`, {variant: 'roman'});
|
|
41
|
+
mathSymbolGroup('rel', `
|
|
42
|
+
le/leq=≤ ge/geq=≥ neq/ne=≠ equiv=≡ sim=∼ simeq=≃ approx=≈ cong=≅ asymp=≍ propto=∝
|
|
43
|
+
ll=≪ gg=≫ prec=≺ succ=≻ preceq=⪯ succeq=⪰ subset=⊂ supset=⊃ subseteq=⊆ supseteq=⊇
|
|
44
|
+
sqsubset=⊏ sqsupset=⊐ sqsubseteq=⊑ sqsupseteq=⊒ in=∈ ni/owns=∋ notin=∉ notni=∌
|
|
45
|
+
vdash=⊢ dashv=⊣ models=⊨ perp=⊥ mid=∣ parallel=∥ smile=⌣ frown=⌢ bowtie=⋈ Join=⋈
|
|
46
|
+
doteq=≐ dotsim=≐ doteqdot/Doteq=≑ triangleq=≜ bumpeq=≏ Bumpeq=≎ eqcirc=≖ circeq=≗
|
|
47
|
+
risingdotseq=≓ fallingdotseq=≒ eqsim=≂ backsim=∽ backsimeq=⋍ approxeq=≊
|
|
48
|
+
lesssim=≲ gtrsim=≳ lessapprox=⪅ gtrapprox=⪆ lessdot=⋖ gtrdot=⋗ lesseqgtr=⋚ gtreqless=⋛
|
|
49
|
+
lesseqqgtr=⪋ gtreqqless=⪌ lessgtr=≶ gtrless=≷ lll/llless=⋘ ggg/gggtr=⋙
|
|
50
|
+
leqq=≦ geqq=≧ leqslant=⩽ geqslant=⩾ eqslantless=⪕ eqslantgtr=⪖
|
|
51
|
+
precsim=≾ succsim=≿ precapprox=⪷ succapprox=⪸ preccurlyeq=≼ succcurlyeq=≽ curlyeqprec=⋞ curlyeqsucc=⋟
|
|
52
|
+
Subset=⋐ Supset=⋑ subseteqq=⫅ supseteqq=⫆ pitchfork=⋔ between=≬ Vdash=⊩ Vvdash=⊪ vDash=⊨ VDash=⊫
|
|
53
|
+
blacktriangleleft=◂ blacktriangleright=▸ vartriangleleft=⊲ vartriangleright=⊳ trianglelefteq=⊴ trianglerighteq=⊵
|
|
54
|
+
varpropto=∝ shortmid=∣ shortparallel=∥ smallsmile=⌣ smallfrown=⌢ therefore=∴ because=∵
|
|
55
|
+
nless=≮ ngtr=≯ nleq=≰ ngeq=≱ nleqq=≰ ngeqq=≱ nleqslant=≰ ngeqslant=≱ lneq=⪇ gneq=⪈ lneqq=≨ gneqq=≩
|
|
56
|
+
lvertneqq=≨ gvertneqq=≩ nsim=≁ ncong=≇ napprox=≉ lnsim=⋦ gnsim=⋧ lnapprox=⪉ gnapprox=⪊
|
|
57
|
+
nprec=⊀ nsucc=⊁ npreceq=⋠ nsucceq=⋡ precneqq=⪵ succneqq=⪶ precnsim=⋨ succnsim=⋩ precnapprox=⪹ succnapprox=⪺
|
|
58
|
+
nsubset=⊄ nsupset=⊅ nsubseteq=⊈ nsupseteq=⊉ nsubseteqq=⊈ nsupseteqq=⊉ subsetneq=⊊ supsetneq=⊋
|
|
59
|
+
varsubsetneq=⊊ varsupsetneq=⊋ subsetneqq=⫋ supsetneqq=⫌ varsubsetneqq=⫋ varsupsetneqq=⫌
|
|
60
|
+
nmid=∤ nparallel=∦ nshortmid=∤ nshortparallel=∦ nvdash=⊬ nVdash=⊮ nvDash=⊭ nVDash=⊯
|
|
61
|
+
ntriangleleft=⋪ ntriangleright=⋫ ntrianglelefteq=⋬ ntrianglerighteq=⋭ coloneqq/coloneq=≔ eqqcolon=≕
|
|
62
|
+
`, {variant: 'roman'});
|
|
63
|
+
mathSymbolGroup('rel', `
|
|
64
|
+
leftarrow/gets=← rightarrow/to=→ leftrightarrow=↔ Leftarrow=⇐ Rightarrow=⇒ Leftrightarrow=⇔
|
|
65
|
+
longleftarrow=⟵ longrightarrow=⟶ longleftrightarrow=⟷ Longleftarrow=⟸ Longrightarrow=⟹ Longleftrightarrow=⟺
|
|
66
|
+
mapsto=↦ longmapsto=⟼ hookleftarrow=↩ hookrightarrow=↪ nearrow=↗ searrow=↘ nwarrow=↖ swarrow=↙
|
|
67
|
+
uparrow=↑ downarrow=↓ updownarrow=↕ Uparrow=⇑ Downarrow=⇓ Updownarrow=⇕
|
|
68
|
+
leftharpoonup=↼ leftharpoondown=↽ rightharpoonup=⇀ rightharpoondown=⇁ rightleftharpoons=⇌ leftrightharpoons=⇋
|
|
69
|
+
upharpoonleft=↿ upharpoonright=↾ downharpoonleft=⇃ downharpoonright=⇂ restriction=↾
|
|
70
|
+
leftrightarrows=⇆ rightleftarrows=⇄ leftleftarrows=⇇ rightrightarrows=⇉ upuparrows=⇈ downdownarrows=⇊
|
|
71
|
+
Lleftarrow=⇚ Rrightarrow=⇛ twoheadleftarrow=↞ twoheadrightarrow=↠ leftarrowtail=↢ rightarrowtail=↣
|
|
72
|
+
looparrowleft=↫ looparrowright=↬ leftrightsquigarrow=↭ rightsquigarrow/leadsto=⇝ curvearrowleft=↶ curvearrowright=↷
|
|
73
|
+
circlearrowleft=↺ circlearrowright=↻ dashleftarrow=⇠ dashrightarrow=⇢ multimap=⊸
|
|
74
|
+
nleftarrow=↚ nrightarrow=↛ nleftrightarrow=↮ nLeftarrow=⇍ nRightarrow=⇏ nLeftrightarrow=⇎
|
|
75
|
+
`, {variant: 'roman'});
|
|
76
|
+
mathSymbolGroup('op', `
|
|
77
|
+
sum=∑ prod=∏ coprod=∐ bigcup=⋃ bigcap=⋂ bigvee=⋁ bigwedge=⋀ biguplus=⨄ bigsqcup=⨆
|
|
78
|
+
bigotimes=⨂ bigoplus=⨁ bigodot=⨀ bigamalg=⨿ bigsqcap=⨅
|
|
79
|
+
`, {large: true, limits: 'display', variant: 'roman'});
|
|
80
|
+
mathSymbolGroup('op', `
|
|
81
|
+
int=∫ iint=∬ iiint=∭ iiiint=⨌ oint=∮ oiint=∯ oiiint=∰ intop=∫ smallint=∫
|
|
82
|
+
intclockwise=∱ varointclockwise=∲ ointctrclockwise=∳ sumint=⨋
|
|
83
|
+
`, {large: true, limits: 'never', variant: 'roman'});
|
|
84
|
+
MATH_SYMBOLS.intop = Object.freeze({...MATH_SYMBOLS.intop, limits: 'display'});
|
|
85
|
+
MATH_SYMBOLS.smallint = Object.freeze({...MATH_SYMBOLS.smallint, large: false});
|
|
86
|
+
mathSymbolGroup('open', 'langle=⟨ lbrace={ lgroup=⟮ lbrack=[ lfloor=⌊ lceil=⌈ lvert=| lVert=‖ lmoustache=⎰', {variant: 'roman'});
|
|
87
|
+
mathSymbolGroup('close', 'rangle=⟩ rbrace=} rgroup=⟯ rbrack=] rfloor=⌋ rceil=⌉ rvert=| rVert=‖ rmoustache=⎱', {variant: 'roman'});
|
|
88
|
+
mathSymbolGroup('ord', 'vert=| Vert=‖ backslash=∖ slash=/', {variant: 'roman'});
|
|
89
|
+
mathSymbolGroup('inner', 'dotsc=… cdots=⋯ dotsb=⋯ dotsm=⋯ dotsi=⋯ dotso=… ddots=⋱ mathellipsis=…', {variant: 'roman'});
|
|
90
|
+
mathSymbolGroup('ord', 'vdots=⋮', {variant: 'roman'});
|
|
91
|
+
mathSymbolGroup('punct', 'colon=: cdotp=⋅', {variant: 'roman'});
|
|
92
|
+
MATH_SYMBOLS.ldots = MATH_SYMBOLS.dots = Object.freeze({type: 'glyph', value: '…', class: 'inner', variant: 'roman'});
|
|
93
|
+
for (const [name, symbol] of Object.entries(MATH_SYMBOLS)) {
|
|
94
|
+
const {variant, ...fields} = symbol;
|
|
95
|
+
MATH_SYMBOLS[name] = Object.freeze({...fields, defaultVariant: variant});
|
|
96
|
+
}
|
|
97
|
+
const MATH_NAMED_OPERATORS = new Set(('arccos arcsin arctan arccot arcsec arccsc cos cosh cot coth csc csch sec sech sin sinh tan tanh exp log ln lg lb det dim gcd hom ker arg deg Pr erf erfc mod').split(' '));
|
|
98
|
+
const MATH_LIMIT_OPERATORS = new Set(('lim limsup liminf sup inf max min injlim projlim varlimsup varliminf varinjlim varprojlim').split(' '));
|
package/src/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bundle": "rapier-math-1.0.1.js",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"license": "MIT AND LicenseRef-GUST-Font-License",
|
|
5
|
+
"bytes": 803451,
|
|
6
|
+
"gzipBytes": 496298,
|
|
7
|
+
"sha256": "77640802b52935b60a66e2fd04c19d4702cee4b1a8b64aece5ca332236c09283",
|
|
8
|
+
"sha384": "1kdn6mohx3a5MAiwqRWY+6oviUHUheHfY1VMsxjDsUrn7woTaa7FSKo4juYtuNaF",
|
|
9
|
+
"sources": [
|
|
10
|
+
{
|
|
11
|
+
"file": "math/font.js",
|
|
12
|
+
"bytes": 651457,
|
|
13
|
+
"sha256": "b697b5ba770e35904fe56a0aa6c2f748253b3d3c5b9ab39c68f4a25afae02a0d"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"file": "math/symbols.js",
|
|
17
|
+
"bytes": 8415,
|
|
18
|
+
"sha256": "80f485e8023afe3783d30c4616709e0a3b0ed0ba8a238bd818085cdd107f03ad"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"file": "math/parser.js",
|
|
22
|
+
"bytes": 69355,
|
|
23
|
+
"sha256": "067594e1b1391a7c29b71ff51e0e812f16785e14a863dbc04069627760435633"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"file": "math/layout.js",
|
|
27
|
+
"bytes": 37394,
|
|
28
|
+
"sha256": "37cd00667aa1c7ef8b375c5e62171b1a7ceef53490cd13c8a77201e7ea6b13e8"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"file": "math/fallbacks.js",
|
|
32
|
+
"bytes": 7958,
|
|
33
|
+
"sha256": "a59c9a32f4a1ea49ad7454ea48aa25489e8e786459055d91fef045b8cc2b5f4b"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"file": "math/responsive.js",
|
|
37
|
+
"bytes": 13566,
|
|
38
|
+
"sha256": "387e2952317ba8124bd7f56a71feb9555f3cb6081de687d79eb4c9e6cab4b6ed"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"file": "math/browser.js",
|
|
42
|
+
"bytes": 8363,
|
|
43
|
+
"sha256": "0bf9fc6c5f02c2b0c5012118b80a8fad337fe25c3a4557e2747d4d4caa5c2e1b"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"file": "math/api.js",
|
|
47
|
+
"bytes": 5825,
|
|
48
|
+
"sha256": "f05bc69c0e13230079526a98072a2ae8ffd58bfb2df2aecb0e2f26fdb43dbeb6"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"recipe": [
|
|
52
|
+
{
|
|
53
|
+
"file": "math/build-font.py",
|
|
54
|
+
"bytes": 24804,
|
|
55
|
+
"sha256": "83f27e773d451a7275f6b7d521a257f79c27b3c61452a869703fe44c4ca8b878"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"file": "math/FONT-SOURCE.md",
|
|
59
|
+
"bytes": 8095,
|
|
60
|
+
"sha256": "05e0bba9e9bf340029c8bcc97492c17958949533717a62644505846905e65499"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"file": "math/FONT-LICENSE.txt",
|
|
64
|
+
"bytes": 21246,
|
|
65
|
+
"sha256": "91ded9a2a371cbeff028bfaf3bd3e97fddfb68af6f186aa261bfaff35baf800c"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"file": "math/package-verify.mjs",
|
|
69
|
+
"bytes": 2691,
|
|
70
|
+
"sha256": "6a53081729c020b7840363812138264195cb851a297c5542ef7aca007fdfc61f"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "tools/build-math.mjs",
|
|
74
|
+
"bytes": 5795,
|
|
75
|
+
"sha256": "7f8582f07832c2c2a50b0b6b552df3819e8e246f8aa7857e409d443b79d463f4"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"file": "math/LICENSE-MIT",
|
|
79
|
+
"bytes": 1071,
|
|
80
|
+
"sha256": "a3bc5d619ab9159f4eef2924e48dfb935556f4bc163f5ea61610b66a5915de70"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"components": [
|
|
84
|
+
{
|
|
85
|
+
"name": "Latin Modern Math",
|
|
86
|
+
"version": "1.959",
|
|
87
|
+
"license": "LicenseRef-GUST-Font-License",
|
|
88
|
+
"source": "https://mirror.apps.cam.ac.uk/pub/tex-archive/fonts/lm-math/opentype/latinmodern-math.otf",
|
|
89
|
+
"sha256": "6075562b771f8b82f0c179e363389684f2dd09de30038269e2628e504bd7be0f"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"font": {
|
|
93
|
+
"name": "Latin Modern Math",
|
|
94
|
+
"version": "1.959",
|
|
95
|
+
"license": "LicenseRef-GUST-Font-License",
|
|
96
|
+
"licenseName": "GUST Font License",
|
|
97
|
+
"source": "https://mirror.apps.cam.ac.uk/pub/tex-archive/fonts/lm-math/opentype/latinmodern-math.otf",
|
|
98
|
+
"sourceSha256": "6075562b771f8b82f0c179e363389684f2dd09de30038269e2628e504bd7be0f",
|
|
99
|
+
"copyright": "Copyright 2012--2014 for Latin Modern Math OTF by B. Jackowski, P. Strzelczyk and P. Pianowski (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.",
|
|
100
|
+
"licenseSource": "https://mirror.apps.cam.ac.uk/pub/tex-archive/fonts/lm-math/doc/GUST-FONT-LICENSE.txt",
|
|
101
|
+
"licenseFile": "math/FONT-LICENSE.txt",
|
|
102
|
+
"licenseSha256": "91ded9a2a371cbeff028bfaf3bd3e97fddfb68af6f186aa261bfaff35baf800c",
|
|
103
|
+
"licenseBytes": 21246
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Assemble the complete SVG math renderer from its own source and embedded font paths.
|
|
3
|
+
import {readFile, writeFile, mkdir} from 'node:fs/promises';
|
|
4
|
+
import {existsSync} from 'node:fs';
|
|
5
|
+
import {dirname, resolve} from 'node:path';
|
|
6
|
+
import {fileURLToPath} from 'node:url';
|
|
7
|
+
import {createHash} from 'node:crypto';
|
|
8
|
+
import {gzipSync} from 'node:zlib';
|
|
9
|
+
import {Script} from 'node:vm';
|
|
10
|
+
|
|
11
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
export const MATH_VERSION = '1.0.1';
|
|
13
|
+
export const MATH_BUNDLE = 'rapier-math-' + MATH_VERSION + '.js';
|
|
14
|
+
export const MATH_SOURCES = ['font.js', 'symbols.js', 'parser.js', 'layout.js', 'fallbacks.js', 'responsive.js', 'browser.js', 'api.js'].map(name => 'math/' + name);
|
|
15
|
+
export const MATH_RECIPE = ['math/build-font.py', 'math/FONT-SOURCE.md', 'math/FONT-LICENSE.txt', 'math/package-verify.mjs', 'tools/build-math.mjs', 'math/LICENSE-MIT'];
|
|
16
|
+
const FONT = {
|
|
17
|
+
name: 'Latin Modern Math', version: '1.959', license: 'LicenseRef-GUST-Font-License',
|
|
18
|
+
licenseName: 'GUST Font License',
|
|
19
|
+
source: 'https://mirror.apps.cam.ac.uk/pub/tex-archive/fonts/lm-math/opentype/latinmodern-math.otf',
|
|
20
|
+
sourceSha256: '6075562b771f8b82f0c179e363389684f2dd09de30038269e2628e504bd7be0f',
|
|
21
|
+
copyright: 'Copyright 2012--2014 for Latin Modern Math OTF by B. Jackowski, P. Strzelczyk and P. Pianowski (on behalf of TeX users groups). This work is released under the GUST Font License -- see http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt for details.',
|
|
22
|
+
licenseSource: 'https://mirror.apps.cam.ac.uk/pub/tex-archive/fonts/lm-math/doc/GUST-FONT-LICENSE.txt',
|
|
23
|
+
licenseFile: 'math/FONT-LICENSE.txt',
|
|
24
|
+
licenseSha256: '91ded9a2a371cbeff028bfaf3bd3e97fddfb68af6f186aa261bfaff35baf800c',
|
|
25
|
+
};
|
|
26
|
+
const digest = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
|
|
27
|
+
|
|
28
|
+
async function fontLicense(root) {
|
|
29
|
+
const bytes = await readFile(resolve(root, FONT.licenseFile));
|
|
30
|
+
if (digest(bytes) !== FONT.licenseSha256 || !bytes.includes(FONT.copyright))
|
|
31
|
+
throw new Error('The math font license differs from its source pin');
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function fillMathLicense(markup, {root = ROOT} = {}) {
|
|
36
|
+
const marker = '<pre class="license-text" data-license="math-font"></pre>';
|
|
37
|
+
if (markup.split(marker).length !== 2) throw new Error('The math font license needs one sheet entry');
|
|
38
|
+
const escaped = (await fontLicense(root)).toString('utf8').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
39
|
+
return markup.replace(marker, () => '<pre class="license-text">' + escaped + '</pre>');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function buildMath({root = ROOT, updatePins = true} = {}) {
|
|
43
|
+
const inputs = [];
|
|
44
|
+
for (const file of MATH_SOURCES) {
|
|
45
|
+
const bytes = await readFile(resolve(root, file));
|
|
46
|
+
inputs.push({file, bytes, text: bytes.toString('utf8')});
|
|
47
|
+
}
|
|
48
|
+
const license = await fontLicense(root);
|
|
49
|
+
if (!inputs[0].text.includes(license.toString('utf8').trim())) throw new Error('The embedded math font is missing its complete license');
|
|
50
|
+
const ownLicense = await readFile(resolve(root, 'math/LICENSE-MIT'), 'utf8');
|
|
51
|
+
const bytes = Buffer.from('/*!\n' + ownLicense.trim() + '\n*/\n(function(){\n\'use strict\';\n' + inputs.map(input => input.text.trimEnd()).join('\n\n') + '\n})();\n');
|
|
52
|
+
new Script(bytes.toString('utf8'), {filename: MATH_BUNDLE});
|
|
53
|
+
const gzipBytes = gzipSync(bytes, {level: 9}).length;
|
|
54
|
+
if (gzipBytes >= 500_000) throw new Error('The math plugin exceeds its 500 KB gzip budget: ' + gzipBytes);
|
|
55
|
+
const sha256 = digest(bytes), sha384 = digest(bytes, 'sha384', 'base64');
|
|
56
|
+
const recipe = [];
|
|
57
|
+
for (const file of MATH_RECIPE) {
|
|
58
|
+
const content = await readFile(resolve(root, file));
|
|
59
|
+
recipe.push({file, bytes: content.length, sha256: digest(content)});
|
|
60
|
+
}
|
|
61
|
+
const resources = {
|
|
62
|
+
bundle: MATH_BUNDLE, version: MATH_VERSION, license: 'MIT AND ' + FONT.license,
|
|
63
|
+
bytes: bytes.length, gzipBytes, sha256, sha384,
|
|
64
|
+
sources: inputs.map(input => ({file: input.file, bytes: input.bytes.length, sha256: digest(input.bytes)})),
|
|
65
|
+
recipe,
|
|
66
|
+
components: [{name: FONT.name, version: FONT.version, license: FONT.license, source: FONT.source, sha256: FONT.sourceSha256}],
|
|
67
|
+
font: {...FONT, licenseBytes: license.length},
|
|
68
|
+
};
|
|
69
|
+
const vendor = resolve(root, 'shell/vendor');
|
|
70
|
+
await mkdir(vendor, {recursive: true});
|
|
71
|
+
await writeFile(resolve(vendor, MATH_BUNDLE), bytes);
|
|
72
|
+
await writeFile(resolve(root, 'shell/math-resources.json'), JSON.stringify(resources, null, 2) + '\n');
|
|
73
|
+
const provenancePath = resolve(vendor, 'PROVENANCE.json');
|
|
74
|
+
const provenance = existsSync(provenancePath) ? JSON.parse(await readFile(provenancePath, 'utf8')) : {files: {}};
|
|
75
|
+
provenance.files[MATH_BUNDLE] = {
|
|
76
|
+
package: 'rapier-math', version: MATH_VERSION, path: 'tools/build-math.mjs; math/; shell/math-resources.json',
|
|
77
|
+
license: 'MIT AND ' + FONT.license + ' (math/LICENSE-MIT; math/FONT-LICENSE.txt)', bytes: bytes.length, sha256,
|
|
78
|
+
};
|
|
79
|
+
await writeFile(provenancePath, JSON.stringify(provenance, null, 2) + '\n');
|
|
80
|
+
const loaderPath = resolve(root, 'shell/plugin-loader.js');
|
|
81
|
+
if (updatePins && existsSync(loaderPath)) {
|
|
82
|
+
const loader = await readFile(loaderPath, 'utf8');
|
|
83
|
+
const block = /(key: 'math',[\s\S]*?bytes: )\d+(,[\s\S]*?sri: ')[^']+(')/;
|
|
84
|
+
if (!block.test(loader)) throw new Error('The math loader pin could not be located');
|
|
85
|
+
await writeFile(loaderPath, loader.replace(block, (_, before, middle, after) => before + bytes.length + middle + sha384 + after));
|
|
86
|
+
}
|
|
87
|
+
return {bundle: MATH_BUNDLE, bytes: bytes.length, gzipBytes, sha256, sha384};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
91
|
+
console.log(JSON.stringify(await buildMath({root: process.argv[2] ? resolve(process.argv[2]) : ROOT})));
|
|
92
|
+
}
|
package/verify.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Verify a standalone package and rebuild its payload from the carried source.
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import {readFile} from 'node:fs/promises';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {dirname, resolve} from 'node:path';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const source = resolve(root, 'src');
|
|
11
|
+
const read = path => readFile(resolve(root, path));
|
|
12
|
+
const hash = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
|
|
13
|
+
const resources = JSON.parse(await read('src/shell/math-resources.json'));
|
|
14
|
+
const payload = await read(resources.bundle);
|
|
15
|
+
assert.equal(payload.length, resources.bytes, 'payload length');
|
|
16
|
+
assert.equal(hash(payload), resources.sha256, 'payload SHA-256');
|
|
17
|
+
assert.equal(hash(payload, 'sha384', 'base64'), resources.sha384, 'payload SHA-384');
|
|
18
|
+
for (const input of [...resources.sources, ...resources.recipe]) {
|
|
19
|
+
const bytes = await read('src/' + input.file);
|
|
20
|
+
assert.equal(bytes.length, input.bytes, input.file + ': length');
|
|
21
|
+
assert.equal(hash(bytes), input.sha256, input.file + ': SHA-256');
|
|
22
|
+
}
|
|
23
|
+
const rendererLicense = await read('src/math/LICENSE-MIT');
|
|
24
|
+
for (const file of ['LICENSE-MIT', 'math-LICENSE.txt']) assert.deepEqual(await read(file), rendererLicense, file);
|
|
25
|
+
const fontLicense = await read('src/' + resources.font.licenseFile);
|
|
26
|
+
assert.equal(hash(fontLicense), resources.font.licenseSha256, 'font license SHA-256');
|
|
27
|
+
assert.deepEqual(await read('math-font-LICENSE.txt'), fontLicense, 'font license bytes');
|
|
28
|
+
assert(payload.includes(rendererLicense.toString('utf8').trim()), 'payload renderer license');
|
|
29
|
+
assert(payload.includes(fontLicense.toString('utf8').trim()), 'payload font license');
|
|
30
|
+
const sbom = JSON.parse(await read('math-components.json'));
|
|
31
|
+
const font = sbom.packages.find(item => item.name === resources.font.name);
|
|
32
|
+
assert.equal(font?.copyrightText, resources.font.copyright, 'font copyright');
|
|
33
|
+
assert.equal(font.licenseDeclared, resources.font.license, 'font license identity');
|
|
34
|
+
assert.equal(sbom.hasExtractedLicensingInfos.find(item => item.licenseId === resources.font.license)?.extractedText,
|
|
35
|
+
fontLicense.toString('utf8'), 'complete extracted font license');
|
|
36
|
+
const {buildMath} = await import('./src/tools/build-math.mjs');
|
|
37
|
+
await buildMath({root: source, updatePins: false});
|
|
38
|
+
assert.deepEqual(await read('src/shell/vendor/' + resources.bundle), payload, 'rebuilt payload bytes');
|
|
39
|
+
console.log(JSON.stringify({file: resources.bundle, bytes: payload.length, sha256: resources.sha256,
|
|
40
|
+
sources: resources.sources.length + resources.recipe.length, rebuild: 'exact', licenses: 'complete'}));
|