rapier-html 1.2.42 → 1.2.44
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 +4 -4
- package/README.md +10 -10
- package/dist/ledger/data.mjs +7 -3
- package/dist/ledger/format.mjs +30 -10
- package/dist/ledger/journal-records.mjs +169 -99
- package/dist/ledger/md-assets.mjs +7 -10
- package/dist/rapier.html +40 -40
- 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.44",
|
|
4
4
|
"path": "dist/rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
5
|
+
"bytes": 5840328,
|
|
6
|
+
"sha256": "f566b21dd12342dfe6438c1ee96be796c63f5ef2916d58b2f325b0bd89808a7d",
|
|
7
|
+
"builtAt": "2026-10-25T10:27:39.193Z",
|
|
8
8
|
"node": "v22.23.4",
|
|
9
9
|
"canonical": true,
|
|
10
10
|
"packing": "zopfli",
|
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.44 notes.md`, then open `notes.rapier.html` in any browser.
|
|
11
|
+
Install: `npm install rapier-html@1.2.44`.
|
|
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.44 notes.md # the editor, notes.md inside it, written beside the file
|
|
30
|
+
npx -- rapier-html@1.2.44 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
31
|
+
npx -- rapier-html@1.2.44 notes.md --include draw,math # carries Draw and Math (default: what the document needs)
|
|
32
|
+
npx -- rapier-html@1.2.44 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
33
|
+
npx -- rapier-html@1.2.44 revised.md --compare original.md # opens showing what changed
|
|
34
|
+
npx -- rapier-html@1.2.44 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
35
|
+
npx -- rapier-html@1.2.44 notes.md out.html # a named output
|
|
36
|
+
npm install rapier-html@1.2.44 # 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
|
@@ -110,11 +110,15 @@ function keyedRow(row, depth) {
|
|
|
110
110
|
}
|
|
111
111
|
return json;
|
|
112
112
|
}
|
|
113
|
+
const plainRowArrays = new WeakSet();
|
|
113
114
|
export function* canonicalRowsParts(rows, depth = 0) {
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
116
120
|
yield '[';
|
|
117
|
-
for (let i = 0; i <
|
|
121
|
+
for (let i = 0; i < rows.length; i++) { if (i) yield ','; yield canonicalRow(rows[i], depth + 1); }
|
|
118
122
|
yield ']';
|
|
119
123
|
}
|
|
120
124
|
export function exactKeys(value, keys) {
|
package/dist/ledger/format.mjs
CHANGED
|
@@ -10,10 +10,10 @@ 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) ||
|
|
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
|
|
|
170
170
|
const sameSplices = (a, b) => a === b || a.length === b.length &&
|
|
171
171
|
a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
|
|
172
172
|
const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
|
|
173
|
+
let recentLedgers = [];
|
|
173
174
|
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
|
|
174
175
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
175
176
|
metadata = {...metadata};
|
|
@@ -191,16 +192,30 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
191
192
|
yield;
|
|
192
193
|
}
|
|
193
194
|
const headBytes = bytes;
|
|
195
|
+
let base = recentLedgers.find(row => row.rows.length <= expanded.length && row.rows.every((value, at) => value === expanded[at]));
|
|
194
196
|
let startSource = new HistorySource(text), startMetadata = {...metadata};
|
|
195
|
-
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
197
|
+
for (let i = expanded.length - 1; i >= (base ? base.rows.length : 0); i--) {
|
|
196
198
|
startSource = startSource.apply(expanded[i].splices, true);
|
|
197
|
-
startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
199
|
+
if (expanded[i].metadata) startMetadata = _rapierTransformMetadata(startMetadata, expanded[i].metadata, true);
|
|
198
200
|
if (startSource === null || !startMetadata) throw fault('history does not reverse from this document');
|
|
199
201
|
for (const row of expanded[i].splices) bytes += utf8Length(row.removed) - utf8Length(row.inserted);
|
|
200
202
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
201
203
|
yield;
|
|
202
204
|
}
|
|
203
|
-
|
|
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();
|
|
204
219
|
const unchangedText = expanded.every(row => !row.splices.length);
|
|
205
220
|
const contentRoot = headFacts.contentRoot;
|
|
206
221
|
revision ??= expanded.at(-1)?.transaction.revision ?? 0;
|
|
@@ -209,19 +224,24 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
209
224
|
const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
|
|
210
225
|
if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
|
|
211
226
|
if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
|
|
212
|
-
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
|
|
227
|
+
const startFacts = unchangedText ? headFacts : base ? base.startFacts : yield* sourceFacts(startText, native);
|
|
213
228
|
if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
|
|
214
229
|
const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
|
|
215
230
|
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
216
231
|
start: {text: startText, revision: startRevision, root: startRoot, sha256: startSHA, metadata: startMetadata},
|
|
217
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)};
|
|
218
|
-
|
|
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;
|
|
219
236
|
if (expanded.some(row => Object.hasOwn(row, 'authored'))) {
|
|
220
237
|
try { authoredHistory(startText, expanded); } catch (_) { throw fault('authored origin differs'); }
|
|
221
238
|
}
|
|
222
239
|
const replayedMetadata = prepared.replayMetadata(startMetadata);
|
|
223
240
|
if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
|
|
224
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);
|
|
225
245
|
ownLedger(ledger);
|
|
226
246
|
proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
|
|
227
247
|
text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
|
|
@@ -273,7 +293,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
|
273
293
|
let traversed = source, traversedMetadata = {...metadata};
|
|
274
294
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
275
295
|
traversed = step(traversed, branch[i], false);
|
|
276
|
-
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);
|
|
277
297
|
if (traversed === null || !traversedMetadata) throw fault('undo branch does not reverse');
|
|
278
298
|
}
|
|
279
299
|
if (ledger.complete) {
|
|
@@ -284,7 +304,7 @@ function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
|
284
304
|
traversed = source; traversedMetadata = {...metadata};
|
|
285
305
|
for (let i = cursor; i < branch.length; i++) {
|
|
286
306
|
traversed = step(traversed, branch[i], true);
|
|
287
|
-
traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
307
|
+
if (byId.get(branch[i]).metadata) traversedMetadata = _rapierTransformMetadata(traversedMetadata, byId.get(branch[i]).metadata);
|
|
288
308
|
if (traversed === null || !traversedMetadata) throw fault('redo branch does not replay');
|
|
289
309
|
}
|
|
290
310
|
if (ledger.complete && pictures.size) {
|
|
@@ -110,7 +110,8 @@ function _rapierRecordIndex(records, shared = false) {
|
|
|
110
110
|
}
|
|
111
111
|
return value;
|
|
112
112
|
};
|
|
113
|
-
|
|
113
|
+
const extend = more => { for (const record of more) { const id = record?.transaction?.id; if (!byId.has(id)) byId.set(id, []); byId.get(id).push(record); } };
|
|
114
|
+
return {byId, extend, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const _rapierLedgerIndexes = new WeakMap();
|
|
@@ -233,87 +234,89 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
|
233
234
|
});
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
inactive.add(ref);
|
|
286
|
-
}
|
|
237
|
+
function _rapierHistoryWalker(records, lookup, visit = null, observe = null) {
|
|
238
|
+
let nodes = new Map(), references = new Map(), pictures = new Map();
|
|
239
|
+
const byId = new Map(), finished = new Set(), earlier = [];
|
|
240
|
+
const update = (target, delta) => {
|
|
241
|
+
const pending = [[target, delta]];
|
|
242
|
+
while (pending.length) {
|
|
243
|
+
const [id, change] = pending.pop(), node = nodes.get(id), was = node.active;
|
|
244
|
+
node.blockers += change;
|
|
245
|
+
if (node.blockers < 0) throw Error('effect');
|
|
246
|
+
node.active = node.blockers === 0;
|
|
247
|
+
if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const activity = new Map();
|
|
251
|
+
const active = id => {
|
|
252
|
+
if (visit) return nodes.get(id).active;
|
|
253
|
+
const pending = [[id, 0]];
|
|
254
|
+
while (pending.length) {
|
|
255
|
+
const frame = pending.at(-1), target = frame[0];
|
|
256
|
+
if (activity.has(target)) { pending.pop(); continue; }
|
|
257
|
+
const dependents = nodes.get(target).dependents;
|
|
258
|
+
if (frame[1] === dependents.length) { activity.set(target, true); pending.pop(); continue; }
|
|
259
|
+
const next = dependents[frame[1]];
|
|
260
|
+
if (!activity.has(next)) pending.push([next, 0]);
|
|
261
|
+
else if (activity.get(next)) { activity.set(target, false); pending.pop(); }
|
|
262
|
+
else frame[1]++;
|
|
263
|
+
}
|
|
264
|
+
return activity.get(id);
|
|
265
|
+
};
|
|
266
|
+
const lastUndo = new Map(), contexts = new Map(), order = [];
|
|
267
|
+
const contextOf = origin => {
|
|
268
|
+
if (!origin) return null;
|
|
269
|
+
if (!_rapierValidAuthored(origin)) throw Error('origin');
|
|
270
|
+
const known = new Set(), pending = origin.basis.slice();
|
|
271
|
+
while (pending.length) {
|
|
272
|
+
const id = pending.pop(), context = contexts.get(id);
|
|
273
|
+
if (!context) throw Error('origin');
|
|
274
|
+
if (known.has(id)) continue;
|
|
275
|
+
known.add(id);
|
|
276
|
+
if (context.basis) pending.push(...context.basis);
|
|
277
|
+
else for (let index = 0; index < context.index; index++) known.add(order[index]);
|
|
278
|
+
}
|
|
279
|
+
const inactive = new Set();
|
|
280
|
+
for (let index = order.length - 1; index >= 0; index--) {
|
|
281
|
+
const id = order[index];
|
|
282
|
+
if (!known.has(id) || inactive.has(id)) continue;
|
|
283
|
+
for (const ref of references.get(id)) {
|
|
284
|
+
if (!known.has(ref)) throw Error('origin');
|
|
285
|
+
inactive.add(ref);
|
|
287
286
|
}
|
|
288
|
-
|
|
289
|
-
};
|
|
290
|
-
|
|
287
|
+
}
|
|
288
|
+
return {known, inactive};
|
|
289
|
+
};
|
|
290
|
+
const walker = {observe, failed: false};
|
|
291
|
+
walker.add = record => {
|
|
292
|
+
if (walker.failed) return false;
|
|
293
|
+
try {
|
|
291
294
|
const tx = record?.transaction;
|
|
292
|
-
if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) return
|
|
295
|
+
if (!tx || typeof tx.id !== 'string' || !tx.id || nodes.has(tx.id) || (tx.reverts && tx.reapplies)) { walker.failed = true; return false; }
|
|
293
296
|
const supplied = tx.sourceTransactionIds || (tx.sourceTransactionId ? [tx.sourceTransactionId] : []);
|
|
294
297
|
if (!Array.isArray(supplied) || new Set(supplied).size !== supplied.length ||
|
|
295
|
-
(tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) return
|
|
298
|
+
(tx.sourceTransactionIds && tx.sourceTransactionId && (supplied.length !== 1 || supplied[0] !== tx.sourceTransactionId))) { walker.failed = true; return false; }
|
|
296
299
|
let refs = supplied;
|
|
297
300
|
if (tx.operation === 'document.finish-picture' && tx.sourceTransactionId) {
|
|
298
301
|
const owner = tx.sourceTransactionId;
|
|
299
302
|
if (!nodes.has(owner) || !active(owner) || finished.has(owner) ||
|
|
300
|
-
!_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) return
|
|
303
|
+
!_rapierPictureFinish(record, byId.get(owner), earlier, lookup ||= _rapierRecordIndex(records))) { walker.failed = true; return false; }
|
|
301
304
|
pictures.set(tx.id, owner); finished.add(owner); refs = [];
|
|
302
305
|
}
|
|
303
306
|
if (tx.reverts || tx.reapplies) {
|
|
304
307
|
const target = tx.reverts || tx.reapplies;
|
|
305
|
-
if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) return
|
|
308
|
+
if (!nodes.has(target) || (supplied.length && (supplied.length !== 1 || supplied[0] !== target))) { walker.failed = true; return false; }
|
|
306
309
|
if (tx.reverts) refs = [target];
|
|
307
310
|
else {
|
|
308
311
|
const context = contextOf(record.authored || tx.authored);
|
|
309
312
|
const undo = (lastUndo.get(target) || []).findLast(id => context
|
|
310
313
|
? context.known.has(id) && !context.inactive.has(id) : active(id));
|
|
311
|
-
if (!undo) return
|
|
314
|
+
if (!undo) { walker.failed = true; return false; }
|
|
312
315
|
refs = [undo];
|
|
313
316
|
}
|
|
314
317
|
}
|
|
315
|
-
if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return
|
|
316
|
-
if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) return
|
|
318
|
+
if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) { walker.failed = true; return false; }
|
|
319
|
+
if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) { walker.failed = true; return false; }
|
|
317
320
|
nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
|
|
318
321
|
byId.set(tx.id, record); earlier.push(record);
|
|
319
322
|
for (const ref of refs) {
|
|
@@ -328,23 +331,45 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
328
331
|
lastUndo.get(tx.reverts).push(tx.id);
|
|
329
332
|
}
|
|
330
333
|
if (visit) visit(record, nodes, refs);
|
|
331
|
-
if (observe) observe(record, refs, active);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
if (walker.observe) walker.observe(record, refs, active);
|
|
335
|
+
return true;
|
|
336
|
+
} catch (_) { walker.failed = true; return false; }
|
|
337
|
+
};
|
|
338
|
+
walker.finish = () => {
|
|
339
|
+
if (walker.failed) return {ok: false};
|
|
340
|
+
const blockers = new Map(), effectiveActIds = [];
|
|
341
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
342
|
+
const id = order[i], node = nodes.get(id), on = visit ? node.active : !(blockers.get(id) > 0);
|
|
343
|
+
if (!visit && on) for (const ref of node.references) blockers.set(ref, (blockers.get(ref) || 0) + 1);
|
|
344
|
+
if (on && !pictures.has(id)) effectiveActIds.push(id);
|
|
336
345
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
effectiveActIds.reverse();
|
|
347
|
+
const result = {ok: true, effectiveActIds, active: new Set(effectiveActIds), references, pictures};
|
|
348
|
+
walker.shared = true;
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
const add = walker.add;
|
|
352
|
+
walker.add = record => {
|
|
353
|
+
if (walker.shared) { references = new Map(references); pictures = new Map(pictures); walker.shared = false; }
|
|
354
|
+
return add(record);
|
|
355
|
+
};
|
|
356
|
+
return walker;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
|
|
360
|
+
if (!Array.isArray(records)) return {ok: false};
|
|
361
|
+
const walker = _rapierHistoryWalker(records, lookup, visit, observe);
|
|
362
|
+
for (const record of records) if (!walker.add(record)) return {ok: false};
|
|
363
|
+
return walker.finish();
|
|
340
364
|
}
|
|
341
365
|
|
|
342
366
|
function _rapierHistoryEffects(records, visit = null) {
|
|
343
367
|
return _rapierWalkHistoryEffects(records, null, visit);
|
|
344
368
|
}
|
|
345
369
|
|
|
346
|
-
function _rapierMetadataProof(lookup) {
|
|
347
|
-
const values = {}, required = {},
|
|
370
|
+
function _rapierMetadataProof(lookup, from = null) {
|
|
371
|
+
const values = {...from?.values}, required = {...from?.required},
|
|
372
|
+
assignments = {filename: [...(from?.assignments.filename || [])], docKind: [...(from?.assignments.docKind || [])]};
|
|
348
373
|
const requireInitial = (key, value) => {
|
|
349
374
|
if (Object.hasOwn(required, key) && required[key] !== value) throw Error('metadata_effect');
|
|
350
375
|
required[key] = value;
|
|
@@ -371,7 +396,7 @@ function _rapierMetadataProof(lookup) {
|
|
|
371
396
|
if (!_rapierValidMetadata(initial) || Object.keys(required).some(key => initial[key] !== required[key])) return null;
|
|
372
397
|
return {...initial, ...values};
|
|
373
398
|
};
|
|
374
|
-
return {observe, replay};
|
|
399
|
+
return {observe, replay, fork: other => _rapierMetadataProof(other || lookup, {values, required, assignments})};
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(records)) {
|
|
@@ -478,16 +503,79 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
478
503
|
frame.descriptors = descriptors; frame.fields = fields;
|
|
479
504
|
frame.leaving = true; active.add(value); pending.push(frame);
|
|
480
505
|
for (let at = fields.length - 1; at >= 0; at--) {
|
|
481
|
-
const key = fields[at]
|
|
506
|
+
const key = fields[at], child = descriptors[key].value;
|
|
507
|
+
const prior = array && child && typeof child === 'object' ? copies.get(child) : undefined;
|
|
508
|
+
if (prior?.immutable && !active.has(child) && frame.depth + 1 + prior.height <= 64) {
|
|
509
|
+
finish({parent: frame, key}, prior.copy, true, prior.height); continue;
|
|
510
|
+
}
|
|
511
|
+
pending.push({value: child, parent: frame, key, depth: frame.depth + 1});
|
|
482
512
|
}
|
|
483
513
|
}
|
|
484
514
|
return result.value;
|
|
485
515
|
};
|
|
516
|
+
let resumable = [];
|
|
517
|
+
const rowOf = (lookup, record) => {
|
|
518
|
+
const splices = lookup.splices(record), metadata = lookup.metadata(record);
|
|
519
|
+
if (splices && !Object.isFrozen(splices)) {
|
|
520
|
+
for (const row of splices) Object.freeze(row);
|
|
521
|
+
Object.freeze(splices);
|
|
522
|
+
}
|
|
523
|
+
if (metadata && !Object.isFrozen(metadata)) {
|
|
524
|
+
for (const pair of Object.values(metadata)) Object.freeze(pair);
|
|
525
|
+
Object.freeze(metadata);
|
|
526
|
+
}
|
|
527
|
+
return Object.freeze({record, splices, metadata});
|
|
528
|
+
};
|
|
529
|
+
const observe = (records, basis, byId, rows, effects, proof) => {
|
|
530
|
+
for (const refs of effects.references.values()) Object.freeze(refs);
|
|
531
|
+
const effectView = Object.freeze({ok: true, effectiveActIds: Object.freeze(effects.effectiveActIds),
|
|
532
|
+
active: view(effects.active), references: view(effects.references), pictures: view(effects.pictures)});
|
|
533
|
+
const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
|
|
534
|
+
replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
|
|
535
|
+
validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
|
|
536
|
+
if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
|
|
537
|
+
const row = rows[index], tx = row.record.transaction;
|
|
538
|
+
return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
|
|
539
|
+
{...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
|
|
540
|
+
}});
|
|
541
|
+
prepared = new WeakMap([[records, {basis, observation}]]);
|
|
542
|
+
return observation;
|
|
543
|
+
};
|
|
544
|
+
const authorityOf = state => state.records[0]?.transaction?.documentAuthority;
|
|
545
|
+
const keep = state => { resumable = [state, ...resumable.filter(row => row !== state && row.records[0] !== state.records[0] &&
|
|
546
|
+
authorityOf(row) === authorityOf(state))].slice(0, 4); };
|
|
547
|
+
const extend = records => {
|
|
548
|
+
const base = resumable.find(row => row.records.length < records.length && row.records[0] === records[0] &&
|
|
549
|
+
row.records.every((record, at) => record === records[at]));
|
|
550
|
+
if (!base) return null;
|
|
551
|
+
resumable = resumable.filter(row => row !== base);
|
|
552
|
+
const more = records.slice(base.records.length), byId = new Map(base.byId);
|
|
553
|
+
for (const record of more) {
|
|
554
|
+
const id = record?.transaction?.id;
|
|
555
|
+
if (typeof id !== 'string' || !id || byId.has(id)) return null;
|
|
556
|
+
byId.set(id, record);
|
|
557
|
+
}
|
|
558
|
+
const {lookup, walker} = base;
|
|
559
|
+
lookup.extend(more);
|
|
560
|
+
const rows = base.rows.concat(more.map(record => rowOf(lookup, record)));
|
|
561
|
+
const proof = base.proof.fork(lookup);
|
|
562
|
+
walker.observe = proof.observe;
|
|
563
|
+
for (const record of more) if (!walker.add(record)) return null;
|
|
564
|
+
const effects = walker.finish();
|
|
565
|
+
if (!effects.ok) return null;
|
|
566
|
+
if (rows.every(row => row.splices && row.metadata !== undefined)) keep({records, lookup, walker, proof, byId, rows});
|
|
567
|
+
return observe(records, records, byId, rows, effects, proof);
|
|
568
|
+
};
|
|
486
569
|
return (input, earlier = input) => {
|
|
487
570
|
try {
|
|
488
571
|
if (!Array.isArray(input) || !Array.isArray(earlier)) return {ok: false};
|
|
489
572
|
const records = capture(input), basis = input === earlier ? records : capture(earlier), previous = prepared.get(records);
|
|
490
573
|
if (previous?.basis === basis) return previous.observation;
|
|
574
|
+
if (basis === records) {
|
|
575
|
+
let grown = null;
|
|
576
|
+
try { grown = extend(records); } catch (_) { grown = null; }
|
|
577
|
+
if (grown) return grown;
|
|
578
|
+
}
|
|
491
579
|
const byId = new Map();
|
|
492
580
|
for (const record of records) {
|
|
493
581
|
const id = record?.transaction?.id;
|
|
@@ -495,32 +583,14 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
495
583
|
byId.set(id, record);
|
|
496
584
|
}
|
|
497
585
|
const lookup = _rapierRecordIndex(basis), proof = _rapierMetadataProof(lookup);
|
|
498
|
-
const rows = records.map(record =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
Object.freeze(splices);
|
|
503
|
-
}
|
|
504
|
-
if (metadata && !Object.isFrozen(metadata)) {
|
|
505
|
-
for (const pair of Object.values(metadata)) Object.freeze(pair);
|
|
506
|
-
Object.freeze(metadata);
|
|
507
|
-
}
|
|
508
|
-
return Object.freeze({record, splices, metadata});
|
|
509
|
-
});
|
|
510
|
-
const effects = _rapierWalkHistoryEffects(records, lookup, null, proof.observe);
|
|
586
|
+
const rows = records.map(record => rowOf(lookup, record));
|
|
587
|
+
const walker = _rapierHistoryWalker(records, lookup, null, proof.observe);
|
|
588
|
+
for (const record of records) if (!walker.add(record)) return {ok: false};
|
|
589
|
+
const effects = walker.finish();
|
|
511
590
|
if (!effects.ok) return {ok: false};
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const observation = Object.freeze({ok: effects.ok, records, rows: Object.freeze(rows), effects: effectView, byId: view(byId),
|
|
516
|
-
replayMetadata: initial => effects.ok ? proof.replay(initial) : null,
|
|
517
|
-
validRecord: (index, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) => {
|
|
518
|
-
if (!effects.ok || !Number.isSafeInteger(index) || index < 0 || index >= records.length) return false;
|
|
519
|
-
const row = rows[index], tx = row.record.transaction;
|
|
520
|
-
return _rapierCheckLedgerRecord(row.record, records[index - 1] || null, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
|
|
521
|
-
{...row, duplicate: false, semantic: !!(tx.reverts || tx.reapplies || tx.sourceTransactionId || tx.sourceTransactionIds?.length)});
|
|
522
|
-
}});
|
|
523
|
-
prepared = new WeakMap([[records, {basis, observation}]]); return observation;
|
|
591
|
+
if (basis === records && rows.every(row => row.splices && row.metadata !== undefined))
|
|
592
|
+
keep({records, lookup, walker, proof, byId, rows});
|
|
593
|
+
return observe(records, basis, byId, rows, effects, proof);
|
|
524
594
|
} catch (_) { return {ok: false}; }
|
|
525
595
|
};
|
|
526
596
|
})();
|
|
@@ -100,7 +100,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
100
100
|
return end < body.length ? sourceOffset(end + 1) : undefined;
|
|
101
101
|
}});
|
|
102
102
|
let lines = 0;
|
|
103
|
-
for (let at =
|
|
103
|
+
for (let at = body.indexOf('\n'); at !== -1; at = body.indexOf('\n', at + 1)) lines++;
|
|
104
104
|
if (lines <= BLOCK_WINDOW) {
|
|
105
105
|
state = new parser.block.State(body, parser, env, tokens);
|
|
106
106
|
state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
|
|
@@ -179,8 +179,7 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
const
|
|
183
|
-
code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
|
|
182
|
+
const INVALID_PERCENT_PAYLOAD = /[^A-Za-z0-9._~:\/?@+$,;=%-]/;
|
|
184
183
|
const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
185
184
|
let lastScan = null;
|
|
186
185
|
export function dataImage(url) {
|
|
@@ -197,13 +196,11 @@ function scanDataImage(url) {
|
|
|
197
196
|
...(prepared.encoding ? {encoding: prepared.encoding} : {})};
|
|
198
197
|
if (/^data:image\/svg\+xml,/i.test(url)) {
|
|
199
198
|
const payloadStart = 19;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
at += 2;
|
|
206
|
-
} else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
|
|
199
|
+
if (INVALID_PERCENT_PAYLOAD.test(url)) return null;
|
|
200
|
+
let byteLength = url.length - payloadStart;
|
|
201
|
+
for (let at = url.indexOf('%', payloadStart); at !== -1; at = url.indexOf('%', at + 3)) {
|
|
202
|
+
if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
|
|
203
|
+
byteLength -= 2;
|
|
207
204
|
}
|
|
208
205
|
return byteLength && byteLength <= IMAGE_LIMITS.bytes
|
|
209
206
|
? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
|