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 +27 -20
- package/README.md +68 -88
- package/dist/ledger/LICENSE +8 -4
- package/dist/ledger/authored-origin.mjs +49 -0
- package/dist/ledger/authorship.mjs +18 -9
- package/dist/ledger/carried.mjs +10 -5
- package/dist/ledger/data.mjs +7 -4
- package/dist/ledger/diff/concerns.mjs +163 -0
- package/dist/ledger/diff/diff.mjs +68 -0
- package/dist/ledger/diff/index.mjs +5 -0
- package/dist/ledger/diff/matching.mjs +157 -0
- package/dist/ledger/diff/merge.mjs +226 -0
- package/dist/ledger/diff/moves.mjs +130 -0
- package/dist/ledger/diff/structure.mjs +50 -0
- package/dist/ledger/diff/tokens.mjs +72 -0
- package/dist/ledger/diff/walk.mjs +219 -0
- package/dist/ledger/format.mjs +37 -11
- package/dist/ledger/hash.mjs +31 -15
- package/dist/ledger/history.mjs +48 -10
- package/dist/ledger/inverse-source.mjs +112 -50
- package/dist/ledger/journal-records.mjs +24 -29
- package/dist/ledger/transport.mjs +30 -45
- package/dist/page.mjs +10 -7
- package/dist/rapier.html +151 -129
- package/dist/return-address.mjs +2 -2
- package/dist/wrap.mjs +1 -1
- package/package.json +1 -1
- package/dist/ledger/diff.mjs +0 -702
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
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":
|
|
18
|
-
"packedBytes":
|
|
17
|
+
"bytes": 691650,
|
|
18
|
+
"packedBytes": 438088,
|
|
19
19
|
"files": [
|
|
20
20
|
{
|
|
21
21
|
"id": "rapier-math",
|
|
22
|
-
"name": "rapier-math-
|
|
23
|
-
"version": "
|
|
24
|
-
"bytes":
|
|
25
|
-
"sri": "
|
|
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": "
|
|
32
|
+
"kind": "files",
|
|
33
33
|
"element": "rapier-plugin-mermaid",
|
|
34
|
-
"bytes":
|
|
35
|
-
"packedBytes":
|
|
34
|
+
"bytes": 7389640,
|
|
35
|
+
"packedBytes": 1526485,
|
|
36
36
|
"files": [
|
|
37
37
|
{
|
|
38
|
-
"id": "rapier-mermaid",
|
|
39
|
-
"name": "
|
|
40
|
-
"version": "
|
|
41
|
-
"bytes":
|
|
42
|
-
"sri": "
|
|
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":
|
|
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":
|
|
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
|
|
4
|
-
computer, with no account. People read, edit, draw and paint
|
|
5
|
-
|
|
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,
|
|
8
|
-
|
|
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
|
|
11
|
-
and nothing
|
|
12
|
-
- `--return` adds a one-use Send back address from the agent door
|
|
13
|
-
|
|
14
|
-
-
|
|
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.
|
|
22
|
-
npx -- rapier-html@1.
|
|
23
|
-
npx -- rapier-html@1.
|
|
24
|
-
npx -- rapier-html@1.
|
|
25
|
-
npx -- rapier-html@1.
|
|
26
|
-
npx -- rapier-html@1.
|
|
27
|
-
npx -- rapier-html@1.
|
|
28
|
-
npm install rapier-html@1.
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
editor
|
|
47
|
-
|
|
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
|
|
59
|
-
and `return_expires_at` as `--return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT"`,
|
|
60
|
-
`wrap(..., {return: return_url, return_expires_at})`. Only
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
`encodeCarried`, `decodeCarried
|
|
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
|
|
97
|
-
|
|
98
|
-
`id="rapier-
|
|
99
|
-
|
|
100
|
-
before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for
|
|
101
|
-
reversible pass each way
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
112
|
-
`--view` other than `draw` or `notes`,
|
|
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
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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.
|
package/dist/ledger/LICENSE
CHANGED
|
@@ -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`),
|
|
25
|
-
(`
|
|
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
|
|
36
|
-
|
|
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)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/ledger/carried.mjs
CHANGED
|
@@ -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
|
-
|
|
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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
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('') +
|
|
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
|
-
|
|
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)};
|
package/dist/ledger/data.mjs
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
7
|
-
if (!array &&
|
|
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);
|