rapier-html 1.2.40 → 1.2.42
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 +55 -2
- package/dist/ledger/format.mjs +35 -12
- package/dist/ledger/history.mjs +1 -1
- package/dist/ledger/inverse-source.mjs +9 -0
- package/dist/ledger/journal-records.mjs +28 -7
- package/dist/ledger/will.mjs +2 -2
- package/dist/rapier.html +72 -71
- 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.42",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
5
|
+
"bytes": 5828319,
|
|
6
|
+
"sha256": "12f1cc548ff4e0d95a105b0170ce2f40aed8989cc58a4937f43eacb05581833c",
|
|
7
|
+
"builtAt": "2026-10-24T21:57:08.348Z",
|
|
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.42 notes.md`, then open `notes.rapier.html` in any browser.
|
|
11
|
+
Install: `npm install rapier-html@1.2.42`.
|
|
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.42 notes.md # the editor, notes.md inside it, written beside the file
|
|
30
|
+
npx -- rapier-html@1.2.42 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
31
|
+
npx -- rapier-html@1.2.42 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
|
|
32
|
+
npx -- rapier-html@1.2.42 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
33
|
+
npx -- rapier-html@1.2.42 revised.md --compare original.md # opens showing what changed
|
|
34
|
+
npx -- rapier-html@1.2.42 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
35
|
+
npx -- rapier-html@1.2.42 notes.md out.html # a named output
|
|
36
|
+
npm install rapier-html@1.2.42 # 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,65 @@ 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
|
+
export function* canonicalRowsParts(rows, depth = 0) {
|
|
114
|
+
const {array, keys} = fields(rows, depth, new Set());
|
|
115
|
+
if (!array) { yield* canonicalParts(rows, depth); return; }
|
|
116
|
+
yield '[';
|
|
117
|
+
for (let i = 0; i < keys.length; i++) { if (i) yield ','; yield canonicalRow(rows[keys[i]], depth + 1); }
|
|
118
|
+
yield ']';
|
|
119
|
+
}
|
|
67
120
|
export function exactKeys(value, keys) {
|
|
68
121
|
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
69
122
|
}
|
package/dist/ledger/format.mjs
CHANGED
|
@@ -4,7 +4,7 @@ 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';
|
|
@@ -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,10 @@ 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);
|
|
154
173
|
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
|
|
155
174
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
156
175
|
metadata = {...metadata};
|
|
@@ -162,10 +181,13 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
162
181
|
const expanded = [];
|
|
163
182
|
for (const {record, splices: rows, metadata: effect} of prepared.rows) {
|
|
164
183
|
if (!rows || effect === undefined) throw fault('navigation target was not retained');
|
|
165
|
-
|
|
184
|
+
const held = expandedRows.get(record);
|
|
185
|
+
if (held && sameSplices(held.rows, rows) && sameEffect(held.effect, effect)) { expanded.push(held.row); yield; continue; }
|
|
186
|
+
expanded.push(ownRow({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
166
187
|
beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: plainData(record.authored)} : {}), ...(effect ? {metadata: plainData(effect)} : {}),
|
|
167
188
|
...(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)})});
|
|
189
|
+
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})}));
|
|
190
|
+
expandedRows.set(record, {row: expanded.at(-1), rows, effect});
|
|
169
191
|
yield;
|
|
170
192
|
}
|
|
171
193
|
const headBytes = bytes;
|
|
@@ -199,8 +221,9 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
199
221
|
}
|
|
200
222
|
const replayedMetadata = prepared.replayMetadata(startMetadata);
|
|
201
223
|
if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
|
|
202
|
-
const ledger = {...payload, sha256: yield* checksum(
|
|
203
|
-
|
|
224
|
+
const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
|
|
225
|
+
ownLedger(ledger);
|
|
226
|
+
proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
|
|
204
227
|
text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
|
|
205
228
|
source: new HistorySource(text)});
|
|
206
229
|
return {ledger, contentRoot, utf8Bytes: headBytes};
|
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)} : {})};
|
|
@@ -76,7 +76,7 @@ function _rapierValidAuthored(origin) {
|
|
|
76
76
|
!/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function _rapierRecordIndex(records) {
|
|
79
|
+
function _rapierRecordIndex(records, shared = false) {
|
|
80
80
|
const byId = new Map(), splices = new Map(), metadata = new Map();
|
|
81
81
|
for (const record of records) {
|
|
82
82
|
const id = record?.transaction?.id;
|
|
@@ -100,9 +100,12 @@ function _rapierRecordIndex(records) {
|
|
|
100
100
|
}
|
|
101
101
|
cache.set(current, value);
|
|
102
102
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
103
|
-
if (path[i].transaction.reverts && value != null)
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
if (path[i].transaction.reverts && value != null) {
|
|
104
|
+
value = effects
|
|
105
|
+
? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
|
|
106
|
+
: value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
|
|
107
|
+
if (shared) { for (const row of Object.values(value)) Object.freeze(row); Object.freeze(value); }
|
|
108
|
+
}
|
|
106
109
|
cache.set(path[i], value);
|
|
107
110
|
}
|
|
108
111
|
return value;
|
|
@@ -110,10 +113,27 @@ function _rapierRecordIndex(records) {
|
|
|
110
113
|
return {byId, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
const _rapierLedgerIndexes = new WeakMap();
|
|
117
|
+
function _rapierLedgerIndex(ledger) {
|
|
118
|
+
const records = ledger || [];
|
|
119
|
+
if (!Array.isArray(records)) return _rapierRecordIndex(records);
|
|
120
|
+
const held = _rapierLedgerIndexes.get(records);
|
|
121
|
+
if (held && held.records.length === records.length && held.records.at(-1) === records.at(-1) &&
|
|
122
|
+
(held.checked || held.records.every((record, at) => record === records[at]))) {
|
|
123
|
+
if (!held.checked) { held.checked = true; queueMicrotask(() => { held.checked = false; }); }
|
|
124
|
+
return held.index;
|
|
125
|
+
}
|
|
126
|
+
if (!records.every(record => record && typeof record === 'object' && Object.isFrozen(record) && Object.isFrozen(record.transaction ?? {})))
|
|
127
|
+
return _rapierRecordIndex(records);
|
|
128
|
+
const index = _rapierRecordIndex(records, true);
|
|
129
|
+
_rapierLedgerIndexes.set(records, {records: records.slice(), index});
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
132
|
+
|
|
113
133
|
function _rapierRecordSplices(entry, ledger) {
|
|
114
134
|
try {
|
|
115
135
|
if (entry && Array.isArray(entry.splices)) return entry.splices;
|
|
116
|
-
return
|
|
136
|
+
return _rapierLedgerIndex(ledger).splices(entry);
|
|
117
137
|
} catch (_) { return null; }
|
|
118
138
|
}
|
|
119
139
|
|
|
@@ -163,7 +183,7 @@ function _rapierRecordMetadata(entry, ledger) {
|
|
|
163
183
|
try {
|
|
164
184
|
if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
|
|
165
185
|
if (Array.isArray(entry.splices)) return null;
|
|
166
|
-
return
|
|
186
|
+
return _rapierLedgerIndex(ledger).metadata(entry);
|
|
167
187
|
} catch (_) { return undefined; }
|
|
168
188
|
}
|
|
169
189
|
|
|
@@ -507,9 +527,10 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
507
527
|
|
|
508
528
|
const _rapierJournalRecord = (() => {
|
|
509
529
|
const copies = new WeakMap();
|
|
530
|
+
const fixed = value => { if (value && typeof value === 'object') { for (const key of Object.keys(value)) fixed(value[key]); Object.freeze(value); } return value; };
|
|
510
531
|
return record => {
|
|
511
532
|
let copy = copies.get(record);
|
|
512
|
-
if (!copy) { copy = JSON.parse(JSON.stringify(record)); copies.set(record, copy); }
|
|
533
|
+
if (!copy) { copy = fixed(JSON.parse(JSON.stringify(record))); copies.set(record, copy); }
|
|
513
534
|
return copy;
|
|
514
535
|
};
|
|
515
536
|
})();
|
package/dist/ledger/will.mjs
CHANGED
|
@@ -215,7 +215,7 @@ export function willTransition(beforeText, afterText, splices, markerSplices = s
|
|
|
215
215
|
return null;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
export function evaluateWill(beforeText, splices) {
|
|
218
|
+
export function evaluateWill(beforeText, splices, {restores = null} = {}) {
|
|
219
219
|
const invalid = rule => ({outcome: 'invalid', reason: 'precondition', rule});
|
|
220
220
|
if (typeof beforeText !== 'string' || !beforeText.isWellFormed()) return invalid('invalid_unicode');
|
|
221
221
|
if (!Array.isArray(splices)) return invalid('malformed_splice');
|
|
@@ -231,7 +231,7 @@ export function evaluateWill(beforeText, splices) {
|
|
|
231
231
|
if (afterText.slice(row.pos, end) !== row.removed) return invalid('source_mismatch');
|
|
232
232
|
afterText = afterText.slice(0, row.pos) + row.inserted + afterText.slice(end);
|
|
233
233
|
}
|
|
234
|
-
const refusal = splices.length && willTransition(beforeText, afterText, splices);
|
|
234
|
+
const refusal = splices.length && willTransition(beforeText, afterText, splices, splices, {restores});
|
|
235
235
|
return refusal ? {outcome: 'refused', reason: 'document_law', ...refusal} : {outcome: 'applied'};
|
|
236
236
|
}
|
|
237
237
|
|