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.
- package/LICENSE +484 -0
- package/LICENSE-MIT +21 -0
- package/README.md +110 -2
- package/check.mjs +97 -0
- package/index.mjs +2953 -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.2.js +2952 -0
- package/src/math/FONT-LICENSE.txt +457 -0
- package/src/math/FONT-SOURCE.md +143 -0
- package/src/math/LICENSE-MIT +21 -0
- package/src/math/api.js +87 -0
- package/src/math/browser.js +181 -0
- package/src/math/build-font.py +622 -0
- package/src/math/fallbacks.js +111 -0
- package/src/math/font.js +547 -0
- package/src/math/layout.js +580 -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 +95 -0
- package/verify.mjs +40 -0
package/src/math/api.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
function mathSvg(box,source,error,utf16,options={}) {
|
|
3
|
+
if(box.svg.length>8*1024*1024)throw Object.assign(new Error('This formula produces too much SVG to render safely.'),{code:'resource-limit'});
|
|
4
|
+
const pad=0.025,left=Math.min(0,box.left)-pad,right=Math.max(box.w,box.right)+pad;
|
|
5
|
+
const width=Math.max(0.05,right-left),height=Math.max(0.025,box.h+pad,-box.inkTop+pad),depth=Math.max(0.025,box.d+pad,box.inkBottom+pad);
|
|
6
|
+
const viewBox=[left*1000,-height*1000,width*1000,(height+depth)*1000];
|
|
7
|
+
const opacity=options.screen?.dark ? .94 : 1;
|
|
8
|
+
if (![width,height,depth,left,...viewBox].every(Number.isFinite)) throw new Error('Mathematical dimensions are not finite');
|
|
9
|
+
const escapedSource=mathEscape(source);
|
|
10
|
+
const svg='<svg xmlns="http://www.w3.org/2000/svg" role="img" aria-label="'+escapedSource+'"'+
|
|
11
|
+
' data-rapier-math="'+(options.displayMode?'display':'inline')+'" data-math-lines="'+(box.lines || 1)+'"'+
|
|
12
|
+
' data-math-natural-width="'+mathNumber(options.naturalWidth ?? width)+'"'+
|
|
13
|
+
(options.screen?' data-math-screen="'+mathNumber(options.screen.fontSize)+'|'+mathNumber(options.screen.pixelRatio)+'|'+options.screen.dark+'"':'')+
|
|
14
|
+
(options.maxWidth?' data-math-max-width="'+mathNumber(options.maxWidth)+'"':'')+
|
|
15
|
+
(utf16?' data-tex-utf16="'+utf16+'"':'')+
|
|
16
|
+
(error?' data-math-error="'+mathEscape(error.code)+'"':'')+' width="'+mathNumber(width)+'em" height="'+mathNumber(height+depth)+'em"'+
|
|
17
|
+
' viewBox="'+viewBox.map(mathNumber).join(' ')+'"'+
|
|
18
|
+
' style="font-size:'+mathNumber(mathFontRatio)+'em;max-width:100%;height:auto;vertical-align:-'+mathNumber(depth)+'em;'+
|
|
19
|
+
'fill-opacity:'+opacity+';stroke-opacity:'+opacity+'" fill="currentColor">'+
|
|
20
|
+
'<title>'+escapedSource+'</title>'+(error?'<desc>'+mathEscape(error.message)+'</desc>':'')+
|
|
21
|
+
'<g transform="scale(1000)">'+box.svg+'</g></svg>';
|
|
22
|
+
return {svg,width,height,depth,lines:box.lines || 1,fitted:!!box.fitted,errors:error?[error]:[]};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mathSourceAlternative(source) {
|
|
26
|
+
let alternative='',first=-1,kept=0;
|
|
27
|
+
for(let i=0;i<source.length;i++) {
|
|
28
|
+
const code=source.charCodeAt(i);
|
|
29
|
+
if(code>=0xd800 && code<=0xdbff && i+1<source.length) {
|
|
30
|
+
const next=source.charCodeAt(i+1);
|
|
31
|
+
if(next>=0xdc00 && next<=0xdfff) {i++;continue;}
|
|
32
|
+
}
|
|
33
|
+
if((code>=0x20 && code<=0xd7ff) || (code>=0xe000 && code<=0xfffd) || code===9 || code===10 || code===13)continue;
|
|
34
|
+
if(first<0)first=i;
|
|
35
|
+
alternative+=source.slice(kept,i)+'\\u'+code.toString(16).toUpperCase().padStart(4,'0');kept=i+1;
|
|
36
|
+
}
|
|
37
|
+
if(first<0)return {alternative:source};
|
|
38
|
+
alternative+=source.slice(kept);
|
|
39
|
+
// Only malformed XML text needs a separate carrier; each four hex digits keep one original code unit.
|
|
40
|
+
let utf16='';
|
|
41
|
+
for(let i=0;i<source.length;i++)utf16+=source.charCodeAt(i).toString(16).padStart(4,'0');
|
|
42
|
+
return {alternative,utf16,error:{code:'invalid-source-character',offset:first,
|
|
43
|
+
message:'A source character cannot be embedded in SVG. The text alternative shows Unicode escapes; data-tex-utf16 keeps the original source.'}};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderMath(source,options={}) {
|
|
47
|
+
const text=String(source ?? ''),safe=mathSourceAlternative(text),context={size:1,level:0,variant:'normal',cramped:false,display:!!options?.displayMode,cache:new WeakMap(),budget:{nodes:0,outlineBytes:0}};
|
|
48
|
+
try {
|
|
49
|
+
if(safe.error)throw safe.error;
|
|
50
|
+
if(options?.screen!==undefined) {
|
|
51
|
+
const {fontSize,pixelRatio,dark}=options.screen || {},scaled=Number.isFinite(fontSize)?fontSize*mathFontRatio:NaN;
|
|
52
|
+
if(!Number.isFinite(fontSize) || fontSize<=0 || !Number.isFinite(pixelRatio) || pixelRatio<=0 ||
|
|
53
|
+
typeof dark!=='boolean' || !Number.isFinite(scaled) || scaled<=0 || !Number.isFinite(scaled*pixelRatio) || scaled*pixelRatio<=0 ||
|
|
54
|
+
!Number.isFinite(1/scaled) || !Number.isFinite(1/pixelRatio) || !Number.isFinite(1/(scaled*pixelRatio)))
|
|
55
|
+
throw Object.assign(new Error('Screen font size, pixel ratio and device scale must be positive and finite; dark must be a boolean.'),{code:'invalid-screen'});
|
|
56
|
+
context.screen={fontSize:scaled,pixelRatio,dark};
|
|
57
|
+
}
|
|
58
|
+
const tree=parseMathTex(text),natural=mathLayout(tree,context),limit=Number(options?.maxWidth);
|
|
59
|
+
const maxWidth=Number.isFinite(limit) && limit>.1?limit:undefined;
|
|
60
|
+
const box=maxWidth && mathInkWidth(natural)>maxWidth-.05?mathResponsiveLayout(tree,context,maxWidth-.05,natural):natural;
|
|
61
|
+
return mathSvg(box,safe.alternative,undefined,undefined,{displayMode:context.display,maxWidth,naturalWidth:mathInkWidth(natural)+.05,screen:context.screen});
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
const error={code:cause.code || 'invalid-tex',message:String(cause.message || cause),offset:Number.isFinite(cause.offset)?cause.offset:0};
|
|
65
|
+
if(cause.command)error.command=cause.command;
|
|
66
|
+
// A diagnostic is visible, but remains distinct from a successfully typeset formula.
|
|
67
|
+
// A hostile command can have a very long name. Keep its full diagnostic in desc/errors,
|
|
68
|
+
// while the visible notice stays finite and can render after the formula exhausted its budget.
|
|
69
|
+
const label=error.message.length>240?'This formula could not be rendered. The complete diagnostic is in its text description.':error.message.replace(/[^\x20-\x7e]/g,'?');
|
|
70
|
+
const diagnostic={...context,variant:'roman',size:0.8,budget:{nodes:0,outlineBytes:0}};
|
|
71
|
+
try {
|
|
72
|
+
return mathSvg(mathText(label,diagnostic),safe.alternative,error,safe.utf16,{displayMode:context.display,screen:context.screen});
|
|
73
|
+
} catch {
|
|
74
|
+
diagnostic.screen=undefined;
|
|
75
|
+
return mathSvg(mathText(label,diagnostic),safe.alternative,error,safe.utf16,{displayMode:context.display});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
globalThis.RapierMath=Object.freeze({
|
|
81
|
+
version:'1.0.2',
|
|
82
|
+
parse:parseMathTex,
|
|
83
|
+
render:renderMath,
|
|
84
|
+
fit:mathFit,
|
|
85
|
+
observe:mathObserve,
|
|
86
|
+
renderToString(source,options) {return renderMath(source,options).svg;},
|
|
87
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The browser supplies the column and physical pixel grid; rendering remains synchronous and pure.
|
|
3
|
+
const mathBrowserStates=new WeakMap(),mathBrowserDocuments=new WeakMap(),mathRuleOriginals=new WeakMap();
|
|
4
|
+
const mathSvgSelector='svg[data-rapier-math]:not([data-math-error])';
|
|
5
|
+
|
|
6
|
+
function mathElements(root) {
|
|
7
|
+
return [...(root?.matches?.(mathSvgSelector)?[root]:[]),...root?.querySelectorAll?.(mathSvgSelector) || []];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function mathColumn(svg) {
|
|
11
|
+
const view=svg.ownerDocument.defaultView;
|
|
12
|
+
for(let node=svg.parentElement;node;node=node.parentElement) {
|
|
13
|
+
const style=view.getComputedStyle(node);
|
|
14
|
+
if(style.display==='contents' || style.display==='inline' || node.matches('.math-rendered'))continue;
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
return svg.ownerDocument.documentElement;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mathDarkPaper(svg) {
|
|
21
|
+
const view=svg.ownerDocument.defaultView;
|
|
22
|
+
if(view.matchMedia('(forced-colors: active)').matches || view.matchMedia('(prefers-contrast: more)').matches)return false;
|
|
23
|
+
for(let node=svg.parentElement;node;node=node.parentElement) {
|
|
24
|
+
const style=view.getComputedStyle(node),color=style.backgroundColor.match(/[\d.]+/g)?.map(Number);
|
|
25
|
+
if(style.backgroundImage!=='none')return false;
|
|
26
|
+
if(!color || color.length<3 || (color[3] ?? 1)<1)continue;
|
|
27
|
+
const rgb=color.slice(0,3).map(value=>value/255).map(value=>value<=.04045?value/12.92:((value+.055)/1.055)**2.4);
|
|
28
|
+
return rgb[0]*.2126+rgb[1]*.7152+rgb[2]*.0722<.18;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mathSnapRule(original, matrix, ratio) {
|
|
34
|
+
if (!original.width || !original.height || !matrix || !matrix.a || !matrix.d ||
|
|
35
|
+
Math.abs(matrix.b) + Math.abs(matrix.c) > 1e-6 || !(ratio > 0)) return null;
|
|
36
|
+
const snap = value => Math.round(value * ratio) / ratio;
|
|
37
|
+
const x = (snap(original.x * matrix.a + matrix.e) - matrix.e) / matrix.a;
|
|
38
|
+
const y = (snap(original.y * matrix.d + matrix.f) - matrix.f) / matrix.d;
|
|
39
|
+
const width = Math.max(1 / ratio, snap(original.width * Math.abs(matrix.a))) / Math.abs(matrix.a);
|
|
40
|
+
const height = Math.max(1 / ratio, snap(original.height * Math.abs(matrix.d))) / Math.abs(matrix.d);
|
|
41
|
+
return {x, y, width, height, d: 'M' + mathNumber(x) + ' ' + mathNumber(y) + 'h' + mathNumber(width) +
|
|
42
|
+
'v' + mathNumber(height) + 'h' + mathNumber(-width) + 'Z'};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mathSnapRules(svg) {
|
|
46
|
+
const ratio=svg.ownerDocument.defaultView.devicePixelRatio || 1;
|
|
47
|
+
for(const rule of svg.querySelectorAll('[data-math-rule],[data-math-line]')) {
|
|
48
|
+
let original=mathRuleOriginals.get(rule);
|
|
49
|
+
if(!original) {
|
|
50
|
+
const box=rule.getBBox(), axis=rule.getAttribute('data-math-line'), thickness=Number(rule.getAttribute('stroke-width'));
|
|
51
|
+
original={x:box.x,y:box.y,width:box.width,height:box.height,axis};
|
|
52
|
+
if(axis==='horizontal') {original.y-=thickness/2;original.height=thickness;}
|
|
53
|
+
if(axis==='vertical') {original.x-=thickness/2;original.width=thickness;}
|
|
54
|
+
mathRuleOriginals.set(rule,original);
|
|
55
|
+
}
|
|
56
|
+
const snapped=mathSnapRule(original,rule.getScreenCTM(),ratio);
|
|
57
|
+
if(!snapped)continue;
|
|
58
|
+
let path=snapped.d;
|
|
59
|
+
if(original.axis==='horizontal') {
|
|
60
|
+
path='M'+mathNumber(snapped.x)+' '+mathNumber(snapped.y+snapped.height/2)+'h'+mathNumber(snapped.width);
|
|
61
|
+
rule.setAttribute('stroke-width',mathNumber(snapped.height));
|
|
62
|
+
}
|
|
63
|
+
if(original.axis==='vertical') {
|
|
64
|
+
path='M'+mathNumber(snapped.x+snapped.width/2)+' '+mathNumber(snapped.y)+'v'+mathNumber(snapped.height);
|
|
65
|
+
rule.setAttribute('stroke-width',mathNumber(snapped.width));
|
|
66
|
+
}
|
|
67
|
+
rule.setAttribute('d',path);
|
|
68
|
+
rule.setAttribute('shape-rendering','crispEdges');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mathFitElement(svg, printing = false) {
|
|
73
|
+
if(!svg.isConnected)return null;
|
|
74
|
+
const doc=svg.ownerDocument,view=doc.defaultView,column=mathColumn(svg),style=view.getComputedStyle(column),font=parseFloat(view.getComputedStyle(svg).fontSize);
|
|
75
|
+
const available=column.clientWidth-(parseFloat(style.paddingLeft) || 0)-(parseFloat(style.paddingRight) || 0);
|
|
76
|
+
if(!(available>0 && font>0))return column;
|
|
77
|
+
const width=Math.max(.1,(available-.25)/font),source=svg.getAttribute('aria-label'),display=svg.getAttribute('data-rapier-math')==='display';
|
|
78
|
+
const dark=!printing && mathDarkPaper(svg),ratio=view.devicePixelRatio || 1;
|
|
79
|
+
const screen=printing?undefined:{fontSize:font/mathFontRatio,pixelRatio:ratio,dark};
|
|
80
|
+
const screenKey=printing?'':mathNumber(font)+'|'+mathNumber(ratio)+'|'+dark;
|
|
81
|
+
let state=mathBrowserStates.get(svg);
|
|
82
|
+
if(!state || state.source!==source) {
|
|
83
|
+
state={source,natural:Number(svg.getAttribute('data-math-natural-width')) || Infinity,width:Number(svg.getAttribute('data-math-max-width')) || null,
|
|
84
|
+
screen:svg.getAttribute('data-math-screen') || ''};
|
|
85
|
+
mathBrowserStates.set(svg,state);
|
|
86
|
+
}
|
|
87
|
+
const target=state.natural>width?width:null;
|
|
88
|
+
if(screenKey!==state.screen || (target===null)!==(state.width===null) || target!==null && Math.abs(target-state.width)>.005) {
|
|
89
|
+
const rendered=renderMath(source,{displayMode:display,maxWidth:(screenKey!==state.screen?width:target) || undefined,screen});
|
|
90
|
+
if(!rendered.errors.length) {
|
|
91
|
+
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']) {
|
|
93
|
+
if(replacement.hasAttribute(name))svg.setAttribute(name,replacement.getAttribute(name));else svg.removeAttribute(name);
|
|
94
|
+
}
|
|
95
|
+
svg.style.verticalAlign=replacement.style.verticalAlign;
|
|
96
|
+
svg.replaceChildren(...Array.from(replacement.childNodes,node=>doc.importNode(node,true)));
|
|
97
|
+
state.natural=Number(svg.getAttribute('data-math-natural-width'));state.screen=screenKey;
|
|
98
|
+
state.width=state.natural>width?width:null;
|
|
99
|
+
if(state.width===null)svg.removeAttribute('data-math-max-width');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const opacity=dark?'.94':'1';
|
|
103
|
+
svg.style.fillOpacity=opacity;svg.style.strokeOpacity=opacity;
|
|
104
|
+
for(const node of svg.querySelectorAll('[fill],[color],[stroke]')) {
|
|
105
|
+
if(['fill','color','stroke'].some(name=>node.hasAttribute(name) && !['none','currentColor','inherit'].includes(node.getAttribute(name)))) {
|
|
106
|
+
node.setAttribute('fill-opacity','1');node.setAttribute('stroke-opacity','1');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if(!printing)mathSnapRules(svg);
|
|
110
|
+
return column;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mathFit(root) {
|
|
114
|
+
const nodes=mathElements(root);
|
|
115
|
+
for(const svg of nodes)mathFitElement(svg,svg.ownerDocument.defaultView.matchMedia('print').matches);
|
|
116
|
+
return nodes.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mathObserveDocument(doc) {
|
|
120
|
+
const view=doc.defaultView,nodes=new Map(),columns=new Set();
|
|
121
|
+
let frame=null,active=true,printing=view.matchMedia('print').matches;
|
|
122
|
+
const refresh=()=> {
|
|
123
|
+
if(!active)return;
|
|
124
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
125
|
+
frame=null;
|
|
126
|
+
const wanted=new Set();
|
|
127
|
+
for(const svg of nodes.keys()) {
|
|
128
|
+
if(!svg.isConnected || svg.ownerDocument!==doc || !svg.matches(mathSvgSelector))continue;
|
|
129
|
+
const column=mathFitElement(svg,printing);if(column)wanted.add(column);
|
|
130
|
+
}
|
|
131
|
+
for(const column of columns)if(!wanted.has(column)) {resize.unobserve(column);columns.delete(column);}
|
|
132
|
+
for(const column of wanted)if(!columns.has(column)) {resize.observe(column);columns.add(column);}
|
|
133
|
+
};
|
|
134
|
+
const schedule=()=>{if(active && frame===null)frame=view.requestAnimationFrame(refresh);};
|
|
135
|
+
const resize=new view.ResizeObserver(schedule),theme=new view.MutationObserver(records=>{
|
|
136
|
+
for(const {target} of records)for(const svg of nodes.keys())
|
|
137
|
+
if(target!==svg && target.contains(svg)) {schedule();return;}
|
|
138
|
+
});
|
|
139
|
+
const beforePrint=()=>{printing=true;refresh();},afterPrint=()=>{printing=false;schedule();};
|
|
140
|
+
const media=['(forced-colors: active)','(prefers-contrast: more)','(prefers-color-scheme: dark)'].map(query=>view.matchMedia(query));
|
|
141
|
+
const fonts=doc.fonts;
|
|
142
|
+
theme.observe(doc.documentElement,{subtree:true,attributes:true,attributeFilter:['class','style','data-rapier-theme','data-md-theme']});
|
|
143
|
+
view.addEventListener('resize',schedule,{passive:true});
|
|
144
|
+
view.addEventListener('beforeprint',beforePrint);view.addEventListener('afterprint',afterPrint);
|
|
145
|
+
for(const query of media)query.addEventListener('change',schedule);
|
|
146
|
+
fonts?.addEventListener('loadingdone',schedule);
|
|
147
|
+
const state={nodes,schedule,dispose:()=>{
|
|
148
|
+
active=false;
|
|
149
|
+
if(frame!==null)view.cancelAnimationFrame(frame);
|
|
150
|
+
frame=null;
|
|
151
|
+
resize.disconnect();theme.disconnect();columns.clear();nodes.clear();
|
|
152
|
+
view.removeEventListener('resize',schedule);
|
|
153
|
+
view.removeEventListener('beforeprint',beforePrint);view.removeEventListener('afterprint',afterPrint);
|
|
154
|
+
for(const query of media)query.removeEventListener('change',schedule);
|
|
155
|
+
fonts?.removeEventListener('loadingdone',schedule);
|
|
156
|
+
mathBrowserDocuments.delete(doc);
|
|
157
|
+
}};
|
|
158
|
+
mathBrowserDocuments.set(doc,state);
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mathObserve(root) {
|
|
163
|
+
const doc=root?.ownerDocument || root;
|
|
164
|
+
if(!doc?.defaultView)return ()=>{};
|
|
165
|
+
let owned=mathElements(root);
|
|
166
|
+
if(!owned.length)return ()=>{};
|
|
167
|
+
let state=mathBrowserDocuments.get(doc) || mathObserveDocument(doc);
|
|
168
|
+
// Each returned cleanup owns its subscription, including overlap and detached nodes.
|
|
169
|
+
for(const svg of owned)state.nodes.set(svg,(state.nodes.get(svg) || 0)+1);
|
|
170
|
+
state.schedule();
|
|
171
|
+
return ()=>{
|
|
172
|
+
if(!owned)return;
|
|
173
|
+
const current=state;state=null;
|
|
174
|
+
for(const svg of owned) {
|
|
175
|
+
const remaining=current.nodes.get(svg)-1;
|
|
176
|
+
if(remaining)current.nodes.set(svg,remaining);else current.nodes.delete(svg);
|
|
177
|
+
}
|
|
178
|
+
owned=null;
|
|
179
|
+
if(current.nodes.size)current.schedule();else current.dispose();
|
|
180
|
+
};
|
|
181
|
+
}
|