rapier-html 1.2.40 → 1.2.43
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 +20 -20
- package/README.md +10 -10
- package/dist/ledger/data.mjs +59 -2
- package/dist/ledger/format.mjs +65 -22
- package/dist/ledger/history.mjs +1 -1
- package/dist/ledger/inverse-source.mjs +9 -0
- package/dist/ledger/journal-records.mjs +197 -106
- package/dist/ledger/md-assets.mjs +7 -10
- package/dist/ledger/will.mjs +2 -2
- package/dist/rapier.html +74 -73
- package/dist/wrap.mjs +1 -0
- package/package.json +1 -1
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.43",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
5
|
+
"bytes": 5832352,
|
|
6
|
+
"sha256": "233e582745310b79d57c2fc26435ed02c00d657f6c1f7d8b676b2d678522e3cd",
|
|
7
|
+
"builtAt": "2026-10-25T00:05:03.516Z",
|
|
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":
|
|
35
|
-
"packedBytes":
|
|
34
|
+
"bytes": 720263,
|
|
35
|
+
"packedBytes": 240280,
|
|
36
36
|
"files": [
|
|
37
37
|
{
|
|
38
38
|
"id": "rapier-mermaid-core",
|
|
39
|
-
"name": "pele-1.
|
|
40
|
-
"version": "1.
|
|
41
|
-
"bytes":
|
|
42
|
-
"sri": "
|
|
39
|
+
"name": "pele-1.9.0-native-1.js",
|
|
40
|
+
"version": "1.9.0-native-1",
|
|
41
|
+
"bytes": 720263,
|
|
42
|
+
"sri": "zPE63bYrlp1ZqlwxePk6pclPq6VoxZ7o769Lz109vEo8OEPtvOl1GPrMDj0OhtVB"
|
|
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":
|
|
52
|
-
"packedBytes":
|
|
51
|
+
"bytes": 420431,
|
|
52
|
+
"packedBytes": 110835,
|
|
53
53
|
"files": [
|
|
54
54
|
{
|
|
55
55
|
"id": "rapier-mermaid-zenuml",
|
|
56
|
-
"name": "rapier-zenuml-4.5.0-
|
|
57
|
-
"version": "4.5.0-
|
|
58
|
-
"bytes":
|
|
59
|
-
"sri": "/
|
|
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"
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
},
|
|
@@ -145,15 +145,15 @@
|
|
|
145
145
|
"delivery": "built-in",
|
|
146
146
|
"kind": "files",
|
|
147
147
|
"element": "rapier-plugin-font-subset",
|
|
148
|
-
"bytes":
|
|
149
|
-
"packedBytes":
|
|
148
|
+
"bytes": 1207570,
|
|
149
|
+
"packedBytes": 439575,
|
|
150
150
|
"files": [
|
|
151
151
|
{
|
|
152
152
|
"id": "rapier-font-subset",
|
|
153
153
|
"name": "rapier-font-subset-3.js",
|
|
154
154
|
"version": "3",
|
|
155
|
-
"bytes":
|
|
156
|
-
"sri": "
|
|
155
|
+
"bytes": 1207570,
|
|
156
|
+
"sri": "eyBreJCziFoHQdGSPtvd0S2IRcHna191kq83/ps/rafZwWWd6reInBALyPdLQmdL"
|
|
157
157
|
}
|
|
158
158
|
]
|
|
159
159
|
}
|
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.
|
|
11
|
-
Install: `npm install rapier-html@1.2.
|
|
10
|
+
Try it: `npx rapier-html@1.2.43 notes.md`, then open `notes.rapier.html` in any browser.
|
|
11
|
+
Install: `npm install rapier-html@1.2.43`.
|
|
12
12
|
|
|
13
13
|
It opens on a phone or computer, with no account. People read, edit, draw and paint; agents hand over documents, show
|
|
14
14
|
what changed and get the edited copy back.
|
|
@@ -26,14 +26,14 @@ The file needs no hosted workspace.
|
|
|
26
26
|
Needs Node 22 or newer.
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npx -- rapier-html@1.2.
|
|
30
|
-
npx -- rapier-html@1.2.
|
|
31
|
-
npx -- rapier-html@1.2.
|
|
32
|
-
npx -- rapier-html@1.2.
|
|
33
|
-
npx -- rapier-html@1.2.
|
|
34
|
-
npx -- rapier-html@1.2.
|
|
35
|
-
npx -- rapier-html@1.2.
|
|
36
|
-
npm install rapier-html@1.2.
|
|
29
|
+
npx -- rapier-html@1.2.43 notes.md # the editor, notes.md inside it, written beside the file
|
|
30
|
+
npx -- rapier-html@1.2.43 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
31
|
+
npx -- rapier-html@1.2.43 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
|
|
32
|
+
npx -- rapier-html@1.2.43 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
33
|
+
npx -- rapier-html@1.2.43 revised.md --compare original.md # opens showing what changed
|
|
34
|
+
npx -- rapier-html@1.2.43 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
35
|
+
npx -- rapier-html@1.2.43 notes.md out.html # a named output
|
|
36
|
+
npm install rapier-html@1.2.43 # as a library: writePage, unwrap
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
|
package/dist/ledger/data.mjs
CHANGED
|
@@ -44,11 +44,13 @@ function* quoted(text, whole = true) {
|
|
|
44
44
|
}
|
|
45
45
|
yield '"';
|
|
46
46
|
}
|
|
47
|
-
function* dataParts(value, depth, active, sorted) {
|
|
47
|
+
function* dataParts(value, depth, active, sorted, known = null) {
|
|
48
48
|
if (typeof value === 'string') { yield* quoted(value); return; }
|
|
49
49
|
if (value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) {
|
|
50
50
|
yield JSON.stringify(value); return;
|
|
51
51
|
}
|
|
52
|
+
const text = known?.(value, depth);
|
|
53
|
+
if (text !== undefined) { yield text; return; }
|
|
52
54
|
const {array, descriptors, keys} = fields(value, depth, active, sorted);
|
|
53
55
|
active.add(value);
|
|
54
56
|
yield array ? '[' : '{';
|
|
@@ -56,14 +58,69 @@ function* dataParts(value, depth, active, sorted) {
|
|
|
56
58
|
if (i) yield ',';
|
|
57
59
|
const key = keys[i];
|
|
58
60
|
if (!array) { yield* quoted(key, false); yield ':'; }
|
|
59
|
-
yield* dataParts(descriptors[key].value, depth + 1, active, sorted);
|
|
61
|
+
yield* dataParts(descriptors[key].value, depth + 1, active, sorted, known);
|
|
60
62
|
}
|
|
61
63
|
yield array ? ']' : '}';
|
|
62
64
|
active.delete(value);
|
|
63
65
|
}
|
|
64
66
|
export function canonicalParts(value, depth = 0, active = new Set()) { return dataParts(value, depth, active, true); }
|
|
65
67
|
export function jsonParts(value) { return dataParts(value, 0, new Set(), false); }
|
|
68
|
+
export function jsonPartsKnown(value, known) { return dataParts(value, 0, new Set(), false, known); }
|
|
69
|
+
export function jsonTextAt(value, depth) { return Array.from(dataParts(value, depth, new Set(), false)).join(''); }
|
|
66
70
|
export function canonicalJSON(value, depth = 0, active = new Set()) { return Array.from(canonicalParts(value, depth, active)).join(''); }
|
|
71
|
+
const ownedRows = new WeakSet(), rowCanonical = new Map();
|
|
72
|
+
let rowCanonicalChars = 0;
|
|
73
|
+
const ROW_CANONICAL_LIMIT = 32 * 1024 * 1024;
|
|
74
|
+
function freezeData(value) {
|
|
75
|
+
if (!value || typeof value !== 'object' || ownedRows.has(value) || ownedLedgers.has(value)) return value;
|
|
76
|
+
for (const key of Object.keys(value)) freezeData(value[key]);
|
|
77
|
+
return Object.freeze(value);
|
|
78
|
+
}
|
|
79
|
+
export function ownRow(row) { if (!ownedRows.has(row)) { freezeData(row); ownedRows.add(row); } return row; }
|
|
80
|
+
const ownedLedgers = new WeakSet();
|
|
81
|
+
export function ownLedger(ledger) {
|
|
82
|
+
if (ledger && typeof ledger === 'object' && !ownedLedgers.has(ledger)) { freezeData(ledger); ownedLedgers.add(ledger); }
|
|
83
|
+
return ledger;
|
|
84
|
+
}
|
|
85
|
+
export function isOwnedLedger(ledger) { return !!ledger && ownedLedgers.has(ledger); }
|
|
86
|
+
const rowText = new WeakMap();
|
|
87
|
+
function canonicalRow(row, depth) {
|
|
88
|
+
if (!ownedRows.has(row)) return canonicalJSON(row, depth);
|
|
89
|
+
const own = rowText.get(row);
|
|
90
|
+
if (own?.depth === depth) return own.json;
|
|
91
|
+
const json = keyedRow(row, depth);
|
|
92
|
+
rowText.set(row, {depth, json});
|
|
93
|
+
return json;
|
|
94
|
+
}
|
|
95
|
+
function keyedRow(row, depth) {
|
|
96
|
+
const id = typeof row.transaction?.id === 'string' ? row.transaction.id : null, seen = JSON.stringify(row);
|
|
97
|
+
const hit = id === null ? null : rowCanonical.get(id);
|
|
98
|
+
if (hit && hit.depth === depth && hit.seen.includes(seen)) return hit.json;
|
|
99
|
+
const json = canonicalJSON(row, depth);
|
|
100
|
+
if (id !== null && 2 * seen.length + json.length <= ROW_CANONICAL_LIMIT / 4) {
|
|
101
|
+
const size = entry => entry.seen.reduce((sum, text) => sum + text.length, entry.json.length);
|
|
102
|
+
const seenAlso = hit && hit.depth === depth && hit.json === json ? [hit.seen.at(-1), seen] : [seen];
|
|
103
|
+
if (hit) { rowCanonical.delete(id); rowCanonicalChars -= size(hit); }
|
|
104
|
+
const entry = {depth, seen: seenAlso, json};
|
|
105
|
+
rowCanonical.set(id, entry); rowCanonicalChars += size(entry);
|
|
106
|
+
for (const [key, value] of rowCanonical) {
|
|
107
|
+
if (rowCanonicalChars <= ROW_CANONICAL_LIMIT) break;
|
|
108
|
+
rowCanonical.delete(key); rowCanonicalChars -= size(value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return json;
|
|
112
|
+
}
|
|
113
|
+
const plainRowArrays = new WeakSet();
|
|
114
|
+
export function* canonicalRowsParts(rows, depth = 0) {
|
|
115
|
+
if (!(depth <= 64 && plainRowArrays.has(rows))) {
|
|
116
|
+
const {array} = fields(rows, depth, new Set());
|
|
117
|
+
if (!array) { yield* canonicalParts(rows, depth); return; }
|
|
118
|
+
if (Object.isFrozen(rows) && depth <= 64) plainRowArrays.add(rows);
|
|
119
|
+
}
|
|
120
|
+
yield '[';
|
|
121
|
+
for (let i = 0; i < rows.length; i++) { if (i) yield ','; yield canonicalRow(rows[i], depth + 1); }
|
|
122
|
+
yield ']';
|
|
123
|
+
}
|
|
67
124
|
export function exactKeys(value, keys) {
|
|
68
125
|
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
69
126
|
}
|
package/dist/ledger/format.mjs
CHANGED
|
@@ -4,16 +4,16 @@ import {pictureHistorySplices} from './history.mjs';
|
|
|
4
4
|
import {HistorySource} from './source.mjs';
|
|
5
5
|
import {authoredHistory} from './transport.mjs';
|
|
6
6
|
import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
|
|
7
|
-
import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
|
|
7
|
+
import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
|
|
8
8
|
import {diff} from './diff/diff.mjs';
|
|
9
9
|
import {minimalHistorySplice} from './inverse-source.mjs';
|
|
10
10
|
import {evaluateWill} from './will.mjs';
|
|
11
11
|
export {textRoot, rootAfter};
|
|
12
12
|
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
13
|
-
function* checkedRecords(ledger, prepared) {
|
|
13
|
+
function* checkedRecords(ledger, prepared, from = null) {
|
|
14
14
|
if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
|
|
15
|
-
let root = ledger.start.root, revision = ledger.start.revision;
|
|
16
|
-
for (let index = 0; index < prepared.records.length; index++) {
|
|
15
|
+
let root = from ? from.root : ledger.start.root, revision = from ? from.revision : ledger.start.revision;
|
|
16
|
+
for (let index = from ? from.index : 0; index < prepared.records.length; index++) {
|
|
17
17
|
const record = ledger.records[index];
|
|
18
18
|
if (!Array.isArray(record.splices) || !prepared.validRecord(index,
|
|
19
19
|
ledger.documentAuthority, 64 * 1024 * 1024, utf8Length, !ledger.complete) ||
|
|
@@ -27,32 +27,47 @@ function* checkedRecords(ledger, prepared) {
|
|
|
27
27
|
}
|
|
28
28
|
if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
|
|
29
29
|
}
|
|
30
|
+
function* envelopeParts(envelope) {
|
|
31
|
+
const keys = Object.keys(envelope).sort();
|
|
32
|
+
yield '{';
|
|
33
|
+
for (let i = 0; i < keys.length; i++) {
|
|
34
|
+
if (i) yield ',';
|
|
35
|
+
yield JSON.stringify(keys[i]); yield ':';
|
|
36
|
+
yield* keys[i] === 'records' ? canonicalRowsParts(envelope.records, 1) : canonicalParts(envelope[keys[i]], 1, new Set());
|
|
37
|
+
}
|
|
38
|
+
yield '}';
|
|
39
|
+
}
|
|
30
40
|
const proofs = new WeakMap();
|
|
41
|
+
function ownedRows(ledger) {
|
|
42
|
+
if (ledger && Array.isArray(ledger.records)) for (const row of ledger.records) if (row && typeof row === 'object') ownRow(row);
|
|
43
|
+
return ledger;
|
|
44
|
+
}
|
|
31
45
|
function copiedLedger(input, proof) {
|
|
32
|
-
if (!proof || !input || typeof input !== 'object') return plainData(input);
|
|
46
|
+
if (!proof || !input || typeof input !== 'object') return ownedRows(plainData(input));
|
|
33
47
|
const fields = Object.getOwnPropertyDescriptors(input), start = fields.start;
|
|
34
|
-
if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return plainData(input);
|
|
48
|
+
if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return ownedRows(plainData(input));
|
|
35
49
|
const parts = Object.getOwnPropertyDescriptors(start.value), text = parts.text;
|
|
36
|
-
if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return plainData(input);
|
|
50
|
+
if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return ownedRows(plainData(input));
|
|
37
51
|
const shell = Object.create(Object.getPrototypeOf(input), {...fields, start: {...start,
|
|
38
52
|
value: Object.create(Object.getPrototypeOf(start.value), {...parts, text: {...text, value: ''}})}});
|
|
39
|
-
const ledger = plainData(shell);
|
|
53
|
+
const ledger = ownedRows(plainData(shell));
|
|
40
54
|
ledger.start.text = proof.startText;
|
|
41
55
|
return ledger;
|
|
42
56
|
}
|
|
43
57
|
function ledgerAdmission(input) {
|
|
44
58
|
const held = input && typeof input === 'object' ? proofs.get(input) : undefined;
|
|
59
|
+
if (held && isOwnedLedger(input)) return {ledger: input, shell: held.shell, proof: held};
|
|
45
60
|
const ledger = copiedLedger(input, held);
|
|
46
61
|
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
|
|
47
62
|
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
48
63
|
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
49
64
|
!exactKeys(ledger.start, ['text','revision','root','sha256','metadata']) || !exactKeys(ledger.head, ['revision','root','sha256','metadata'])) throw fault('unsupported or invalid envelope');
|
|
50
|
-
const shell =
|
|
65
|
+
const shell = Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join('');
|
|
51
66
|
const proof = held?.shell === shell && held.startText === ledger.start.text ? held : null;
|
|
52
67
|
return {ledger, shell, proof};
|
|
53
68
|
}
|
|
54
69
|
function ledgerResult(ledger, proof, expectedText, expectedMetadata) {
|
|
55
|
-
proofs.set(ledger, proof);
|
|
70
|
+
proofs.set(ledger, proof); ownLedger(ledger);
|
|
56
71
|
const {text, metadata, root, revision, prepared, source} = proof;
|
|
57
72
|
if (expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata)))
|
|
58
73
|
throw fault('metadata does not describe this document');
|
|
@@ -67,7 +82,7 @@ function readLedgerState(input, expectedText, expectedMetadata) {
|
|
|
67
82
|
}
|
|
68
83
|
function* proveLedger(ledger, native = false) {
|
|
69
84
|
const {sha256: claimed, ...payload} = ledger;
|
|
70
|
-
if (claimed !== (yield* checksum(
|
|
85
|
+
if (claimed !== (yield* checksum(envelopeParts(payload), native))) throw fault('envelope checksum differs');
|
|
71
86
|
let text = ledger.start.text, root = ledger.start.root, revision = ledger.start.revision;
|
|
72
87
|
const startFacts = yield* sourceFacts(text, native);
|
|
73
88
|
if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
|
|
@@ -151,6 +166,11 @@ function* sourceFacts(text, native = false) {
|
|
|
151
166
|
return {utf8Bytes: bytes, sha256: digest, contentRoot: text.length + ':' +
|
|
152
167
|
(fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36)};
|
|
153
168
|
}
|
|
169
|
+
const expandedRows = new WeakMap();
|
|
170
|
+
const sameSplices = (a, b) => a === b || a.length === b.length &&
|
|
171
|
+
a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
|
|
172
|
+
const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
|
|
173
|
+
let recentLedgers = [];
|
|
154
174
|
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
|
|
155
175
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
156
176
|
metadata = {...metadata};
|
|
@@ -162,23 +182,40 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
162
182
|
const expanded = [];
|
|
163
183
|
for (const {record, splices: rows, metadata: effect} of prepared.rows) {
|
|
164
184
|
if (!rows || effect === undefined) throw fault('navigation target was not retained');
|
|
165
|
-
|
|
185
|
+
const held = expandedRows.get(record);
|
|
186
|
+
if (held && sameSplices(held.rows, rows) && sameEffect(held.effect, effect)) { expanded.push(held.row); yield; continue; }
|
|
187
|
+
expanded.push(ownRow({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
166
188
|
beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: plainData(record.authored)} : {}), ...(effect ? {metadata: plainData(effect)} : {}),
|
|
167
189
|
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: typeof record.changeSet.label === 'string' ? record.changeSet.label : plainData(record.changeSet.label)}} : {}),
|
|
168
|
-
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})});
|
|
190
|
+
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})}));
|
|
191
|
+
expandedRows.set(record, {row: expanded.at(-1), rows, effect});
|
|
169
192
|
yield;
|
|
170
193
|
}
|
|
171
194
|
const headBytes = bytes;
|
|
195
|
+
let base = recentLedgers.find(row => row.rows.length <= expanded.length && row.rows.every((value, at) => value === expanded[at]));
|
|
172
196
|
let startSource = new HistorySource(text), startMetadata = {...metadata};
|
|
173
|
-
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
197
|
+
for (let i = expanded.length - 1; i >= (base ? base.rows.length : 0); i--) {
|
|
174
198
|
startSource = startSource.apply(expanded[i].splices, true);
|
|
175
|
-
startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
199
|
+
if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
176
200
|
if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
|
|
177
201
|
for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
|
|
178
202
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
179
203
|
yield;
|
|
180
204
|
}
|
|
181
|
-
|
|
205
|
+
if (base && (bytes !== base.headBytes || startSource.length !== base.headText.length || !startSource.matches(0, base.headText) ||
|
|
206
|
+
canonicalJSON(startMetadata) !== base.headMetadata)) {
|
|
207
|
+
base = null; bytes = headBytes; startSource = new HistorySource(text); startMetadata = {...metadata};
|
|
208
|
+
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
209
|
+
startSource = startSource.apply(expanded[i].splices, true);
|
|
210
|
+
if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
211
|
+
if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
|
|
212
|
+
for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
|
|
213
|
+
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
214
|
+
yield;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (base) { startMetadata = {...base.startMetadata}; bytes = base.startBytes; }
|
|
218
|
+
const startText = base ? base.startText : startSource.slice();
|
|
182
219
|
const unchangedText = expanded.every(row => !row.splices.length);
|
|
183
220
|
const contentRoot = headFacts.contentRoot;
|
|
184
221
|
revision ??= expanded.at(-1)?.transaction.revision ?? 0;
|
|
@@ -187,20 +224,26 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
187
224
|
const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
|
|
188
225
|
if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
|
|
189
226
|
if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
|
|
190
|
-
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
|
|
227
|
+
const startFacts = unchangedText ? headFacts : base ? base.startFacts : yield* sourceFacts(startText, native);
|
|
191
228
|
if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
|
|
192
229
|
const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
|
|
193
230
|
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
194
231
|
start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
|
|
195
232
|
records: expanded, head: {revision, root, sha256: headSHA, metadata: {...metadata}}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
196
|
-
|
|
233
|
+
const from = base && base.documentAuthority === documentAuthority && base.complete === payload.complete &&
|
|
234
|
+
base.startRoot === startRoot && base.startRevision === startRevision ? {index: base.rows.length, root: base.chainRoot, revision: base.chainRevision} : null;
|
|
235
|
+
for (const _record of checkedRecords(payload, prepared, from)) yield;
|
|
197
236
|
if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
|
|
198
237
|
try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
|
|
199
238
|
}
|
|
200
239
|
const replayedMetadata = prepared.replayMetadata(startMetadata);
|
|
201
240
|
if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
|
|
202
|
-
const ledger = {...payload, sha256: yield* checksum(
|
|
203
|
-
|
|
241
|
+
const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
|
|
242
|
+
recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
|
|
243
|
+
startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
|
|
244
|
+
chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
|
|
245
|
+
ownLedger(ledger);
|
|
246
|
+
proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
|
|
204
247
|
text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
|
|
205
248
|
source: new HistorySource(text)});
|
|
206
249
|
return {ledger, contentRoot, utf8Bytes: headBytes};
|
|
@@ -250,7 +293,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
|
250
293
|
let traversed = source, traversedMetadata = {...metadata};
|
|
251
294
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
252
295
|
traversed = step(traversed, branch[i], false);
|
|
253
|
-
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
|
|
296
|
+
if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata, true);
|
|
254
297
|
if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
|
|
255
298
|
}
|
|
256
299
|
if (ledger.complete) {
|
|
@@ -261,7 +304,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
|
261
304
|
traversed = source; traversedMetadata = {...metadata};
|
|
262
305
|
for (let i = cursor; i < branch.length; i++) {
|
|
263
306
|
traversed = step(traversed, branch[i], true);
|
|
264
|
-
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
307
|
+
if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
265
308
|
if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
|
|
266
309
|
}
|
|
267
310
|
if (ledger.complete && pictures.size) {
|
package/dist/ledger/history.mjs
CHANGED
|
@@ -363,7 +363,7 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
363
363
|
const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
|
|
364
364
|
if (!result.ok) return result;
|
|
365
365
|
if (options.actor?.kind === 'agent' && prepared.metadata.docKind === 'markdown' &&
|
|
366
|
-
evaluateWill(prepared.source, result.splices).outcome !== 'applied')
|
|
366
|
+
evaluateWill(prepared.source, result.splices, {restores: result.splices}).outcome !== 'applied')
|
|
367
367
|
return historyFailure('will_refused');
|
|
368
368
|
return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
|
|
369
369
|
effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
|
|
@@ -32,6 +32,8 @@ function historyHunks(row) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
const differentActors = (a, b) => !!a && !!b && (a.kind !== b.kind || a.id !== b.id);
|
|
36
|
+
|
|
35
37
|
export function projectInverseSource(prepared, selected, projectedEffects, options = {}) {
|
|
36
38
|
const records = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
37
39
|
if (!options.origins && !records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
@@ -40,6 +42,7 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
40
42
|
try {
|
|
41
43
|
let pieces = prepared.initialSource ? [{text: prepared.initialSource, owner: null, lineage: []}] : [];
|
|
42
44
|
const all = [], byId = new Map(), byAct = new Map(), processed = [], keptInverseAncestors = new Set();
|
|
45
|
+
const actorOf = new Map(records.map(record => [record.transaction.id, record.transaction.actor]));
|
|
43
46
|
const recordsForCover = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
44
47
|
const coverGroups = new Map();
|
|
45
48
|
let coverOrigins = null;
|
|
@@ -280,6 +283,12 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
280
283
|
const id = tx.id + '#' + rowIndex + ':' + index;
|
|
281
284
|
const lineage = new Set([id]);
|
|
282
285
|
for (const piece of removed) for (const parent of piece.lineage || piece.mark?.lineage || []) lineage.add(parent);
|
|
286
|
+
if (!row.removed.length && !count && at > 0 && /[\p{L}\p{N}\p{M}]/u.test(row.inserted)) {
|
|
287
|
+
const left = pieces[at - 1], right = pieces[at];
|
|
288
|
+
const inner = left?.text && right?.text && left.owner === right.owner ? byId.get(left.owner) : null;
|
|
289
|
+
if (inner && inner.actId !== tx.id && inner.removed && differentActors(actorOf.get(inner.actId), tx.actor) && /[\p{L}\p{N}\p{M}]$/u.test(left.text) && /^[\p{L}\p{N}\p{M}]/u.test(right.text))
|
|
290
|
+
for (const parent of [inner.id, ...(left.lineage || [])]) lineage.add(parent);
|
|
291
|
+
}
|
|
283
292
|
const hunk = {id, actId: tx.id, removed: row.removed, inserted: row.inserted, before: removed, derived,
|
|
284
293
|
chosen, kept: selected.has(tx.id) && !chosen, lineage: [...lineage], supersededBy: new Set(), active: true,
|
|
285
294
|
...(coverIdsFor(tx.id) ? {coverIds: coverIdsFor(tx.id)} : {})};
|