rapier-html 1.2.45 → 1.2.46

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 (44) hide show
  1. package/BUILD.json +16 -16
  2. package/README.md +11 -11
  3. package/dist/ledger/LICENSE +12 -5
  4. package/dist/ledger/authored-origin.mjs +2 -2
  5. package/dist/ledger/authorship.mjs +5 -5
  6. package/dist/ledger/carried.mjs +5 -5
  7. package/dist/ledger/diff/concerns.mjs +1 -1
  8. package/dist/ledger/diff/diff.mjs +6 -6
  9. package/dist/ledger/diff/index.mjs +2 -2
  10. package/dist/ledger/diff/merge.mjs +3 -3
  11. package/dist/ledger/diff/moves.mjs +1 -1
  12. package/dist/ledger/diff/walk.mjs +1 -1
  13. package/dist/ledger/document-fonts.mjs +242 -0
  14. package/dist/ledger/document-settings.mjs +2 -2
  15. package/dist/ledger/format.mjs +14 -12
  16. package/dist/ledger/history.mjs +7 -7
  17. package/dist/ledger/inverse-source.mjs +5 -5
  18. package/dist/ledger/journal-records.mjs +3 -3
  19. package/dist/ledger/math/delimiters.mjs +38 -0
  20. package/dist/ledger/math/markdown-rules.mjs +103 -0
  21. package/dist/ledger/md-assets.mjs +3 -2
  22. package/dist/ledger/md-comments.mjs +384 -0
  23. package/dist/ledger/md-engine.mjs +157 -0
  24. package/dist/ledger/md-marks.mjs +10 -1
  25. package/dist/ledger/md-parser-config.mjs +463 -0
  26. package/dist/ledger/md-parser-runtime.mjs +23 -0
  27. package/dist/ledger/md-source.mjs +1 -1
  28. package/dist/ledger/template-page.mjs +1 -1
  29. package/dist/ledger/transport.mjs +66 -7
  30. package/dist/ledger/vendor/BROWSER-ENTITIES.json +36 -0
  31. package/dist/ledger/vendor/MARKDOWN-RUNTIME.json +136 -0
  32. package/dist/ledger/vendor/MARKDOWNIT.json +582 -0
  33. package/dist/ledger/vendor/NOTICE.txt +60 -0
  34. package/dist/ledger/vendor/markdown-licenses/entities-LICENSE.txt +11 -0
  35. package/dist/ledger/vendor/markdown-licenses/linkify-it-LICENSE.txt +22 -0
  36. package/dist/ledger/vendor/markdown-licenses/markdown-it-LICENSE.txt +22 -0
  37. package/dist/ledger/vendor/markdown-licenses/mdurl-LICENSE.txt +45 -0
  38. package/dist/ledger/vendor/markdown-licenses/punycode.js-LICENSE-MIT.txt +20 -0
  39. package/dist/ledger/vendor/markdown-licenses/uc.micro-LICENSE.txt +20 -0
  40. package/dist/ledger/vendor/markdown-plugins.json +46 -0
  41. package/dist/ledger/vendor/markdown-plugins.mjs +27 -0
  42. package/dist/ledger/vendor/markdown-runtime.mjs +13 -0
  43. package/dist/rapier.html +59 -67
  44. package/package.json +1 -1
package/BUILD.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "rapier-html",
3
- "version": "1.2.45",
3
+ "version": "1.2.46",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 5843562,
6
- "sha256": "4377466b87c89fcfbd71dd6c8de0f73fc72de2f7e2529c89b0004828ce6855c4",
7
- "builtAt": "2026-10-25T17:51:41.653Z",
5
+ "bytes": 5934524,
6
+ "sha256": "f517fef79e14aa898b4bfaf519c679032f6df7538128892e6374280cb9511432",
7
+ "builtAt": "2026-10-27T10:37:55.443Z",
8
8
  "node": "v22.23.4",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
@@ -31,15 +31,15 @@
31
31
  "delivery": "built-in",
32
32
  "kind": "files",
33
33
  "element": "rapier-plugin-mermaid",
34
- "bytes": 720263,
35
- "packedBytes": 240280,
34
+ "bytes": 722820,
35
+ "packedBytes": 241155,
36
36
  "files": [
37
37
  {
38
38
  "id": "rapier-mermaid-core",
39
- "name": "pele-1.9.0-native-1.js",
40
- "version": "1.9.0-native-1",
41
- "bytes": 720263,
42
- "sri": "zPE63bYrlp1ZqlwxePk6pclPq6VoxZ7o769Lz109vEo8OEPtvOl1GPrMDj0OhtVB"
39
+ "name": "pele-1.9.0-native-2.js",
40
+ "version": "1.9.0-native-2",
41
+ "bytes": 722820,
42
+ "sri": "vPm/+HAI9fOqGc1GFl8wFvzMcZjXMhIUXWSdd2qAAVW6fBqjmK6Np6MVprxE8DFK"
43
43
  }
44
44
  ]
45
45
  },
@@ -48,15 +48,15 @@
48
48
  "delivery": "built-in",
49
49
  "kind": "files",
50
50
  "element": "rapier-plugin-mermaid-zenuml",
51
- "bytes": 420431,
52
- "packedBytes": 110835,
51
+ "bytes": 421169,
52
+ "packedBytes": 111122,
53
53
  "files": [
54
54
  {
55
55
  "id": "rapier-mermaid-zenuml",
56
- "name": "rapier-zenuml-svg-4.5.0-1.js",
57
- "version": "4.5.0-svg-1",
58
- "bytes": 420431,
59
- "sri": "Dma7QZLWfZx3gWM0ie8JpjR4KwOXqndKcIJywkdxJ2shsdw/gDRFByjGlUv+eNoQ"
56
+ "name": "rapier-zenuml-svg-4.5.0-2.js",
57
+ "version": "4.5.0-svg-2",
58
+ "bytes": 421169,
59
+ "sri": "g5vdITVAGyvxD4BJdaFJjNZKvlTUXxXlJkqeFyfXREsJVsopr/iH4DC8gPhuEjDK"
60
60
  }
61
61
  ]
62
62
  },
package/README.md CHANGED
@@ -7,8 +7,8 @@ Turn Markdown into one offline HTML file that contains the full Rapier editor.
7
7
  <img src="https://raw.githubusercontent.com/jackskip22/rapier-plugins/main/assets/write-390-light.png" alt="The Rapier editor on a phone: a launch plan with an agent's change marked" width="320">
8
8
  </picture>
9
9
 
10
- Try it: `npx rapier-html@1.2.45 notes.md`, then open `notes.rapier.html` in any browser.
11
- Install: `npm install rapier-html@1.2.45`.
10
+ Try it: `npx rapier-html@1.2.46 notes.md`, then open `notes.rapier.html` in any browser.
11
+ Install: `npm install rapier-html@1.2.46`.
12
12
 
13
13
  It opens on a phone or computer, with no account. People read, edit, draw and paint; agents hand over documents, show
14
14
  what changed and get the edited copy back.
@@ -26,14 +26,14 @@ The file needs no hosted workspace.
26
26
  Needs Node 22 or newer.
27
27
 
28
28
  ```sh
29
- npx -- rapier-html@1.2.45 notes.md # the editor, notes.md inside it, written beside the file
30
- npx -- rapier-html@1.2.45 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
31
- npx -- rapier-html@1.2.45 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
32
- npx -- rapier-html@1.2.45 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
33
- npx -- rapier-html@1.2.45 revised.md --compare original.md # opens showing what changed
34
- npx -- rapier-html@1.2.45 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
35
- npx -- rapier-html@1.2.45 notes.md out.html # a named output
36
- npm install rapier-html@1.2.45 # as a library: writePage, unwrap
29
+ npx -- rapier-html@1.2.46 notes.md # the editor, notes.md inside it, written beside the file
30
+ npx -- rapier-html@1.2.46 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
31
+ npx -- rapier-html@1.2.46 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
32
+ npx -- rapier-html@1.2.46 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
33
+ npx -- rapier-html@1.2.46 revised.md --compare original.md # opens showing what changed
34
+ npx -- rapier-html@1.2.46 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
35
+ npx -- rapier-html@1.2.46 notes.md out.html # a named output
36
+ npm install rapier-html@1.2.46 # as a library: writePage, unwrap
37
37
  ```
38
38
 
39
39
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
@@ -117,7 +117,7 @@ objects, and `unwrap(page)` returns the checked parts beside the text. Both are
117
117
  history.
118
118
  - Markdown saved with the kit's `ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a page
119
119
  without parts drops the old parts.
120
- - The published package carries its own MIT validator; no source checkout is needed.
120
+ - The published package carries the portable MIT validator and canonical Markdown parser support, with the parser vendors' original MIT, ISC and BSD-2-Clause notices. Accepted comment history reopens without a source checkout or a parser supplied by the caller.
121
121
 
122
122
  ## License
123
123
 
@@ -21,14 +21,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
23
  Files under this license carry `SPDX-License-Identifier: MIT` in their first lines: the standard's
24
- grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/md-source.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`), ink geometry
25
- (`spec/ink.mjs`),
24
+ grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/md-source.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`),
25
+ the reusable diagram source, layout and raising helpers (`draw/flowchart-mermaid.mjs`, `draw/flowchart-layout.mjs`,
26
+ `draw/flowchart-raise.mjs`) and diagram-core export facade (`shell/mermaid/api.mjs`),
27
+ ink geometry (`spec/ink.mjs`),
26
28
  the layout model and line planner (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.mjs`,
27
29
  `layout/prepared-paragraph.mjs`, `layout/hyphenation.mjs`, `layout/paragraph-fit.mjs`), the animated-picture reader and playback
28
30
  the HTML blocks use (`images/gif.mjs`, `images/playback.mjs`),
29
31
  the portable HTML and document metadata helpers (`spec/html-blocks.mjs`, `spec/frontmatter.mjs`,
30
32
  `spec/document-settings.mjs`, `spec/document-fonts.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
31
- (`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
33
+ (`spec/md-assets.mjs`), the portable comment and canonical parser support owners
34
+ (`spec/md-comments.mjs`, `spec/md-parser-config.mjs`, `spec/md-parser-runtime.mjs`),
35
+ the neutral math boundary and parser adapters (`math/delimiters.mjs`, `math/markdown-rules.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
32
36
  `tools/read-shared-page.py`), and the host helper `packages/rapier-embed/embed.mjs` with its
33
37
  contract owner `packages/rapier-embed/contract.mjs`. The vendored Pretext (`agent/vendor/pretext`) and micromark carry their
34
38
  own MIT notices. The edit-ledger module (`kit/rapier-ledger.mjs`, `kit/ledger/*.mjs`) and
@@ -39,11 +43,14 @@ AGPL-3.0-only. The page wrapper (`skills/rapier-html/wrap.mjs`, `skills/rapier-h
39
43
  `skills/rapier-html/return-address.mjs`) and its carrier-staging helper
40
44
  (`skills/rapier-html/package-files.mjs`) are MIT. The kit's MIT-marked facades,
41
45
  `kit/render-entry.mjs`, `kit/render*.mjs`, `kit/export-fonts.mjs`, `kit/print-capitals.mjs`,
42
- `kit/pdf-vectors.mjs`, `kit/pdf-glyphs.mjs`, `kit/stage.mjs` and MIT-marked conformance
46
+ `kit/pdf-vectors.mjs`, `kit/pdf-glyphs.mjs`, `kit/stage.mjs`, `kit/markdown-vendor.mjs` and MIT-marked conformance
43
47
  modules are covered too. These names describe the Rapier-owned files carrying the
44
48
  MIT notice, not a grant over the editor or third-party code. Other Rapier-owned
45
49
  files retain their stated license; the editor is AGPL-3.0-only (see `LICENSE`).
46
- Third-party components retain their own copyright and license notices.
50
+ Third-party components retain their own copyright and license notices. The published Markdown parser,
51
+ its plugins and their dependencies retain their original MIT, ISC and BSD-2-Clause notices in `agent/vendor/NOTICE.txt` and
52
+ `agent/vendor/markdown-licenses/`; this inventory does not replace them. The Feather callout paths
53
+ in the canonical configuration retain the Feather copyright and MIT notice there.
47
54
 
48
55
  The portable font helpers read document definitions and accept explicit host validation and
49
56
  subsetting functions. The application binding (`editor/export-fonts.mjs`), captured-font
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierTransformSplices, _rapierValidAuthored} from './journal-records.mjs';
3
- import {sha256} from './hash.mjs';
2
+ import {_rapierTransformSplices, _rapierValidAuthored} from "./journal-records.mjs";
3
+ import {sha256} from "./hash.mjs";
4
4
 
5
5
  function authoredFrontier(records, visit = null) {
6
6
  const dependencies = new Map(), frontier = new Set();
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {readLedger, exportLedger, textRoot, rootAfter} from './format.mjs';
3
- import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
4
- import {projectInverseSource} from './inverse-source.mjs';
5
- import {sha256} from './hash.mjs';
6
- import {plainData, documentText, nonnegative, exactKeys, fault} from './data.mjs';
2
+ import {readLedger, exportLedger, textRoot, rootAfter} from "./format.mjs";
3
+ import {_rapierTransformSplices, _rapierHistoryEffects} from "./journal-records.mjs";
4
+ import {projectInverseSource} from "./inverse-source.mjs";
5
+ import {sha256} from "./hash.mjs";
6
+ import {plainData, documentText, nonnegative, exactKeys, fault} from "./data.mjs";
7
7
  function coalesce(runs) {
8
8
  const out = [];
9
9
  for (const run of runs) {
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {readLedger} from './format.mjs';
3
- import {sha256} from './hash.mjs';
4
- import {authorship as projectAuthorship, readAuthorship} from './authorship.mjs';
5
- import {canonicalJSON, fault} from './data.mjs';
2
+ import {readLedger} from "./format.mjs";
3
+ import {sha256} from "./hash.mjs";
4
+ import {authorship as projectAuthorship, readAuthorship} from "./authorship.mjs";
5
+ import {canonicalJSON, fault} from "./data.mjs";
6
6
  export function encodeCarried(text) {
7
7
  return String(text).replace(/[\\\r\0]|<[/!]/g, m => m === '\\' ? '\\\\' : m === '\r' ? '\\r' : m === '\0' ? '\\0' : m === '</' ? '\\/' : '\\!');
8
8
  }
@@ -57,8 +57,8 @@ export function writeDocument(text, options) {
57
57
  return text + documentTrailer(text, options);
58
58
  }
59
59
  function unfinishedTrailer(source) {
60
+ if (!source.includes(OPEN)) return false;
60
61
  const at = source.lastIndexOf(OPEN);
61
- if (at < 0) return false;
62
62
  let fence = null;
63
63
  for (const line of source.slice(0, at).split('\n')) {
64
64
  const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {letter, within, prev, next} from './tokens.mjs';
2
+ import {letter, within, prev, next} from "./tokens.mjs";
3
3
 
4
4
  export function separate(c, endA = c.before.length, endB = c.after.length) {
5
5
  const {before, after, safe, raw, spend} = c;
@@ -1,10 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {observe} from './structure.mjs';
3
- import {boundaries, tokenizer, wordSegmenter} from './tokens.mjs';
4
- import {matcher} from './matching.mjs';
5
- import {walker} from './walk.mjs';
6
- import {moves} from './moves.mjs';
7
- import {separate, concerns} from './concerns.mjs';
2
+ import {observe} from "./structure.mjs";
3
+ import {boundaries, tokenizer, wordSegmenter} from "./tokens.mjs";
4
+ import {matcher} from "./matching.mjs";
5
+ import {walker} from "./walk.mjs";
6
+ import {moves} from "./moves.mjs";
7
+ import {separate, concerns} from "./concerns.mjs";
8
8
 
9
9
  export function diff(before, after, options = {}) {
10
10
  if (before === after) return {edits: [], changes: []};
@@ -1,4 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {diff} from './diff.mjs';
3
- import {merge} from './merge.mjs';
2
+ import {diff} from "./diff.mjs";
3
+ import {merge} from "./merge.mjs";
4
4
  export {diff, merge};
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {diff} from './diff.mjs';
3
- import {observe} from './structure.mjs';
4
- import {boundaries, words, wordSegmenter} from './tokens.mjs';
2
+ import {diff} from "./diff.mjs";
3
+ import {observe} from "./structure.mjs";
4
+ import {boundaries, words, wordSegmenter} from "./tokens.mjs";
5
5
 
6
6
  export function merge(base, mine, theirs, options = {}) {
7
7
  if (mine === theirs || base === theirs || base === mine) return {text: base === mine ? theirs : mine, conflicts: []};
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {separate, phrases} from './concerns.mjs';
2
+ import {separate, phrases} from "./concerns.mjs";
3
3
 
4
4
  export function moves(c) {
5
5
  const {before, after, spend, safe, budget, raw, code, tokens, matches, walk, structures, pa, pb, la, aliases} = c;
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {words, letter, within, prev, next} from './tokens.mjs';
2
+ import {words, letter, within, prev, next} from "./tokens.mjs";
3
3
 
4
4
  export function walker(c) {
5
5
  const {before, after, spend, safe, budget, raw, chars, code, segmenter, tokens, matches} = c;
@@ -0,0 +1,242 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {markdownParser, installMarkdownImages, normalizeLabel} from "./md-assets.mjs";
3
+ import {splitOpeningFrontmatter} from "./frontmatter.mjs";
4
+ import {readDocumentSettings, documentFont} from "./document-settings.mjs";
5
+ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS, typography, fontDataUrl} from "./md-marks.mjs";
6
+ export {fontDataUrl};
7
+
8
+ export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
9
+ 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'initial', 'revert', 'revert-layer']);
10
+
11
+ function parserFor(parser) {
12
+ const md = installMarkdownImages(markdownParser(parser));
13
+ if (!md.__rapierFontsInstalled) {
14
+ const validate = md.validateLink;
15
+ md.validateLink = url => !!fontDataUrl(url) || validate(url);
16
+ md.__rapierFontsInstalled = true;
17
+ }
18
+ return md;
19
+ }
20
+
21
+ function faceOf(value) {
22
+ const family = normalizeFontFamily(value?.family), style = value?.style || 'normal';
23
+ const weight = value?.weight ?? 400, stretch = value?.stretch ?? 100;
24
+ if (!family || !['normal', 'italic', 'oblique'].includes(style) || !Number.isFinite(weight) || weight < 1 || weight > 1000 ||
25
+ !Number.isFinite(stretch) || stretch <= 0 || stretch > 1000) return null;
26
+ const result = {family, style, weight, stretch};
27
+ if (value.weightRange != null) {
28
+ const range = value.weightRange;
29
+ if (!Array.isArray(range) || range.length !== 2 || range.some(number => !Number.isFinite(number) || number < 1 || number > 1000) || range[0] > range[1]) return null;
30
+ result.weightRange = range.slice();
31
+ }
32
+ if (value.axes != null) {
33
+ if (!Array.isArray(value.axes) || value.axes.length > 64) return null;
34
+ const seen = new Set(), axes = [];
35
+ for (const axis of value.axes) {
36
+ if (!axis || !/^[ -~]{4}$/.test(axis.tag) || seen.has(axis.tag) ||
37
+ ![axis.min, axis.default, axis.max].every(Number.isFinite) || axis.min > axis.default || axis.default > axis.max) return null;
38
+ seen.add(axis.tag);
39
+ axes.push({tag: axis.tag, min: axis.min, default: axis.default, max: axis.max, ...(typeof axis.name === 'string' ? {name: axis.name} : {})});
40
+ }
41
+ if (axes.length) result.axes = axes;
42
+ }
43
+ if (value.sourceId != null) {
44
+ if (!/^f[0-9a-f]{24}$/.test(value.sourceId)) return null;
45
+ result.sourceId = value.sourceId;
46
+ }
47
+ return result;
48
+ }
49
+
50
+ export function fontAssetKey(font) {
51
+ const face = faceOf(font);
52
+ if (!face) return '';
53
+ return [face.family.toLowerCase(), face.style, face.weight, face.stretch,
54
+ (face.axes || []).map(axis => [axis.tag, axis.min, axis.max].join(':')).sort().join(',')].join('/');
55
+ }
56
+
57
+ export function readFontAssets(markdown, parser) {
58
+ const source = String(markdown ?? ''), assets = [], blocks = [], refused = [];
59
+ if (!/\[font:/i.test(source)) return {assets, blocks, refused, references: Object.create(null)};
60
+ const md = parserFor(parser), frame = splitOpeningFrontmatter(source), env = {}, tokens = [], starts = [frame.bodyOffset];
61
+ for (let at = frame.bodyOffset; at < source.length; at++) {
62
+ if (source[at] === '\r') { if (source[at + 1] === '\n') at++; starts.push(at + 1); }
63
+ else if (source[at] === '\n') starts.push(at + 1);
64
+ }
65
+ md.block.parse(frame.body.replace(/\r\n?/g, '\n').replace(/\0/g, '\ufffd'), md, env, tokens);
66
+ const labels = new Set(), faces = new Set();
67
+ for (const token of tokens) {
68
+ if (token.type !== 'reference_definition' || !token.map) continue;
69
+ const id = token.meta.label, first = !labels.has(id);
70
+ labels.add(id);
71
+ const definition = token.meta.mdImageDefinition || env.references?.[id];
72
+ const url = definition?.href, data = fontDataUrl(url);
73
+ if (!/^FONT:/i.test(id)) continue;
74
+ const start = starts[token.map[0]], next = starts[token.map[1]] ?? source.length;
75
+ let end = next;
76
+ if (source[end - 1] === '\n') end--;
77
+ if (source[end - 1] === '\r') end--;
78
+ const raw = source.slice(start, end), match = /^[ \t]{0,3}\[((?:\\.|[^\]\\\r\n])+)\]:/.exec(raw);
79
+ if (!match) continue;
80
+ const label = md.utils.unescapeAll(match[1]);
81
+ if (!data) { refused.push({name: label, reason: 'The font reference must contain an embedded font.'}); continue; }
82
+ let metadata = {family: label.slice(5)};
83
+ if (definition.title) {
84
+ try { metadata = JSON.parse(definition.title); }
85
+ catch (_) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
86
+ }
87
+ const face = faceOf(metadata);
88
+ if (!face) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
89
+ const key = fontAssetKey(face), active = first && !faces.has(key);
90
+ const row = {...face, ...data, url, label, id, key, source: raw, start, end, next, active, topLevel: token.level === 0};
91
+ blocks.push(row);
92
+ if (active) { assets.push(row); faces.add(key); }
93
+ }
94
+ return {assets, blocks, refused, references: env.references || Object.create(null)};
95
+ }
96
+
97
+ function fontLabel(face) {
98
+ const variants = face.style !== 'normal' || face.weight !== 400 || face.stretch !== 100 || face.axes?.length;
99
+ return 'font:' + face.family + (variants ? '/' + face.style + '/' + face.weight + (face.stretch !== 100 ? '/' + face.stretch : '')
100
+ + (face.axes?.length ? '/' + face.axes.map(axis => axis.tag + '=' + axis.min + ':' + axis.max).join(',') : '') : '');
101
+ }
102
+
103
+ export function serializeFontAsset(asset, label = null) {
104
+ const face = faceOf(asset), data = fontDataUrl(asset?.url);
105
+ if (!face || !data) throw new TypeError('font: use an embedded font with valid family metadata');
106
+ const escaped = escapeFontLabel(label || fontLabel(face));
107
+ const title = JSON.stringify(face).replace(/&/g, '&amp;').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
108
+ return '[' + escaped + ']: ' + asset.url + ' "' + title + '"';
109
+ }
110
+
111
+ function escapeFontLabel(label) {
112
+ return String(label).replace(/[\\[\]]/g, '\\$&').replace(/&/g, '&amp;');
113
+ }
114
+
115
+ export function writeFontAssets(markdown, assets, parser) {
116
+ const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map();
117
+ for (const asset of assets || []) {
118
+ const key = fontAssetKey(asset);
119
+ if (!key || !fontDataUrl(asset?.url)) throw new TypeError('font: invalid embedded asset');
120
+ if (wanted.has(key) && wanted.get(key).url !== asset.url) throw new TypeError('font: one file owns each family face');
121
+ wanted.set(key, asset);
122
+ }
123
+ const edits = [], consumed = new Set(), labels = new Set(Object.keys(index.references));
124
+ const retired = index.assets.filter(row => !wanted.has(row.key));
125
+ for (const row of index.blocks) {
126
+ if (!row.active) {
127
+ if (retired.some(active => active.key === row.key || active.id === row.id)) edits.push({start: row.start, end: row.next, text: ''});
128
+ continue;
129
+ }
130
+ const asset = wanted.get(row.key);
131
+ if (!asset) { edits.push({start: row.start, end: row.next, text: ''}); continue; }
132
+ consumed.add(row.key);
133
+ if (asset.url === row.url && JSON.stringify(faceOf(asset)) === JSON.stringify(faceOf(row))) continue;
134
+ edits.push({start: row.start, end: row.end, text: serializeFontAsset(asset, row.label)});
135
+ }
136
+ let result = source;
137
+ for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
138
+ const pending = [];
139
+ for (const [key, asset] of wanted) {
140
+ if (consumed.has(key)) continue;
141
+ const base = fontLabel(faceOf(asset));
142
+ let label = base, suffix = 2;
143
+ while (labels.has(normalizeLabel(escapeFontLabel(label)))) label = base + '/' + suffix++;
144
+ labels.add(normalizeLabel(escapeFontLabel(label)));
145
+ pending.push(serializeFontAsset(asset, label));
146
+ }
147
+ if (pending.length) {
148
+ const eol = /\r\n|\r|\n/.exec(source)?.[0] || '\n';
149
+ result += (result ? (/(?:\r\n|\r(?!\n)|\n)[ \t]*(?:\r\n|\r(?!\n)|\n)$/.test(result) ? '' : /[\r\n]$/.test(result) ? eol : eol + eol) : '') + pending.join(eol) + eol;
150
+ const written = readFontAssets(result, md).assets;
151
+ for (const [key, asset] of wanted) if (!written.some(row => row.key === key && row.url === asset.url)) throw new Error('font: close the code or HTML block before embedding a font');
152
+ }
153
+ return result;
154
+ }
155
+
156
+ export function documentFontsCss(assets) {
157
+ const rules = [];
158
+ for (const asset of assets || []) {
159
+ const face = faceOf(asset);
160
+ if (!face || !fontDataUrl(asset?.url)) continue;
161
+ const weight = face.axes?.find(axis => axis.tag === 'wght');
162
+ const width = face.axes?.find(axis => axis.tag === 'wdth');
163
+ const range = weight ? [weight.min, weight.max] : face.weightRange || [face.weight, face.weight];
164
+ const weightCss = range[0] === range[1] ? String(range[0]) : range.join(' ');
165
+ const stretchCss = width ? width.min + '% ' + width.max + '%' : face.stretch + '%';
166
+ rules.push('@font-face{font-family:' + fontFamilyCss(face.family) + ';font-style:' + face.style + ';font-weight:' + weightCss +
167
+ ';font-stretch:' + stretchCss + ';src:url("' + asset.url + '");font-display:swap}');
168
+ }
169
+ return rules.join('');
170
+ }
171
+
172
+ function htmlFontFamilies(html, md) {
173
+ const families = new Set();
174
+ const decode = value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\'));
175
+ for (const tag of html.matchAll(/<[A-Za-z][A-Za-z0-9:-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
176
+ for (const attr of tag[0].matchAll(/\s(data-md-font|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
177
+ const value = decode(attr[2] ?? attr[3] ?? attr[4]), names = [];
178
+ if (/^<span(?=[\s>])/i.test(tag[0])) {
179
+ const family = attr[1].toLowerCase() === 'style' ? typography.parseStyle(value).family : null;
180
+ if (family?.custom) families.add(family.name);
181
+ continue;
182
+ }
183
+ if (attr[1].toLowerCase() === 'data-md-font') names.push(value);
184
+ else for (const declared of value.matchAll(/(?:^|;)\s*font-family\s*:\s*((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^;])*)/gi)) {
185
+ const match = /^\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^,!]+))/.exec(declared[1]);
186
+ if (!match) continue;
187
+ const name = (match[1] ?? match[2] ?? match[3]).replace(/\\(?:([0-9a-f]{1,6})\s?|([^\r\n\f]))/gi, (_whole, hex, escaped) => {
188
+ const code = hex ? parseInt(hex, 16) : 0;
189
+ return hex ? code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd' : escaped;
190
+ });
191
+ if (match[3] != null && CSS_GENERIC_FONT_FAMILIES.includes(name.trim().toLowerCase())) continue;
192
+ names.push(name);
193
+ }
194
+ for (const name of names) { const family = normalizeFontFamily(name); if (family) families.add(family); }
195
+ }
196
+ }
197
+ return families;
198
+ }
199
+
200
+ export function documentFontUsage(markdown, parser, defaultFont) {
201
+ const source = String(markdown ?? ''), md = parserFor(parser), settings = readDocumentSettings(source), use = new Map();
202
+ const family = setting => setting?.stack === 'named' ? setting.family : null;
203
+ const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
204
+ const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
205
+ const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
206
+ if (!main && !sans && !mono && !source.includes('<')) return use;
207
+ const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
208
+ let blockFamily = main;
209
+ for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
210
+ if (token.type === 'heading_open') blockFamily = sans;
211
+ else if (token.type === 'heading_close' || token.type === 'paragraph_open') blockFamily = main;
212
+ else if (token.type === 'fence' && /^html(?:\s+\{[^{}]*\})?\s*$/i.test(token.info)) {
213
+ const text = plain(token.content);
214
+ add(blockFamily, text);
215
+ for (const name of htmlFontFamilies(token.content, md)) add(name, text);
216
+ }
217
+ else if (token.type === 'fence' || token.type === 'code_block') add(mono, token.content);
218
+ else if (token.type === 'html_block') {
219
+ const text = plain(token.content);
220
+ add(blockFamily, text);
221
+ for (const name of htmlFontFamilies(token.content, md)) add(name, text);
222
+ }
223
+ else if (token.type === 'inline') {
224
+ const stack = [blockFamily];
225
+ let html = '', text = '';
226
+ for (const child of token.children || []) {
227
+ if (child.type === 'html_inline') html += child.content;
228
+ if (['text', 'text_special', 'emoji', 'image', 'code_inline'].includes(child.type)) text += child.content;
229
+ const typed = child.meta?.typography;
230
+ if (typed && child.nesting === 1) {
231
+ stack.push(typed.style.family ? typed.style.family.custom ? typed.style.family.name : null : stack.at(-1)); continue;
232
+ }
233
+ if (typed && child.nesting === -1) { if (stack.length > 1) stack.pop(); continue; }
234
+ if (child.type === 'code_inline') add(mono, child.content);
235
+ else if (['text', 'text_special', 'emoji', 'image'].includes(child.type)) add(stack.at(-1), child.content);
236
+ }
237
+ for (const name of htmlFontFamilies(html, md)) add(name, text);
238
+ }
239
+ }
240
+ add(main, settings.title); add(main, settings.subtitle);
241
+ return use;
242
+ }
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
3
- import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
2
+ import {frontMatterFrame, frontMatterLine} from "./frontmatter.mjs";
3
+ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from "./md-marks.mjs";
4
4
 
5
5
  const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
6
6
  export const DOCUMENT_LEADING = 1.75 / 1.1;
@@ -1,13 +1,13 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMetadata} from './journal-records.mjs';
3
- import {pictureHistorySplices} from './history.mjs';
4
- import {HistorySource} from './source.mjs';
5
- import {authoredHistory} from './transport.mjs';
6
- import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
7
- import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
8
- import {diff} from './diff/diff.mjs';
9
- import {minimalHistorySplice} from './inverse-source.mjs';
10
- import {evaluateWill} from './will.mjs';
2
+ import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMetadata} from "./journal-records.mjs";
3
+ import {pictureHistorySplices} from "./history.mjs";
4
+ import {HistorySource} from "./source.mjs";
5
+ import {authoredHistory} from "./transport.mjs";
6
+ import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from "./hash.mjs";
7
+ import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from "./data.mjs";
8
+ import {diff} from "./diff/diff.mjs";
9
+ import {minimalHistorySplice} from "./inverse-source.mjs";
10
+ import {evaluateWill} from "./will.mjs";
11
11
  export {textRoot, rootAfter};
12
12
  const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
13
13
  function* checkedRecords(ledger, prepared, from = null) {
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
170
170
  const sameSplices = (a, b) => a === b || a.length === b.length &&
171
171
  a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
172
  const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ const RECENT_TEXT_LIMIT = 1 << 19;
173
174
  let recentLedgers = [];
174
175
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
175
176
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
@@ -239,9 +240,10 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
239
240
  const replayedMetadata = prepared.replayMetadata(startMetadata);
240
241
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
241
242
  const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
242
- recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
243
- startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
244
- chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
243
+ const others = recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority);
244
+ recentLedgers = text.length + startText.length > RECENT_TEXT_LIMIT ? [] : [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata),
245
+ headBytes, startText, startMetadata: {...startMetadata}, startFacts, startBytes: bytes, documentAuthority, complete: payload.complete,
246
+ startRoot, startRevision, chainRoot: payload.head.root, chainRevision: payload.head.revision}, ...others].slice(0, 4);
245
247
  ownLedger(ledger);
246
248
  proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
247
249
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from './journal-records.mjs';
3
- import {projectInverseSource} from './inverse-source.mjs';
4
- import {sourceView, HistorySource} from './source.mjs';
5
- import {textRootParts, rootAfter} from './hash.mjs';
6
- import {authoredHistory, authoredPlacements} from './transport.mjs';
7
- import {plainData, canonicalJSON} from './data.mjs';
8
- import {evaluateWill} from './will.mjs';
2
+ import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from "./journal-records.mjs";
3
+ import {projectInverseSource} from "./inverse-source.mjs";
4
+ import {sourceView, HistorySource} from "./source.mjs";
5
+ import {textRootParts, rootAfter} from "./hash.mjs";
6
+ import {authoredHistory, authoredPlacements} from "./transport.mjs";
7
+ import {plainData, canonicalJSON} from "./data.mjs";
8
+ import {evaluateWill} from "./will.mjs";
9
9
 
10
10
  const historyFailure = reason => ({ok: false, reason});
11
11
  const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
3
- import {HistorySource} from './source.mjs';
4
- import {authoredPlacements} from './authored-origin.mjs';
5
- import {canonicalJSON} from './data.mjs';
6
- import {diff} from './diff/index.mjs';
2
+ import {_rapierTransformSplices, _rapierHistoryEffects} from "./journal-records.mjs";
3
+ import {HistorySource} from "./source.mjs";
4
+ import {authoredPlacements} from "./authored-origin.mjs";
5
+ import {canonicalJSON} from "./data.mjs";
6
+ import {diff} from "./diff/index.mjs";
7
7
 
8
8
  const historyFailure = reason => ({ok: false, reason});
9
9
  const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !Object.is(at, -0) && at <= source.length &&
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {fields, scalar, plainPrototype} from './data.mjs';
3
- import {transformText} from './source.mjs';
4
- import {dataImage} from './md-assets.mjs';
2
+ import {fields, scalar, plainPrototype} from "./data.mjs";
3
+ import {transformText} from "./source.mjs";
4
+ import {dataImage} from "./md-assets.mjs";
5
5
 
6
6
  const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
7
7
  const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;