rapier-math 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -85
- package/adoption-check.mjs +149 -0
- package/check.mjs +21 -0
- package/classic.d.ts +4 -0
- package/delimiters.d.ts +8 -0
- package/delimiters.mjs +39 -0
- package/examples/README.md +25 -0
- package/examples/browser.html +10 -0
- package/examples/browser.mjs +24 -0
- package/examples/formula.mjs +12 -0
- package/examples/markdown.mjs +9 -0
- package/examples/react.tsx +19 -0
- package/examples/stream.mjs +19 -0
- package/examples/unified.mjs +26 -0
- package/examples/worker.mjs +7 -0
- package/index.d.ts +43 -0
- package/index.mjs +43 -15
- package/integration.d.ts +9 -0
- package/integration.mjs +28 -0
- package/markdown-it.d.ts +6 -0
- package/markdown-it.mjs +45 -0
- package/markdown-rules.mjs +65 -0
- package/math-components.json +6 -6
- package/package.json +78 -5
- package/{rapier-math-1.0.2.js → rapier-math-1.1.0.js} +43 -15
- package/rehype.d.ts +6 -0
- package/rehype.mjs +37 -0
- package/src/math/README.md +210 -0
- package/src/math/adoption-verify.mjs +149 -0
- package/src/math/api.js +35 -13
- package/src/math/browser.js +7 -1
- package/src/math/classic.d.ts +4 -0
- package/src/math/delimiters.d.ts +8 -0
- package/src/math/delimiters.mjs +39 -0
- package/src/math/examples/README.md +25 -0
- package/src/math/examples/browser.html +10 -0
- package/src/math/examples/browser.mjs +24 -0
- package/src/math/examples/formula.mjs +12 -0
- package/src/math/examples/markdown.mjs +9 -0
- package/src/math/examples/react.tsx +19 -0
- package/src/math/examples/stream.mjs +19 -0
- package/src/math/examples/unified.mjs +26 -0
- package/src/math/examples/worker.mjs +7 -0
- package/src/math/index.d.ts +43 -0
- package/src/math/integration.d.ts +9 -0
- package/src/math/integration.mjs +28 -0
- package/src/math/layout.js +1 -1
- package/src/math/markdown-it.d.ts +6 -0
- package/src/math/markdown-it.mjs +45 -0
- package/src/math/markdown-rules.mjs +65 -0
- package/src/math/package-build.mjs +4 -4
- package/src/math/package-check.mjs +118 -0
- package/src/math/package-readme.mjs +8 -0
- package/src/math/package-verify.mjs +7 -2
- package/src/math/rehype.d.ts +6 -0
- package/src/math/rehype.mjs +37 -0
- package/src/math/stream.d.ts +14 -0
- package/src/math/stream.mjs +46 -0
- package/src/package.json +1 -1
- package/src/shell/math-resources.json +176 -17
- package/src/tools/build-math.mjs +19 -3
- package/stream.d.ts +14 -0
- package/stream.mjs +46 -0
- package/verify.mjs +7 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import rapierMath from 'rapier-math/markdown-it';
|
|
4
|
+
|
|
5
|
+
export const md = new MarkdownIt({html: false}).use(rapierMath);
|
|
6
|
+
const source = 'It costs $5 and $10. Code: `$$x$$`. Math: \\(x^2\\).\n\n$$\\frac{1}{2}$$\n';
|
|
7
|
+
const env = {};
|
|
8
|
+
console.log(md.render(source, env));
|
|
9
|
+
console.error(env.rapierMathErrors || []);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {useEffect, useMemo, useRef} from 'react';
|
|
3
|
+
import {render, observe} from 'rapier-math';
|
|
4
|
+
|
|
5
|
+
export function Equation({source, displayMode = false}: {source: string; displayMode?: boolean}) {
|
|
6
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
7
|
+
const result = useMemo(() => {
|
|
8
|
+
try { return render(source, {displayMode}); }
|
|
9
|
+
catch { return null; }
|
|
10
|
+
}, [source, displayMode]);
|
|
11
|
+
const svg = result && result.errors.length === 0 ? result.svg : null;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (ref.current && svg) return observe(ref.current);
|
|
14
|
+
}, [svg]); // Cleanup runs before replacement setup and on unmount.
|
|
15
|
+
if (!svg) return <code>{source}</code>;
|
|
16
|
+
// The server and initial client render use identical source/options. Fit only
|
|
17
|
+
// after hydration; never pass authored HTML to dangerouslySetInnerHTML.
|
|
18
|
+
return <span ref={ref} dangerouslySetInnerHTML={{__html: svg}} />;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import MarkdownIt from 'markdown-it';
|
|
3
|
+
import rapierMath from 'rapier-math/markdown-it';
|
|
4
|
+
import {createMarkdownStream} from 'rapier-math/stream';
|
|
5
|
+
|
|
6
|
+
const md = new MarkdownIt({html: false}).use(rapierMath);
|
|
7
|
+
const stream = createMarkdownStream(source => md.render(source, {}), {previewEvery: 16});
|
|
8
|
+
for (const chunk of ['The answer is \\(', '\\frac{1}', '{2}', '\\)', '. Code: `$$x$$`.']) {
|
|
9
|
+
const preview = stream.write(chunk);
|
|
10
|
+
if (preview) console.log({phase: 'preview', ...preview});
|
|
11
|
+
}
|
|
12
|
+
const final = stream.finish();
|
|
13
|
+
console.log({phase: 'complete', ...final});
|
|
14
|
+
if (final.html !== md.render(final.source, {})) throw new Error('Batch mismatch');
|
|
15
|
+
|
|
16
|
+
// One stream per message. On replacement: cancel its transport, reset, write.
|
|
17
|
+
stream.reset();
|
|
18
|
+
stream.write('Replacement \\(y\\).');
|
|
19
|
+
console.log(stream.finish());
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {unified} from 'unified';
|
|
3
|
+
import remarkParse from 'remark-parse';
|
|
4
|
+
import remarkMath from 'remark-math';
|
|
5
|
+
import remarkRehype from 'remark-rehype';
|
|
6
|
+
import rehypeSanitize, {defaultSchema} from 'rehype-sanitize';
|
|
7
|
+
import rehypeRapierMath from 'rapier-math/rehype';
|
|
8
|
+
import rehypeStringify from 'rehype-stringify';
|
|
9
|
+
import {pathToFileURL} from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const processor = unified().use(remarkParse)
|
|
12
|
+
.use(remarkMath, {singleDollarTextMath: false}).use(remarkRehype)
|
|
13
|
+
.use(rehypeSanitize, {
|
|
14
|
+
...defaultSchema,
|
|
15
|
+
attributes: {...defaultSchema.attributes, code: [
|
|
16
|
+
...(defaultSchema.attributes.code || []).filter(rule => !(Array.isArray(rule) && rule[0] === 'className')),
|
|
17
|
+
['className', 'language-math', 'math-inline', 'math-display'],
|
|
18
|
+
]},
|
|
19
|
+
})
|
|
20
|
+
.use(rehypeRapierMath).use(rehypeStringify);
|
|
21
|
+
|
|
22
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
23
|
+
const file = await processor.process('Price $5. Code `$$x$$`. Math $$x^2$$.\n\n$$\n\\frac{1}{2}\n$$');
|
|
24
|
+
console.log(String(file));
|
|
25
|
+
console.error(file.messages);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {render} from '../index.mjs';
|
|
3
|
+
self.onmessage = ({data: {id, source}}) => {
|
|
4
|
+
try { self.postMessage({id, source, result: render(source)}); }
|
|
5
|
+
catch (error) { self.postMessage({id, source, error: {code: error.code, message: error.message}}); }
|
|
6
|
+
};
|
|
7
|
+
// Keep a request id per message; install a reply only if it is still current.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export interface MathError {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
/** Zero-based UTF-16 offset in the formula. */
|
|
6
|
+
offset: number;
|
|
7
|
+
command?: string;
|
|
8
|
+
messageTruncated?: boolean;
|
|
9
|
+
commandTruncated?: boolean;
|
|
10
|
+
/** True only when refused oversized source is not embedded in diagnostic SVG. */
|
|
11
|
+
sourceOmitted?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface RenderOptions {
|
|
14
|
+
displayMode?: boolean;
|
|
15
|
+
maxWidth?: number;
|
|
16
|
+
screen?: {fontSize: number; pixelRatio: number; dark: boolean};
|
|
17
|
+
}
|
|
18
|
+
export interface RenderResult {
|
|
19
|
+
svg: string;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
depth: number;
|
|
23
|
+
lines: number;
|
|
24
|
+
fitted: boolean;
|
|
25
|
+
errors: MathError[];
|
|
26
|
+
}
|
|
27
|
+
/** Parser nodes are inspectable; the internal node vocabulary can grow. */
|
|
28
|
+
export interface MathNode {type: string; [field: string]: unknown}
|
|
29
|
+
/** Browser nodes (including Element, Document and DocumentFragment) satisfy this
|
|
30
|
+
* structural type without requiring the DOM type library in server projects. */
|
|
31
|
+
export interface MathContainer {readonly nodeType: number}
|
|
32
|
+
export const version: string;
|
|
33
|
+
/** Oversized source returns a bounded resource-limit diagnostic with sourceOmitted. */
|
|
34
|
+
export function render(source: unknown, options?: RenderOptions): RenderResult;
|
|
35
|
+
export function renderToString(source: unknown, options?: RenderOptions): string;
|
|
36
|
+
export function parse(source: string): MathNode;
|
|
37
|
+
export function fit(container: MathContainer): number;
|
|
38
|
+
export function observe(container: MathContainer): () => void;
|
|
39
|
+
declare const RapierMath: Readonly<{
|
|
40
|
+
version: typeof version; render: typeof render; renderToString: typeof renderToString;
|
|
41
|
+
parse: typeof parse; fit: typeof fit; observe: typeof observe;
|
|
42
|
+
}>;
|
|
43
|
+
export default RapierMath;
|
package/index.mjs
CHANGED
|
@@ -2309,7 +2309,7 @@ function mathLayout(n,c) {
|
|
|
2309
2309
|
case 'smash': {const b=mathLayout(n.body,c);return {...b,h:n.mode==='b'?b.h:0,d:n.mode==='t'?b.d:0};}
|
|
2310
2310
|
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})});}
|
|
2311
2311
|
case 'rule': {const s=mathScale(c);return mathRule(n.width*s,(n.height+n.depth)*s,-n.height*s);}
|
|
2312
|
-
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;}
|
|
2312
|
+
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)+'" aria-label="'+mathEscape(n.href)+'" tabindex="0" target="_blank" rel="noopener noreferrer">'+b.svg+'</a>';return b;}
|
|
2313
2313
|
default: throw Object.assign(new Error('Unsupported maths construct: '+n.type),{code:'unsupported-construct'});
|
|
2314
2314
|
}
|
|
2315
2315
|
}
|
|
@@ -2768,12 +2768,18 @@ function mathFitElement(svg, printing = false) {
|
|
|
2768
2768
|
if(screenKey!==state.screen || (target===null)!==(state.width===null) || target!==null && Math.abs(target-state.width)>.005) {
|
|
2769
2769
|
const rendered=renderMath(source,{displayMode:display,maxWidth:(screenKey!==state.screen?width:target) || undefined,screen});
|
|
2770
2770
|
if(!rendered.errors.length) {
|
|
2771
|
+
const active=doc.activeElement,links=active && svg.contains(active)?[...svg.querySelectorAll('a')]:[];
|
|
2772
|
+
const focusIndex=links.indexOf(active),focusHref=focusIndex<0?null:active.getAttribute('href');
|
|
2771
2773
|
const replacement=new view.DOMParser().parseFromString(rendered.svg,'image/svg+xml').documentElement;
|
|
2772
|
-
for(const name of ['width','height','viewBox','data-math-natural-width','data-math-max-width','data-math-lines','data-math-screen']) {
|
|
2774
|
+
for(const name of ['role','width','height','viewBox','data-math-natural-width','data-math-max-width','data-math-lines','data-math-screen']) {
|
|
2773
2775
|
if(replacement.hasAttribute(name))svg.setAttribute(name,replacement.getAttribute(name));else svg.removeAttribute(name);
|
|
2774
2776
|
}
|
|
2775
2777
|
svg.style.verticalAlign=replacement.style.verticalAlign;
|
|
2776
2778
|
svg.replaceChildren(...Array.from(replacement.childNodes,node=>doc.importNode(node,true)));
|
|
2779
|
+
if(focusIndex>=0 && (!doc.activeElement || doc.activeElement===doc.body)) {
|
|
2780
|
+
const link=svg.querySelectorAll('a')[focusIndex];
|
|
2781
|
+
if(link?.getAttribute('href')===focusHref)link.focus({preventScroll:true});
|
|
2782
|
+
}
|
|
2777
2783
|
state.natural=Number(svg.getAttribute('data-math-natural-width'));state.screen=screenKey;
|
|
2778
2784
|
state.width=state.natural>width?width:null;
|
|
2779
2785
|
if(state.width===null)svg.removeAttribute('data-math-max-width');
|
|
@@ -2869,23 +2875,27 @@ function mathSvg(box,source,error,utf16,options={}) {
|
|
|
2869
2875
|
const opacity=options.screen?.dark ? .94 : 1;
|
|
2870
2876
|
if (![width,height,depth,left,...viewBox].every(Number.isFinite)) throw new Error('Mathematical dimensions are not finite');
|
|
2871
2877
|
const escapedSource=mathEscape(source);
|
|
2872
|
-
|
|
2878
|
+
// A group exposes authored links to accessibility APIs; an image would hide them.
|
|
2879
|
+
const svg='<svg xmlns="http://www.w3.org/2000/svg" role="'+(box.svg.includes('<a ')?'group':'img')+'" aria-label="'+escapedSource+'"'+
|
|
2873
2880
|
' data-rapier-math="'+(options.displayMode?'display':'inline')+'" data-math-lines="'+(box.lines || 1)+'"'+
|
|
2874
2881
|
' data-math-natural-width="'+mathNumber(options.naturalWidth ?? width)+'"'+
|
|
2875
2882
|
(options.screen?' data-math-screen="'+mathNumber(options.screen.fontSize)+'|'+mathNumber(options.screen.pixelRatio)+'|'+options.screen.dark+'"':'')+
|
|
2876
2883
|
(options.maxWidth?' data-math-max-width="'+mathNumber(options.maxWidth)+'"':'')+
|
|
2877
2884
|
(utf16?' data-tex-utf16="'+utf16+'"':'')+
|
|
2885
|
+
(error?.sourceOmitted?' data-math-source-omitted="true"':'')+
|
|
2878
2886
|
(error?' data-math-error="'+mathEscape(error.code)+'"':'')+' width="'+mathNumber(width)+'em" height="'+mathNumber(height+depth)+'em"'+
|
|
2879
2887
|
' viewBox="'+viewBox.map(mathNumber).join(' ')+'"'+
|
|
2880
2888
|
' style="font-size:'+mathNumber(mathFontRatio)+'em;max-width:100%;height:auto;vertical-align:-'+mathNumber(depth)+'em;'+
|
|
2881
2889
|
'fill-opacity:'+opacity+';stroke-opacity:'+opacity+'" fill="currentColor">'+
|
|
2882
2890
|
'<title>'+escapedSource+'</title>'+(error?'<desc>'+mathEscape(error.message)+'</desc>':'')+
|
|
2883
2891
|
'<g transform="scale(1000)">'+box.svg+'</g></svg>';
|
|
2892
|
+
if(svg.length>24*1024*1024)throw Object.assign(new Error('The complete formula markup exceeds its output budget.'),{code:'resource-limit'});
|
|
2884
2893
|
return {svg,width,height,depth,lines:box.lines || 1,fitted:!!box.fitted,errors:error?[error]:[]};
|
|
2885
2894
|
}
|
|
2886
2895
|
|
|
2887
2896
|
function mathSourceAlternative(source) {
|
|
2888
|
-
|
|
2897
|
+
const alternatives=[],parts=[];
|
|
2898
|
+
let first=-1,kept=0;
|
|
2889
2899
|
for(let i=0;i<source.length;i++) {
|
|
2890
2900
|
const code=source.charCodeAt(i);
|
|
2891
2901
|
if(code>=0xd800 && code<=0xdbff && i+1<source.length) {
|
|
@@ -2894,19 +2904,32 @@ function mathSourceAlternative(source) {
|
|
|
2894
2904
|
}
|
|
2895
2905
|
if((code>=0x20 && code<=0xd7ff) || (code>=0xe000 && code<=0xfffd) || code===9 || code===10 || code===13)continue;
|
|
2896
2906
|
if(first<0)first=i;
|
|
2897
|
-
|
|
2907
|
+
if(i>kept)parts.push(source.slice(kept,i));
|
|
2908
|
+
parts.push('\\u'+code.toString(16).toUpperCase().padStart(4,'0'));kept=i+1;
|
|
2909
|
+
// Flatten small batches instead of retaining one rope node per bad code unit.
|
|
2910
|
+
if(parts.length>=1024) {alternatives.push(parts.join(''));parts.length=0;}
|
|
2898
2911
|
}
|
|
2899
2912
|
if(first<0)return {alternative:source};
|
|
2900
|
-
|
|
2913
|
+
parts.push(source.slice(kept));alternatives.push(parts.join(''));
|
|
2901
2914
|
// Only malformed XML text needs a separate carrier; each four hex digits keep one original code unit.
|
|
2902
|
-
|
|
2903
|
-
for(let i=0;i<source.length;i++)
|
|
2904
|
-
|
|
2915
|
+
const encoded=[];parts.length=0;
|
|
2916
|
+
for(let i=0;i<source.length;i++) {
|
|
2917
|
+
parts.push(source.charCodeAt(i).toString(16).padStart(4,'0'));
|
|
2918
|
+
if(parts.length===1024) {encoded.push(parts.join(''));parts.length=0;}
|
|
2919
|
+
}
|
|
2920
|
+
encoded.push(parts.join(''));
|
|
2921
|
+
return {alternative:alternatives.join(''),utf16:encoded.join(''),error:{code:'invalid-source-character',offset:first,
|
|
2905
2922
|
message:'A source character cannot be embedded in SVG. The text alternative shows Unicode escapes; data-tex-utf16 keeps the original source.'}};
|
|
2906
2923
|
}
|
|
2907
2924
|
|
|
2908
2925
|
function renderMath(source,options={}) {
|
|
2909
|
-
const text=String(source ?? '')
|
|
2926
|
+
const text=String(source ?? '');
|
|
2927
|
+
// Reject before scanning, escaping or parsing. Keep the diagnostic-return
|
|
2928
|
+
// contract, but explicitly omit refused source instead of expanding it.
|
|
2929
|
+
const safe=text.length>1000000?{alternative:'Formula source omitted: source limit exceeded.',error:{
|
|
2930
|
+
code:'resource-limit',offset:0,sourceOmitted:true,
|
|
2931
|
+
message:'This formula is too long to render safely. Its source was not embedded; retain the original input.'
|
|
2932
|
+
}}:mathSourceAlternative(text),context={size:1,level:0,variant:'normal',cramped:false,display:!!options?.displayMode,cache:new WeakMap(),budget:{nodes:0,outlineBytes:0}};
|
|
2910
2933
|
try {
|
|
2911
2934
|
if(safe.error)throw safe.error;
|
|
2912
2935
|
if(options?.screen!==undefined) {
|
|
@@ -2923,11 +2946,16 @@ function renderMath(source,options={}) {
|
|
|
2923
2946
|
return mathSvg(box,safe.alternative,undefined,undefined,{displayMode:context.display,maxWidth,naturalWidth:mathInkWidth(natural)+.05,screen:context.screen});
|
|
2924
2947
|
}
|
|
2925
2948
|
catch (cause) {
|
|
2926
|
-
const
|
|
2927
|
-
|
|
2949
|
+
const message=String(cause.message || cause);
|
|
2950
|
+
const error={code:cause.code || 'invalid-tex',message:message.length>1024?message.slice(0,1024).replace(/[\ud800-\udbff]$/,'')+'…':message,offset:Number.isFinite(cause.offset)?cause.offset:0};
|
|
2951
|
+
if(message.length>1024)error.messageTruncated=true;
|
|
2952
|
+
if(cause.sourceOmitted)error.sourceOmitted=true;
|
|
2953
|
+
if(cause.command) {
|
|
2954
|
+
error.command=String(cause.command).slice(0,128);
|
|
2955
|
+
if(cause.command.length>128)error.commandTruncated=true;
|
|
2956
|
+
}
|
|
2928
2957
|
// A diagnostic is visible, but remains distinct from a successfully typeset formula.
|
|
2929
|
-
//
|
|
2930
|
-
// while the visible notice stays finite and can render after the formula exhausted its budget.
|
|
2958
|
+
// Diagnostics have an explicit bound. Exact input stays in the source alternative.
|
|
2931
2959
|
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,'?');
|
|
2932
2960
|
const diagnostic={...context,variant:'roman',size:0.8,budget:{nodes:0,outlineBytes:0}};
|
|
2933
2961
|
try {
|
|
@@ -2940,7 +2968,7 @@ function renderMath(source,options={}) {
|
|
|
2940
2968
|
}
|
|
2941
2969
|
|
|
2942
2970
|
const RapierMath=Object.freeze({
|
|
2943
|
-
version:'1.0
|
|
2971
|
+
version:'1.1.0',
|
|
2944
2972
|
parse:parseMathTex,
|
|
2945
2973
|
render:renderMath,
|
|
2946
2974
|
fit:mathFit,
|
package/integration.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import type {RenderOptions} from './index.js';
|
|
3
|
+
export interface IntegrationOptions {
|
|
4
|
+
renderOptions?: RenderOptions;
|
|
5
|
+
/** Per document. Default 1024. Exhaustion leaves subsequent math as source. */
|
|
6
|
+
maxFormulas?: number;
|
|
7
|
+
/** Combined successful SVG code units per document. Default 16 * 1024 * 1024. */
|
|
8
|
+
maxOutputLength?: number;
|
|
9
|
+
}
|
package/integration.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {render} from './index.mjs';
|
|
3
|
+
|
|
4
|
+
export const escapeText = text => text.replace(/[&<>"'\r]/g, char =>
|
|
5
|
+
({'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '\r': ' '}[char]));
|
|
6
|
+
|
|
7
|
+
// One budget per document render. Rejection leaves the host's text node intact.
|
|
8
|
+
export function createFormulaRenderer(options = {}) {
|
|
9
|
+
const maxFormulas = options.maxFormulas ?? 1024, maxOutputLength = options.maxOutputLength ?? 16 * 1024 * 1024;
|
|
10
|
+
for (const value of [maxFormulas, maxOutputLength])
|
|
11
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError('Math integration budgets must be finite nonnegative integers.');
|
|
12
|
+
let count = 0, length = 0;
|
|
13
|
+
return (source, displayMode) => {
|
|
14
|
+
let result;
|
|
15
|
+
if (count >= maxFormulas || length >= maxOutputLength) return {errors: [{code: 'resource-limit', offset: 0,
|
|
16
|
+
message: 'The document math budget is exhausted.'}]};
|
|
17
|
+
count++;
|
|
18
|
+
try { result = render(source, {...options.renderOptions, displayMode}); }
|
|
19
|
+
catch (error) { return {errors: [{code: error.code || 'invalid-tex', message: String(error.message), offset: error.offset || 0}]}; }
|
|
20
|
+
if (result.errors.length) return {errors: result.errors};
|
|
21
|
+
if (result.svg.length > maxOutputLength - length) {
|
|
22
|
+
length = maxOutputLength;
|
|
23
|
+
return {errors: [{code: 'resource-limit', offset: 0, message: 'The document math output budget is exhausted.'}]};
|
|
24
|
+
}
|
|
25
|
+
length += result.svg.length;
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
}
|
package/markdown-it.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import type MarkdownIt from 'markdown-it';
|
|
3
|
+
import type {DelimiterOptions} from './delimiters.js';
|
|
4
|
+
import type {IntegrationOptions} from './integration.js';
|
|
5
|
+
export interface MarkdownMathOptions extends DelimiterOptions, IntegrationOptions {}
|
|
6
|
+
export default function rapierMath(md: InstanceType<typeof MarkdownIt>, options?: MarkdownMathOptions): void;
|
package/markdown-it.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {installMathRules} from './markdown-rules.mjs';
|
|
3
|
+
import {createFormulaRenderer, escapeText} from './integration.mjs';
|
|
4
|
+
|
|
5
|
+
export default function rapierMath(md, options = {}) {
|
|
6
|
+
installMathRules(md, options);
|
|
7
|
+
let session;
|
|
8
|
+
function withSession(renderer, render, tokens, opts, env) {
|
|
9
|
+
const previous = session, current = {render: createFormulaRenderer(options), children: new WeakSet()};
|
|
10
|
+
const visit = tokens => {
|
|
11
|
+
for (const token of tokens) {
|
|
12
|
+
if (token.children) { current.children.add(token.children); visit(token.children); }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
visit(tokens);
|
|
16
|
+
session = current;
|
|
17
|
+
try { return render.call(renderer, tokens, opts, env); }
|
|
18
|
+
finally { session = previous; }
|
|
19
|
+
}
|
|
20
|
+
const render = md.renderer.render, renderInline = md.renderer.renderInline;
|
|
21
|
+
md.renderer.render = function(tokens, opts, env) {
|
|
22
|
+
return withSession(this, render, tokens, opts, env);
|
|
23
|
+
};
|
|
24
|
+
md.renderer.renderInline = function(tokens, opts, env) {
|
|
25
|
+
// The block renderer's inline children share its document budget. A separate
|
|
26
|
+
// inline tree, including a nested host renderInline call, owns a fresh one.
|
|
27
|
+
if (session?.children.has(tokens)) return renderInline.call(this, tokens, opts, env);
|
|
28
|
+
return withSession(this, renderInline, tokens, opts, env);
|
|
29
|
+
};
|
|
30
|
+
function formula(tokens, index, _options, env) {
|
|
31
|
+
const token = tokens[index], display = token.type === 'math_block';
|
|
32
|
+
const result = (session?.render || createFormulaRenderer(options))(token.content, display);
|
|
33
|
+
if (result.errors.length) {
|
|
34
|
+
(env.rapierMathErrors ||= []).push({source: token.content, errors: result.errors});
|
|
35
|
+
return '<code data-math-error="' + escapeText(result.errors[0].code) + '">' +
|
|
36
|
+
escapeText(token.meta?.mathSource ?? token.content) + '</code>';
|
|
37
|
+
}
|
|
38
|
+
return result.svg;
|
|
39
|
+
}
|
|
40
|
+
md.renderer.rules.math_inline = formula;
|
|
41
|
+
md.renderer.rules.math_block = (tokens, index, opts, env) => {
|
|
42
|
+
const result = formula(tokens, index, opts, env);
|
|
43
|
+
return tokens[index].block ? '<div class="math-display">' + result + '</div>\n' : result;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Original neutral adapter. The host owns Markdown structure and normalization.
|
|
3
|
+
import {createMathMatcher} from './delimiters.mjs';
|
|
4
|
+
|
|
5
|
+
export function installMathRules(md, {singleDollar = false, brackets = true, ruleName = 'rapier_math', inLink = false} = {}) {
|
|
6
|
+
// The host's image-alt renderer ignores unknown token types. Keep math as
|
|
7
|
+
// literal source in text-only output, while leaving escaping to that host.
|
|
8
|
+
const renderText = md.renderer.renderInlineAsText;
|
|
9
|
+
md.renderer.renderInlineAsText = function(tokens, options, env) {
|
|
10
|
+
return renderText.call(this, tokens.map(token =>
|
|
11
|
+
token.type === 'math_inline' || token.type === 'math_block'
|
|
12
|
+
? {...token, type: 'text', content: token.meta?.mathSource ?? token.content}
|
|
13
|
+
: token), options, env);
|
|
14
|
+
};
|
|
15
|
+
const matchers = new WeakMap();
|
|
16
|
+
function match(state, at, limit) {
|
|
17
|
+
let entry = matchers.get(state);
|
|
18
|
+
if (!entry || entry.source !== state.src) {
|
|
19
|
+
entry = {source: state.src, match: createMathMatcher(state.src, {singleDollar, brackets})};
|
|
20
|
+
matchers.set(state, entry);
|
|
21
|
+
}
|
|
22
|
+
return entry.match(at, limit);
|
|
23
|
+
}
|
|
24
|
+
md.inline.ruler.before('escape', ruleName, (state, silent) => {
|
|
25
|
+
if (!inLink && state.linkLevel > 0) return false;
|
|
26
|
+
if (state.src[state.pos] !== '$' && state.src[state.pos] !== '\\') return false;
|
|
27
|
+
const span = match(state, state.pos, state.posMax);
|
|
28
|
+
if (!span) return false;
|
|
29
|
+
if (!silent) {
|
|
30
|
+
const token = state.push(span.displayMode ? 'math_block' : 'math_inline', '', 0);
|
|
31
|
+
token.markup = span.open;
|
|
32
|
+
token.content = state.src.slice(span.bodyStart, span.bodyEnd);
|
|
33
|
+
token.meta = {mathSource: state.src.slice(span.start, span.end)};
|
|
34
|
+
}
|
|
35
|
+
state.pos = span.end;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
md.block.ruler.before('fence', ruleName, (state, startLine, endLine, silent) => {
|
|
39
|
+
// An indented code block belongs to the host even when it begins with dollars.
|
|
40
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) return false;
|
|
41
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
42
|
+
if (state.src.slice(start, start + 2) !== '$$' && (!brackets || state.src.slice(start, start + 2) !== '\\[')) return false;
|
|
43
|
+
const span = match(state, start, state.eMarks[endLine - 1]);
|
|
44
|
+
if (!span?.displayMode) return false;
|
|
45
|
+
let last = startLine;
|
|
46
|
+
while (last < endLine && state.eMarks[last] < span.end) last++;
|
|
47
|
+
if (last >= endLine || span.bodyEnd < state.bMarks[last] + state.tShift[last] ||
|
|
48
|
+
state.src.slice(span.end, state.eMarks[last]).trim()) return false;
|
|
49
|
+
if (silent) return true;
|
|
50
|
+
const parts = [];
|
|
51
|
+
for (let line = startLine; line <= last; line++) {
|
|
52
|
+
const from = line === startLine ? span.bodyStart : state.bMarks[line] + state.tShift[line];
|
|
53
|
+
const to = line === last ? span.bodyEnd : state.eMarks[line];
|
|
54
|
+
parts.push(state.src.slice(from, to));
|
|
55
|
+
}
|
|
56
|
+
const token = state.push('math_block', 'math', 0);
|
|
57
|
+
token.block = true;
|
|
58
|
+
token.markup = span.open;
|
|
59
|
+
token.content = parts.join('\n');
|
|
60
|
+
token.meta = {mathSource: span.open + token.content + span.close};
|
|
61
|
+
token.map = [startLine, last + 1];
|
|
62
|
+
state.line = last + 1;
|
|
63
|
+
return true;
|
|
64
|
+
}, {alt: ['paragraph', 'reference', 'blockquote', 'list']});
|
|
65
|
+
}
|
package/math-components.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "rapier-math-1.0
|
|
6
|
-
"documentNamespace": "https://rapier.website/spdx/rapier-math-1.0
|
|
5
|
+
"name": "rapier-math-1.1.0",
|
|
6
|
+
"documentNamespace": "https://rapier.website/spdx/rapier-math-1.1.0-22e5e40abbd1087922335893ad546fb7ffaeaa710338c97e5686e7b7b0b62021",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-10-
|
|
8
|
+
"created": "2026-10-12T15:47:04.000Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: rapier-math-components",
|
|
11
11
|
"Organization: rapier.website"
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
{
|
|
16
16
|
"name": "rapier-math",
|
|
17
17
|
"SPDXID": "SPDXRef-rapier-math",
|
|
18
|
-
"versionInfo": "1.0
|
|
19
|
-
"downloadLocation": "
|
|
18
|
+
"versionInfo": "1.1.0",
|
|
19
|
+
"downloadLocation": "NOASSERTION",
|
|
20
20
|
"filesAnalyzed": false,
|
|
21
21
|
"licenseConcluded": "MIT AND LicenseRef-GUST-Font-License",
|
|
22
22
|
"licenseDeclared": "MIT AND LicenseRef-GUST-Font-License",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"checksums": [
|
|
25
25
|
{
|
|
26
26
|
"algorithm": "SHA256",
|
|
27
|
-
"checksumValue": "
|
|
27
|
+
"checksumValue": "22e5e40abbd1087922335893ad546fb7ffaeaa710338c97e5686e7b7b0b62021"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
30
|
},
|
package/package.json
CHANGED
|
@@ -1,17 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-math",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Self-contained TeX-to-SVG math renderer with Latin Modern outlines.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"./rapier-math-1.1.0.js"
|
|
10
|
+
],
|
|
7
11
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"default": "./index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./delimiters": {
|
|
17
|
+
"types": "./delimiters.d.ts",
|
|
18
|
+
"default": "./delimiters.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./markdown-it": {
|
|
21
|
+
"types": "./markdown-it.d.ts",
|
|
22
|
+
"default": "./markdown-it.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./rehype": {
|
|
25
|
+
"types": "./rehype.d.ts",
|
|
26
|
+
"default": "./rehype.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./stream": {
|
|
29
|
+
"types": "./stream.d.ts",
|
|
30
|
+
"default": "./stream.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./rapier-math-1.1.0.js": {
|
|
33
|
+
"types": "./classic.d.ts",
|
|
34
|
+
"default": "./rapier-math-1.1.0.js"
|
|
35
|
+
},
|
|
10
36
|
"./package.json": "./package.json"
|
|
11
37
|
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"markdown-it": "^14.1.0 || ^15.0.0",
|
|
40
|
+
"@types/markdown-it": "^14.1.0",
|
|
41
|
+
"hast-util-from-html": "^2.0.0",
|
|
42
|
+
"@types/hast": "^3.0.0",
|
|
43
|
+
"vfile": "^6.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"markdown-it": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@types/markdown-it": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"hast-util-from-html": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@types/hast": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"vfile": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
12
62
|
"files": [
|
|
13
63
|
"index.mjs",
|
|
14
|
-
"rapier-math-1.0.
|
|
64
|
+
"rapier-math-1.1.0.js",
|
|
15
65
|
"LICENSE",
|
|
16
66
|
"LICENSE-MIT",
|
|
17
67
|
"math-LICENSE.txt",
|
|
@@ -20,10 +70,33 @@
|
|
|
20
70
|
"README.md",
|
|
21
71
|
"verify.mjs",
|
|
22
72
|
"check.mjs",
|
|
73
|
+
"adoption-check.mjs",
|
|
23
74
|
"src/math",
|
|
24
75
|
"src/tools/build-math.mjs",
|
|
25
76
|
"src/shell/math-resources.json",
|
|
26
|
-
"src/package.json"
|
|
77
|
+
"src/package.json",
|
|
78
|
+
"delimiters.mjs",
|
|
79
|
+
"markdown-rules.mjs",
|
|
80
|
+
"integration.mjs",
|
|
81
|
+
"markdown-it.mjs",
|
|
82
|
+
"rehype.mjs",
|
|
83
|
+
"stream.mjs",
|
|
84
|
+
"index.d.ts",
|
|
85
|
+
"classic.d.ts",
|
|
86
|
+
"delimiters.d.ts",
|
|
87
|
+
"integration.d.ts",
|
|
88
|
+
"markdown-it.d.ts",
|
|
89
|
+
"rehype.d.ts",
|
|
90
|
+
"stream.d.ts",
|
|
91
|
+
"examples/formula.mjs",
|
|
92
|
+
"examples/markdown.mjs",
|
|
93
|
+
"examples/stream.mjs",
|
|
94
|
+
"examples/unified.mjs",
|
|
95
|
+
"examples/browser.html",
|
|
96
|
+
"examples/browser.mjs",
|
|
97
|
+
"examples/react.tsx",
|
|
98
|
+
"examples/worker.mjs",
|
|
99
|
+
"examples/README.md"
|
|
27
100
|
],
|
|
28
101
|
"license": "SEE LICENSE IN LICENSE",
|
|
29
102
|
"author": "Jack Skipworth",
|