rapier-html 1.2.8 → 1.2.9
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 +10 -10
- package/README.md +9 -9
- package/dist/cli.mjs +0 -1
- package/dist/ledger/authored-origin.mjs +0 -5
- package/dist/ledger/authorship.mjs +0 -2
- package/dist/ledger/carried.mjs +4 -7
- package/dist/ledger/data.mjs +0 -8
- package/dist/ledger/diff/concerns.mjs +0 -16
- package/dist/ledger/diff/diff.mjs +0 -7
- package/dist/ledger/diff/index.mjs +0 -1
- package/dist/ledger/diff/matching.mjs +0 -19
- package/dist/ledger/diff/merge.mjs +0 -9
- package/dist/ledger/diff/moves.mjs +0 -7
- package/dist/ledger/diff/structure.mjs +0 -2
- package/dist/ledger/diff/tokens.mjs +0 -6
- package/dist/ledger/diff/walk.mjs +0 -12
- package/dist/ledger/format.mjs +0 -25
- package/dist/ledger/hash.mjs +11 -7
- package/dist/ledger/history.mjs +10 -29
- package/dist/ledger/inverse-source.mjs +0 -46
- package/dist/ledger/journal-records.mjs +24 -56
- package/dist/ledger/transport.mjs +0 -47
- package/dist/page.mjs +0 -6
- package/dist/rapier.html +169 -98
- package/dist/return-address.mjs +0 -4
- package/dist/wrap.mjs +32 -22
- package/package.json +1 -1
|
@@ -5,14 +5,10 @@ const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
|
|
|
5
5
|
const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
|
|
6
6
|
const _RAPIER_TRANSACTION_REQUEST_LIMIT = 160;
|
|
7
7
|
|
|
8
|
-
/* Validate every row, not only the final string: a half-pair replacement can produce a whole
|
|
9
|
-
character while leaving unencodable fragments in the ledger; two invalid rows can also repair
|
|
10
|
-
each other. Refusal is atomic. Pre-existing damage elsewhere stays untouched and editable. */
|
|
11
8
|
function _rapierTransformSplices(text, splices, inverse = false) {
|
|
12
9
|
try {
|
|
13
10
|
if (!Array.isArray(splices)) return null;
|
|
14
11
|
let value = String(text == null ? '' : text);
|
|
15
|
-
// In Unicode mode a complete pair is one scalar, outside the surrogate range.
|
|
16
12
|
const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
|
|
17
13
|
const splitsPair = at => at > 0 && at < value.length &&
|
|
18
14
|
(value.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
|
|
@@ -81,7 +77,6 @@ function _rapierTransformMetadata(value, effect, inverse = false) {
|
|
|
81
77
|
return next;
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
// An origin binds transported placement to exact authored rows and their causal basis.
|
|
85
80
|
function _rapierValidAuthored(origin) {
|
|
86
81
|
if (!_rapierPlainLedgerData(origin) || !origin || Array.isArray(origin) ||
|
|
87
82
|
Object.keys(origin).length !== 3 || !Array.isArray(origin.basis) ||
|
|
@@ -95,9 +90,6 @@ function _rapierValidAuthored(origin) {
|
|
|
95
90
|
!/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
// The lookup is an observation of one call's supplied records. Material rows keep
|
|
99
|
-
// their existing borrowed-row API; an inverse owns fresh rows. Navigation only
|
|
100
|
-
// follows strictly earlier revisions, including when an unadmitted list is unordered.
|
|
101
93
|
function _rapierRecordIndex(records) {
|
|
102
94
|
const byId = new Map(), splices = new Map(), metadata = new Map();
|
|
103
95
|
for (const record of records) {
|
|
@@ -139,8 +131,6 @@ function _rapierRecordSplices(entry, ledger) {
|
|
|
139
131
|
} catch (_) { return null; }
|
|
140
132
|
}
|
|
141
133
|
|
|
142
|
-
// Source identifies a retained checkpoint even when host revision numbers differ.
|
|
143
|
-
// The History Slider compares now → earlier: that answer is the earlier → now walk, inverted.
|
|
144
134
|
function journalOperations(before, after, journal) {
|
|
145
135
|
if (typeof before !== 'string' || typeof after !== 'string' || !Array.isArray(journal)) return;
|
|
146
136
|
if (before === after) return [];
|
|
@@ -151,15 +141,34 @@ function journalOperations(before, after, journal) {
|
|
|
151
141
|
|
|
152
142
|
function journalWalk(before, after, journal) {
|
|
153
143
|
try {
|
|
154
|
-
|
|
144
|
+
const pieces = [after];
|
|
155
145
|
const lookup = _rapierRecordIndex(journal), groups = [];
|
|
146
|
+
let length = after.length;
|
|
147
|
+
const locate = at => { let index = 0; for (; index < pieces.length - 1 && at >= pieces[index].length; index++) at -= pieces[index].length; return [index, at]; };
|
|
148
|
+
const codeAt = at => { const [index, offset] = locate(at); return pieces[index].charCodeAt(offset); };
|
|
149
|
+
const read = (from, to) => { let out = ''; for (let [index, offset] = locate(from); from < to && index < pieces.length; index++, offset = 0) { const part = pieces[index].slice(offset, offset + to - from); out += part; from += part.length; } return out; };
|
|
150
|
+
const cut = at => { const [index, offset] = locate(at); if (offset > 0 && offset < pieces[index].length) pieces.splice(index, 1, pieces[index].slice(0, offset), pieces[index].slice(offset)); };
|
|
151
|
+
const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
|
|
152
|
+
const splitsPair = at => at > 0 && at < length && (codeAt(at - 1) & 0xFC00) === 0xD800 && (codeAt(at) & 0xFC00) === 0xDC00;
|
|
156
153
|
for (let index = journal.length - 1; index >= 0; index--) {
|
|
157
154
|
const rows = lookup.splices(journal[index]);
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
if (!Array.isArray(rows)) return;
|
|
156
|
+
for (const row of rows.slice().reverse()) {
|
|
157
|
+
if (!row || !Number.isSafeInteger(row.pos) || row.pos < 0 || Object.is(row.pos, -0)) return;
|
|
158
|
+
const pos = row.pos, removed = row.inserted, inserted = row.removed;
|
|
159
|
+
if (!whole(removed) || !whole(inserted) || pos > length || pos + removed.length > length ||
|
|
160
|
+
splitsPair(pos) || splitsPair(pos + removed.length) || read(pos, pos + removed.length) !== removed) return;
|
|
161
|
+
cut(pos); cut(pos + removed.length);
|
|
162
|
+
let [first] = locate(pos), count = 0;
|
|
163
|
+
if (pos === length) first = pieces.length;
|
|
164
|
+
else if (locate(pos)[1] !== 0) first++;
|
|
165
|
+
for (let left = removed.length; left > 0; count++) left -= pieces[first + count].length;
|
|
166
|
+
pieces.splice(first, count, ...(inserted ? [inserted] : []));
|
|
167
|
+
length += inserted.length - removed.length;
|
|
168
|
+
for (let at = pieces.length - 1; at >= 0; at--) if (!pieces[at] && pieces.length > 1) pieces.splice(at, 1);
|
|
169
|
+
}
|
|
160
170
|
groups.push(rows);
|
|
161
|
-
if (
|
|
162
|
-
text = prior;
|
|
171
|
+
if (length === before.length && pieces.join('') === before) return groups.reverse().flat().map(({pos, removed, inserted}) => ({pos, removed, inserted}));
|
|
163
172
|
}
|
|
164
173
|
} catch (_) { return; }
|
|
165
174
|
}
|
|
@@ -167,14 +176,11 @@ function journalWalk(before, after, journal) {
|
|
|
167
176
|
function _rapierRecordMetadata(entry, ledger) {
|
|
168
177
|
try {
|
|
169
178
|
if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
|
|
170
|
-
// An expanded record owns its complete effect; only compact navigation inherits one.
|
|
171
179
|
if (Array.isArray(entry.splices)) return null;
|
|
172
180
|
return _rapierRecordIndex(ledger || []).metadata(entry);
|
|
173
181
|
} catch (_) { return undefined; }
|
|
174
182
|
}
|
|
175
183
|
|
|
176
|
-
// A codec replacement owns only one inserted definition and its reference. Its
|
|
177
|
-
// source act remains the picture's author; an arbitrary edit cannot claim upkeep.
|
|
178
184
|
function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
179
185
|
const tx = record?.transaction, rows = record?.splices;
|
|
180
186
|
if (tx?.operation !== 'document.finish-picture' || !tx.sourceTransactionId || tx.sourceTransactionIds ||
|
|
@@ -221,9 +227,6 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
|
221
227
|
});
|
|
222
228
|
}
|
|
223
229
|
|
|
224
|
-
// Every active inverse keeps its original targets suppressed. Independent inverses
|
|
225
|
-
// can share a target; withdrawing one restores it only after the last blocker leaves.
|
|
226
|
-
// This includes assignments hidden by later choices. No second history is retained.
|
|
227
230
|
function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null) {
|
|
228
231
|
try {
|
|
229
232
|
if (!Array.isArray(records)) return {ok: false};
|
|
@@ -238,9 +241,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
238
241
|
if (was !== node.active) for (const ref of node.references) pending.push([ref, node.active ? 1 : -1]);
|
|
239
242
|
}
|
|
240
243
|
};
|
|
241
|
-
// Most admission needs only the final effect graph. A backwards pass proves it
|
|
242
|
-
// in linear work; prefix activity is asked only by native Redo, picture custody,
|
|
243
|
-
// or a metadata assignment. The explicit stack also bounds long inverse chains.
|
|
244
244
|
const activity = new Map();
|
|
245
245
|
const active = id => {
|
|
246
246
|
if (visit) return nodes.get(id).active;
|
|
@@ -258,8 +258,6 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
258
258
|
return activity.get(id);
|
|
259
259
|
};
|
|
260
260
|
const lastUndo = new Map(), contexts = new Map(), order = [];
|
|
261
|
-
// Native Redo names an original act. Its inverse target is the last Undo
|
|
262
|
-
// in the author's proved causal view, not an unrelated later arrival.
|
|
263
261
|
const contextOf = origin => {
|
|
264
262
|
if (!origin) return null;
|
|
265
263
|
if (!_rapierValidAuthored(origin)) throw Error('origin');
|
|
@@ -338,8 +336,6 @@ function _rapierHistoryEffects(records, visit = null) {
|
|
|
338
336
|
return _rapierWalkHistoryEffects(records, null, visit);
|
|
339
337
|
}
|
|
340
338
|
|
|
341
|
-
// Every transition constrains the two initial metadata scalars. This proves each
|
|
342
|
-
// inverse's result once, before the caller supplies the exact initial metadata.
|
|
343
339
|
function _rapierMetadataProof(lookup) {
|
|
344
340
|
const values = {}, required = {}, assignments = {filename: [], docKind: []};
|
|
345
341
|
const requireInitial = (key, value) => {
|
|
@@ -355,7 +351,6 @@ function _rapierMetadataProof(lookup) {
|
|
|
355
351
|
values[key] = pair.after;
|
|
356
352
|
if (!refs.length) assignments[key].push({id: record.transaction.id, value: pair.after});
|
|
357
353
|
}
|
|
358
|
-
// Source-only rows cannot change a previously proved metadata assignment.
|
|
359
354
|
if (!effect && !refs.length) return;
|
|
360
355
|
for (const key of ['filename', 'docKind']) {
|
|
361
356
|
const expected = assignments[key].findLast(row => active(row.id));
|
|
@@ -385,7 +380,6 @@ function _rapierMetadataState(initial, records, effects = _rapierHistoryEffects(
|
|
|
385
380
|
} catch (_) { return null; }
|
|
386
381
|
}
|
|
387
382
|
|
|
388
|
-
// Prove each recorded transition and each inverse's semantic result, not only the final values.
|
|
389
383
|
function _rapierReplayMetadata(initial, records) {
|
|
390
384
|
if (!_rapierValidMetadata(initial)) return null;
|
|
391
385
|
try {
|
|
@@ -405,16 +399,9 @@ function _rapierHasHistoryEffect(record, earlier = []) {
|
|
|
405
399
|
return _rapierHistoryEffects([...earlier, {...record, transaction: {...tx, id}}]).ok;
|
|
406
400
|
}
|
|
407
401
|
|
|
408
|
-
// A cached observation owns every byte-bearing record. Mutable graphs are compared
|
|
409
|
-
// through descriptors on every call; only a proved, recursively frozen graph can
|
|
410
|
-
// bypass that walk. Per-object weak entries keep one latest copy, not a revision log.
|
|
411
402
|
const _rapierPrepareHistoryRecords = (() => {
|
|
412
403
|
const copies = new WeakMap();
|
|
413
|
-
// Repeated queries share the latest proved snapshot. A live but inactive input
|
|
414
|
-
// does not need to retain its record/effect indexes; immutable copies remain weak.
|
|
415
404
|
let prepared = new WeakMap();
|
|
416
|
-
// Object.freeze does not freeze Map/Set entries. Keep those collections inside
|
|
417
|
-
// the preparation and expose only immutable values through reading closures.
|
|
418
405
|
const view = collection => Object.freeze({size: collection.size, has: key => collection.has(key),
|
|
419
406
|
...(collection instanceof Map ? {get: key => collection.get(key)} : {}),
|
|
420
407
|
keys: () => collection.keys(), values: () => collection.values(), entries: () => collection.entries(),
|
|
@@ -456,8 +443,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
456
443
|
frame.previous = previous?.copy;
|
|
457
444
|
frame.height = 0;
|
|
458
445
|
if (previous?.frozen) {
|
|
459
|
-
// A frozen shell fixes its descriptors, but every mutable child is still
|
|
460
|
-
// observed afresh. Fixed scalar fields need no second Unicode/data proof.
|
|
461
446
|
frame.frozen = frame.immutable = true;
|
|
462
447
|
frame.descriptors = previous.descriptors; frame.fields = previous.fields;
|
|
463
448
|
frame.out = null; frame.leaving = true; active.add(value); pending.push(frame);
|
|
@@ -467,8 +452,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
467
452
|
}
|
|
468
453
|
continue;
|
|
469
454
|
}
|
|
470
|
-
// Check frozen-ness before capturing descriptors. A caller freezing a mutable
|
|
471
|
-
// input later cannot retroactively certify an earlier view of its properties.
|
|
472
455
|
frame.frozen = frame.immutable = Object.isFrozen(value);
|
|
473
456
|
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
474
457
|
if (!array && !plainPrototype(proto)) throw Error('history_data');
|
|
@@ -535,12 +518,6 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
535
518
|
};
|
|
536
519
|
})();
|
|
537
520
|
|
|
538
|
-
// A ledger record as the journal and the Notes return validate it below: plain data, nothing
|
|
539
|
-
// undefined anywhere in it. A live record could carry a row whose type or order is not yet known (a
|
|
540
|
-
// block just inserted, before its first render: `type: undefined`), and the validator would refuse
|
|
541
|
-
// the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and
|
|
542
|
-
// as "Rapier could not be reopened: the document history could not be restored" on a return from
|
|
543
|
-
// Notes. The copy is made once per record; records are frozen, so it never goes stale.
|
|
544
521
|
const _rapierJournalRecord = (() => {
|
|
545
522
|
const copies = new WeakMap();
|
|
546
523
|
return record => {
|
|
@@ -551,10 +528,6 @@ const _rapierJournalRecord = (() => {
|
|
|
551
528
|
})();
|
|
552
529
|
|
|
553
530
|
function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) {
|
|
554
|
-
// Recovery records are data, not executable objects. Inspect descriptors without invoking
|
|
555
|
-
// accessors, and use an ancestor set (not a global seen set) so shared acyclic data stays valid.
|
|
556
|
-
// Iterative traversal refuses cycles without exhausting the call stack. Proxies/counters that
|
|
557
|
-
// throw are refused by the same gate rather than escaping into document recovery.
|
|
558
531
|
try {
|
|
559
532
|
if (!_rapierPlainLedgerData(record) || !Array.isArray(earlier)) return false;
|
|
560
533
|
const own = Array.isArray(record?.splices), lookup = own ? null : _rapierRecordIndex(earlier);
|
|
@@ -566,8 +539,6 @@ function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, max
|
|
|
566
539
|
} catch (_) { return false; }
|
|
567
540
|
}
|
|
568
541
|
|
|
569
|
-
// Both callers proved plain data themselves: the public single-row validator above,
|
|
570
|
-
// or the private owned-snapshot preparation. No caller-supplied trusted flag exists.
|
|
571
542
|
function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentBytes, utf8Length, allowGap,
|
|
572
543
|
{splices, metadata, semantic, duplicate}) {
|
|
573
544
|
try {
|
|
@@ -579,8 +550,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
|
|
|
579
550
|
if ((!own && !navigation) || (own && record.splices.length > 64) ||
|
|
580
551
|
(record.transaction?.reverts && record.transaction?.reapplies)) return false;
|
|
581
552
|
const metadataChanged = Object.values(metadata || {}).some(pair => pair.before !== pair.after);
|
|
582
|
-
// An identical concurrent replacement keeps its authored rows and an empty
|
|
583
|
-
// physical splice. The origin proof, not this gate, shows the effect is already present.
|
|
584
553
|
const coincidentWitness = !splices?.length && record.beforeHash === record.afterHash &&
|
|
585
554
|
record.authored && _rapierValidAuthored(record.authored) && record.authored.splices.some(row => row.removed.length);
|
|
586
555
|
if (metadata === undefined || (!splices?.length && !metadataChanged && !semantic && !coincidentWitness) ||
|
|
@@ -597,7 +566,6 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
|
|
|
597
566
|
const transaction = record.transaction;
|
|
598
567
|
const nullableText = (value, limit, empty = true) => value === null ||
|
|
599
568
|
(typeof value === 'string' && value.length <= limit && (empty || value.length > 0));
|
|
600
|
-
// Identity fields are bounded individually; durable history has no record-count limit.
|
|
601
569
|
const optional = (value, valid) => value === undefined || valid(value);
|
|
602
570
|
const identity = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
|
|
603
571
|
if (!transaction || typeof transaction.id !== 'string' || !transaction.id.length ||
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Paired source placement, shared by live editing and canonical-history proof.
|
|
3
2
|
import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
|
|
4
3
|
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
4
|
import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
|
|
@@ -25,8 +24,6 @@ function emit(rows, at, remove, insert = '') {
|
|
|
25
24
|
} else rows.push({at, remove, insert});
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Sweep original-source boundaries. Each counter names a coordinate in one input's
|
|
29
|
-
// resulting document; concurrent insertions never enter the other input's removals.
|
|
30
27
|
export function transformPair(left, right, leftFirst) {
|
|
31
28
|
admitted(left); admitted(right);
|
|
32
29
|
const points = [...new Set([0, ...left.flatMap(row => [row.at, row.at + row.remove]),
|
|
@@ -62,9 +59,6 @@ function applyBatch(text, rows) {
|
|
|
62
59
|
return {text: after, splices: exact};
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
// Transport one sequential canonical act and every crossed record together. The
|
|
66
|
-
// reciprocal path is checked against exact source, rather than only equal end text.
|
|
67
|
-
// Canonical transport refuses deleting any authored unit already removed elsewhere.
|
|
68
62
|
export function transposeRecord(base, splices, id, records) {
|
|
69
63
|
if (typeof base !== 'string' || !Array.isArray(splices) || typeof id !== 'string' || !Array.isArray(records))
|
|
70
64
|
throw new RangeError('canonical_transport_invalid');
|
|
@@ -75,10 +69,6 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
75
69
|
if (next === null) throw new RangeError('canonical_transport_invalid');
|
|
76
70
|
current = next;
|
|
77
71
|
}
|
|
78
|
-
// A concurrent act that already removed these same authored units and wrote the
|
|
79
|
-
// same replacement is one physical edit. This act stays in history with no second
|
|
80
|
-
// deletion. Pure insertions are never collapsed: each one is its own unit. An
|
|
81
|
-
// inverse of that act means the replacement is not still in force.
|
|
82
72
|
const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
|
|
83
73
|
row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
|
|
84
74
|
const matched = crossed.filter(record => sameRows(record.splices, splices));
|
|
@@ -122,8 +112,6 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
122
112
|
return {splices: moved, sourceIndices, against: crossed, text: current};
|
|
123
113
|
}
|
|
124
114
|
|
|
125
|
-
// Hosted and editor callers already own a reciprocal transport log. Convert only
|
|
126
|
-
// its admitted exact source to records, then prove canonical-unit preservation.
|
|
127
115
|
export function transposeSource(base, splices, client, log) {
|
|
128
116
|
let source = base;
|
|
129
117
|
const records = log.map(entry => {
|
|
@@ -135,8 +123,6 @@ export function transposeSource(base, splices, client, log) {
|
|
|
135
123
|
|
|
136
124
|
export {authoredBasis, authoredPlacement, authoredPlacements};
|
|
137
125
|
|
|
138
|
-
// Reconstruct an authored view from the same records, then prove its exact physical
|
|
139
|
-
// placement. Equality of resulting text alone cannot prove a repeated-text anchor.
|
|
140
126
|
function authoredReplay(base, records, append = null) {
|
|
141
127
|
const origins = new Map(), byId = new Map(), originalOrder = [], cache = new Map(), frontier = new Set();
|
|
142
128
|
const effects = _rapierHistoryEffects(append ? [...records, append] : records);
|
|
@@ -156,8 +142,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
156
142
|
if (!result.ok) throw new RangeError('canonical_origin_invalid');
|
|
157
143
|
return result;
|
|
158
144
|
};
|
|
159
|
-
// Only immutable text digests are reused, within this one replay. Every
|
|
160
|
-
// authored-source comparison and physical placement proof still runs.
|
|
161
145
|
const textHashes = new Map();
|
|
162
146
|
const hashText = text => {
|
|
163
147
|
if (typeof text !== 'string') return sha256(text);
|
|
@@ -166,7 +150,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
166
150
|
return digest;
|
|
167
151
|
};
|
|
168
152
|
const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
|
|
169
|
-
// Only owner-created ordered views of already admitted string IDs use this.
|
|
170
153
|
const sameIds = (a, b) => {
|
|
171
154
|
if (a.length !== b.length) return false;
|
|
172
155
|
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
@@ -192,8 +175,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
192
175
|
})()))) throw new RangeError('canonical_origin_invalid');
|
|
193
176
|
const refs = effects.references.get(id) || [];
|
|
194
177
|
if (refs.some(id => !seen.has(id))) throw new RangeError('canonical_origin_invalid');
|
|
195
|
-
// An explicit origin can prove the same linear placement as an implicit one.
|
|
196
|
-
// Preserve the origin, but do not reconstruct every growing prefix to prove it again.
|
|
197
178
|
if (linearPrefix === originalOrder.length && same(origin.basis, [...frontier].sort()) &&
|
|
198
179
|
same(origin.splices, record.splices) && (!Object.hasOwn(record, 'authored') || origin.source === sha256(source)))
|
|
199
180
|
linearPrefix++;
|
|
@@ -212,7 +193,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
212
193
|
const key = coverKey(origin);
|
|
213
194
|
if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
|
|
214
195
|
}
|
|
215
|
-
// Two inverses can name different acts and still withdraw one physical replacement.
|
|
216
196
|
const sharesCover = id => (effects.references.get(id) || []).some(ref => {
|
|
217
197
|
const key = coverKey(origins.get(ref));
|
|
218
198
|
return !!key && coverCounts.get(key) > 1;
|
|
@@ -250,14 +230,9 @@ function authoredReplay(base, records, append = null) {
|
|
|
250
230
|
return closure;
|
|
251
231
|
};
|
|
252
232
|
const canonicalOrder = ordered(originalOrder);
|
|
253
|
-
// Only the three immediate fresh-spread call sites use this entry. Nothing
|
|
254
|
-
// retains allocation trust after an array has crossed an observable method.
|
|
255
233
|
const literalKey = order => {
|
|
256
|
-
// An input iterator can supply arbitrary values. Preserve the whole-array
|
|
257
|
-
// depth/refusal path before any per-string encoding in that case.
|
|
258
234
|
for (let index = 0; index < order.length; index++)
|
|
259
235
|
if (typeof order[index] !== 'string') return canonicalJSON(order);
|
|
260
|
-
// Keep each canonical primitive encoder call and its serializer behavior.
|
|
261
236
|
return '[' + Array.from({length: order.length}, (_, index) => canonicalJSON(order[index])).join(',') + ']';
|
|
262
237
|
};
|
|
263
238
|
const materialize = (order, rawSubmitted = false) => materializeKey(order, canonicalJSON(order), rawSubmitted);
|
|
@@ -266,14 +241,10 @@ function authoredReplay(base, records, append = null) {
|
|
|
266
241
|
if (cache.has(key)) return cache.get(key);
|
|
267
242
|
let prefixEnds = null;
|
|
268
243
|
const prefixKey = length => {
|
|
269
|
-
// The original submitted array keeps its existing slice/species/data gate.
|
|
270
|
-
// Only this module's own order allocations use the validated key string.
|
|
271
244
|
if (rawSubmitted) return canonicalJSON(order.slice(0, length));
|
|
272
245
|
if (!prefixEnds) {
|
|
273
246
|
prefixEnds = [1];
|
|
274
247
|
let quoted = false, depth = 0;
|
|
275
|
-
// Scan canonical JSON only: an escaped code unit cannot end a string,
|
|
276
|
-
// and commas inside strings or nested values cannot split this array.
|
|
277
248
|
for (let at = 1; at < key.length - 1; at++) {
|
|
278
249
|
const code = key.charCodeAt(at);
|
|
279
250
|
if (quoted) {
|
|
@@ -290,11 +261,7 @@ function authoredReplay(base, records, append = null) {
|
|
|
290
261
|
};
|
|
291
262
|
let placed = [], ids = new Set(), sourceIndices = new Map(), text = base;
|
|
292
263
|
if (!order.length) { const empty = {text, records: [], sourceIndices}; cache.set(key, empty); return empty; }
|
|
293
|
-
// A proved linear prefix already is its authored view. Replay it once;
|
|
294
|
-
// reconstructing every growing prefix adds no evidence.
|
|
295
264
|
let prefix = 0;
|
|
296
|
-
// Resume only an exact ordered prefix already proved in this replay. Copy its
|
|
297
|
-
// mutable containers before extending it; transported rows retain their placement.
|
|
298
265
|
for (let length = order.length - 1; length > 0; length--) {
|
|
299
266
|
const prior = cache.get(prefixKey(length));
|
|
300
267
|
if (!prior) continue;
|
|
@@ -314,8 +281,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
314
281
|
throw new RangeError('canonical_origin_invalid');
|
|
315
282
|
const closure = closureOf(origin.basis);
|
|
316
283
|
if (closure.size === ids.size && [...closure].every(prior => ids.has(prior))) {
|
|
317
|
-
// The current view already is the complete authored context. There are no
|
|
318
|
-
// independent rows to cross, even when earlier acts were concurrent.
|
|
319
284
|
if (hashText(text) !== origin.source) throw new RangeError('canonical_origin_invalid');
|
|
320
285
|
const next = _rapierTransformSplices(text, origin.splices);
|
|
321
286
|
if (next === null) throw new RangeError('canonical_origin_invalid');
|
|
@@ -326,8 +291,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
326
291
|
}
|
|
327
292
|
const basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
|
|
328
293
|
if (hashText(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
|
|
329
|
-
// Reconstruct causal context before crossing it. Physical gaps may already
|
|
330
|
-
// have collapsed under a deletion; they cannot choose concurrent ordering.
|
|
331
294
|
const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = sameIds(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
|
|
332
295
|
if (context.text !== text) throw new RangeError('canonical_origin_invalid');
|
|
333
296
|
const alignedOrder = [...basisOrder, ...contextOrder.filter(prior => !closure.has(prior))];
|
|
@@ -336,10 +299,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
336
299
|
const crossed = aligned.records.slice(basisOrder.length);
|
|
337
300
|
let moved;
|
|
338
301
|
if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
|
|
339
|
-
// Each inverse retains its own targets. First prove the exact intention
|
|
340
|
-
// in its causal source, then place that choice in the accepted effect
|
|
341
|
-
// graph. Another inverse may already suppress the same original bytes,
|
|
342
|
-
// including when the two inverses name different authors of one replacement.
|
|
343
302
|
const authored = projectInverse(basis.text, basis.records, id);
|
|
344
303
|
if (!same(origin.splices, authored.splices) ||
|
|
345
304
|
_rapierTransformSplices(basis.text, origin.splices) !== authored.after)
|
|
@@ -357,7 +316,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
357
316
|
const physicalOrder = append ? originalOrder.slice(0, -1) : originalOrder, physical = materialize(physicalOrder);
|
|
358
317
|
if (physical.text !== source || physical.records.some((row, index) => !same(row.splices, records[index].splices)))
|
|
359
318
|
throw new RangeError('canonical_origin_invalid');
|
|
360
|
-
// Raw submission is selected here by a private call site, never an input flag.
|
|
361
319
|
return {source, origins, originalOrder, materialize, materializeLiteral,
|
|
362
320
|
materializeSubmitted: order => materialize(order, true), ordered};
|
|
363
321
|
}
|
|
@@ -365,14 +323,11 @@ function authoredReplay(base, records, append = null) {
|
|
|
365
323
|
export function authoredHistory(base, records, selected = null) {
|
|
366
324
|
const replay = authoredReplay(base, records);
|
|
367
325
|
if (!selected) return {text: replay.source, records, origins: replay.origins};
|
|
368
|
-
// Targets follow independent work only in this transient Undo projection.
|
|
369
326
|
const projected = replay.materialize(replay.ordered(replay.originalOrder, selected));
|
|
370
327
|
if (projected.text !== replay.source) throw new RangeError('canonical_origin_invalid');
|
|
371
328
|
return {...projected, origins: replay.origins};
|
|
372
329
|
}
|
|
373
330
|
|
|
374
|
-
// Semantic annotations use the original splice coordinates, even when an accepted
|
|
375
|
-
// physical placement split those rows. The same replay proves the causal basis first.
|
|
376
331
|
export function authoredSourceReader(base, records) {
|
|
377
332
|
const replay = authoredReplay(base, records);
|
|
378
333
|
return actId => {
|
|
@@ -390,8 +345,6 @@ export function authoredSourceReader(base, records) {
|
|
|
390
345
|
};
|
|
391
346
|
}
|
|
392
347
|
|
|
393
|
-
// The original view and the accepted view are both reconstructed from the same
|
|
394
|
-
// immutable acts. The reciprocal rows are an exact ACK, not another history.
|
|
395
348
|
export function transposeAuthored(base, records, incoming, submitted) {
|
|
396
349
|
if (!incoming.authored || !Array.isArray(submitted)) throw new RangeError('canonical_origin_invalid');
|
|
397
350
|
const replay = authoredReplay(base, records, incoming), id = incoming.transaction.id;
|
package/dist/page.mjs
CHANGED
|
@@ -5,16 +5,12 @@ import {wrap, carry, portableNeeds} from './wrap.mjs';
|
|
|
5
5
|
import {returnAddress, returnExpiresAt} from './return-address.mjs';
|
|
6
6
|
export {wrap, unwrap, carry, portableNeeds, encodeCarried, decodeCarried} from './wrap.mjs';
|
|
7
7
|
|
|
8
|
-
// The page this package carries, read once on first use.
|
|
9
8
|
let carried = null;
|
|
10
9
|
const carriedPage = () => carried ??= readFile(new URL('./rapier.html', import.meta.url), 'utf8');
|
|
11
|
-
// One call from Markdown to a page: the carried editor, cut to what the document displays with (and Draw for
|
|
12
|
-
// view 'draw' or a drawing), with the document inside. `include` names the parts instead: drawing, math, mermaid.
|
|
13
10
|
export async function writePage(text, name = 'document.md', {include, ...options} = {}) {
|
|
14
11
|
const carries = include ?? [...(options.view === 'draw' || options.drawing != null ? ['drawing'] : []), ...portableNeeds(String(text), {base: options.base})];
|
|
15
12
|
return wrap(carry(await carriedPage(), carries), text, name, options);
|
|
16
13
|
}
|
|
17
|
-
// Unknown options, a bare --drawing or --compare and a third name are refused.
|
|
18
14
|
const USAGE = 'usage: rapier-html <document.md> [out.html] [--include draw,math,mermaid] [--view draw] [--drawing sketch.svg] [--compare original.md [--by <your name>]] [--return <url> --return-expires-at <timestamp>]\n' +
|
|
19
15
|
' writes <document>.rapier.html beside the file: Rapier with the document inside it, one file, offline.\n' +
|
|
20
16
|
' It never overwrites: an output that already exists, the document itself included, is refused.\n' +
|
|
@@ -28,7 +24,6 @@ const USAGE = 'usage: rapier-html <document.md> [out.html] [--include draw,math,
|
|
|
28
24
|
' --return-expires-at carries the same mint\'s expiry; Share offers Save once the return expires\n' +
|
|
29
25
|
' -- everything after it is a filename, even one that starts with a dash\n' +
|
|
30
26
|
' --help this text';
|
|
31
|
-
// Damaged UTF-8 is refused, never carried as U+FFFD; a BOM is carried.
|
|
32
27
|
const text = async path => { try { return new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}).decode(await readFile(path)); } catch (error) { throw error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA' ? new Error(path + ' is not valid UTF-8') : error; } };
|
|
33
28
|
|
|
34
29
|
export async function main(argv, pagesDir, {log = console.log} = {}) {
|
|
@@ -83,7 +78,6 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
|
|
|
83
78
|
if (view) wrapOptions.view = view;
|
|
84
79
|
if (address) wrapOptions.return = address;
|
|
85
80
|
if (expiry) wrapOptions.return_expires_at = expiry;
|
|
86
|
-
// Always a new file; 'wx' refuses any existing path, links included.
|
|
87
81
|
const carries = include || [...(view === 'draw' || drawingPath ? ['drawing'] : []), ...portableNeeds(source, {base: wrapOptions.base})];
|
|
88
82
|
const html = wrap(carry(page, carries), source, name, wrapOptions);
|
|
89
83
|
try { await writeFile(out, html, {flag: 'wx'}); }
|