rapier-markdown-kit 1.1.101 → 1.1.104
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/LICENSE +2 -2
- package/README.md +213 -53
- package/dist/agent/vendor/hyphen-en-us/LICENSE +14 -0
- package/dist/agent/vendor/hyphen-en-us/SOURCE.json +33 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.hyp.txt +14 -0
- package/dist/agent/vendor/hyphen-en-us/hyph-en-us.pat.txt +4938 -0
- package/dist/agent/vendor/hyphen-en-us/patterns.js +25 -0
- package/dist/agent/vendor/pretext/LICENSE +208 -0
- package/dist/agent/vendor/pretext/NOTICE +26 -13
- package/dist/agent/vendor/pretext/SOURCE.json +213 -45
- package/dist/agent/vendor/pretext/analysis.js +675 -1224
- package/dist/agent/vendor/pretext/entry-geometry.js +183 -0
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +688 -0
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +57 -0
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +80 -0
- package/dist/agent/vendor/pretext/graphemes.js +98 -0
- package/dist/agent/vendor/pretext/han-kerning.js +253 -0
- package/dist/agent/vendor/pretext/layout.js +352 -290
- package/dist/agent/vendor/pretext/line-break.js +1154 -588
- package/dist/agent/vendor/pretext/line-breaks.js +892 -0
- package/dist/agent/vendor/pretext/line-text.js +81 -39
- package/dist/agent/vendor/pretext/measurement.js +1105 -176
- package/dist/agent/vendor/pretext/prepare.js +807 -0
- package/dist/agent/vendor/pretext/rich-inline.js +1242 -273
- package/dist/agent/will.mjs +2 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/run.mjs +4 -4
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +9 -7
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +166 -53
- package/dist/kit/ledger/hash.mjs +75 -18
- package/dist/kit/ledger/history.mjs +143 -115
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/render-markdown.mjs +259 -122
- package/dist/kit/render-sanitize.mjs +24 -4
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +101 -65
- package/dist/kit/typography.mjs +2 -0
- package/dist/layout/hyphenation.mjs +127 -0
- package/dist/layout/line-plan.mjs +88 -96
- package/dist/layout/model.mjs +61 -80
- package/dist/layout/paragraph-fit.mjs +191 -0
- package/dist/layout/preparation.mjs +373 -0
- package/dist/layout/prepared-paragraph.mjs +223 -0
- package/dist/layout/typography.mjs +345 -111
- package/dist/packages/rapier-diff/index.mjs +288 -196
- package/dist/spec/document-fonts.mjs +13 -4
- package/dist/spec/md-assets.mjs +149 -30
- package/dist/spec/md-layout.mjs +5 -2
- package/dist/spec/md-marks.mjs +218 -3
- package/dist/spec/md-source.mjs +122 -0
- package/package.json +2 -1
- package/style.css +9 -0
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
package/dist/kit/ledger/hash.mjs
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
|
|
3
3
|
export function textRoot(text) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
return textRootParts([text]);
|
|
5
|
+
}
|
|
6
|
+
// The same UTF-16 root over shared source pieces, without joining a historical body.
|
|
7
|
+
export function textRootParts(parts) {
|
|
8
|
+
let fnv = 0x811c9dc5, a = 1, b = 0, length = 0;
|
|
9
|
+
for (const text of parts) {
|
|
10
|
+
length += text.length;
|
|
11
|
+
for (let i = 0; i < text.length;) {
|
|
12
|
+
const end = Math.min(text.length, i + 4096);
|
|
13
|
+
for (; i < end; i++) { const c = text.charCodeAt(i); fnv = Math.imul(fnv ^ c, 16777619); a += c; b += a; }
|
|
14
|
+
a %= 65521; b %= 65521;
|
|
15
|
+
}
|
|
8
16
|
}
|
|
9
|
-
return
|
|
17
|
+
return length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
|
|
10
18
|
}
|
|
11
19
|
export function rootAfter(root, row) {
|
|
12
20
|
return textRoot(String(root || '') + ':' + row.pos + ':' + textRoot(row.removed) + ':' + textRoot(row.inserted));
|
|
@@ -22,26 +30,75 @@ const K = new Uint32Array([
|
|
|
22
30
|
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
23
31
|
]);
|
|
24
32
|
const rotate = (x, n) => (x >>> n) | (x << (32 - n));
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < 16; i++)
|
|
33
|
+
// A bounded encoder and one compression block. No full UTF-8 or padded-input copy.
|
|
34
|
+
// Updates retain a trailing high surrogate so an arbitrary string boundary stays exact.
|
|
35
|
+
export function createSHA256() {
|
|
36
|
+
const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
|
|
37
|
+
const w = new Uint32Array(64), block = new Uint8Array(64), encoded = new Uint8Array(65536);
|
|
38
|
+
const encoder = new TextEncoder();
|
|
39
|
+
let used = 0, bytes = 0, high = '', finished = false;
|
|
40
|
+
const compress = (input, offset) => {
|
|
41
|
+
for (let i = 0; i < 16; i++) {
|
|
42
|
+
const at = offset + i * 4;
|
|
43
|
+
w[i] = (input[at] << 24) | (input[at + 1] << 16) | (input[at + 2] << 8) | input[at + 3];
|
|
44
|
+
}
|
|
34
45
|
for (let i = 16; i < 64; i++) {
|
|
35
46
|
const x = w[i - 15], y = w[i - 2];
|
|
36
47
|
w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
|
|
37
48
|
}
|
|
38
|
-
let [
|
|
49
|
+
let a=h[0], b=h[1], c=h[2], d=h[3], e=h[4], f=h[5], g=h[6], k=h[7];
|
|
39
50
|
for (let i = 0; i < 64; i++) {
|
|
40
51
|
const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
41
52
|
const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
42
53
|
k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
43
54
|
}
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
55
|
+
h[0]+=a; h[1]+=b; h[2]+=c; h[3]+=d; h[4]+=e; h[5]+=f; h[6]+=g; h[7]+=k;
|
|
56
|
+
};
|
|
57
|
+
const feed = (input, size) => {
|
|
58
|
+
bytes += size;
|
|
59
|
+
let at = 0;
|
|
60
|
+
if (used) {
|
|
61
|
+
const take = Math.min(64 - used, size);
|
|
62
|
+
block.set(input.subarray(0, take), used); used += take; at = take;
|
|
63
|
+
if (used === 64) { compress(block, 0); used = 0; }
|
|
64
|
+
}
|
|
65
|
+
for (; at + 64 <= size; at += 64) compress(input, at);
|
|
66
|
+
if (at < size) { block.set(input.subarray(at, size), 0); used = size - at; }
|
|
67
|
+
};
|
|
68
|
+
const write = text => {
|
|
69
|
+
for (let at = 0; at < text.length;) {
|
|
70
|
+
// Bound both the substring and byte allocation; encodeInto never cuts a scalar.
|
|
71
|
+
let end = Math.min(text.length, at + 32768);
|
|
72
|
+
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
73
|
+
const part = encoder.encodeInto(text.slice(at, end), encoded);
|
|
74
|
+
feed(encoded, part.written); at += part.read;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
update(value) {
|
|
79
|
+
if (finished) throw new Error('SHA-256 is finished');
|
|
80
|
+
let text = high + String(value === undefined ? '' : value); high = '';
|
|
81
|
+
if (text.length && (text.charCodeAt(text.length - 1) & 0xfc00) === 0xd800) { high = text.slice(-1); text = text.slice(0, -1); }
|
|
82
|
+
write(text); return this;
|
|
83
|
+
},
|
|
84
|
+
digest() {
|
|
85
|
+
if (finished) throw new Error('SHA-256 is finished');
|
|
86
|
+
if (high) { write(high); high = ''; }
|
|
87
|
+
finished = true;
|
|
88
|
+
const bits = bytes * 8;
|
|
89
|
+
block[used++] = 0x80; block.fill(0, used);
|
|
90
|
+
if (used > 56) { compress(block, 0); block.fill(0); }
|
|
91
|
+
const view = new DataView(block.buffer);
|
|
92
|
+
view.setUint32(56, Math.floor(bits / 0x100000000)); view.setUint32(60, bits >>> 0);
|
|
93
|
+
compress(block, 0);
|
|
94
|
+
return Array.from(h, x => x.toString(16).padStart(8, '0')).join('');
|
|
95
|
+
},
|
|
96
|
+
get bytes() { return bytes; },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function sha256Parts(parts) {
|
|
100
|
+
const hash = createSHA256();
|
|
101
|
+
for (const part of parts) hash.update(part);
|
|
102
|
+
return hash.digest();
|
|
47
103
|
}
|
|
104
|
+
export function sha256(text) { return createSHA256().update(text).digest(); }
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Transient projections and selective inverses of the existing canonical journal.
|
|
3
|
-
import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata} from './journal-records.mjs';
|
|
4
|
-
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
|
-
import {
|
|
3
|
+
import {_rapierTransformSplices, _rapierRecordSplices, _rapierRecordMetadata, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierReplayMetadata, _rapierPrepareHistoryRecords} from './journal-records.mjs';
|
|
4
|
+
import {projectInverseSource, HistorySource} from './inverse-source.mjs';
|
|
5
|
+
import {textRootParts, rootAfter} from './hash.mjs';
|
|
6
6
|
import {authoredHistory} from './transport.mjs';
|
|
7
7
|
import {plainData} from './data.mjs';
|
|
8
8
|
|
|
@@ -12,16 +12,16 @@ const historyBoundary = (source, at) => historyInteger(at) && at <= source.lengt
|
|
|
12
12
|
(source.charCodeAt(at - 1) & 0xFC00) === 0xD800 && (source.charCodeAt(at) & 0xFC00) === 0xDC00);
|
|
13
13
|
|
|
14
14
|
function historyAct(record, effects = null) {
|
|
15
|
-
const tx = record.transaction;
|
|
16
|
-
return {id: tx.id, actor:
|
|
17
|
-
operation: tx.operation || 'document.change',
|
|
15
|
+
const tx = record.transaction, copy = value => value && typeof value === 'object' ? plainData(value) : value;
|
|
16
|
+
return {id: tx.id, actor: Object.fromEntries(Object.entries(tx.actor).map(([key, value]) => [key, copy(value)])), createdAt: tx.createdAt, baseRevision: tx.baseRevision, revision: tx.revision,
|
|
17
|
+
operation: copy(tx.operation) || 'document.change',
|
|
18
18
|
...(tx.turnId ? {turnId: tx.turnId} : {}),
|
|
19
|
-
...(tx.contribution ? {contribution: tx.contribution} : {}),
|
|
19
|
+
...(tx.contribution ? {contribution: copy(tx.contribution)} : {}),
|
|
20
20
|
...(tx.sourceTransactionId ? {sourceTransactionId: tx.sourceTransactionId} : {}),
|
|
21
21
|
...(tx.sourceTransactionIds?.length ? {sourceTransactionIds: tx.sourceTransactionIds.slice()} : {}),
|
|
22
22
|
...(tx.reverts ? {reverts: tx.reverts} : {}), ...(tx.reapplies ? {reapplies: tx.reapplies} : {}),
|
|
23
|
-
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: record.changeSet.label} : {}),
|
|
24
|
-
...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.
|
|
23
|
+
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {label: copy(record.changeSet.label)} : {}),
|
|
24
|
+
...(Array.isArray(tx.affectedBlockIds) ? {affectedBlockIds: tx.affectedBlockIds.map(copy)} : {}),
|
|
25
25
|
...(record.metadata ? {metadata: plainData(record.metadata)} : {}),
|
|
26
26
|
...(effects ? {active: effects.active.has(tx.id)} : {})};
|
|
27
27
|
}
|
|
@@ -74,76 +74,75 @@ export function pictureHistorySplices({source, records}, target, redo) {
|
|
|
74
74
|
try { return plan(true); } catch (_) { return null; }
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
for (let index = splices.length - 1; index >= 0; index--) {
|
|
82
|
-
const row = splices[index];
|
|
83
|
-
if (!row || !historyInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
|
|
84
|
-
row.pos > value.length || row.inserted.length > value.length - row.pos ||
|
|
85
|
-
value.slice(row.pos, row.pos + row.inserted.length) !== row.inserted) return null;
|
|
86
|
-
value = value.slice(0, row.pos) + row.removed + value.slice(row.pos + row.inserted.length);
|
|
87
|
-
}
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// This is a projection of the existing records. No source snapshots or provenance survive the call.
|
|
77
|
+
// Keep only the latest source observation. Inactive live documents must not retain
|
|
78
|
+
// every revision's source tree. The weak key also lets the latest input be collected.
|
|
79
|
+
// Mutable caller data is compared/captured before this cache can be consulted.
|
|
80
|
+
let preparedHistories = new WeakMap();
|
|
92
81
|
function prepareHistory(input) {
|
|
93
82
|
try {
|
|
94
83
|
if (!input || typeof input.source !== 'string' || !Array.isArray(input.records) || !_rapierValidMetadata(input.metadata)) return historyFailure('history_invalid');
|
|
95
|
-
const source = input.source,
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
const source = input.source, metadata = {...input.metadata}, index = _rapierPrepareHistoryRecords(input.records);
|
|
85
|
+
if (!index.ok) return historyFailure('history_invalid');
|
|
86
|
+
const {records, effects} = index;
|
|
98
87
|
const revision = input.revision ?? records.at(-1)?.transaction?.revision ?? 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
88
|
+
let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision;
|
|
89
|
+
if (!historyInteger(revision) || !historyInteger(prior)) return historyFailure('history_invalid');
|
|
90
|
+
const earliest = prior, suppliedComplete = input.complete !== false, cached = preparedHistories.get(records);
|
|
91
|
+
if (cached && cached.source === source && cached.revision === revision && cached.earliest === earliest &&
|
|
92
|
+
cached.suppliedComplete === suppliedComplete && cached.metadata.filename === metadata.filename && cached.metadata.docKind === metadata.docKind) return cached;
|
|
93
|
+
let complete = prior === 0;
|
|
94
|
+
const rows = [], byId = new Map();
|
|
95
|
+
for (const owned of index.rows) {
|
|
96
|
+
const {record, splices, metadata: effect} = owned, tx = record?.transaction, actor = tx?.actor;
|
|
97
|
+
if (!tx || typeof tx.id !== 'string' || !tx.id || byId.has(tx.id) || !historyInteger(tx.baseRevision) ||
|
|
105
98
|
!historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
|
|
106
99
|
!actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
|
|
107
100
|
!(tx.createdAt === null || historyInteger(tx.createdAt)) ||
|
|
108
101
|
(tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
|
|
109
102
|
if (tx.baseRevision !== prior) complete = false;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rows.push(
|
|
103
|
+
if (!splices || effect === undefined || (!splices.length &&
|
|
104
|
+
!Object.values(effect || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
|
|
105
|
+
const row = {record, splices, metadata: effect};
|
|
106
|
+
rows.push(row); byId.set(tx.id, row); prior = tx.revision;
|
|
114
107
|
}
|
|
115
108
|
if (prior !== revision) return historyFailure('history_unavailable');
|
|
116
|
-
let
|
|
117
|
-
for (let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
let initial = new HistorySource(source), initialMetadata = {...metadata};
|
|
110
|
+
for (let at = rows.length - 1; at >= 0; at--) {
|
|
111
|
+
const row = rows[at];
|
|
112
|
+
row.afterSource = initial; row.afterMetadata = initialMetadata;
|
|
113
|
+
initial = initial.before(row.splices);
|
|
114
|
+
initialMetadata = _rapierTransformMetadata(initialMetadata, row.metadata, true);
|
|
115
|
+
if (!initial || !initialMetadata) return historyFailure('history_invalid');
|
|
116
|
+
row.beforeSource = initial; row.beforeMetadata = initialMetadata;
|
|
121
117
|
}
|
|
122
|
-
const earliestRevision =
|
|
123
|
-
let
|
|
118
|
+
const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
|
|
119
|
+
let root = earliestRevision === 0 ? textRootParts(initial.parts()) : null, rootRevision = earliestRevision;
|
|
124
120
|
for (const {record, splices} of rows) {
|
|
125
121
|
const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
|
|
126
122
|
if (hashed && (![record.beforeHash, record.afterHash].every(value =>
|
|
127
123
|
typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value)) ||
|
|
128
|
-
(splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash)))
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
// omitted neutral pairs change that root, so a stated revision gap begins a new chain.
|
|
124
|
+
(splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
|
|
125
|
+
// Revision gaps begin a new path root; a continuous revision-zero prefix
|
|
126
|
+
// still binds every untouched source byte through the initial text root.
|
|
132
127
|
if (record.transaction.baseRevision !== rootRevision) root = null;
|
|
133
128
|
if (hashed) {
|
|
134
129
|
if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
|
|
135
130
|
root = record.beforeHash;
|
|
136
131
|
}
|
|
137
|
-
|
|
138
|
-
if (replay === null) return historyFailure('history_invalid');
|
|
139
|
-
if (root !== null) for (const row of splices) root = rootAfter(root, row);
|
|
132
|
+
if (root !== null) for (const splice of splices) root = rootAfter(root, splice);
|
|
140
133
|
if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
|
|
141
134
|
rootRevision = record.transaction.revision;
|
|
142
135
|
}
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
136
|
+
let initialText;
|
|
137
|
+
const initialSource = () => initialText ??= initial.slice();
|
|
138
|
+
if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource(), rows.map(({record, splices}) => ({...record, splices})));
|
|
139
|
+
const replayedMetadata = index.replayMetadata(initialMetadata);
|
|
140
|
+
if (!replayedMetadata || ['filename', 'docKind'].some(key => replayedMetadata[key] !== metadata[key])) return historyFailure('history_invalid');
|
|
141
|
+
const resolvedRecords = Object.freeze(rows.map(({record, splices}) => record.splices === splices ? record : Object.freeze({...record, splices})));
|
|
142
|
+
const prepared = {ok: true, source, records: resolvedRecords, get initialSource() { return initialSource(); }, initial, metadata, initialMetadata, effects, rows, byId,
|
|
143
|
+
revision, earliest, suppliedComplete, earliestRevision, complete: suppliedComplete && complete && earliestRevision === 0};
|
|
144
|
+
preparedHistories = new WeakMap([[records, prepared]]);
|
|
145
|
+
return prepared;
|
|
147
146
|
} catch (_) { return historyFailure('history_invalid'); }
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -152,23 +151,17 @@ export function replayHistory(input) {
|
|
|
152
151
|
if (!prepared.ok) return prepared;
|
|
153
152
|
const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
|
|
154
153
|
const acts = rows.filter(({record}) => !effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record, effects));
|
|
155
|
-
return {ok: true, source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, acts,
|
|
154
|
+
return {ok: true, source, initialSource, metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
|
|
156
155
|
effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
export function sourceBefore(input, actId) {
|
|
160
159
|
const prepared = prepareHistory(input);
|
|
161
160
|
if (!prepared.ok) return prepared;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (source === null) return historyFailure('history_invalid');
|
|
167
|
-
metadata = _rapierTransformMetadata(metadata, prepared.rows[index].metadata, true);
|
|
168
|
-
if (record.transaction.id === actId) return {ok: true, source, metadata, revision: record.transaction.baseRevision,
|
|
169
|
-
act: historyAct(record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
|
|
170
|
-
}
|
|
171
|
-
return historyFailure('act_unavailable');
|
|
161
|
+
const row = prepared.byId.get(actId);
|
|
162
|
+
if (!row) return historyFailure('act_unavailable');
|
|
163
|
+
return {ok: true, source: row.beforeSource.slice(), metadata: {...row.beforeMetadata}, revision: row.record.transaction.baseRevision,
|
|
164
|
+
act: historyAct(row.record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
function historyRangeBefore(range, splice) {
|
|
@@ -186,21 +179,23 @@ function historyRangeBefore(range, splice) {
|
|
|
186
179
|
|
|
187
180
|
function projectHistoryRange(prepared, from, to, actId) {
|
|
188
181
|
if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
|
|
189
|
-
let
|
|
182
|
+
let range = {from, to}, before = null;
|
|
190
183
|
const acts = [];
|
|
191
184
|
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
192
|
-
const {record, splices} = prepared.rows[index],
|
|
185
|
+
const {record, splices, beforeSource, afterSource} = prepared.rows[index], afterRange = range;
|
|
193
186
|
let touched = false;
|
|
194
187
|
for (let at = splices.length - 1; at >= 0; at--) {
|
|
195
188
|
const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
|
|
196
189
|
range = {from: mapped.from, to: mapped.to};
|
|
197
190
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
acts.push({...historyAct(record, prepared.effects), before: previous,
|
|
191
|
+
const include = !prepared.effects.pictures.has(record.transaction.id) &&
|
|
192
|
+
(touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length);
|
|
193
|
+
if (include || record.transaction.id === actId) {
|
|
194
|
+
const previous = {...range, source: beforeSource.slice(range.from, range.to)};
|
|
195
|
+
if (record.transaction.id === actId) before = previous;
|
|
196
|
+
if (include) acts.push({...historyAct(record, prepared.effects), before: previous,
|
|
197
|
+
after: {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)}});
|
|
198
|
+
}
|
|
204
199
|
}
|
|
205
200
|
if (actId != null && !before) return historyFailure('act_unavailable');
|
|
206
201
|
acts.reverse();
|
|
@@ -220,10 +215,47 @@ export function historyProjections(input, ranges) {
|
|
|
220
215
|
if (!prepared.ok) return prepared;
|
|
221
216
|
if (!Array.isArray(ranges)) return historyFailure('range_invalid');
|
|
222
217
|
return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
|
|
223
|
-
|
|
218
|
+
projectHistoryRange(prepared, from, to, actId))};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Block intervals stay ordered under reverse splices. A fixed balanced tree can
|
|
222
|
+
// shift an untouched suffix lazily and visit only intervals crossing the edit.
|
|
223
|
+
function historyBlockIndex(projected) {
|
|
224
|
+
const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
|
|
225
|
+
.sort((a, b) => a.range.from - b.range.from);
|
|
226
|
+
const refresh = node => {
|
|
227
|
+
node.minFrom = node.left?.minFrom ?? node.range.from;
|
|
228
|
+
node.maxTo = Math.max(node.range.to, node.left?.maxTo ?? -1, node.right?.maxTo ?? -1);
|
|
229
|
+
};
|
|
230
|
+
const build = (from, to, parent = null) => {
|
|
231
|
+
if (from === to) return null;
|
|
232
|
+
const mid = Math.floor((from + to) / 2), node = items[mid];
|
|
233
|
+
node.parent = parent; node.left = build(from, mid, node); node.right = build(mid + 1, to, node);
|
|
234
|
+
refresh(node); return node;
|
|
235
|
+
};
|
|
236
|
+
const root = build(0, items.length);
|
|
237
|
+
const shift = (node, delta) => {
|
|
238
|
+
if (!node || !delta) return;
|
|
239
|
+
node.range = {from: node.range.from + delta, to: node.range.to + delta};
|
|
240
|
+
node.minFrom += delta; node.maxTo += delta; node.shift += delta;
|
|
241
|
+
};
|
|
242
|
+
const move = (node, splice, touch) => {
|
|
243
|
+
if (!node || node.maxTo < splice.pos) return;
|
|
244
|
+
const end = splice.pos + splice.inserted.length;
|
|
245
|
+
if (node.minFrom > end) { shift(node, splice.removed.length - splice.inserted.length); return; }
|
|
246
|
+
if (node.shift) { shift(node.left, node.shift); shift(node.right, node.shift); node.shift = 0; }
|
|
247
|
+
const mapped = historyRangeBefore(node.range, splice);
|
|
248
|
+
if (mapped.touched) touch(node);
|
|
249
|
+
node.range = {from: mapped.from, to: mapped.to};
|
|
250
|
+
move(node.left, splice, touch); move(node.right, splice, touch); refresh(node);
|
|
251
|
+
};
|
|
252
|
+
return {move: (splice, touch) => move(root, splice, touch), range(node) {
|
|
253
|
+
let delta = 0;
|
|
254
|
+
for (let parent = node.parent; parent; parent = parent.parent) delta += parent.shift;
|
|
255
|
+
return {from: node.range.from + delta, to: node.range.to + delta};
|
|
256
|
+
}};
|
|
224
257
|
}
|
|
225
258
|
|
|
226
|
-
// Project all reading places in one replay. Typing only invalidates this transient view.
|
|
227
259
|
export function historyPlaces(input, blocks) {
|
|
228
260
|
const prepared = prepareHistory(input);
|
|
229
261
|
if (!prepared.ok) return prepared;
|
|
@@ -231,47 +263,43 @@ export function historyPlaces(input, blocks) {
|
|
|
231
263
|
!historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
|
|
232
264
|
return historyFailure('range_invalid');
|
|
233
265
|
const projected = blocks.map(block => ({id: block.id, from: block.from, to: block.to,
|
|
234
|
-
source: prepared.source.slice(block.from, block.to),
|
|
235
|
-
|
|
266
|
+
source: prepared.source.slice(block.from, block.to), acts: []}));
|
|
267
|
+
const ranges = historyBlockIndex(projected);
|
|
236
268
|
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
237
|
-
const {record, splices} = prepared.rows[index],
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
text = before;
|
|
250
|
-
}
|
|
251
|
-
const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
|
|
252
|
-
const present = new Map(), effects = new Map();
|
|
253
|
-
for (const act of acts) {
|
|
254
|
-
const changes = [];
|
|
255
|
-
const set = (id, value) => {
|
|
256
|
-
const prior = present.get(id) === true;
|
|
257
|
-
present.set(id, value); changes.push({id, before: prior, after: value});
|
|
258
|
-
};
|
|
259
|
-
if (act.reapplies) {
|
|
260
|
-
for (const change of effects.get(act.reapplies) || [{id: act.reapplies, after: true}]) set(change.id, change.after);
|
|
261
|
-
} else {
|
|
262
|
-
const ids = act.sourceTransactionIds || (act.sourceTransactionId ? [act.sourceTransactionId] : act.reverts ? [act.reverts] : []);
|
|
263
|
-
for (const id of ids) {
|
|
264
|
-
for (const change of (effects.get(id) || [{id, before: false, after: true}]).slice().reverse()) {
|
|
265
|
-
if (present.get(change.id) === change.after) set(change.id, change.before);
|
|
269
|
+
const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
|
|
270
|
+
for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
|
|
271
|
+
if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
|
|
272
|
+
// Earlier splices in this reverse walk only shifted this interval. Recover
|
|
273
|
+
// its coordinates at the record's after-state before recording the first touch.
|
|
274
|
+
let after = {...node.range};
|
|
275
|
+
for (let prior = at + 1; prior < splices.length; prior++) {
|
|
276
|
+
const row = splices[prior];
|
|
277
|
+
if (after.from >= row.pos + row.removed.length) {
|
|
278
|
+
const delta = row.inserted.length - row.removed.length;
|
|
279
|
+
after = {from: after.from + delta, to: after.to + delta};
|
|
266
280
|
}
|
|
267
281
|
}
|
|
282
|
+
touched.set(node, after);
|
|
283
|
+
});
|
|
284
|
+
for (const [node, afterRange] of touched) {
|
|
285
|
+
const beforeRange = ranges.range(node), previous = {...beforeRange, source: beforeSource.slice(beforeRange.from, beforeRange.to)},
|
|
286
|
+
after = {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)};
|
|
287
|
+
if (previous.source !== after.source) node.block.acts.push({...historyAct(record), before: previous, after});
|
|
268
288
|
}
|
|
269
|
-
set(act.id, true); effects.set(act.id, changes);
|
|
270
289
|
}
|
|
271
|
-
|
|
290
|
+
|
|
291
|
+
const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
|
|
292
|
+
// Native Redo consumes its Undo as navigation while keeping that receipt in
|
|
293
|
+
// the reading history. All material withdrawals use the canonical blockers,
|
|
294
|
+
// including independent inverses holding the same original act suppressed.
|
|
295
|
+
const reversed = new Set();
|
|
296
|
+
for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
|
|
297
|
+
for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
|
|
298
|
+
|
|
299
|
+
for (const block of projected) block.acts.reverse();
|
|
272
300
|
return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
|
|
273
301
|
earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
|
|
274
|
-
reversed: acts.filter(act =>
|
|
302
|
+
reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
|
|
275
303
|
}
|
|
276
304
|
|
|
277
305
|
// Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
|
|
@@ -282,26 +310,26 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
282
310
|
if (!options || typeof options !== 'object' || Array.isArray(options) ||
|
|
283
311
|
(options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
|
|
284
312
|
(options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
|
|
285
|
-
const requested = new Set(actIds), available =
|
|
313
|
+
const requested = new Set(actIds), available = prepared.byId;
|
|
286
314
|
if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
|
|
287
315
|
const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
|
|
288
|
-
const records = prepared.
|
|
316
|
+
const records = prepared.records;
|
|
289
317
|
let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
|
|
290
318
|
const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
|
|
291
319
|
const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
|
|
292
320
|
const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
|
|
293
321
|
if (!afterMetadata) return historyFailure('history_invalid');
|
|
294
322
|
const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
|
|
295
|
-
const
|
|
323
|
+
const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
296
324
|
prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
|
|
297
|
-
if (!
|
|
325
|
+
if (!sourceEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
|
|
298
326
|
sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
|
|
299
327
|
superseded: [], overlaps: []};
|
|
300
328
|
try {
|
|
301
329
|
const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
|
|
302
330
|
const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
|
|
303
331
|
sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
|
|
304
|
-
const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
|
|
332
|
+
const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
|
|
305
333
|
if (!result.ok) return result;
|
|
306
334
|
return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
|
|
307
335
|
effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
|