rapier-html 1.2.45 → 1.2.48

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 (55) hide show
  1. package/BUILD.json +16 -16
  2. package/README.md +13 -12
  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 +21 -12
  7. package/dist/ledger/diff/concerns.mjs +3 -3
  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 +296 -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/source.mjs +31 -2
  29. package/dist/ledger/template-page.mjs +1 -1
  30. package/dist/ledger/transport.mjs +66 -7
  31. package/dist/ledger/vendor/NOTICE.txt +60 -0
  32. package/dist/ledger/vendor/SOURCES.json +253 -0
  33. package/dist/ledger/vendor/entities/decode-codepoint.mjs +27 -0
  34. package/dist/ledger/vendor/entities.mjs +499 -0
  35. package/dist/ledger/vendor/linkify-it.mjs +427 -0
  36. package/dist/ledger/vendor/markdown-it.mjs +4027 -0
  37. package/dist/ledger/vendor/markdown-licenses/entities-LICENSE.txt +11 -0
  38. package/dist/ledger/vendor/markdown-licenses/linkify-it-LICENSE.txt +22 -0
  39. package/dist/ledger/vendor/markdown-licenses/markdown-it-LICENSE.txt +22 -0
  40. package/dist/ledger/vendor/markdown-licenses/mdurl-LICENSE.txt +45 -0
  41. package/dist/ledger/vendor/markdown-licenses/punycode.js-LICENSE-MIT.txt +20 -0
  42. package/dist/ledger/vendor/markdown-licenses/uc.micro-LICENSE.txt +20 -0
  43. package/dist/ledger/vendor/markdown-plugins.mjs +1344 -0
  44. package/dist/ledger/vendor/markdown-runtime.mjs +5 -0
  45. package/dist/ledger/vendor/mdurl/index.mjs +11 -0
  46. package/dist/ledger/vendor/mdurl/lib/decode.mjs +107 -0
  47. package/dist/ledger/vendor/mdurl/lib/encode.mjs +77 -0
  48. package/dist/ledger/vendor/mdurl/lib/format.mjs +20 -0
  49. package/dist/ledger/vendor/mdurl/lib/parse.mjs +231 -0
  50. package/dist/ledger/vendor/punycode.mjs +282 -0
  51. package/dist/ledger/vendor/uc.micro.mjs +49 -0
  52. package/dist/page.mjs +11 -3
  53. package/dist/rapier.html +113 -75
  54. package/dist/wrap.mjs +10 -5
  55. 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.48",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 5843562,
6
- "sha256": "4377466b87c89fcfbd71dd6c8de0f73fc72de2f7e2529c89b0004828ce6855c4",
7
- "builtAt": "2026-10-25T17:51:41.653Z",
5
+ "bytes": 5967960,
6
+ "sha256": "be629bc50b6a201e8889ce4c2768ee350015e8dfbafe7cfe880a6a6113865595",
7
+ "builtAt": "2026-10-28T02:28:38.799Z",
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.48 notes.md`, then open `notes.rapier.html` in any browser.
11
+ Install: `npm install rapier-html@1.2.48`.
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.
@@ -21,19 +21,20 @@ The file needs no hosted workspace.
21
21
  - Where scripts don't run (a mail preview, a strict viewer), the file shows the document's words.
22
22
  - `--return` adds a one-use Send back address from the agent door.
23
23
  - `--view draw` opens on Draw. `--compare original.md` opens the edit with its diff; `--by <name>` attributes it.
24
+ - `--editor page.html` uses another Rapier page as the editor, such as one the agent door exported.
24
25
  - `writePage(text, name)` makes the page in one call; `unwrap` gives the document back, exactly.
25
26
 
26
27
  Needs Node 22 or newer.
27
28
 
28
29
  ```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
30
+ npx -- rapier-html@1.2.48 notes.md # the editor, notes.md inside it, written beside the file
31
+ npx -- rapier-html@1.2.48 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
32
+ npx -- rapier-html@1.2.48 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
33
+ npx -- rapier-html@1.2.48 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
34
+ npx -- rapier-html@1.2.48 revised.md --compare original.md # opens showing what changed
35
+ npx -- rapier-html@1.2.48 notes.md --return <return_url> --return-expires-at <return_expires_at> # Send back
36
+ npx -- rapier-html@1.2.48 notes.md out.html # a named output
37
+ npm install rapier-html@1.2.48 # as a library: writePage, unwrap
37
38
  ```
38
39
 
39
40
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
@@ -52,7 +53,7 @@ Will markers in the Markdown (`keep`, `append`, `edit`) control what agents may
52
53
  ## Send an edited document back
53
54
 
54
55
  1. Call `show {target:"return"}` on the Rapier agent door for the document's workspace.
55
- 2. Pass its `return_url` and `return_expires_at` as `--return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT"`,
56
+ 2. Pass its `return_url` and `return_expires_at` as `--return <return_url> --return-expires-at <return_expires_at>`,
56
57
  or `wrap(..., {return: return_url, return_expires_at})`. Only `https://mcp.rapier.website/return/…` is accepted.
57
58
  Share then shows Send back; a page without an address doesn't.
58
59
  3. The person edits offline and taps Send back. The source and filename go once; the page shows acceptance or
@@ -117,7 +118,7 @@ objects, and `unwrap(page)` returns the checked parts beside the text. Both are
117
118
  history.
118
119
  - Markdown saved with the kit's `ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a page
119
120
  without parts drops the old parts.
120
- - The published package carries its own MIT validator; no source checkout is needed.
121
+ - 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
122
 
122
123
  ## License
123
124
 
@@ -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
  }
@@ -56,11 +56,19 @@ export function documentTrailer(text, options) {
56
56
  export function writeDocument(text, options) {
57
57
  return text + documentTrailer(text, options);
58
58
  }
59
+ function trailerOpening(source) {
60
+ const marker = '<!-- rapier-carried/1', at = source.lastIndexOf(marker);
61
+ if (at < 0) return null;
62
+ if (source.slice(at - 2, at + marker.length + 1) === OPEN) return {start: at - 2, body: at + marker.length + 1};
63
+ const before = /(?:\r\n|\r|\n){2}$/.exec(source.slice(Math.max(0, at - 4), at));
64
+ const after = /^(?:\r\n|\r|\n)/.exec(source.slice(at + marker.length, at + marker.length + 2));
65
+ return before && after ? {start: at - before[0].length, body: at + marker.length + after[0].length} : null;
66
+ }
59
67
  function unfinishedTrailer(source) {
60
- const at = source.lastIndexOf(OPEN);
61
- if (at < 0) return false;
68
+ const opening = trailerOpening(source);
69
+ if (!opening) return false;
62
70
  let fence = null;
63
- for (const line of source.slice(0, at).split('\n')) {
71
+ for (const line of source.slice(0, opening.start).split(/\r\n?|\n/)) {
64
72
  const found = /^ {0,3}(`{3,}|~{3,})/.exec(line);
65
73
  if (!found) continue;
66
74
  if (!fence) fence = found[1];
@@ -69,17 +77,18 @@ function unfinishedTrailer(source) {
69
77
  return !fence;
70
78
  }
71
79
  export function readDocument(source) {
72
- if (!source.endsWith(CLOSE)) {
80
+ const closing = /(?:\r\n|\r|\n)-->(?:\r\n|\r|\n)$/.exec(source.slice(-7));
81
+ if (!closing) {
73
82
  if (unfinishedTrailer(source)) throw fault('unfinished document metadata');
74
83
  return {text: source, ledger: null, authorship: null};
75
84
  }
76
- const at = source.lastIndexOf(OPEN);
77
- if (at < 0) return {text: source, ledger: null, authorship: null};
85
+ const opening = trailerOpening(source);
86
+ if (!opening) return {text: source, ledger: null, authorship: null};
78
87
  let parts;
79
- try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
88
+ try { parts = JSON.parse(source.slice(opening.body, -closing[0].length)); }
80
89
  catch (_) { throw fault('invalid document metadata'); }
81
90
  if (!parts || Object.keys(parts).length !== 2 || !Object.hasOwn(parts, 'ledger') || !Object.hasOwn(parts, 'authorship')) throw fault('invalid document parts');
82
- const text = source.slice(0, at);
91
+ const text = source.slice(0, opening.start);
83
92
  return {text, ...validateParts(text, parts)};
84
93
  }
85
94
 
@@ -1,11 +1,11 @@
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;
6
6
  if (!raw) for (let i = c.result.length - 1; i > 0; i--) {
7
7
  const p = c.result[i - 1], q = c.result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
8
- if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
8
+ if (!c.evidence && side >= 0 && !/\S/.test(text.slice(...p[side]))) {
9
9
  let n = 0;
10
10
  while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n] && spend()) n++;
11
11
  if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
@@ -20,7 +20,7 @@ export function separate(c, endA = c.before.length, endB = c.after.length) {
20
20
  keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
21
21
  c.result.splice(i - 1, 2, [x, y, false, keep[3]]);
22
22
  }
23
- if (!raw) for (let i = 0; i < c.result.length; i++) {
23
+ if (!raw && !c.evidence) for (let i = 0; i < c.result.length; i++) {
24
24
  const p = c.result[i], q = c.result[i + 1], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1;
25
25
  if (side < 0 || p[2]) continue;
26
26
  const text = side ? after : before;
@@ -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,296 @@
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
+ const sourceMetadataFields = ['license', 'origin'];
22
+ function sourceMetadata(value) {
23
+ return Object.fromEntries(sourceMetadataFields.filter(field => value[field] != null).map(field => [field, value[field]]));
24
+ }
25
+
26
+ function faceOf(value) {
27
+ const family = normalizeFontFamily(value?.family), style = value?.style || 'normal';
28
+ const weight = value?.weight ?? 400, stretch = value?.stretch ?? 100;
29
+ if (!family || !['normal', 'italic', 'oblique'].includes(style) || !Number.isFinite(weight) || weight < 1 || weight > 1000 ||
30
+ !Number.isFinite(stretch) || stretch <= 0 || stretch > 1000) return null;
31
+ const result = {family, style, weight, stretch};
32
+ if (value.subsetted != null && typeof value.subsetted !== 'boolean') return null;
33
+ if (value.subsetted === true || value.subset?.subset === true) result.subsetted = true;
34
+ if (value.weightRange != null) {
35
+ const range = value.weightRange;
36
+ if (!Array.isArray(range) || range.length !== 2 || range.some(number => !Number.isFinite(number) || number < 1 || number > 1000) || range[0] > range[1]) return null;
37
+ result.weightRange = range.slice();
38
+ }
39
+ if (value.axes != null) {
40
+ if (!Array.isArray(value.axes) || value.axes.length > 64) return null;
41
+ const seen = new Set(), axes = [];
42
+ for (const axis of value.axes) {
43
+ if (!axis || !/^[ -~]{4}$/.test(axis.tag) || seen.has(axis.tag) ||
44
+ ![axis.min, axis.default, axis.max].every(Number.isFinite) || axis.min > axis.default || axis.default > axis.max) return null;
45
+ seen.add(axis.tag);
46
+ axes.push({tag: axis.tag, min: axis.min, default: axis.default, max: axis.max, ...(typeof axis.name === 'string' ? {name: axis.name} : {})});
47
+ }
48
+ if (axes.length) result.axes = axes;
49
+ }
50
+ if (value.sourceId != null) {
51
+ if (!/^f[0-9a-f]{24}$/.test(value.sourceId)) return null;
52
+ result.sourceId = value.sourceId;
53
+ }
54
+ if (value.license != null) {
55
+ const license = value.license;
56
+ if (!license || !['OFL-1.0', 'OFL-1.1', 'Apache-2.0', 'Ubuntu-font-1.0'].includes(license.id) ||
57
+ typeof license.text !== 'string' || !license.text.trim() || new TextEncoder().encode(license.text).length > 128 * 1024 ||
58
+ typeof license.url !== 'string' || typeof license.sri !== 'string' || !/^sha384-[A-Za-z0-9+/]{64}$/.test(license.sri)) return null;
59
+ let url;
60
+ try { url = new URL(license.url); } catch (_) { return null; }
61
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) return null;
62
+ result.license = {id: license.id, text: license.text, url: license.url, sri: license.sri};
63
+ }
64
+ if (value.origin != null) {
65
+ const origin = value.origin;
66
+ if (!result.license || result.subsetted || !origin || origin.catalog !== 'google-fonts' ||
67
+ typeof origin.family !== 'string' || !/^[a-z0-9][a-z0-9-]{0,95}$/.test(origin.family) ||
68
+ typeof origin.version !== 'string' || !origin.version || origin.version.length > 128 || /[\u0000-\u001f\u007f]/u.test(origin.version) ||
69
+ typeof origin.file !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\.woff2$/.test(origin.file) ||
70
+ typeof origin.sri !== 'string' || !/^sha384-[A-Za-z0-9+/]{64}$/.test(origin.sri)) return null;
71
+ result.origin = {catalog: origin.catalog, family: origin.family, version: origin.version, file: origin.file, sri: origin.sri};
72
+ }
73
+ return result;
74
+ }
75
+
76
+ export function fontAssetKey(font) {
77
+ const face = faceOf(font);
78
+ if (!face) return '';
79
+ return [face.family.toLowerCase(), face.style, face.weight, face.stretch,
80
+ (face.axes || []).map(axis => [axis.tag, axis.min, axis.max].join(':')).sort().join(',')].join('/');
81
+ }
82
+
83
+ export function readFontAssets(markdown, parser) {
84
+ const source = String(markdown ?? ''), assets = [], blocks = [], refused = [];
85
+ if (!/\[font:/i.test(source)) return {assets, blocks, refused, references: Object.create(null)};
86
+ const md = parserFor(parser), frame = splitOpeningFrontmatter(source), env = {}, tokens = [], starts = [frame.bodyOffset];
87
+ for (let at = frame.bodyOffset; at < source.length; at++) {
88
+ if (source[at] === '\r') { if (source[at + 1] === '\n') at++; starts.push(at + 1); }
89
+ else if (source[at] === '\n') starts.push(at + 1);
90
+ }
91
+ md.block.parse(frame.body.replace(/\r\n?/g, '\n').replace(/\0/g, '\ufffd'), md, env, tokens);
92
+ const labels = new Set(), faces = new Set(), metadata = new Map(), faceMetadata = new Map(), subsetUrls = new Set();
93
+ for (const token of tokens) {
94
+ if (token.type !== 'reference_definition' || !token.map) continue;
95
+ const id = token.meta.label, first = !labels.has(id);
96
+ labels.add(id);
97
+ const definition = token.meta.mdImageDefinition || env.references?.[id];
98
+ const url = definition?.href, data = fontDataUrl(url);
99
+ if (!/^FONT:/i.test(id)) continue;
100
+ const start = starts[token.map[0]], next = starts[token.map[1]] ?? source.length;
101
+ let end = next;
102
+ if (source[end - 1] === '\n') end--;
103
+ if (source[end - 1] === '\r') end--;
104
+ const raw = source.slice(start, end), match = /^[ \t]{0,3}\[((?:\\.|[^\]\\\r\n])+)\]:/.exec(raw);
105
+ if (!match) continue;
106
+ const label = md.utils.unescapeAll(match[1]);
107
+ if (!data) { refused.push({name: label, reason: 'The font reference must contain an embedded font.'}); continue; }
108
+ let definitionMetadata = {family: label.slice(5)};
109
+ if (definition.title) {
110
+ try { definitionMetadata = JSON.parse(definition.title); }
111
+ catch (_) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
112
+ }
113
+ const face = faceOf(definitionMetadata);
114
+ if (!face) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
115
+ const key = fontAssetKey(face), active = first && !faces.has(key);
116
+ const conflict = sourceMetadataFields.find(field => face[field] && [metadata.get(url)?.[field], faceMetadata.get(key)?.[field]]
117
+ .some(prior => prior && JSON.stringify(prior) !== JSON.stringify(face[field])));
118
+ if (conflict) { refused.push({name: label, reason: 'The font references have conflicting ' + conflict + ' metadata.'}); continue; }
119
+ if (face.subsetted && metadata.get(url)?.origin || face.origin && subsetUrls.has(url)) {
120
+ refused.push({name: label, reason: 'A font subset cannot claim an original catalog file.'}); continue;
121
+ }
122
+ metadata.set(url, {...metadata.get(url), ...sourceMetadata(face)});
123
+ faceMetadata.set(key, {...faceMetadata.get(key), ...sourceMetadata(face)});
124
+ if (face.subsetted) subsetUrls.add(url);
125
+ const row = {...face, ...data, url, label, id, key, source: raw, start, end, next, active, topLevel: token.level === 0};
126
+ blocks.push(row);
127
+ if (active) { assets.push(row); faces.add(key); }
128
+ }
129
+ for (let index = 0; index < assets.length; index++) {
130
+ const asset = assets[index], declared = metadata.get(asset.url);
131
+ for (const field of sourceMetadataFields) if (!asset[field] && declared?.[field])
132
+ assets[index] = {...assets[index], [field]: declared[field]};
133
+ if (subsetUrls.has(asset.url) && !asset.subsetted) assets[index] = {...assets[index], subsetted: true};
134
+ }
135
+ return {assets, blocks, refused, references: env.references || Object.create(null)};
136
+ }
137
+
138
+ function fontLabel(face) {
139
+ const variants = face.style !== 'normal' || face.weight !== 400 || face.stretch !== 100 || face.axes?.length;
140
+ return 'font:' + face.family + (variants ? '/' + face.style + '/' + face.weight + (face.stretch !== 100 ? '/' + face.stretch : '')
141
+ + (face.axes?.length ? '/' + face.axes.map(axis => axis.tag + '=' + axis.min + ':' + axis.max).join(',') : '') : '');
142
+ }
143
+
144
+ export function serializeFontAsset(asset, label = null) {
145
+ const face = faceOf(asset), data = fontDataUrl(asset?.url);
146
+ if (!face || !data) throw new TypeError('font: use an embedded font with valid family metadata');
147
+ const escaped = escapeFontLabel(label || fontLabel(face));
148
+ const title = JSON.stringify(face).replace(/&/g, '&amp;').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
149
+ return '[' + escaped + ']: ' + asset.url + ' "' + title + '"';
150
+ }
151
+
152
+ function escapeFontLabel(label) {
153
+ return String(label).replace(/[\\[\]]/g, '\\$&').replace(/&/g, '&amp;');
154
+ }
155
+
156
+ export function writeFontAssets(markdown, assets, parser) {
157
+ const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map(), metadata = new Map(), subsetUrls = new Set();
158
+ for (const asset of assets || []) {
159
+ const key = fontAssetKey(asset), face = faceOf(asset);
160
+ if (!key || !fontDataUrl(asset?.url)) throw new TypeError('font: invalid embedded asset');
161
+ if (wanted.has(key) && wanted.get(key).url !== asset.url) throw new TypeError('font: one file owns each family face');
162
+ const conflict = sourceMetadataFields.find(field => face[field] && metadata.get(asset.url)?.[field] &&
163
+ JSON.stringify(metadata.get(asset.url)[field]) !== JSON.stringify(face[field]));
164
+ if (conflict) throw new TypeError('font: conflicting ' + conflict + ' metadata');
165
+ if (face.subsetted && metadata.get(asset.url)?.origin || face.origin && subsetUrls.has(asset.url))
166
+ throw new TypeError('font: a subset cannot claim an original catalog file');
167
+ metadata.set(asset.url, {...metadata.get(asset.url), ...sourceMetadata(face)});
168
+ if (face.subsetted) subsetUrls.add(asset.url);
169
+ wanted.set(key, asset);
170
+ }
171
+ for (const [key, asset] of wanted) {
172
+ const declared = metadata.get(asset.url);
173
+ for (const field of sourceMetadataFields) if (!asset[field] && declared?.[field])
174
+ wanted.set(key, {...wanted.get(key), [field]: declared[field]});
175
+ if (subsetUrls.has(asset.url) && !asset.subsetted) wanted.set(key, {...wanted.get(key), subsetted: true});
176
+ }
177
+ const edits = [], consumed = new Set(), labels = new Set(Object.keys(index.references));
178
+ const retired = index.assets.filter(row => !wanted.has(row.key));
179
+ for (const row of index.blocks) {
180
+ if (!row.active) {
181
+ if (retired.some(active => active.key === row.key || active.id === row.id)) edits.push({start: row.start, end: row.next, text: ''});
182
+ continue;
183
+ }
184
+ const asset = wanted.get(row.key);
185
+ if (!asset) { edits.push({start: row.start, end: row.next, text: ''}); continue; }
186
+ consumed.add(row.key);
187
+ if (asset.url === row.url && JSON.stringify(faceOf(asset)) === JSON.stringify(faceOf(row))) continue;
188
+ edits.push({start: row.start, end: row.end, text: serializeFontAsset(asset, row.label)});
189
+ }
190
+ let result = source;
191
+ for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
192
+ const pending = [];
193
+ for (const [key, asset] of wanted) {
194
+ if (consumed.has(key)) continue;
195
+ const base = fontLabel(faceOf(asset));
196
+ let label = base, suffix = 2;
197
+ while (labels.has(normalizeLabel(escapeFontLabel(label)))) label = base + '/' + suffix++;
198
+ labels.add(normalizeLabel(escapeFontLabel(label)));
199
+ pending.push(serializeFontAsset(asset, label));
200
+ }
201
+ if (pending.length) {
202
+ const eol = /\r\n|\r|\n/.exec(source)?.[0] || '\n';
203
+ 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;
204
+ const written = readFontAssets(result, md).assets;
205
+ 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');
206
+ }
207
+ return result;
208
+ }
209
+
210
+ export function documentFontsCss(assets) {
211
+ const rules = [];
212
+ for (const asset of assets || []) {
213
+ const face = faceOf(asset);
214
+ if (!face || !fontDataUrl(asset?.url)) continue;
215
+ const weight = face.axes?.find(axis => axis.tag === 'wght');
216
+ const width = face.axes?.find(axis => axis.tag === 'wdth');
217
+ const range = weight ? [weight.min, weight.max] : face.weightRange || [face.weight, face.weight];
218
+ const weightCss = range[0] === range[1] ? String(range[0]) : range.join(' ');
219
+ const stretchCss = width ? width.min + '% ' + width.max + '%' : face.stretch + '%';
220
+ rules.push('@font-face{font-family:' + fontFamilyCss(face.family) + ';font-style:' + face.style + ';font-weight:' + weightCss +
221
+ ';font-stretch:' + stretchCss + ';src:url("' + asset.url + '");font-display:swap}');
222
+ }
223
+ return rules.join('');
224
+ }
225
+
226
+ function htmlFontFamilies(html, md) {
227
+ const families = new Set();
228
+ const decode = value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\'));
229
+ for (const tag of html.matchAll(/<[A-Za-z][A-Za-z0-9:-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
230
+ for (const attr of tag[0].matchAll(/\s(data-md-font|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
231
+ const value = decode(attr[2] ?? attr[3] ?? attr[4]), names = [];
232
+ if (/^<span(?=[\s>])/i.test(tag[0])) {
233
+ const family = attr[1].toLowerCase() === 'style' ? typography.parseStyle(value).family : null;
234
+ if (family?.custom) families.add(family.name);
235
+ continue;
236
+ }
237
+ if (attr[1].toLowerCase() === 'data-md-font') names.push(value);
238
+ else for (const declared of value.matchAll(/(?:^|;)\s*font-family\s*:\s*((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^;])*)/gi)) {
239
+ const match = /^\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^,!]+))/.exec(declared[1]);
240
+ if (!match) continue;
241
+ const name = (match[1] ?? match[2] ?? match[3]).replace(/\\(?:([0-9a-f]{1,6})\s?|([^\r\n\f]))/gi, (_whole, hex, escaped) => {
242
+ const code = hex ? parseInt(hex, 16) : 0;
243
+ return hex ? code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd' : escaped;
244
+ });
245
+ if (match[3] != null && CSS_GENERIC_FONT_FAMILIES.includes(name.trim().toLowerCase())) continue;
246
+ names.push(name);
247
+ }
248
+ for (const name of names) { const family = normalizeFontFamily(name); if (family) families.add(family); }
249
+ }
250
+ }
251
+ return families;
252
+ }
253
+
254
+ export function documentFontUsage(markdown, parser, defaultFont) {
255
+ const source = String(markdown ?? ''), md = parserFor(parser), settings = readDocumentSettings(source), use = new Map();
256
+ const family = setting => setting?.stack === 'named' ? setting.family : null;
257
+ const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
258
+ const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
259
+ const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
260
+ if (!main && !sans && !mono && !source.includes('<')) return use;
261
+ const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
262
+ let blockFamily = main;
263
+ for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
264
+ if (token.type === 'heading_open') blockFamily = sans;
265
+ else if (token.type === 'heading_close' || token.type === 'paragraph_open') blockFamily = main;
266
+ else if (token.type === 'fence' && /^html(?:\s+\{[^{}]*\})?\s*$/i.test(token.info)) {
267
+ const text = plain(token.content);
268
+ add(blockFamily, text);
269
+ for (const name of htmlFontFamilies(token.content, md)) add(name, text);
270
+ }
271
+ else if (token.type === 'fence' || token.type === 'code_block') add(mono, token.content);
272
+ else if (token.type === 'html_block') {
273
+ const text = plain(token.content);
274
+ add(blockFamily, text);
275
+ for (const name of htmlFontFamilies(token.content, md)) add(name, text);
276
+ }
277
+ else if (token.type === 'inline') {
278
+ const stack = [blockFamily];
279
+ let html = '', text = '';
280
+ for (const child of token.children || []) {
281
+ if (child.type === 'html_inline') html += child.content;
282
+ if (['text', 'text_special', 'emoji', 'image', 'code_inline'].includes(child.type)) text += child.content;
283
+ const typed = child.meta?.typography;
284
+ if (typed && child.nesting === 1) {
285
+ stack.push(typed.style.family ? typed.style.family.custom ? typed.style.family.name : null : stack.at(-1)); continue;
286
+ }
287
+ if (typed && child.nesting === -1) { if (stack.length > 1) stack.pop(); continue; }
288
+ if (child.type === 'code_inline') add(mono, child.content);
289
+ else if (['text', 'text_special', 'emoji', 'image'].includes(child.type)) add(stack.at(-1), child.content);
290
+ }
291
+ for (const name of htmlFontFamilies(html, md)) add(name, text);
292
+ }
293
+ }
294
+ add(main, settings.title); add(main, settings.subtitle);
295
+ return use;
296
+ }
@@ -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;