rapier-html 1.1.104 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BUILD.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "rapier-html",
3
- "version": "1.1.104",
3
+ "version": "1.2.5",
4
4
  "path": "dist/rapier.html",
5
- "bytes": 3565876,
6
- "sha256": "11ef86ddc14ffd60276d7266331ae27bd47c3a4a395aedf8c8601a3a395ae5d9",
7
- "builtAt": "2026-10-12T17:02:11.693Z",
5
+ "bytes": 4949290,
6
+ "sha256": "5e2cbc912f760dd5ffad630f92dcec40151df5d55d0e6c7a2796ef907671d8bc",
7
+ "builtAt": "2026-10-16T06:38:46.417Z",
8
8
  "node": "v22.23.3",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
@@ -14,32 +14,39 @@
14
14
  "delivery": "built-in",
15
15
  "kind": "script",
16
16
  "element": "rapier-plugin-math",
17
- "bytes": 714051,
18
- "packedBytes": 495012,
17
+ "bytes": 691650,
18
+ "packedBytes": 438088,
19
19
  "files": [
20
20
  {
21
21
  "id": "rapier-math",
22
- "name": "rapier-math-1.1.0.js",
23
- "version": "1.1.0",
24
- "bytes": 714051,
25
- "sri": "dYIPrqCf4rrTcAKw1Zzzo7vujbNtk2C7Zik/tIYOaLVT0U66WOXcGNE6waFbKych"
22
+ "name": "rapier-math-2.4.0.js",
23
+ "version": "2.4.0",
24
+ "bytes": 691650,
25
+ "sri": "C92x26+pBI4q9FoWypMhBb8CqSGaaPLw8dpYigWiYNsYQRTcPVrFyPVjrhc/Tnwh"
26
26
  }
27
27
  ]
28
28
  },
29
29
  {
30
30
  "id": "mermaid",
31
31
  "delivery": "built-in",
32
- "kind": "script",
32
+ "kind": "files",
33
33
  "element": "rapier-plugin-mermaid",
34
- "bytes": 292169,
35
- "packedBytes": 102242,
34
+ "bytes": 7389640,
35
+ "packedBytes": 1526485,
36
36
  "files": [
37
37
  {
38
- "id": "rapier-mermaid",
39
- "name": "rapier-diagrams-1.1.104.js",
40
- "version": "1.1.104",
41
- "bytes": 292169,
42
- "sri": "b+ByE92VMwroMyB4gJIAdisjv+ONeQERVFCCP9nBfchHTUxACYXg1YNbdm2bp7FT"
38
+ "id": "rapier-mermaid-core",
39
+ "name": "mermaid.min.js",
40
+ "version": "12.1.0",
41
+ "bytes": 5493176,
42
+ "sri": "EbBpjO7rlR6eqZEcG7GaPpyk9H9WrMyPWX4d3KvPYltgt8Z8l0z6R56B1qP40pR4"
43
+ },
44
+ {
45
+ "id": "rapier-mermaid-zenuml",
46
+ "name": "rapier-zenuml-4.5.0-2.js",
47
+ "version": "4.5.0-2",
48
+ "bytes": 1896464,
49
+ "sri": "8Ptjhh1zNTOvy1dxFHhXgYKfs290DSlpzow5rF9FvP38cohtbsBW381Whym1WI6E"
43
50
  }
44
51
  ]
45
52
  },
@@ -49,7 +56,7 @@
49
56
  "kind": "files",
50
57
  "element": "rapier-plugin-letters",
51
58
  "bytes": 463027,
52
- "packedBytes": 204178,
59
+ "packedBytes": 198962,
53
60
  "files": [
54
61
  {
55
62
  "id": "rapier-letters-field",
@@ -87,7 +94,7 @@
87
94
  "kind": "files",
88
95
  "element": "rapier-plugin-font-subset",
89
96
  "bytes": 1214356,
90
- "packedBytes": 453061,
97
+ "packedBytes": 441192,
91
98
  "files": [
92
99
  {
93
100
  "id": "rapier-font-subset",
package/README.md CHANGED
@@ -1,79 +1,62 @@
1
1
  # rapier-html
2
2
 
3
- Turn Markdown into one offline HTML file with the full Rapier editor. It opens in any browser on a phone or
4
- computer, with no account. People read, edit, draw and paint watercolor. Agents deliver documents, show what
5
- changed and receive the person's edited copy through the door. Start with `npx rapier-html@1.1.104 notes.md`.
3
+ Turn Markdown into one offline HTML file that contains the full Rapier editor. It opens in any browser, on a phone or
4
+ computer, with no account. People read, edit, draw and paint; agents hand over documents, show what changed and get
5
+ the edited copy back. Start with `npx rapier-html@1.2.5 notes.md`.
6
6
 
7
- Use it for a plan to rearrange, an illustrated guide to annotate, a draft to continue writing, or a revision
8
- to review. Text and drawings stay editable, and the file remains usable independently of a hosted workspace.
7
+ Use it for a plan to rearrange, a guide to annotate, a draft to keep writing, or a revision to review. The file works
8
+ without any hosted workspace.
9
9
 
10
- - The page writes, draws and paints, saves Markdown, exports Word, PDF and a web page, and prints, with no account
11
- and nothing fetched to open, edit or save.
12
- - `--return` adds a one-use Send back address from the agent door: the person edits offline, presses Send back,
13
- and the agent reads the returned copy.
14
- - `--view draw` opens on Draw; `--compare original.md` opens the edited document showing what changed;
15
- `--by <name>` attributes the comparison with the time the page was made.
16
- - `wrap` and `unwrap` carry a document into a page and out again, exactly, for programs.
10
+ - The page writes, draws and paints, saves Markdown, exports Word, PDF and a web page, and prints. It needs no account
11
+ and fetches nothing to open, edit or save.
12
+ - `--return` adds a one-use Send back address from the agent door.
13
+ - `--view draw` opens on Draw. `--compare original.md` opens the edit with its diff; `--by <name>` attributes it.
14
+ - `wrap` and `unwrap` move a document into a page and back out, exactly.
17
15
 
18
- Node 22 or newer.
16
+ Needs Node 22 or newer.
19
17
 
20
18
  ```sh
21
- npx -- rapier-html@1.1.104 notes.md # the editor, notes.md inside it, written beside the file
22
- npx -- rapier-html@1.1.104 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
23
- npx -- rapier-html@1.1.104 notes.md --view notes # opens the Notes library
24
- npx -- rapier-html@1.1.104 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
25
- npx -- rapier-html@1.1.104 revised.md --compare original.md # opens showing what changed
26
- npx -- rapier-html@1.1.104 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
27
- npx -- rapier-html@1.1.104 notes.md out.html # a named output
28
- npm install rapier-html@1.1.104 # as a library: wrap, unwrap
19
+ npx -- rapier-html@1.2.5 notes.md # the editor, notes.md inside it, written beside the file
20
+ npx -- rapier-html@1.2.5 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
21
+ npx -- rapier-html@1.2.5 notes.md --view notes # opens the Notes library
22
+ npx -- rapier-html@1.2.5 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
23
+ npx -- rapier-html@1.2.5 revised.md --compare original.md # opens showing what changed
24
+ npx -- rapier-html@1.2.5 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
25
+ npx -- rapier-html@1.2.5 notes.md out.html # a named output
26
+ npm install rapier-html@1.2.5 # as a library: wrap, unwrap
29
27
  ```
30
28
 
31
29
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
32
30
 
33
- For live shared editing, open the document through Rapier's MCP tools. The agent edits beside the person or
34
- while they are away. Rapier shows its presence and change marks; the person taps a change to see the before
35
- and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately. Export Markdown or an offline
36
- editor when the result needs an independent copy. Send back returns the edited copy alongside the hosted
37
- document, preserving both.
38
-
39
- Will markers in the Markdown control agent edits: `keep`, `append`, `edit`.
31
+ Will markers in the Markdown (`keep`, `append`, `edit`) control what agents may change.
40
32
 
41
33
  ## Choose a Rapier workflow
42
34
 
43
- Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
44
- as one HTML file on Android, Web and Windows.
45
- Collaborate over MCP and WebMCP, use optional encrypted sync to your own Cloudflare account, or embed the
46
- editor and lightweight reader (about 190 kB gzipped). The agent edits live; the person sees its presence and
47
- changes, taps a change to see the before and undoes anything. Use `show {target:"comparison"}` to show a diff deliberately.
48
-
49
- 1. **Work together live.** Connect to `https://mcp.rapier.website/mcp`, then call `open` to share one document with the person.
50
- 2. **Deliver an offline editor.** Run `npx rapier-html@1.1.104 notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
51
- 3. **Embed in your app.** Install [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed); mount the editor or reader, with your app's agent over WebMCP in the editor.
52
- 4. **Keep portable Markdown.** Use [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit) for pictures, editable SVG drawings and layout in one `.md` file. Respect [Will/1](https://github.com/jackskip22/will) `keep`, `append` and `edit` regions.
53
- 5. **Encode JPEG XL.** Install [rapier-jxl](https://github.com/jackskip22/rapier-jxl) to encode pixels, photographs and existing JPEGs in JavaScript.
54
- 6. **Host the door.** Run [rapier-server](https://github.com/jackskip22/rapier/tree/main/server) over your own folder or S3-compatible bucket.
35
+ 1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
36
+ 2. **Hand over an offline editor.** Use this package.
37
+ 3. **Embed the editor.** [rapier-embed](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-embed)
38
+ mounts the editor or reader in your app.
39
+ 4. **Keep portable Markdown.** [rapier-markdown-kit](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-markdown-kit)
40
+ keeps pictures, drawings and layout in one `.md` file.
55
41
 
56
42
  ## Send an edited document back
57
43
 
58
- Call `show {target:"return"}` on the Rapier agent door for the document's workspace and pass its `return_url`
59
- and `return_expires_at` as `--return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT"`, or
60
- `wrap(..., {return: return_url, return_expires_at})`. Only the worker's HTTPS return route at
61
- `https://mcp.rapier.website/return/…` is accepted. The page then shows Send back in Share; a page without an
62
- address has no such action.
63
-
64
- The person edits offline and presses Send back: the current source and filename go once, the page shows the
65
- acceptance or the worker's refusal, and it sends nothing again. The return lasts up to one day, takes one
66
- document with a 64 MiB decoded-source ceiling, subject to the workspace’s remaining storage quota, and grants no
67
- access to the agent's workspace. After the expiry, or a refusal, Share offers
68
- Save instead; bring that file back to the assistant for a fresh return.
44
+ 1. Call `show {target:"return"}` on the Rapier agent door for the document's workspace.
45
+ 2. Pass its `return_url` and `return_expires_at` as `--return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT"`,
46
+ or `wrap(..., {return: return_url, return_expires_at})`. Only `https://mcp.rapier.website/return/…` is accepted.
47
+ Share then shows Send back; a page without an address doesn't.
48
+ 3. The person edits offline and taps Send back. The source and filename go once; the page shows acceptance or
49
+ refusal and never sends again.
50
+ 4. `show {target:"ask",wait:"message"}` returns a `returned` receipt with the `return_id`, name and time, and
51
+ `read {state:true}` lists receipts. Read the source with `read {return_id}`, then `cursor` alone until complete.
69
52
 
70
- `show {target:"ask",wait:"message"}` returns a `returned` receipt with the `return_id`, name and time;
71
- `read {state:true}` lists returned-copy receipts. Use `read {return_id}` for the source, then `cursor`
72
- alone to continue until complete. The returned copy stays separate from the workspace's current document.
53
+ A return lasts up to one day and takes one document of up to 64 MiB of decoded source, within the workspace's
54
+ remaining storage. It grants no access to the workspace, and the returned copy stays separate from the workspace's
55
+ current document. After expiry or a refusal, Share offers Save instead; bring that file back for a fresh return.
73
56
 
74
57
  ## As a library
75
58
 
76
- Save this as `example.mjs` in the folder you installed into and run `node example.mjs`:
59
+ Save this as `example.mjs` in the folder you installed into, then run `node example.mjs`:
77
60
 
78
61
  ```js
79
62
  import {readFile, writeFile} from 'node:fs/promises';
@@ -86,50 +69,47 @@ console.log('words back exactly:', unwrap(page).text === words);
86
69
  await writeFile('hello.rapier.html', page, {flag: 'wx'}); // 'wx': never over an existing file
87
70
  ```
88
71
 
89
- `wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName, by, at, return: returnURL, return_expires_at})`
90
- and `unwrap(pageHtml)`, which returns the same fields plus the validated `comparisonBase` record, with `null`
91
- for what a page does not carry. Also exported:
92
- `encodeCarried`, `decodeCarried`, `main`.
72
+ - `wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName, by, at, return: returnURL, return_expires_at})`
73
+ - `unwrap(pageHtml)` returns the same fields, plus the validated `comparisonBase` record, with `null` for anything the
74
+ page doesn't carry.
75
+ - Also exported: `encodeCarried`, `decodeCarried` and `main`.
93
76
 
94
77
  ## How the page carries the document
95
78
 
96
- One `<script type="text/markdown" id="rapier-document">` block at the top of the body, never executed,
97
- read once at boot. A drawing rides in `id="rapier-drawing"`, the original text for comparison in
98
- `id="rapier-base"`, the view to open on in the document block's `data-view`, the optional return URL
99
- in `data-return` and its exact expiry in `data-return-expires-at`. The block holds no `<`
100
- before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for them, one
101
- reversible pass each way, so a note about HTML is as safe as any other. A byte-order mark is kept as a
102
- fact of the file and written back on Save.
103
-
104
- `comparisonBase` contains the original `text`, `sha256`, `revision`, `name`, `by` and `at`. Rewrapping retains
105
- its original text, hash and revision; a baseline equal to the current text is omitted. It is a display
106
- reference, never a write grant or a history record. The edited document remains the source used by Save and
107
- Send back.
79
+ - One `<script type="text/markdown" id="rapier-document">` block at the top of the body, never run, read once at
80
+ start-up.
81
+ - A drawing goes in `id="rapier-drawing"` and the comparison original in `id="rapier-base"`. The document block's
82
+ `data-view` names the opening view, `data-return` the return URL, and `data-return-expires-at` its expiry.
83
+ - The block holds no `<` before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for
84
+ them, one reversible pass each way. A byte-order mark is kept and written back on Save.
85
+ - `comparisonBase` holds the original `text`, `sha256`, `revision`, `name`, `by` and `at`. Rewrapping keeps the
86
+ original text, hash and revision; a baseline equal to the current text is left out. It is for display only, never a
87
+ write grant or history. Save and Send back use the edited document.
108
88
 
109
89
  ## Safety
110
90
 
111
- - It never overwrites: an existing output, the document itself included, is refused, as are unknown options, a
112
- `--view` other than `draw` or `notes`, a `--drawing` or `--compare` with no file, a return without its expiry, an
91
+ - It never overwrites: an existing output, including the document itself, is refused. So are unknown options, a
92
+ `--view` other than `draw` or `notes`, `--drawing` or `--compare` without a file, a return without its expiry, an
113
93
  invalid expiry, an unsafe return URL and a third filename.
114
- - Opening and rendering use carried resources; the page cannot fetch pictures, media, fonts or plug-ins.
115
- The staged editor includes verified math, Mermaid and lettering resources. Embed any document pictures.
116
- An explicit Send back connects only to the minted return origin. Its URL names the worker's exact route;
117
- credentials, query strings and fragments are refused.
94
+ - The page opens and renders from what it carries; it can't fetch pictures, media, fonts or plugins. The editor
95
+ includes verified math, Mermaid and lettering resources. Embed any document pictures.
96
+ - Send back connects only to the return origin it was given. Credentials, query strings and fragments in the return
97
+ URL are refused.
118
98
 
119
99
  ## Optional edit history
120
100
 
121
- `wrap(page, text, filename, {ledger, authorship})` accepts the public kit's
122
- `rapier-ledger/1` and `rapier-authorship/1` objects. `unwrap(page)` returns the
123
- checked parts beside its text. Neither is on by default. An authorship projection
124
- contains only attribution of current characters; a full ledger contains deleted
125
- text too. A wrong-text, malformed, duplicate or inconsistent part throws instead
126
- of attaching false history. Saved Markdown written with the kit's
127
- `ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a
128
- page without parts drops its previous document's parts. Published npm files carry
129
- their own staged MIT validator closure; no source checkout is needed.
101
+ `wrap(page, text, filename, {ledger, authorship})` accepts the kit's `rapier-ledger/2` and `rapier-authorship/1`
102
+ objects, and `unwrap(page)` returns the checked parts beside the text. Both are off by default.
103
+
104
+ - An authorship projection holds only who wrote the current characters; a full ledger also holds deleted text.
105
+ - A part that doesn't match the text, or is malformed, duplicated or inconsistent, throws instead of attaching false
106
+ history.
107
+ - Markdown saved with the kit's `ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a page
108
+ without parts drops the old parts.
109
+ - The published package carries its own MIT validator; no source checkout is needed.
130
110
 
131
111
  ## Licence
132
112
 
133
- AGPL-3.0-only (`LICENSE`): the page is the Rapier editor with the notices of the libraries it carries.
134
- The Markdown standard without the editor is `rapier-markdown-kit` (MIT). Rapier is https://rapier.website.
113
+ AGPL-3.0-only (`LICENSE`): the page is the Rapier editor, with the notices of the libraries it carries. For the
114
+ Markdown standard without the editor, use `rapier-markdown-kit` (MIT). Rapier is at https://rapier.website.
135
115
  `BUILD.json` records the page's size, SHA-256, build time and Node version.
@@ -21,8 +21,11 @@ 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 licence 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`), the layout model and line planner
25
- (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.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`), ink geometry
25
+ (`spec/ink.mjs`),
26
+ the layout model and line planner (`layout/model.mjs`, `layout/line-plan.mjs`, `layout/typography.mjs`, `layout/preparation.mjs`,
27
+ `layout/prepared-paragraph.mjs`, `layout/hyphenation.mjs`, `layout/paragraph-fit.mjs`), the animated-picture reader and playback
28
+ the HTML blocks use (`images/gif.mjs`, `images/playback.mjs`),
26
29
  the portable HTML and document metadata helpers (`spec/html-blocks.mjs`, `spec/frontmatter.mjs`,
27
30
  `spec/document-settings.mjs`, `spec/document-fonts.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
28
31
  (`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
@@ -32,8 +35,9 @@ own MIT notices. The edit-ledger module (`kit/rapier-ledger.mjs`, `kit/ledger/*.
32
35
  its editor compatibility import (`editor/journal-records.mjs`) are also covered. The
33
36
  record validator/replayer and text-only conflict merger were moved, not copied,
34
37
  into that module; the Notes index merger and the rest of the agent/editor remain
35
- AGPL-3.0-only. The wrapper's carrier-staging helper
36
- (`skills/rapier-html/package-files.mjs`) is MIT. The kit's MIT-marked facades,
38
+ AGPL-3.0-only. The page wrapper (`skills/rapier-html/wrap.mjs`, `skills/rapier-html/page.mjs`,
39
+ `skills/rapier-html/return-address.mjs`) and its carrier-staging helper
40
+ (`skills/rapier-html/package-files.mjs`) are MIT. The kit's MIT-marked facades,
37
41
  `kit/render-entry.mjs`, `kit/render*.mjs`, `kit/export-fonts.mjs`, `kit/print-capitals.mjs`,
38
42
  `kit/pdf-vectors.mjs`, `kit/pdf-glyphs.mjs`, `kit/stage.mjs` and MIT-marked conformance
39
43
  modules are covered too. These names describe the Rapier-owned files carrying the
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Immutable authored origins shared by causal transport and inverse projection.
3
+ import {_rapierTransformSplices, _rapierValidAuthored} from './journal-records.mjs';
4
+ import {sha256} from './hash.mjs';
5
+
6
+ // The frontier names only causal maxima. Linear history needs one predecessor,
7
+ // and concurrent predecessors remain separate until an act observes both.
8
+ function authoredFrontier(records, visit = null) {
9
+ const dependencies = new Map(), frontier = new Set();
10
+ for (const record of records) {
11
+ const id = record.transaction.id, basis = record.authored?.basis;
12
+ if (visit) visit(record, basis ?? [...frontier].sort());
13
+ if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
14
+ else {
15
+ const pending = [...basis], seen = new Set();
16
+ while (pending.length) {
17
+ const prior = pending.pop();
18
+ if (seen.has(prior)) continue;
19
+ if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
20
+ seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
21
+ }
22
+ dependencies.set(id, basis);
23
+ }
24
+ frontier.add(id);
25
+ }
26
+ return [...frontier].sort();
27
+ }
28
+
29
+ export function authoredBasis(records) { return authoredFrontier(records); }
30
+
31
+ export function authoredPlacements(base, records) {
32
+ const origins = new Map(); let source = base;
33
+ authoredFrontier(records, (record, basis) => {
34
+ origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
35
+ source = _rapierTransformSplices(source, record.splices);
36
+ if (source === null) throw new RangeError('canonical_origin_invalid');
37
+ });
38
+ return origins;
39
+ }
40
+
41
+ // Immutable authored placement stays on its own act. Basis IDs refer only to this
42
+ // canonical history; no source snapshot or duplicate history is stored.
43
+ export function authoredPlacement(source, splices, basis) {
44
+ const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
45
+ if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
46
+ throw new RangeError('canonical_origin_invalid');
47
+ return origin;
48
+ }
49
+
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {readLedger, exportLedger, textRoot, rootAfter} from './format.mjs';
3
- import {_rapierTransformSplices} from './journal-records.mjs';
3
+ import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
4
4
  import {sha256} from './hash.mjs';
5
5
  import {plainData, documentText, nonnegative, exactKeys, fault} from './data.mjs';
6
6
  function coalesce(runs) {
@@ -14,16 +14,25 @@ function coalesce(runs) {
14
14
  return out;
15
15
  }
16
16
  export function authorship(input) {
17
- const {ledger, text} = readLedger(input);
17
+ const {ledger, text} = readLedger(input), references = _rapierHistoryEffects(ledger.records).references, removed = new Map();
18
18
  let runs = ledger.start.text ? [[0, ledger.start.text.length, 'system', 'unattributed', 0]] : [];
19
- for (const record of ledger.records) for (const row of record.splices) {
20
- const cut = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length, before = [], after = [];
21
- for (const run of runs) {
22
- if (run[0] < row.pos) before.push([run[0], Math.min(run[1], row.pos), ...run.slice(2)]);
23
- if (run[1] > cut) after.push([Math.max(run[0], cut) + delta, run[1] + delta, ...run.slice(2)]);
19
+ for (const record of ledger.records) {
20
+ // An Undo or Redo puts back text the act it references removed: that text keeps the runs it had.
21
+ const tx = record.transaction, gone = [], kept = (references?.get(tx.id) || []).flatMap(id => removed.get(id) || []);
22
+ for (const row of record.splices) {
23
+ const cut = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length, before = [], after = [], inside = [];
24
+ for (const run of runs) {
25
+ if (run[0] < row.pos) before.push([run[0], Math.min(run[1], row.pos), ...run.slice(2)]);
26
+ if (run[1] > cut) after.push([Math.max(run[0], cut) + delta, run[1] + delta, ...run.slice(2)]);
27
+ if (run[0] < cut && run[1] > row.pos) inside.push([Math.max(run[0], row.pos) - row.pos, Math.min(run[1], cut) - row.pos, ...run.slice(2)]);
28
+ }
29
+ if (row.removed) gone.push({text: row.removed, runs: inside});
30
+ const at = row.inserted ? kept.findIndex(entry => entry.text === row.inserted) : -1;
31
+ const put = at < 0 ? [[row.pos, row.pos + row.inserted.length, tx.actor.kind, tx.actor.id, tx.createdAt]]
32
+ : kept.splice(at, 1)[0].runs.map(run => [run[0] + row.pos, run[1] + row.pos, ...run.slice(2)]);
33
+ runs = coalesce([...before, ...put, ...after]);
24
34
  }
25
- const tx = record.transaction;
26
- runs = coalesce([...before, [row.pos, row.pos + row.inserted.length, tx.actor.kind, tx.actor.id, tx.createdAt], ...after]);
35
+ removed.set(tx.id, gone);
27
36
  }
28
37
  return {format: 'rapier-authorship/1', sha256: sha256(text), runs};
29
38
  }
@@ -73,7 +73,7 @@ export function readDocument(source) {
73
73
  export function readBase(value) {
74
74
  if (!value || typeof value !== 'object') throw fault('comparison base SHA-256 differs from its text');
75
75
  // Capture each caller-owned field once; admission and serialization use these same scalars.
76
- const {text, sha256: digest, revision, name, by, at} = value;
76
+ const {text, sha256: digest, revision, name, by, at, fonts} = value;
77
77
  if (typeof text !== 'string' || typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest) || sha256(text) !== digest)
78
78
  throw fault('comparison base SHA-256 differs from its text');
79
79
  if (typeof revision !== 'string' || !revision || revision.length > 256 || /[\u0000-\u001f\u007f]/.test(revision))
@@ -84,7 +84,8 @@ export function readBase(value) {
84
84
  throw fault('a comparison needs its author name');
85
85
  if (typeof at !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(at) || !Number.isFinite(Date.parse(at)))
86
86
  throw fault('the time of a comparison is an ISO 8601 UTC timestamp');
87
- return {text, sha256: digest, revision, name, by, at};
87
+ if (fonts != null && typeof fonts !== 'string') throw fault('invalid comparison font resources');
88
+ return {text, sha256: digest, revision, name, by, at, ...(fonts ? {fonts} : {})};
88
89
  }
89
90
  const escapeAttribute = value => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
90
91
  const readAttribute = value => value.replace(/&(quot|lt|gt|amp);/g, (_, name) => ({quot: '"', lt: '<', gt: '>', amp: '&'})[name]);
@@ -93,7 +94,8 @@ export function writeBase(text, value) {
93
94
  const base = readBase(value);
94
95
  if (base.text === text) return '';
95
96
  return '<script type="text/markdown" id="rapier-base"' + ['name', 'sha256', 'revision', 'by', 'at']
96
- .map(key => ' data-' + key + '="' + escapeAttribute(base[key]) + '"').join('') + '>' + encodeCarried(base.text) + '</script>';
97
+ .map(key => ' data-' + key + '="' + escapeAttribute(base[key]) + '"').join('') +
98
+ (base.fonts ? ' data-fonts="' + escapeAttribute(JSON.stringify(base.fonts)) + '"' : '') + '>' + encodeCarried(base.text) + '</script>';
97
99
  }
98
100
  export function readBaseElements(elements) {
99
101
  const rows = [...elements].filter(element => element.id === 'rapier-base');
@@ -102,7 +104,10 @@ export function readBaseElements(elements) {
102
104
  if (rows.length !== 1 || row.type !== 'text/markdown' || (row.tagName && row.tagName.toLowerCase() !== 'script'))
103
105
  throw fault('duplicate or executable comparison base');
104
106
  const get = name => row.getAttribute ? row.getAttribute('data-' + name) : row[name];
105
- return readBase({text: decodeCarried(row.textContent), ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, get(key)]))});
107
+ let fonts;
108
+ try { const value = get('fonts'); if (value != null) fonts = JSON.parse(value); }
109
+ catch (_) { throw fault('invalid comparison font resources'); }
110
+ return readBase({text: decodeCarried(row.textContent), fonts, ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, get(key)]))});
106
111
  }
107
112
  export function takeBase(html) {
108
113
  const elements = [];
@@ -115,7 +120,7 @@ export function takeBase(html) {
115
120
  seen.add(key); values[key] = value;
116
121
  }
117
122
  elements.push({id: values.id, type: values.type, textContent: content,
118
- ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at'].map(key => [key, values['data-' + key]]))});
123
+ ...Object.fromEntries(['name', 'sha256', 'revision', 'by', 'at', 'fonts'].map(key => [key, values['data-' + key]]))});
119
124
  return '';
120
125
  });
121
126
  return {html: stripped, base: readBaseElements(elements)};
@@ -1,10 +1,13 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Only plain JSON data enters the public format. Accessors and toJSON are never executed.
3
3
  export function fault(message) { return Object.assign(new Error('Ledger: ' + message), {code: 'ledger_invalid'}); }
4
- function fields(value, depth, active, sorted = true) {
4
+ // A plain object's prototype is null or Object's own. Both lookups read descriptors, so no getter runs.
5
+ export const plainPrototype = proto => proto === null || Object.getPrototypeOf(proto) === null &&
6
+ Object.getOwnPropertyDescriptor(Object.getOwnPropertyDescriptor(proto, 'constructor')?.value || {}, 'name')?.value === 'Object';
7
+ export function fields(value, depth, active, sorted = true) {
5
8
  if (!value || typeof value !== 'object' || depth > 64 || active.has(value)) throw fault('not plain JSON data');
6
- const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
7
- if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null || Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) throw fault('not a plain object');
9
+ const array = Array.isArray(value);
10
+ if (!array && !plainPrototype(Object.getPrototypeOf(value))) throw fault('not a plain object');
8
11
  const descriptors = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(descriptors);
9
12
  for (const key of keys) {
10
13
  if (array && key === 'length') continue;
@@ -14,7 +17,7 @@ function fields(value, depth, active, sorted = true) {
14
17
  if (array && (keys.length !== value.length + 1 || keys.some(k => k !== 'length' && (!/^(0|[1-9]\d*)$/.test(k) || +k >= value.length)))) throw fault('sparse array');
15
18
  return {array, descriptors, keys: array ? keys.filter(key => key !== 'length') : sorted ? keys.sort() : keys};
16
19
  }
17
- function scalar(value) {
20
+ export function scalar(value) {
18
21
  if (value === null || typeof value === 'boolean') return true;
19
22
  if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return true; }
20
23
  return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0);