rapier-markdown-kit 1.1.102 → 1.2.3
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 +5 -2
- package/README.md +245 -54
- 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/images/gif.mjs +47 -0
- package/dist/images/playback.mjs +101 -0
- package/dist/kit/assets.mjs +5 -1
- package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
- package/dist/kit/conformance/04-wrap-box.md +1 -1
- package/dist/kit/conformance/05-behind.md +1 -1
- package/dist/kit/conformance/06-front.md +1 -1
- package/dist/kit/conformance/07-rotate-raster.md +1 -1
- package/dist/kit/conformance/08-drawing-rotation.md +1 -1
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
- package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
- package/dist/kit/conformance/10-both-sides.md +1 -1
- package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
- package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
- package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
- package/dist/kit/conformance/README.md +19 -28
- package/dist/kit/conformance/run.mjs +13 -11
- package/dist/kit/export-fonts.mjs +26 -11
- package/dist/kit/index.mjs +1 -0
- package/dist/kit/ledger/carried.mjs +17 -10
- package/dist/kit/ledger/data.mjs +73 -13
- package/dist/kit/ledger/format.mjs +176 -54
- package/dist/kit/ledger/hash.mjs +91 -18
- package/dist/kit/ledger/history.mjs +156 -116
- package/dist/kit/ledger/inverse-source.mjs +158 -19
- package/dist/kit/ledger/journal-records.mjs +292 -67
- package/dist/kit/ledger/merge.mjs +4 -3
- package/dist/kit/model.mjs +1 -1
- package/dist/kit/pdf-glyphs.mjs +4 -4
- package/dist/kit/pdf-vectors.mjs +4 -1
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +400 -148
- package/dist/kit/render-print.mjs +131 -55
- package/dist/kit/render-sanitize.mjs +30 -7
- package/dist/kit/render-work.mjs +99 -20
- package/dist/kit/render.mjs +133 -140
- 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 +71 -88
- 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 +359 -224
- package/dist/spec/document-fonts.mjs +14 -5
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +178 -36
- package/dist/spec/md-layout.mjs +13 -2
- package/dist/spec/md-marks.mjs +220 -127
- package/dist/spec/md-source.mjs +157 -0
- package/package.json +2 -1
- package/style.css +15 -50
- package/dist/agent/vendor/pretext/bidi.js +0 -151
- package/dist/agent/vendor/pretext/generated/bidi-data.js +0 -3831
|
@@ -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,
|
|
4
|
-
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
|
-
import {
|
|
3
|
+
import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _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
|
}
|
|
@@ -43,11 +43,15 @@ export function groupHistoryActs(acts) {
|
|
|
43
43
|
|
|
44
44
|
// A picture's codec replacement travels with its original navigation step.
|
|
45
45
|
// Match every complete replacement exactly before moving any source bytes.
|
|
46
|
-
export function pictureHistorySplices(
|
|
46
|
+
export function pictureHistorySplices(input, target, redo) {
|
|
47
|
+
const {records} = input;
|
|
47
48
|
const id = target?.transaction?.id, at = records.indexOf(target);
|
|
48
49
|
if (!id || at < 0 || target.transaction.operation === 'document.finish-picture' || !Array.isArray(target.splices)) return null;
|
|
49
50
|
const later = records.slice(at + 1).filter(row => row?.transaction?.operation === 'document.finish-picture' && Array.isArray(row.splices) && row.splices.length);
|
|
50
51
|
if (!later.length) return null;
|
|
52
|
+
// A caller may hold a persistent source tree: request its text only when
|
|
53
|
+
// a later picture replacement can actually affect this navigation step.
|
|
54
|
+
const source = input.source;
|
|
51
55
|
const own = new Set(later.filter(row => row.transaction.sourceTransactionId === id));
|
|
52
56
|
const step = redo ? target.splices : target.splices.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
|
|
53
57
|
const plan = moveOthers => {
|
|
@@ -74,76 +78,77 @@ export function pictureHistorySplices({source, records}, target, redo) {
|
|
|
74
78
|
try { return plan(true); } catch (_) { return null; }
|
|
75
79
|
}
|
|
76
80
|
|
|
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.
|
|
81
|
+
// Keep only the latest source observation. Inactive live documents must not retain
|
|
82
|
+
// every revision's source tree. The weak key also lets the latest input be collected.
|
|
83
|
+
// Mutable caller data is compared/captured before this cache can be consulted.
|
|
84
|
+
let preparedHistories = new WeakMap();
|
|
92
85
|
function prepareHistory(input) {
|
|
93
86
|
try {
|
|
94
87
|
if (!input || typeof input.source !== 'string' || !Array.isArray(input.records) || !_rapierValidMetadata(input.metadata)) return historyFailure('history_invalid');
|
|
95
|
-
const source = input.source,
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
const source = input.source, metadata = {...input.metadata}, index = _rapierPrepareHistoryRecords(input.records);
|
|
89
|
+
if (!index.ok) return historyFailure('history_invalid');
|
|
90
|
+
const {records, effects} = index;
|
|
98
91
|
const revision = input.revision ?? records.at(-1)?.transaction?.revision ?? 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
let prior = input.earliestRevision ?? records[0]?.transaction?.baseRevision ?? revision;
|
|
93
|
+
if (!historyInteger(revision) || !historyInteger(prior)) return historyFailure('history_invalid');
|
|
94
|
+
const earliest = prior, suppliedComplete = input.complete !== false, cached = preparedHistories.get(records);
|
|
95
|
+
if (cached && cached.source === source && cached.revision === revision && cached.earliest === earliest &&
|
|
96
|
+
cached.suppliedComplete === suppliedComplete && cached.metadata.filename === metadata.filename && cached.metadata.docKind === metadata.docKind) return cached;
|
|
97
|
+
let complete = prior === 0;
|
|
98
|
+
const rows = [], byId = new Map();
|
|
99
|
+
for (const owned of index.rows) {
|
|
100
|
+
const {record, splices, metadata: effect} = owned, tx = record?.transaction, actor = tx?.actor;
|
|
101
|
+
if (!tx || typeof tx.id !== 'string' || !tx.id || byId.has(tx.id) || !historyInteger(tx.baseRevision) ||
|
|
105
102
|
!historyInteger(tx.revision) || tx.revision !== tx.baseRevision + 1 || tx.baseRevision < prior ||
|
|
106
103
|
!actor || !['human', 'agent', 'system'].includes(actor.kind) || typeof actor.id !== 'string' || !actor.id ||
|
|
107
104
|
!(tx.createdAt === null || historyInteger(tx.createdAt)) ||
|
|
108
105
|
(tx.turnId != null && (typeof tx.turnId !== 'string' || !tx.turnId || tx.turnId.length > 160))) return historyFailure('history_invalid');
|
|
109
106
|
if (tx.baseRevision !== prior) complete = false;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rows.push(
|
|
107
|
+
if (!splices || effect === undefined || (!splices.length &&
|
|
108
|
+
!Object.values(effect || {}).some(pair => pair.before !== pair.after) && !effects.references.get(tx.id)?.length)) return historyFailure('history_unavailable');
|
|
109
|
+
const row = {record, splices, metadata: effect};
|
|
110
|
+
rows.push(row); byId.set(tx.id, row); prior = tx.revision;
|
|
114
111
|
}
|
|
115
112
|
if (prior !== revision) return historyFailure('history_unavailable');
|
|
116
|
-
let
|
|
117
|
-
for (let
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
let initial = new HistorySource(source), initialMetadata = {...metadata};
|
|
114
|
+
for (let at = rows.length - 1; at >= 0; at--) {
|
|
115
|
+
const row = rows[at];
|
|
116
|
+
row.afterSource = initial; row.afterMetadata = initialMetadata;
|
|
117
|
+
initial = initial.before(row.splices);
|
|
118
|
+
initialMetadata = _rapierTransformMetadata(initialMetadata, row.metadata, true);
|
|
119
|
+
if (!initial || !initialMetadata) return historyFailure('history_invalid');
|
|
120
|
+
row.beforeSource = initial; row.beforeMetadata = initialMetadata;
|
|
121
121
|
}
|
|
122
|
-
const earliestRevision =
|
|
123
|
-
|
|
122
|
+
const earliestRevision = records[0]?.transaction.baseRevision ?? revision;
|
|
123
|
+
// The window's owner may name its root (the source store mints it once per text); otherwise hash the initial text.
|
|
124
|
+
let root = earliestRevision === 0 ? (typeof input.earliestRoot === 'string' && input.earliestRoot || textRootParts(initial.parts())) : null,
|
|
125
|
+
rootRevision = earliestRevision;
|
|
124
126
|
for (const {record, splices} of rows) {
|
|
125
127
|
const hashed = Object.hasOwn(record, 'beforeHash') || Object.hasOwn(record, 'afterHash');
|
|
126
128
|
if (hashed && (![record.beforeHash, record.afterHash].every(value =>
|
|
127
129
|
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.
|
|
130
|
+
(splices.length ? record.beforeHash === record.afterHash : record.beforeHash !== record.afterHash))) return historyFailure('history_invalid');
|
|
131
|
+
// Revision gaps begin a new path root; a continuous revision-zero prefix
|
|
132
|
+
// still binds every untouched source byte through the initial text root.
|
|
132
133
|
if (record.transaction.baseRevision !== rootRevision) root = null;
|
|
133
134
|
if (hashed) {
|
|
134
135
|
if (root !== null && record.beforeHash !== root) return historyFailure('history_invalid');
|
|
135
136
|
root = record.beforeHash;
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
if (replay === null) return historyFailure('history_invalid');
|
|
139
|
-
if (root !== null) for (const row of splices) root = rootAfter(root, row);
|
|
138
|
+
if (root !== null) for (const splice of splices) root = rootAfter(root, splice);
|
|
140
139
|
if (hashed && root !== record.afterHash) return historyFailure('history_invalid');
|
|
141
140
|
rootRevision = record.transaction.revision;
|
|
142
141
|
}
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
142
|
+
let initialText;
|
|
143
|
+
const initialSource = () => initialText ??= initial.slice();
|
|
144
|
+
if (records.some(row => Object.hasOwn(row, 'authored'))) authoredHistory(initialSource(), rows.map(({record, splices}) => ({...record, splices})));
|
|
145
|
+
const replayedMetadata = index.replayMetadata(initialMetadata);
|
|
146
|
+
if (!replayedMetadata || ['filename', 'docKind'].some(key => replayedMetadata[key] !== metadata[key])) return historyFailure('history_invalid');
|
|
147
|
+
const resolvedRecords = Object.freeze(rows.map(({record, splices}) => record.splices === splices ? record : Object.freeze({...record, splices})));
|
|
148
|
+
const prepared = {ok: true, source, records: resolvedRecords, get initialSource() { return initialSource(); }, initial, metadata, initialMetadata, effects, rows, byId,
|
|
149
|
+
revision, earliest, suppliedComplete, earliestRevision, complete: suppliedComplete && complete && earliestRevision === 0};
|
|
150
|
+
preparedHistories = new WeakMap([[records, prepared]]);
|
|
151
|
+
return prepared;
|
|
147
152
|
} catch (_) { return historyFailure('history_invalid'); }
|
|
148
153
|
}
|
|
149
154
|
|
|
@@ -152,23 +157,17 @@ export function replayHistory(input) {
|
|
|
152
157
|
if (!prepared.ok) return prepared;
|
|
153
158
|
const {source, initialSource, metadata, initialMetadata, revision, earliestRevision, complete, rows, effects} = prepared;
|
|
154
159
|
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,
|
|
160
|
+
return {ok: true, source, initialSource, metadata: {...metadata}, initialMetadata: {...initialMetadata}, revision, earliestRevision, complete, acts,
|
|
156
161
|
effectiveActIds: effects.effectiveActIds.slice(), groups: groupHistoryActs(acts)};
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
export function sourceBefore(input, actId) {
|
|
160
165
|
const prepared = prepareHistory(input);
|
|
161
166
|
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');
|
|
167
|
+
const row = prepared.byId.get(actId);
|
|
168
|
+
if (!row) return historyFailure('act_unavailable');
|
|
169
|
+
return {ok: true, source: row.beforeSource.slice(), metadata: {...row.beforeMetadata}, revision: row.record.transaction.baseRevision,
|
|
170
|
+
act: historyAct(row.record, prepared.effects), complete: prepared.complete, earliestRevision: prepared.earliestRevision};
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
function historyRangeBefore(range, splice) {
|
|
@@ -186,21 +185,23 @@ function historyRangeBefore(range, splice) {
|
|
|
186
185
|
|
|
187
186
|
function projectHistoryRange(prepared, from, to, actId) {
|
|
188
187
|
if (!historyBoundary(prepared.source, from) || !historyBoundary(prepared.source, to) || to < from) return historyFailure('range_invalid');
|
|
189
|
-
let
|
|
188
|
+
let range = {from, to}, before = null;
|
|
190
189
|
const acts = [];
|
|
191
190
|
for (let index = prepared.rows.length - 1; index >= 0; index--) {
|
|
192
|
-
const {record, splices} = prepared.rows[index],
|
|
191
|
+
const {record, splices, beforeSource, afterSource} = prepared.rows[index], afterRange = range;
|
|
193
192
|
let touched = false;
|
|
194
193
|
for (let at = splices.length - 1; at >= 0; at--) {
|
|
195
194
|
const mapped = historyRangeBefore(range, splices[at]); touched ||= mapped.touched;
|
|
196
195
|
range = {from: mapped.from, to: mapped.to};
|
|
197
196
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
acts.push({...historyAct(record, prepared.effects), before: previous,
|
|
197
|
+
const include = !prepared.effects.pictures.has(record.transaction.id) &&
|
|
198
|
+
(touched || record.metadata || prepared.effects.references.get(record.transaction.id)?.length);
|
|
199
|
+
if (include || record.transaction.id === actId) {
|
|
200
|
+
const previous = {...range, source: beforeSource.slice(range.from, range.to)};
|
|
201
|
+
if (record.transaction.id === actId) before = previous;
|
|
202
|
+
if (include) acts.push({...historyAct(record, prepared.effects), before: previous,
|
|
203
|
+
after: {...afterRange, source: afterSource.slice(afterRange.from, afterRange.to)}});
|
|
204
|
+
}
|
|
204
205
|
}
|
|
205
206
|
if (actId != null && !before) return historyFailure('act_unavailable');
|
|
206
207
|
acts.reverse();
|
|
@@ -220,10 +221,47 @@ export function historyProjections(input, ranges) {
|
|
|
220
221
|
if (!prepared.ok) return prepared;
|
|
221
222
|
if (!Array.isArray(ranges)) return historyFailure('range_invalid');
|
|
222
223
|
return {ok: true, projections: ranges.map(({from = 0, to = prepared.source.length, actId} = {}) =>
|
|
223
|
-
|
|
224
|
+
projectHistoryRange(prepared, from, to, actId))};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Block intervals stay ordered under reverse splices. A fixed balanced tree can
|
|
228
|
+
// shift an untouched suffix lazily and visit only intervals crossing the edit.
|
|
229
|
+
function historyBlockIndex(projected) {
|
|
230
|
+
const items = projected.map(block => ({block, range: {from: block.from, to: block.to}, shift: 0}))
|
|
231
|
+
.sort((a, b) => a.range.from - b.range.from);
|
|
232
|
+
const refresh = node => {
|
|
233
|
+
node.minFrom = node.left?.minFrom ?? node.range.from;
|
|
234
|
+
node.maxTo = Math.max(node.range.to, node.left?.maxTo ?? -1, node.right?.maxTo ?? -1);
|
|
235
|
+
};
|
|
236
|
+
const build = (from, to, parent = null) => {
|
|
237
|
+
if (from === to) return null;
|
|
238
|
+
const mid = Math.floor((from + to) / 2), node = items[mid];
|
|
239
|
+
node.parent = parent; node.left = build(from, mid, node); node.right = build(mid + 1, to, node);
|
|
240
|
+
refresh(node); return node;
|
|
241
|
+
};
|
|
242
|
+
const root = build(0, items.length);
|
|
243
|
+
const shift = (node, delta) => {
|
|
244
|
+
if (!node || !delta) return;
|
|
245
|
+
node.range = {from: node.range.from + delta, to: node.range.to + delta};
|
|
246
|
+
node.minFrom += delta; node.maxTo += delta; node.shift += delta;
|
|
247
|
+
};
|
|
248
|
+
const move = (node, splice, touch) => {
|
|
249
|
+
if (!node || node.maxTo < splice.pos) return;
|
|
250
|
+
const end = splice.pos + splice.inserted.length;
|
|
251
|
+
if (node.minFrom > end) { shift(node, splice.removed.length - splice.inserted.length); return; }
|
|
252
|
+
if (node.shift) { shift(node.left, node.shift); shift(node.right, node.shift); node.shift = 0; }
|
|
253
|
+
const mapped = historyRangeBefore(node.range, splice);
|
|
254
|
+
if (mapped.touched) touch(node);
|
|
255
|
+
node.range = {from: mapped.from, to: mapped.to};
|
|
256
|
+
move(node.left, splice, touch); move(node.right, splice, touch); refresh(node);
|
|
257
|
+
};
|
|
258
|
+
return {move: (splice, touch) => move(root, splice, touch), range(node) {
|
|
259
|
+
let delta = 0;
|
|
260
|
+
for (let parent = node.parent; parent; parent = parent.parent) delta += parent.shift;
|
|
261
|
+
return {from: node.range.from + delta, to: node.range.to + delta};
|
|
262
|
+
}};
|
|
224
263
|
}
|
|
225
264
|
|
|
226
|
-
// Project all reading places in one replay. Typing only invalidates this transient view.
|
|
227
265
|
export function historyPlaces(input, blocks) {
|
|
228
266
|
const prepared = prepareHistory(input);
|
|
229
267
|
if (!prepared.ok) return prepared;
|
|
@@ -231,47 +269,49 @@ export function historyPlaces(input, blocks) {
|
|
|
231
269
|
!historyBoundary(prepared.source, block.from) || !historyBoundary(prepared.source, block.to) || block.to < block.from))
|
|
232
270
|
return historyFailure('range_invalid');
|
|
233
271
|
const projected = blocks.map(block => ({id: block.id, from: block.from, to: block.to,
|
|
234
|
-
source: prepared.source.slice(block.from, block.to),
|
|
235
|
-
|
|
272
|
+
source: prepared.source.slice(block.from, block.to), acts: []}));
|
|
273
|
+
const ranges = historyBlockIndex(projected);
|
|
236
274
|
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);
|
|
275
|
+
const {record, splices, beforeSource, afterSource} = prepared.rows[index], touched = new Map();
|
|
276
|
+
for (let at = splices.length - 1; at >= 0; at--) ranges.move(splices[at], node => {
|
|
277
|
+
if (touched.has(node) || prepared.effects.pictures.has(record.transaction.id)) return;
|
|
278
|
+
// Earlier splices in this reverse walk only shifted this interval. Recover
|
|
279
|
+
// its coordinates at the record's after-state before recording the first touch.
|
|
280
|
+
let after = {...node.range};
|
|
281
|
+
for (let prior = at + 1; prior < splices.length; prior++) {
|
|
282
|
+
const row = splices[prior];
|
|
283
|
+
if (after.from >= row.pos + row.removed.length) {
|
|
284
|
+
const delta = row.inserted.length - row.removed.length;
|
|
285
|
+
after = {from: after.from + delta, to: after.to + delta};
|
|
266
286
|
}
|
|
267
287
|
}
|
|
288
|
+
touched.set(node, after);
|
|
289
|
+
});
|
|
290
|
+
// Blocks one splice covers share its range (a whole-document delete maps every block to all it removed): slice each range once.
|
|
291
|
+
const slices = new Map(), cut = (source, side, from, to) => {
|
|
292
|
+
const key = side + from + ':' + to;
|
|
293
|
+
if (!slices.has(key)) slices.set(key, source.slice(from, to));
|
|
294
|
+
return slices.get(key);
|
|
295
|
+
};
|
|
296
|
+
for (const [node, afterRange] of touched) {
|
|
297
|
+
const beforeRange = ranges.range(node), previous = {...beforeRange, source: cut(beforeSource, 'b', beforeRange.from, beforeRange.to)},
|
|
298
|
+
after = {...afterRange, source: cut(afterSource, 'a', afterRange.from, afterRange.to)};
|
|
299
|
+
if (previous.source !== after.source) node.block.acts.push({...historyAct(record), before: previous, after});
|
|
268
300
|
}
|
|
269
|
-
set(act.id, true); effects.set(act.id, changes);
|
|
270
301
|
}
|
|
271
|
-
|
|
302
|
+
|
|
303
|
+
const acts = prepared.rows.filter(({record}) => !prepared.effects.pictures.has(record.transaction.id)).map(({record}) => historyAct(record));
|
|
304
|
+
// Native Redo consumes its Undo as navigation while keeping that receipt in
|
|
305
|
+
// the reading history. All material withdrawals use the canonical blockers,
|
|
306
|
+
// including independent inverses holding the same original act suppressed.
|
|
307
|
+
const reversed = new Set();
|
|
308
|
+
for (const {record} of prepared.rows) if (prepared.effects.active.has(record.transaction.id) && !record.transaction.reapplies)
|
|
309
|
+
for (const id of prepared.effects.references.get(record.transaction.id) || []) reversed.add(id);
|
|
310
|
+
|
|
311
|
+
for (const block of projected) block.acts.reverse();
|
|
272
312
|
return {ok: true, source: prepared.source, initialSource: prepared.initialSource, revision: prepared.revision,
|
|
273
313
|
earliestRevision: prepared.earliestRevision, complete: prepared.complete, acts, blocks: projected,
|
|
274
|
-
reversed: acts.filter(act =>
|
|
314
|
+
reversed: acts.filter(act => reversed.has(act.id)).map(act => act.id)};
|
|
275
315
|
}
|
|
276
316
|
|
|
277
317
|
// Runs retain only exact replacement ancestry. A later replacement owns an ambiguous hunk whole.
|
|
@@ -282,26 +322,26 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
282
322
|
if (!options || typeof options !== 'object' || Array.isArray(options) ||
|
|
283
323
|
(options.keepHunk !== undefined && typeof options.keepHunk !== 'function') ||
|
|
284
324
|
(options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
|
|
285
|
-
const requested = new Set(actIds), available =
|
|
325
|
+
const requested = new Set(actIds), available = prepared.byId;
|
|
286
326
|
if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
|
|
287
327
|
const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
|
|
288
|
-
const records = prepared.
|
|
328
|
+
const records = prepared.records;
|
|
289
329
|
let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
|
|
290
330
|
const inverse = {splices: [], transaction: {id: inverseId, sourceTransactionIds: [...selected]}};
|
|
291
331
|
const projectedEffects = selected.size ? _rapierHistoryEffects([...records, inverse]) : prepared.effects;
|
|
292
332
|
const afterMetadata = _rapierMetadataState(prepared.initialMetadata, records, projectedEffects);
|
|
293
333
|
if (!afterMetadata) return historyFailure('history_invalid');
|
|
294
334
|
const metadata = _rapierMetadataDelta(prepared.metadata, afterMetadata), effectChanged = selected.size > 0;
|
|
295
|
-
const
|
|
335
|
+
const sourceEffectChanged = records.some(record => record.splices.length && !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
296
336
|
prepared.effects.active.has(record.transaction.id) !== projectedEffects.active.has(record.transaction.id));
|
|
297
|
-
if (!
|
|
337
|
+
if (!sourceEffectChanged) return {ok: true, splices: [], after: prepared.source, actIds: [...requested],
|
|
298
338
|
sourceTransactionIds: [...selected], metadata, afterMetadata, effectChanged, unchanged: !metadata && !effectChanged,
|
|
299
339
|
superseded: [], overlaps: []};
|
|
300
340
|
try {
|
|
301
341
|
const transported = records.some(row => row.authored) ? authoredHistory(prepared.initialSource, records, selected) : null;
|
|
302
342
|
const rows = transported ? transported.records.map(record => ({record, splices: record.splices,
|
|
303
343
|
sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
|
|
304
|
-
const result = projectInverseSource({...prepared, rows}, selected, projectedEffects, options);
|
|
344
|
+
const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
|
|
305
345
|
if (!result.ok) return result;
|
|
306
346
|
return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
|
|
307
347
|
effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
|