rapier-math 1.1.0 → 1.1.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.
Files changed (61) hide show
  1. package/README.md +23 -195
  2. package/{index.mjs → dist/index.mjs} +1 -1
  3. package/{rapier-math-1.1.0.js → dist/rapier-math.js} +1 -1
  4. package/{src/math/README.md → docs/api.md} +9 -9
  5. package/{math-components.json → docs/math-components.json} +6 -6
  6. package/{src/shell → docs}/math-resources.json +62 -36
  7. package/examples/browser.mjs +1 -1
  8. package/examples/worker.mjs +1 -1
  9. package/package.json +31 -55
  10. package/src/math/api.js +1 -1
  11. package/src/math/package-build.mjs +0 -13
  12. package/src/math/package-layout.mjs +17 -0
  13. package/src/math/package-readme.mjs +42 -5
  14. package/src/tools/build-math.mjs +36 -17
  15. package/{src/math/adoption-verify.mjs → tests/adoption.mjs} +1 -1
  16. package/{check.mjs → tests/check.mjs} +8 -8
  17. package/tests/verify.mjs +46 -0
  18. package/LICENSE-MIT +0 -21
  19. package/adoption-check.mjs +0 -149
  20. package/math-LICENSE.txt +0 -21
  21. package/math-font-LICENSE.txt +0 -457
  22. package/src/math/classic.d.ts +0 -4
  23. package/src/math/delimiters.d.ts +0 -8
  24. package/src/math/delimiters.mjs +0 -39
  25. package/src/math/examples/README.md +0 -25
  26. package/src/math/examples/browser.html +0 -10
  27. package/src/math/examples/browser.mjs +0 -24
  28. package/src/math/examples/formula.mjs +0 -12
  29. package/src/math/examples/markdown.mjs +0 -9
  30. package/src/math/examples/react.tsx +0 -19
  31. package/src/math/examples/stream.mjs +0 -19
  32. package/src/math/examples/unified.mjs +0 -26
  33. package/src/math/examples/worker.mjs +0 -7
  34. package/src/math/index.d.ts +0 -43
  35. package/src/math/integration.d.ts +0 -9
  36. package/src/math/integration.mjs +0 -28
  37. package/src/math/markdown-it.d.ts +0 -6
  38. package/src/math/markdown-it.mjs +0 -45
  39. package/src/math/markdown-rules.mjs +0 -65
  40. package/src/math/package-check.mjs +0 -118
  41. package/src/math/package-verify.mjs +0 -45
  42. package/src/math/rehype.d.ts +0 -6
  43. package/src/math/rehype.mjs +0 -37
  44. package/src/package.json +0 -9
  45. package/stream.d.ts +0 -14
  46. package/stream.mjs +0 -46
  47. package/verify.mjs +0 -45
  48. /package/{classic.d.ts → dist/classic.d.ts} +0 -0
  49. /package/{delimiters.d.ts → dist/delimiters.d.ts} +0 -0
  50. /package/{delimiters.mjs → dist/delimiters.mjs} +0 -0
  51. /package/{index.d.ts → dist/index.d.ts} +0 -0
  52. /package/{integration.d.ts → dist/integration.d.ts} +0 -0
  53. /package/{integration.mjs → dist/integration.mjs} +0 -0
  54. /package/{markdown-it.d.ts → dist/markdown-it.d.ts} +0 -0
  55. /package/{markdown-it.mjs → dist/markdown-it.mjs} +0 -0
  56. /package/{markdown-rules.mjs → dist/markdown-rules.mjs} +0 -0
  57. /package/{rehype.d.ts → dist/rehype.d.ts} +0 -0
  58. /package/{rehype.mjs → dist/rehype.mjs} +0 -0
  59. /package/{src/math → dist}/stream.d.ts +0 -0
  60. /package/{src/math → dist}/stream.mjs +0 -0
  61. /package/{src/math/FONT-SOURCE.md → docs/font-source.md} +0 -0
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
1
  {
2
2
  "name": "rapier-math",
3
- "version": "1.1.0",
4
- "description": "Self-contained TeX-to-SVG math renderer with Latin Modern outlines.",
3
+ "version": "1.1.1",
4
+ "description": "Self-contained offline LaTeX-to-SVG rendering with Latin Modern outlines, optional Markdown integrations and streaming helpers.",
5
5
  "type": "module",
6
- "main": "./index.mjs",
7
- "types": "./index.d.ts",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
8
  "sideEffects": [
9
- "./rapier-math-1.1.0.js"
9
+ "./dist/rapier-math.js"
10
10
  ],
11
11
  "exports": {
12
12
  ".": {
13
- "types": "./index.d.ts",
14
- "default": "./index.mjs"
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.mjs"
15
15
  },
16
16
  "./delimiters": {
17
- "types": "./delimiters.d.ts",
18
- "default": "./delimiters.mjs"
17
+ "types": "./dist/delimiters.d.ts",
18
+ "default": "./dist/delimiters.mjs"
19
19
  },
20
20
  "./markdown-it": {
21
- "types": "./markdown-it.d.ts",
22
- "default": "./markdown-it.mjs"
21
+ "types": "./dist/markdown-it.d.ts",
22
+ "default": "./dist/markdown-it.mjs"
23
23
  },
24
24
  "./rehype": {
25
- "types": "./rehype.d.ts",
26
- "default": "./rehype.mjs"
25
+ "types": "./dist/rehype.d.ts",
26
+ "default": "./dist/rehype.mjs"
27
27
  },
28
28
  "./stream": {
29
- "types": "./stream.d.ts",
30
- "default": "./stream.mjs"
29
+ "types": "./dist/stream.d.ts",
30
+ "default": "./dist/stream.mjs"
31
31
  },
32
- "./rapier-math-1.1.0.js": {
33
- "types": "./classic.d.ts",
34
- "default": "./rapier-math-1.1.0.js"
32
+ "./classic": {
33
+ "types": "./dist/classic.d.ts",
34
+ "default": "./dist/rapier-math.js"
35
35
  },
36
36
  "./package.json": "./package.json"
37
37
  },
@@ -60,43 +60,13 @@
60
60
  }
61
61
  },
62
62
  "files": [
63
- "index.mjs",
64
- "rapier-math-1.1.0.js",
63
+ "dist",
64
+ "src",
65
+ "docs",
66
+ "examples",
67
+ "tests",
65
68
  "LICENSE",
66
- "LICENSE-MIT",
67
- "math-LICENSE.txt",
68
- "math-font-LICENSE.txt",
69
- "math-components.json",
70
- "README.md",
71
- "verify.mjs",
72
- "check.mjs",
73
- "adoption-check.mjs",
74
- "src/math",
75
- "src/tools/build-math.mjs",
76
- "src/shell/math-resources.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"
69
+ "README.md"
100
70
  ],
101
71
  "license": "SEE LICENSE IN LICENSE",
102
72
  "author": "Jack Skipworth",
@@ -114,6 +84,12 @@
114
84
  "latex",
115
85
  "svg",
116
86
  "typesetting",
117
- "latin-modern"
87
+ "latin-modern",
88
+ "markdown",
89
+ "markdown-it",
90
+ "rehype",
91
+ "unified",
92
+ "offline",
93
+ "math-rendering"
118
94
  ]
119
95
  }
package/src/math/api.js CHANGED
@@ -100,7 +100,7 @@ function renderMath(source,options={}) {
100
100
  }
101
101
 
102
102
  globalThis.RapierMath=Object.freeze({
103
- version:'1.1.0',
103
+ version:'1.1.1',
104
104
  parse:parseMathTex,
105
105
  render:renderMath,
106
106
  fit:mathFit,
@@ -1,8 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // The module has the exact classic renderer body, with module-local exports.
3
- import {writeFile} from 'node:fs/promises';
4
- import {dirname, resolve} from 'node:path';
5
- import {fileURLToPath} from 'node:url';
6
3
 
7
4
  export function mathModule(bundle) {
8
5
  const text = String(bundle), opening = "(function(){\n'use strict';\n", ending = '\n})();\n';
@@ -15,13 +12,3 @@ export function mathModule(bundle) {
15
12
  'const {version, parse, render, fit, observe, renderToString} = RapierMath;\n' +
16
13
  'export {version, parse, render, fit, observe, renderToString};\nexport default RapierMath;\n';
17
14
  }
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 {assembleMath} = await import('../tools/build-math.mjs');
23
- const {bytes} = await assembleMath({root});
24
- const text = mathModule(bytes);
25
- await writeFile(resolve(process.argv[2]), text);
26
- console.log(JSON.stringify({file: process.argv[2], bytes: Buffer.byteLength(text)}));
27
- }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export const MATH_CLASSIC = 'dist/rapier-math.js';
3
+
4
+ // One public location per authored input. Runtime formats share dist/.
5
+ export function mathPackagePath(file) {
6
+ const owners = {
7
+ 'math/README.md': 'docs/api.md',
8
+ 'math/FONT-SOURCE.md': 'docs/font-source.md',
9
+ 'math/package-check.mjs': 'tests/check.mjs',
10
+ 'math/package-verify.mjs': 'tests/verify.mjs',
11
+ 'math/adoption-verify.mjs': 'tests/adoption.mjs',
12
+ };
13
+ if (owners[file]) return owners[file];
14
+ if (file.startsWith('math/examples/')) return file.slice(5);
15
+ if (/^math\/(?!package-).+\.(?:mjs|d\.ts)$/.test(file)) return 'dist/' + file.slice(5);
16
+ return 'src/' + file;
17
+ }
@@ -1,8 +1,45 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {readFileSync} from 'node:fs';
3
2
  export function mathReadme(resources) {
4
- return readFileSync(new URL('./README.md', import.meta.url), 'utf8')
5
- .replace('rapier-math-VERSION.js', resources.bundle) +
6
- '\nBuild: ' + resources.version + '. Classic payload: ' + resources.bytes +
7
- ' bytes; gzip (level 9): ' + resources.gzipBytes + ' bytes.\nSHA-384: `' + resources.sha384 + '`\n';
3
+ return `# Rapier Math
4
+
5
+ A self-contained TeX-to-SVG renderer with Latin Modern outlines. Synchronous, no runtime downloads.
6
+
7
+ ## Use
8
+
9
+ \`\`\`sh
10
+ npm install rapier-math
11
+ \`\`\`
12
+
13
+ \`\`\`js
14
+ import {renderToString} from 'rapier-math';
15
+ const svg = renderToString(String.raw\`\\frac{a}{b}\`);
16
+ \`\`\`
17
+
18
+ For a browser script, load [${resources.packageBundle}](${resources.packageBundle}) and use
19
+ \`globalThis.RapierMath\`. The native ESM entry is [dist/index.mjs](dist/index.mjs).
20
+ Both include the font and return synchronously. Type declarations accompany the runtime files.
21
+
22
+ - [API and integration guide](docs/api.md)
23
+ - [Runnable examples](examples/README.md)
24
+ - [Font source and regeneration](docs/font-source.md)
25
+
26
+ ## Develop
27
+
28
+ The current runtime formats live in \`dist/\`; editable renderer source and its build tools live in
29
+ \`src/\`. Optional adapters have one source copy in \`dist/\`. Examples, documentation and tests each
30
+ have their own directory. Only the current release is kept in this checkout.
31
+
32
+ From a repository checkout, run \`node tests/check.mjs\` and \`node tests/verify.mjs\` to check the package and rebuild its exact bytes.
33
+ Run \`node src/tools/build-math.mjs --package .\` to regenerate the classic and ESM files from source.
34
+ Optional host checks run with \`node tests/adoption.mjs\` after installing the example dependencies.
35
+
36
+ ## License
37
+
38
+ The renderer and adapters are MIT-licensed. Embedded Latin Modern Math outlines and metrics are
39
+ licensed under the GUST Font License. [LICENSE](LICENSE) contains both complete grants.
40
+ [Component provenance](docs/math-components.json) and [source hashes](docs/math-resources.json)
41
+ travel with the package.
42
+
43
+ Version ${resources.version}. Classic payload: ${resources.bytes} bytes; gzip (level 9): ${resources.gzipBytes} bytes.
44
+ `;
8
45
  }
@@ -7,17 +7,20 @@ import {fileURLToPath} from 'node:url';
7
7
  import {createHash} from 'node:crypto';
8
8
  import {gzipSync} from 'node:zlib';
9
9
  import {Script} from 'node:vm';
10
+ import {mathPackagePath, MATH_CLASSIC} from '../math/package-layout.mjs';
11
+ import {mathModule} from '../math/package-build.mjs';
12
+ import {mathReadme} from '../math/package-readme.mjs';
10
13
 
11
14
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
12
- export const MATH_VERSION = '1.1.0';
15
+ export const MATH_VERSION = '1.1.1';
13
16
  export const MATH_BUNDLE = 'rapier-math-' + MATH_VERSION + '.js';
14
17
  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', 'math/adoption-verify.mjs', 'math/package-build.mjs', 'math/package-check.mjs', 'math/package-readme.mjs', 'math/README.md'];
18
+ 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', 'math/adoption-verify.mjs', 'math/package-build.mjs', 'math/package-layout.mjs', 'math/package-check.mjs', 'math/package-readme.mjs', 'math/README.md'];
16
19
  export const MATH_ADOPTION = ['delimiters.mjs', 'markdown-rules.mjs', 'integration.mjs', 'markdown-it.mjs', 'rehype.mjs', 'stream.mjs',
17
20
  'index.d.ts', 'classic.d.ts', 'delimiters.d.ts', 'integration.d.ts', 'markdown-it.d.ts', 'rehype.d.ts', 'stream.d.ts',
18
21
  'examples/formula.mjs', 'examples/markdown.mjs', 'examples/stream.mjs', 'examples/unified.mjs',
19
22
  'examples/browser.html', 'examples/browser.mjs', 'examples/react.tsx', 'examples/worker.mjs', 'examples/README.md']
20
- .map(file => ({file: 'math/' + file, packageFile: file}));
23
+ .map(file => ({file: 'math/' + file, packageFile: mathPackagePath('math/' + file)}));
21
24
  const FONT = {
22
25
  name: 'Latin Modern Math', version: '1.959', license: 'LicenseRef-GUST-Font-License',
23
26
  licenseName: 'GUST Font License',
@@ -30,8 +33,8 @@ const FONT = {
30
33
  };
31
34
  const digest = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
32
35
 
33
- async function fontLicense(root) {
34
- const bytes = await readFile(resolve(root, FONT.licenseFile));
36
+ async function fontLicense(root, packageLayout = false) {
37
+ const bytes = await readFile(resolve(root, packageLayout ? mathPackagePath(FONT.licenseFile) : FONT.licenseFile));
35
38
  if (digest(bytes) !== FONT.licenseSha256 || !bytes.includes(FONT.copyright))
36
39
  throw new Error('The math font license differs from its source pin');
37
40
  return bytes;
@@ -44,15 +47,16 @@ export async function fillMathLicense(markup, {root = ROOT} = {}) {
44
47
  return markup.replace(marker, () => '<pre class="license-text">' + escaped + '</pre>');
45
48
  }
46
49
 
47
- export async function assembleMath({root = ROOT} = {}) {
50
+ export async function assembleMath({root = ROOT, packageLayout = false} = {}) {
51
+ const read = file => readFile(resolve(root, packageLayout ? mathPackagePath(file) : file));
48
52
  const inputs = [];
49
53
  for (const file of MATH_SOURCES) {
50
- const bytes = await readFile(resolve(root, file));
54
+ const bytes = await read(file);
51
55
  inputs.push({file, bytes, text: bytes.toString('utf8')});
52
56
  }
53
- const license = await fontLicense(root);
57
+ const license = await fontLicense(root, packageLayout);
54
58
  if (!inputs[0].text.includes(license.toString('utf8').trim())) throw new Error('The embedded math font is missing its complete license');
55
- const ownLicense = await readFile(resolve(root, 'math/LICENSE-MIT'), 'utf8');
59
+ const ownLicense = (await read('math/LICENSE-MIT')).toString('utf8');
56
60
  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');
57
61
  new Script(bytes.toString('utf8'), {filename: MATH_BUNDLE});
58
62
  const gzipBytes = gzipSync(bytes, {level: 9}).length;
@@ -60,27 +64,40 @@ export async function assembleMath({root = ROOT} = {}) {
60
64
  const sha256 = digest(bytes), sha384 = digest(bytes, 'sha384', 'base64');
61
65
  const recipe = [];
62
66
  for (const file of MATH_RECIPE) {
63
- const content = await readFile(resolve(root, file));
64
- recipe.push({file, bytes: content.length, sha256: digest(content)});
67
+ const content = await read(file);
68
+ recipe.push({file, packageFile: mathPackagePath(file), bytes: content.length, sha256: digest(content)});
65
69
  }
66
70
  const resources = {
67
- bundle: MATH_BUNDLE, version: MATH_VERSION, license: 'MIT AND ' + FONT.license,
71
+ bundle: MATH_BUNDLE, packageBundle: MATH_CLASSIC, version: MATH_VERSION, license: 'MIT AND ' + FONT.license,
68
72
  bytes: bytes.length, gzipBytes, sha256, sha384,
69
- sources: inputs.map(input => ({file: input.file, bytes: input.bytes.length, sha256: digest(input.bytes)})),
73
+ sources: inputs.map(input => ({file: input.file, packageFile: mathPackagePath(input.file), bytes: input.bytes.length, sha256: digest(input.bytes)})),
70
74
  recipe,
71
75
  components: [{name: FONT.name, version: FONT.version, license: FONT.license, source: FONT.source, sha256: FONT.sourceSha256}],
72
76
  font: {...FONT, licenseBytes: license.length},
73
77
  };
74
78
  resources.adoption = [];
75
79
  for (const {file, packageFile} of MATH_ADOPTION) {
76
- const content = await readFile(resolve(root, file));
80
+ const content = await read(file);
77
81
  resources.adoption.push({file, packageFile, bytes: content.length, sha256: digest(content)});
78
82
  }
79
83
  return {resources, bytes};
80
84
  }
81
85
 
82
- export async function buildMath({root = ROOT, updatePins = true} = {}) {
83
- const {resources, bytes} = await assembleMath({root});
86
+ export async function buildMath({root = ROOT, updatePins = true, packageLayout = false} = {}) {
87
+ const {resources, bytes} = await assembleMath({root, packageLayout});
88
+ if (packageLayout) {
89
+ await mkdir(resolve(root, 'dist'), {recursive: true});
90
+ await writeFile(resolve(root, MATH_CLASSIC), bytes);
91
+ await writeFile(resolve(root, 'dist/index.mjs'), mathModule(bytes));
92
+ await writeFile(resolve(root, 'docs/math-resources.json'), JSON.stringify(resources, null, 2) + '\n');
93
+ await writeFile(resolve(root, 'README.md'), mathReadme(resources));
94
+ const sbomPath = resolve(root, 'docs/math-components.json');
95
+ const sbom = JSON.parse(await readFile(sbomPath, 'utf8'));
96
+ sbom.packages.find(item => item.name === 'rapier-math').checksums = [{algorithm: 'SHA256', checksumValue: resources.sha256}];
97
+ sbom.documentNamespace = 'https://rapier.website/spdx/rapier-math-' + resources.version + '-' + resources.sha256;
98
+ await writeFile(sbomPath, JSON.stringify(sbom, null, 2) + '\n');
99
+ return {bundle: MATH_CLASSIC, bytes: bytes.length, sha256: resources.sha256};
100
+ }
84
101
  const {sha256, sha384, gzipBytes} = resources;
85
102
  const vendor = resolve(root, 'shell/vendor');
86
103
  await mkdir(vendor, {recursive: true});
@@ -107,5 +124,7 @@ export async function buildMath({root = ROOT, updatePins = true} = {}) {
107
124
  }
108
125
 
109
126
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
110
- console.log(JSON.stringify(await buildMath({root: process.argv[2] ? resolve(process.argv[2]) : ROOT})));
127
+ const packageLayout = process.argv[2] === '--package';
128
+ const directory = process.argv[packageLayout ? 3 : 2];
129
+ console.log(JSON.stringify(await buildMath({root: directory ? resolve(directory) : ROOT, packageLayout})));
111
130
  }
@@ -7,7 +7,7 @@ import {render} from 'rapier-math';
7
7
  import rapierMath from 'rapier-math/markdown-it';
8
8
  import {createMathMatcher} from 'rapier-math/delimiters';
9
9
  import {createMarkdownStream} from 'rapier-math/stream';
10
- import {processor} from './examples/unified.mjs';
10
+ import {processor} from '../examples/unified.mjs';
11
11
 
12
12
  const md = new MarkdownIt({html: false}).use(rapierMath);
13
13
  const dollar = new MarkdownIt({html: false}).use(rapierMath, {singleDollar: true});
@@ -3,21 +3,21 @@
3
3
  import assert from 'node:assert/strict';
4
4
  import {readFile} from 'node:fs/promises';
5
5
  import {Script, createContext} from 'node:vm';
6
- import {mathModule} from './src/math/package-build.mjs';
6
+ import {mathModule} from '../src/math/package-build.mjs';
7
7
 
8
- const read = path => readFile(new URL(path, import.meta.url), 'utf8');
8
+ const read = path => readFile(new URL('../' + path, import.meta.url), 'utf8');
9
9
  const pkg = JSON.parse(await read('./package.json'));
10
- const resources = JSON.parse(await read('./src/shell/math-resources.json'));
11
- const classicSource = await read('./' + resources.bundle);
10
+ const resources = JSON.parse(await read('./docs/math-resources.json'));
11
+ const classicSource = await read('./' + resources.packageBundle);
12
12
  assert.equal(pkg.name, 'rapier-math');
13
13
  assert.equal(pkg.version, resources.version);
14
14
  assert.equal(pkg.type, 'module');
15
15
  assert.equal(pkg.license, 'SEE LICENSE IN LICENSE');
16
16
  assert.equal(Object.keys(pkg.dependencies || {}).length, 0);
17
17
  assert.equal(Object.keys(pkg.scripts || {}).length, 0);
18
- assert.equal(await read('./index.mjs'), mathModule(classicSource), 'exact generated module');
19
- const rendererLicense = await read('./LICENSE-MIT'), fontLicense = await read('./math-font-LICENSE.txt');
20
- for (const name of ['./LICENSE', './index.mjs', './' + resources.bundle]) {
18
+ assert.equal(await read('./dist/index.mjs'), mathModule(classicSource), 'exact generated module');
19
+ const rendererLicense = await read('./src/math/LICENSE-MIT'), fontLicense = await read('./src/math/FONT-LICENSE.txt');
20
+ for (const name of ['./LICENSE', './dist/index.mjs', './' + resources.packageBundle]) {
21
21
  const text = await read(name);
22
22
  assert(text.includes(rendererLicense.trim()), name + ': renderer license');
23
23
  assert(text.includes(fontLicense.trim()), name + ': complete font license');
@@ -27,7 +27,7 @@ for (const name of ['./LICENSE', './index.mjs', './' + resources.bundle]) {
27
27
  // allocates a local Response only; no network request or renderer evaluation.
28
28
  new Response();
29
29
  const globals = Object.getOwnPropertyDescriptors(globalThis);
30
- const module = await import('./index.mjs');
30
+ const module = await import('../dist/index.mjs');
31
31
  const afterImport = Object.getOwnPropertyDescriptors(globalThis);
32
32
  assert.deepEqual(Reflect.ownKeys(afterImport), Reflect.ownKeys(globals), 'module adds no globals');
33
33
  for (const key of Reflect.ownKeys(globals)) for (const field of Object.keys(globals[key]))
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Verify each carried owner once and rebuild both current runtime formats.
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
+ import {assembleMath} from '../src/tools/build-math.mjs';
9
+ import {mathModule} from '../src/math/package-build.mjs';
10
+ import {mathReadme} from '../src/math/package-readme.mjs';
11
+
12
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ const read = path => readFile(resolve(root, path));
14
+ const hash = (bytes, algorithm = 'sha256', encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
15
+ const resources = JSON.parse(await read('docs/math-resources.json'));
16
+ const payload = await read(resources.packageBundle);
17
+ assert.equal(payload.length, resources.bytes, 'payload length');
18
+ assert.equal(hash(payload), resources.sha256, 'payload SHA-256');
19
+ assert.equal(hash(payload, 'sha384', 'base64'), resources.sha384, 'payload SHA-384');
20
+ const paths = new Set();
21
+ for (const input of [...resources.sources, ...resources.recipe, ...resources.adoption]) {
22
+ assert(!paths.has(input.packageFile), 'one public location per owner');
23
+ paths.add(input.packageFile);
24
+ const bytes = await read(input.packageFile);
25
+ assert.equal(bytes.length, input.bytes, input.file + ': length');
26
+ assert.equal(hash(bytes), input.sha256, input.file + ': SHA-256');
27
+ }
28
+ const rendererLicense = await read('src/math/LICENSE-MIT');
29
+ const fontLicense = await read('src/math/FONT-LICENSE.txt');
30
+ assert.equal(hash(fontLicense), resources.font.licenseSha256, 'font license SHA-256');
31
+ for (const text of [payload, await read('LICENSE'), await read('dist/index.mjs')]) {
32
+ assert(text.includes(rendererLicense.toString('utf8').trim()), 'complete renderer license');
33
+ assert(text.includes(fontLicense.toString('utf8').trim()), 'complete font license');
34
+ }
35
+ const sbom = JSON.parse(await read('docs/math-components.json'));
36
+ const font = sbom.packages.find(item => item.name === resources.font.name);
37
+ assert.equal(font?.copyrightText, resources.font.copyright, 'font copyright');
38
+ assert.equal(font.licenseDeclared, resources.font.license, 'font license identity');
39
+ assert.equal(sbom.hasExtractedLicensingInfos.find(item => item.licenseId === resources.font.license)?.extractedText,
40
+ fontLicense.toString('utf8'), 'complete extracted font license');
41
+ assert.equal((await read('README.md')).toString('utf8'), mathReadme(resources), 'generated README');
42
+ const rebuilt = await assembleMath({root, packageLayout: true});
43
+ assert.deepEqual(rebuilt.bytes, payload, 'rebuilt classic payload');
44
+ assert.equal((await read('dist/index.mjs')).toString('utf8'), mathModule(rebuilt.bytes), 'rebuilt ESM payload');
45
+ console.log(JSON.stringify({file: resources.packageBundle, bytes: payload.length, sha256: resources.sha256,
46
+ sourceFiles: paths.size, rebuild: 'exact', licenses: 'complete'}));
package/LICENSE-MIT DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jack Skipworth
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,149 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Optional host checks: run from the installed package with the example peers.
3
- // One source-custody table; no appearance or timing assertions.
4
- import assert from 'node:assert/strict';
5
- import MarkdownIt from 'markdown-it';
6
- import {render} from 'rapier-math';
7
- import rapierMath from 'rapier-math/markdown-it';
8
- import {createMathMatcher} from 'rapier-math/delimiters';
9
- import {createMarkdownStream} from 'rapier-math/stream';
10
- import {processor} from './examples/unified.mjs';
11
-
12
- const md = new MarkdownIt({html: false}).use(rapierMath);
13
- const dollar = new MarkdownIt({html: false}).use(rapierMath, {singleDollar: true});
14
- const mathNodes = (host, source) => host.parse(source, {}).flatMap(token => token.children || [token])
15
- .filter(token => token.type === 'math_inline' || token.type === 'math_block');
16
- const rows = [
17
- ['escaped pair incomplete', String.raw`$$x\$$`, 0],
18
- ['escaped pair then closer', String.raw`$$x\$$y$$`, 1],
19
- ['even escape run', String.raw`$$x\\$$`, 1],
20
- ['brackets', String.raw`A \(x+1\) B \[y\]`, 2],
21
- ['escaped bracket opener', String.raw`A \\(x\\)`, 0],
22
- ['currency', 'It costs $5 and $10.', 0],
23
- ['code and fence', '`$$x$$`\n\n```tex\n$$y$$\n```\n\n $$z$$\n', 0],
24
- ['links', String.raw`[\(x\)](https://example.com/$$x$$)`, 0],
25
- ['literal backslashes', String.raw`Path C:\\temp. \(\text{a\ b}\)`, 1],
26
- ['CRLF BOM Unicode', '\ufeffπ 🐈\r\n\r\n$$\r\nx+1\r\n$$\r\n', 1],
27
- ['incomplete', 'Before \\(x+1 and after', 0],
28
- ['malformed UTF-16', '\ud800 and \\(x\\) and \udfff', 1],
29
- ];
30
- for (const [name, source, count] of rows) {
31
- assert.equal(mathNodes(md, source).length, count, name);
32
- const batch = md.render(source, {});
33
- // Every two-piece boundary, including CRLF, escape runs and surrogate pairs.
34
- for (let split = 0; split <= source.length; split++) {
35
- const stream = createMarkdownStream(text => md.render(text, {}), {previewEvery: 8});
36
- stream.write(source.slice(0, split)); stream.write(source.slice(split));
37
- const final = stream.finish();
38
- assert.equal(final.source, source, name + ': exact transport source');
39
- assert.equal(final.html, batch, name + ': final batch agreement');
40
- }
41
- }
42
- for (const [source, count] of [[String.raw`$x\$y$`, 1], ['$x$5', 0], ['$ x$', 0], ['$x $', 0], ['$$$x$$$', 0]])
43
- assert.equal(mathNodes(dollar, source).length, count);
44
- // Image descriptions are text-only host output: unknown math token types must
45
- // not disappear from alt text or spend a formula-render budget.
46
- for (const [source, alt] of [
47
- [String.raw`![before $$x$$ after](image.png)`, 'before $$x$$ after'],
48
- [String.raw`![before \(x\) after](image.png)`, String.raw`before \(x\) after`],
49
- [String.raw`![a ![b $$x$$](inner.png) c](outer.png)`, 'a b $$x$$ c'],
50
- [String.raw`![a $$\text{<>&"}$$ b](image.png)`, 'a $$\\text{&lt;&gt;&amp;&quot;}$$ b'],
51
- ]) {
52
- const host = new MarkdownIt({html: false}).use(rapierMath, {maxFormulas: 1});
53
- const tokens = host.parse(source + String.raw` \(y\)`, {});
54
- for (let pass = 0; pass < 2; pass++) {
55
- const env = {}, html = host.renderer.render(tokens, host.options, env);
56
- assert(html.includes('alt="' + alt + '"'), 'image math remains literal source');
57
- assert.equal((html.match(/<svg /g) || []).length, 1, 'alt text does not spend the formula budget');
58
- assert.equal(env.rapierMathErrors, undefined);
59
- }
60
- }
61
- const exact = '\ufeff$$\r\n α+\\text{a\\ b} 🐈\r\n$$';
62
- const span = createMathMatcher(exact)(1);
63
- assert.equal(exact.slice(span.bodyStart, span.bodyEnd), '\r\n α+\\text{a\\ b} 🐈\r\n');
64
- assert.equal(createMathMatcher(String.raw`$$x\$$`)(0), null);
65
-
66
- const first = createMarkdownStream(text => text, {maxSourceLength: 8, previewEvery: 2});
67
- const second = createMarkdownStream(text => text);
68
- first.write('ab'); second.write('other'); first.abort(); first.abort();
69
- assert.throws(() => first.write('c'));
70
- first.reset(); first.write('new');
71
- assert.throws(() => first.write('123456'), {code: 'resource-limit'});
72
- assert.equal(first.finish().source, 'new');
73
- assert.equal(second.finish().source, 'other');
74
- assert.throws(() => second.finish());
75
- let failingHost = true, supplied = 0;
76
- const failing = createMarkdownStream(text => {
77
- supplied += text.length;
78
- if (failingHost) throw new Error('host failed');
79
- return text;
80
- }, {previewEvery: 2});
81
- const failedSource = 'x'.repeat(32);
82
- for (const chunk of failedSource) {
83
- try { failing.write(chunk); } catch (error) { assert.equal(error.message, 'host failed'); }
84
- }
85
- failingHost = false;
86
- assert.equal(failing.finish().source, failedSource, 'failed previews retain every accepted chunk');
87
- assert(supplied < 3 * failedSource.length, 'failed previews keep the repeated-work bound');
88
- failing.abort(); failing.reset();
89
-
90
- const hostile = String.raw`Before \(\text{</title><script>alert(1)</script>}\). <img src=x onerror=alert(1)>`;
91
- assert(!/<script|<img/i.test(md.render(hostile, {})));
92
- // Cached syntax can serve multiple outputs. Budgets belong to each render, not
93
- // to parsing or to an env object; every paragraph still shares one document cap.
94
- const formulaSource = String.raw`\(x\)`, svgCount = html => (html.match(/<svg /g) || []).length;
95
- for (const budgets of [{maxFormulas: 1}, {maxOutputLength: render('x').svg.length}]) {
96
- const host = new MarkdownIt({html: false}).use(rapierMath, budgets);
97
- for (const inline of [false, true]) {
98
- const source = formulaSource + (inline ? ' ' : '\n\n') + formulaSource;
99
- const tree = host.parse(source, {});
100
- const tokens = inline ? tree[1].children : tree;
101
- const renderer = inline ? host.renderer.renderInline : host.renderer.render;
102
- for (let pass = 0; pass < 2; pass++) {
103
- const env = {}, out = renderer.call(host.renderer, tokens, host.options, env);
104
- assert.equal(svgCount(out), 1);
105
- assert.equal(env.rapierMathErrors.length, 1);
106
- assert.equal(env.rapierMathErrors[0].errors[0].code, 'resource-limit');
107
- assert(out.includes(formulaSource));
108
- }
109
- }
110
- }
111
- const nestedHost = new MarkdownIt({html: false}).use(rapierMath, {maxFormulas: 1});
112
- const nestedTree = nestedHost.parse(formulaSource + ' fail', {});
113
- const textRule = nestedHost.renderer.rules.text;
114
- nestedHost.renderer.rules.text = (tokens, index, opts, env, self) => {
115
- if (tokens[index].content === ' fail') throw new Error('host render failed');
116
- if (tokens[index].content.includes('nested')) {
117
- for (const inline of [false, true]) {
118
- const nestedEnv = {};
119
- assert.throws(() => inline
120
- ? self.renderInline(nestedTree[1].children, opts, nestedEnv)
121
- : self.render(nestedTree, opts, nestedEnv), /host render failed/);
122
- assert.equal(nestedEnv.rapierMathErrors, undefined, 'nested render has its own budget');
123
- }
124
- }
125
- return textRule(tokens, index, opts, env, self);
126
- };
127
- const outerTree = nestedHost.parse(formulaSource + ' nested ' + formulaSource, {});
128
- // A custom host can run the same renderer twice; a failed nested render must
129
- // restore the outer cap and leave subsequent root renders fresh.
130
- for (let pass = 0; pass < 2; pass++) {
131
- const env = {}, out = nestedHost.renderer.render(outerTree, nestedHost.options, env);
132
- assert.equal(svgCount(out), 1);
133
- assert.equal(env.rapierMathErrors.length, 1);
134
- assert.equal(env.rapierMathErrors[0].errors[0].code, 'resource-limit');
135
- }
136
- const tiny = new MarkdownIt({html: false}).use(rapierMath, {maxOutputLength: 1});
137
- assert(!tiny.render(String.raw`\(x\)`).includes('<svg'));
138
- const bad = {}, fallback = md.render(String.raw`Good \(x\). Bad \(\unknown{x}\).`, bad);
139
- assert.equal(bad.rapierMathErrors[0].errors[0].code, 'unsupported-command');
140
- assert(fallback.includes('\\unknown{x}'));
141
-
142
- const unified = await processor.process('Code `$$x$$`. Math $$x+1$$.\n\n<script>alert(1)</script>\n\n$$\\unknown{x}$$');
143
- assert.equal((String(unified).match(/<svg /g) || []).length, 1);
144
- assert(!String(unified).includes('<script'));
145
- assert.equal(unified.messages[0].ruleId, 'unsupported-command');
146
- assert(String(unified).includes('\\unknown{x}'));
147
- console.log(JSON.stringify({sourceCustodyRows: rows.length, splitAgreement: 'all two-piece boundaries',
148
- lifetimes: 'abort, reset, replacement, rejection, interleaved messages', adapters: 'markdown-it and sanitized unified',
149
- security: 'hostile source stays inert; document math budgets fall back'}));
package/math-LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jack Skipworth
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.