rapier-markdown-kit 1.2.33 → 1.2.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -9
- package/dist/agent/vendor/pretext/NOTICE +2 -2
- package/dist/agent/vendor/pretext/SOURCE.json +62 -78
- package/dist/agent/vendor/pretext/analysis.js +24 -6
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
- package/dist/agent/vendor/pretext/graphemes.js +1 -1
- package/dist/agent/vendor/pretext/han-kerning.js +1 -1
- package/dist/agent/vendor/pretext/layout.js +14 -11
- package/dist/agent/vendor/pretext/line-break.js +170 -195
- package/dist/agent/vendor/pretext/line-breaks.js +1 -7
- package/dist/agent/vendor/pretext/line-text.js +2 -2
- package/dist/agent/vendor/pretext/measurement.js +454 -113
- package/dist/agent/vendor/pretext/prepare.js +164 -244
- package/dist/agent/vendor/pretext/rich-inline.js +57 -37
- package/dist/agent/will.mjs +73 -0
- package/dist/kit/ledger/data.mjs +57 -4
- package/dist/kit/ledger/format.mjs +114 -47
- package/dist/kit/ledger/hash.mjs +43 -2
- package/dist/kit/ledger/history.mjs +6 -0
- package/dist/kit/ledger/inverse-source.mjs +15 -2
- package/dist/kit/ledger/journal-records.mjs +32 -9
- package/dist/kit/ledger/merge.mjs +3 -17
- package/dist/kit/ledger/text-merge.mjs +28 -2
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-markdown.mjs +170 -12
- package/dist/kit/render-print.mjs +3 -3
- package/dist/kit/render-sanitize.mjs +66 -23
- package/dist/kit/render.mjs +10 -10
- package/dist/kit/wiki.mjs +34 -0
- package/dist/kit/will.d.mts +3 -0
- package/dist/layout/hyphenation.mjs +6 -4
- package/dist/layout/line-plan.mjs +96 -0
- package/dist/layout/model.mjs +2 -1
- package/dist/layout/paragraph-fit.mjs +4 -4
- package/dist/layout/prepared-paragraph.mjs +19 -2
- package/dist/layout/typography.mjs +57 -63
- package/dist/spec/document-fonts.mjs +1 -0
- package/dist/spec/document-settings.mjs +1 -0
- package/dist/spec/html-blocks.mjs +10 -7
- package/dist/spec/md-assets.mjs +52 -12
- package/package.json +1 -1
- package/style.css +33 -23
- package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
|
@@ -3,10 +3,12 @@ import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMeta
|
|
|
3
3
|
import {pictureHistorySplices} from './history.mjs';
|
|
4
4
|
import {HistorySource} from './source.mjs';
|
|
5
5
|
import {authoredHistory} from './transport.mjs';
|
|
6
|
-
import {textRoot, rootAfter, sha256,
|
|
7
|
-
import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
|
|
6
|
+
import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
|
|
7
|
+
import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
|
|
8
|
+
import {diff} from '../../packages/rapier-diff/diff.mjs';
|
|
9
|
+
import {minimalHistorySplice} from './inverse-source.mjs';
|
|
10
|
+
import {evaluateWill} from '../../agent/will.mjs';
|
|
8
11
|
export {textRoot, rootAfter};
|
|
9
|
-
const digest = value => sha256Parts(canonicalParts(value));
|
|
10
12
|
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
11
13
|
function* checkedRecords(ledger, prepared) {
|
|
12
14
|
if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
|
|
@@ -25,43 +27,77 @@ function* checkedRecords(ledger, prepared) {
|
|
|
25
27
|
}
|
|
26
28
|
if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
|
|
27
29
|
}
|
|
30
|
+
function* envelopeParts(envelope) {
|
|
31
|
+
const keys = Object.keys(envelope).sort();
|
|
32
|
+
yield '{';
|
|
33
|
+
for (let i = 0; i < keys.length; i++) {
|
|
34
|
+
if (i) yield ',';
|
|
35
|
+
yield JSON.stringify(keys[i]); yield ':';
|
|
36
|
+
yield* keys[i] === 'records' ? canonicalRowsParts(envelope.records, 1) : canonicalParts(envelope[keys[i]], 1, new Set());
|
|
37
|
+
}
|
|
38
|
+
yield '}';
|
|
39
|
+
}
|
|
28
40
|
const proofs = new WeakMap();
|
|
29
|
-
function
|
|
30
|
-
const ledger
|
|
41
|
+
function ownedRows(ledger) {
|
|
42
|
+
if (ledger && Array.isArray(ledger.records)) for (const row of ledger.records) if (row && typeof row === 'object') ownRow(row);
|
|
43
|
+
return ledger;
|
|
44
|
+
}
|
|
45
|
+
function copiedLedger(input, proof) {
|
|
46
|
+
if (!proof || !input || typeof input !== 'object') return ownedRows(plainData(input));
|
|
47
|
+
const fields = Object.getOwnPropertyDescriptors(input), start = fields.start;
|
|
48
|
+
if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return ownedRows(plainData(input));
|
|
49
|
+
const parts = Object.getOwnPropertyDescriptors(start.value), text = parts.text;
|
|
50
|
+
if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return ownedRows(plainData(input));
|
|
51
|
+
const shell = Object.create(Object.getPrototypeOf(input), {...fields, start: {...start,
|
|
52
|
+
value: Object.create(Object.getPrototypeOf(start.value), {...parts, text: {...text, value: ''}})}});
|
|
53
|
+
const ledger = ownedRows(plainData(shell));
|
|
54
|
+
ledger.start.text = proof.startText;
|
|
55
|
+
return ledger;
|
|
56
|
+
}
|
|
57
|
+
function ledgerAdmission(input) {
|
|
58
|
+
const held = input && typeof input === 'object' ? proofs.get(input) : undefined;
|
|
59
|
+
if (held && isOwnedLedger(input)) return {ledger: input, shell: held.shell, proof: held};
|
|
60
|
+
const ledger = copiedLedger(input, held);
|
|
31
61
|
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
|
|
32
62
|
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
33
63
|
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
34
64
|
!exactKeys(ledger.start, ['text','revision','root','sha256','metadata']) || !exactKeys(ledger.head, ['revision','root','sha256','metadata'])) throw fault('unsupported or invalid envelope');
|
|
35
|
-
const shell =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
proofs.set(ledger, proof);
|
|
65
|
+
const shell = Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join('');
|
|
66
|
+
const proof = held?.shell === shell && held.startText === ledger.start.text ? held : null;
|
|
67
|
+
return {ledger, shell, proof};
|
|
68
|
+
}
|
|
69
|
+
function ledgerResult(ledger, proof, expectedText, expectedMetadata) {
|
|
70
|
+
proofs.set(ledger, proof); ownLedger(ledger);
|
|
42
71
|
const {text, metadata, root, revision, prepared, source} = proof;
|
|
43
72
|
if (expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata)))
|
|
44
73
|
throw fault('metadata does not describe this document');
|
|
45
74
|
if (expectedText !== undefined && text !== expectedText) throw fault('history does not describe this document');
|
|
46
|
-
return {value: {ledger, text, metadata: {...metadata}, root, revision, complete: ledger.complete}, prepared, source};
|
|
75
|
+
return {value: {ledger, text, metadata: {...metadata}, root, revision, complete: ledger.complete}, prepared, source, proof};
|
|
76
|
+
}
|
|
77
|
+
function readLedgerState(input, expectedText, expectedMetadata) {
|
|
78
|
+
const {ledger, shell, proof: held} = ledgerAdmission(input);
|
|
79
|
+
const proof = held || {shell, startText: ledger.start.text, ...finish(proveLedger(ledger))};
|
|
80
|
+
if (typeof input === 'object') proofs.set(input, proof);
|
|
81
|
+
return ledgerResult(ledger, proof, expectedText, expectedMetadata);
|
|
47
82
|
}
|
|
48
|
-
function proveLedger(ledger) {
|
|
83
|
+
function* proveLedger(ledger, native = false) {
|
|
49
84
|
const {sha256: claimed, ...payload} = ledger;
|
|
50
|
-
if (claimed !==
|
|
51
|
-
let text =
|
|
85
|
+
if (claimed !== (yield* checksum(envelopeParts(payload), native))) throw fault('envelope checksum differs');
|
|
86
|
+
let text = ledger.start.text, root = ledger.start.root, revision = ledger.start.revision;
|
|
87
|
+
const startFacts = yield* sourceFacts(text, native);
|
|
52
88
|
if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
|
|
53
|
-
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !==
|
|
89
|
+
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== startFacts.contentRoot) || ledger.start.sha256 !== startFacts.sha256 ||
|
|
54
90
|
(ledger.complete && revision !== 0)) throw fault('invalid start');
|
|
55
91
|
const prepared = _rapierPrepareHistoryRecords(ledger.records);
|
|
56
92
|
if (!prepared.ok) throw fault('record chain differs');
|
|
57
|
-
let source = new HistorySource(text), bytes =
|
|
93
|
+
let source = new HistorySource(text), bytes = startFacts.utf8Bytes;
|
|
58
94
|
for (const record of checkedRecords(ledger, prepared)) {
|
|
59
95
|
const next = source.apply(record.splices);
|
|
60
96
|
if (next === null) throw fault('splice does not replay');
|
|
61
97
|
source = next;
|
|
62
98
|
for (const row of record.splices) bytes += utf8Length(row.inserted) - utf8Length(row.removed);
|
|
63
99
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
64
|
-
root = record.afterHash; revision = record.transaction.revision;
|
|
100
|
+
root = record.afterHash; revision = record.transaction.revision; yield;
|
|
65
101
|
}
|
|
66
102
|
text = source.slice();
|
|
67
103
|
if (ledger.records.some(row => Object.hasOwn(row, 'authored'))) {
|
|
@@ -69,7 +105,8 @@ function proveLedger(ledger) {
|
|
|
69
105
|
}
|
|
70
106
|
const metadata = prepared.replayMetadata(ledger.start.metadata);
|
|
71
107
|
if (!metadata || canonicalJSON(metadata) !== canonicalJSON(ledger.head.metadata)) throw fault('metadata does not describe this document');
|
|
72
|
-
|
|
108
|
+
const headFacts = ledger.records.every(row => !row.splices.length) ? startFacts : yield* sourceFacts(text, native);
|
|
109
|
+
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== headFacts.sha256) throw fault('history does not describe this document');
|
|
73
110
|
return {text, metadata, root, revision, prepared, source};
|
|
74
111
|
}
|
|
75
112
|
export function ledgerWithStart(input, text) {
|
|
@@ -83,14 +120,25 @@ export function ledgerWithStart(input, text) {
|
|
|
83
120
|
export function readLedger(input, expectedText, expectedMetadata) {
|
|
84
121
|
return readLedgerState(input, expectedText, expectedMetadata).value;
|
|
85
122
|
}
|
|
86
|
-
function
|
|
123
|
+
export async function readLedgerAsync(input, expectedText, expectedMetadata, options = {}) {
|
|
124
|
+
options.signal?.throwIfAborted();
|
|
125
|
+
const {ledger, shell, proof: held} = ledgerAdmission(input);
|
|
126
|
+
const metadata = expectedMetadata === undefined ? undefined : plainData(expectedMetadata);
|
|
127
|
+
const proof = held || {shell, startText: ledger.start.text,
|
|
128
|
+
...await finishAsync(proveLedger(ledger, !!globalThis.crypto?.subtle), options)};
|
|
129
|
+
options.signal?.throwIfAborted();
|
|
130
|
+
if (typeof input === 'object') proofs.set(input, proof);
|
|
131
|
+
return ledgerResult(ledger, proof, expectedText, metadata).value;
|
|
132
|
+
}
|
|
133
|
+
function* checksum(parts, native = false) {
|
|
134
|
+
if (native) return yield {parts};
|
|
87
135
|
const hash = createSHA256();
|
|
88
136
|
for (const part of parts) { hash.update(part); yield; }
|
|
89
137
|
return hash.digest();
|
|
90
138
|
}
|
|
91
|
-
function* sourceFacts(text) {
|
|
139
|
+
function* sourceFacts(text, native = false) {
|
|
92
140
|
if (typeof text !== 'string') throw fault('document is not whole-character text');
|
|
93
|
-
const hash = createSHA256();
|
|
141
|
+
const hash = native ? null : createSHA256();
|
|
94
142
|
let fnv = 0x811c9dc5, a = 1, b = 0, bytes = 0;
|
|
95
143
|
for (let at = 0; at < text.length;) {
|
|
96
144
|
const start = at, end = Math.min(text.length, at + 65536);
|
|
@@ -112,26 +160,34 @@ function* sourceFacts(text) {
|
|
|
112
160
|
a %= 65521; b %= 65521;
|
|
113
161
|
}
|
|
114
162
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
115
|
-
hash
|
|
163
|
+
hash?.update(text.slice(start, end)); yield;
|
|
116
164
|
}
|
|
117
|
-
|
|
165
|
+
const digest = hash ? hash.digest() : yield {parts: [text], byteLength: bytes};
|
|
166
|
+
return {utf8Bytes: bytes, sha256: digest, contentRoot: text.length + ':' +
|
|
118
167
|
(fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36)};
|
|
119
168
|
}
|
|
120
|
-
|
|
169
|
+
const expandedRows = new WeakMap();
|
|
170
|
+
const sameSplices = (a, b) => a === b || a.length === b.length &&
|
|
171
|
+
a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
|
|
172
|
+
const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
|
|
173
|
+
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
|
|
121
174
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
122
175
|
metadata = {...metadata};
|
|
123
176
|
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
124
177
|
const prepared = _rapierPrepareHistoryRecords(records, earlier);
|
|
125
178
|
if (!prepared.ok) throw fault('navigation target was not retained');
|
|
126
|
-
const headFacts = yield* sourceFacts(text);
|
|
179
|
+
const headFacts = yield* sourceFacts(text, native);
|
|
127
180
|
let bytes = headFacts.utf8Bytes;
|
|
128
181
|
const expanded = [];
|
|
129
182
|
for (const {record, splices: rows, metadata: effect} of prepared.rows) {
|
|
130
183
|
if (!rows || effect === undefined) throw fault('navigation target was not retained');
|
|
131
|
-
|
|
184
|
+
const held = expandedRows.get(record);
|
|
185
|
+
if (held && sameSplices(held.rows, rows) && sameEffect(held.effect, effect)) { expanded.push(held.row); yield; continue; }
|
|
186
|
+
expanded.push(ownRow({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
132
187
|
beforeHash: record.beforeHash, afterHash: record.afterHash, ...(Object.hasOwn(record, 'authored') ? {authored: plainData(record.authored)} : {}), ...(effect ? {metadata: plainData(effect)} : {}),
|
|
133
188
|
...(record.changeSet && Object.hasOwn(record.changeSet, 'label') ? {changeSet: {label: typeof record.changeSet.label === 'string' ? record.changeSet.label : plainData(record.changeSet.label)}} : {}),
|
|
134
|
-
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})});
|
|
189
|
+
...(record.derivedCommentIndex == null ? {} : {derivedCommentIndex: typeof record.derivedCommentIndex === 'string' ? record.derivedCommentIndex : plainData(record.derivedCommentIndex)})}));
|
|
190
|
+
expandedRows.set(record, {row: expanded.at(-1), rows, effect});
|
|
135
191
|
yield;
|
|
136
192
|
}
|
|
137
193
|
const headBytes = bytes;
|
|
@@ -153,7 +209,7 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
153
209
|
const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
|
|
154
210
|
if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
|
|
155
211
|
if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
|
|
156
|
-
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText);
|
|
212
|
+
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
|
|
157
213
|
if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
|
|
158
214
|
const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
|
|
159
215
|
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
@@ -165,7 +221,11 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
165
221
|
}
|
|
166
222
|
const replayedMetadata = prepared.replayMetadata(startMetadata);
|
|
167
223
|
if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
|
|
168
|
-
const ledger = {...payload, sha256: yield* checksum(
|
|
224
|
+
const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
|
|
225
|
+
ownLedger(ledger);
|
|
226
|
+
proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
|
|
227
|
+
text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
|
|
228
|
+
source: new HistorySource(text)});
|
|
169
229
|
return {ledger, contentRoot, utf8Bytes: headBytes};
|
|
170
230
|
}
|
|
171
231
|
function finish(producer) {
|
|
@@ -174,13 +234,14 @@ function finish(producer) {
|
|
|
174
234
|
return step.value;
|
|
175
235
|
}
|
|
176
236
|
export function exportLedger(input) { return finish(produceLedger(input)).ledger; }
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let step;
|
|
237
|
+
async function finishAsync(producer, {yield: yieldWork, signal} = {}) {
|
|
238
|
+
let step, digest;
|
|
180
239
|
do {
|
|
181
240
|
signal?.throwIfAborted();
|
|
182
|
-
step = producer.next();
|
|
183
|
-
if (!step.done &&
|
|
241
|
+
step = producer.next(digest); digest = undefined;
|
|
242
|
+
if (!step.done && step.value) digest = await sha256PartsAsync(step.value.parts,
|
|
243
|
+
{yield: yieldWork, signal, byteLength: step.value.byteLength});
|
|
244
|
+
else if (!step.done && yieldWork) {
|
|
184
245
|
const pending = yieldWork();
|
|
185
246
|
if (pending) await pending;
|
|
186
247
|
}
|
|
@@ -188,8 +249,10 @@ export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {})
|
|
|
188
249
|
signal?.throwIfAborted();
|
|
189
250
|
return step.value;
|
|
190
251
|
}
|
|
191
|
-
export function
|
|
192
|
-
|
|
252
|
+
export function prepareLedgerAsync(input, options = {}) {
|
|
253
|
+
return finishAsync(produceLedger(input, !!globalThis.crypto?.subtle), options);
|
|
254
|
+
}
|
|
255
|
+
function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
193
256
|
const {byId, effects: {pictures}} = prepared;
|
|
194
257
|
let branch = [], cursor = 0;
|
|
195
258
|
for (const {transaction: tx} of ledger.records) {
|
|
@@ -232,8 +295,13 @@ export function historyEnvelope(input, segmentIdentity = [], expectedText, expec
|
|
|
232
295
|
if (active === null || !activeMetadata || (i + 1 === cursor && (active.length !== text.length || !active.matches(0, text) || canonicalJSON(activeMetadata) !== canonicalJSON(metadata)))) throw fault('complete branch differs');
|
|
233
296
|
}
|
|
234
297
|
}
|
|
298
|
+
return {branch, cursor};
|
|
299
|
+
}
|
|
300
|
+
export function historyEnvelope(input, segmentIdentity = [], expectedText, expectedMetadata) {
|
|
301
|
+
const {value: {ledger, text, metadata}, prepared, source, proof} = readLedgerState(input, expectedText, expectedMetadata);
|
|
302
|
+
const {branch, cursor} = proof.navigation ??= historyNavigation(ledger, text, metadata, prepared, source);
|
|
235
303
|
return {schemaVersion: 5, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
|
|
236
|
-
ledger: ledger.records, branch, cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
304
|
+
ledger: ledger.records, branch: branch.slice(), cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
237
305
|
sourceRootId: ledger.head.root, earliestMetadata: {...ledger.start.metadata}, documentMetadata: {...metadata}, segmentIdentity, historyComplete: ledger.complete,
|
|
238
306
|
trimReason: ledger.complete ? '' : 'carried_start', trimmedBytes: 0};
|
|
239
307
|
}
|
|
@@ -249,15 +317,14 @@ export function agentActorId(door, agent) {
|
|
|
249
317
|
export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'rapier'}, operation = 'document.replace', at} = {}) {
|
|
250
318
|
const current = readLedger(input); documentText(text);
|
|
251
319
|
if (current.text === text) return current.ledger;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const revision = current.revision + 1, root = rootAfter(current.root, splices[0]);
|
|
320
|
+
const splices = diff(current.text, text, {_unit: 'word'}).edits.map(edit =>
|
|
321
|
+
minimalHistorySplice(current.text.slice(...edit.before), text.slice(...edit.after), edit.before[0])).reverse();
|
|
322
|
+
const revision = current.revision + 1, root = splices.reduce(rootAfter, current.root);
|
|
323
|
+
if (actor.kind === 'agent' && current.metadata.docKind === 'markdown' && evaluateWill(current.text, splices).outcome !== 'applied')
|
|
324
|
+
throw fault('will_violation');
|
|
325
|
+
const identity = sha256(canonicalJSON({before: current.ledger.sha256, text, actor, operation, at: at ?? null}));
|
|
259
326
|
const record = {beforeHash: current.root, afterHash: root, splices, transaction: {
|
|
260
|
-
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' +
|
|
327
|
+
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' + identity,
|
|
261
328
|
documentAuthority: current.ledger.documentAuthority, baseRevision: current.revision, revision,
|
|
262
329
|
actor, transport: 'platform', operation, requestId: null, sourceTransactionId: null,
|
|
263
330
|
affectedBlockIds: [], parent: current.ledger.records.at(-1)?.transaction.id ?? null,
|
package/dist/kit/ledger/hash.mjs
CHANGED
|
@@ -111,9 +111,50 @@ export function sha256Parts(parts) {
|
|
|
111
111
|
for (const part of parts) hash.update(part);
|
|
112
112
|
return hash.digest();
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
export async function sha256PartsAsync(parts, {yield: yieldWork, signal, byteLength} = {}) {
|
|
115
|
+
const encoder = new TextEncoder(), chunks = [];
|
|
116
|
+
let bytes = byteLength === undefined ? null : new Uint8Array(byteLength);
|
|
117
|
+
let high = '', length = 0;
|
|
118
|
+
const write = text => {
|
|
119
|
+
if (bytes) {
|
|
120
|
+
const result = encoder.encodeInto(text, bytes.subarray(length));
|
|
121
|
+
if (result.read !== text.length) throw new RangeError('SHA-256 byte length differs');
|
|
122
|
+
length += result.written;
|
|
123
|
+
} else {
|
|
124
|
+
const chunk = encoder.encode(text); chunks.push(chunk); length += chunk.length;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
for (const value of parts) {
|
|
128
|
+
let text = high + String(value === undefined ? '' : value); high = '';
|
|
129
|
+
if (text.length && (text.charCodeAt(text.length - 1) & 0xfc00) === 0xd800) {
|
|
130
|
+
high = text.slice(-1); text = text.slice(0, -1);
|
|
131
|
+
}
|
|
132
|
+
for (let at = 0; at < text.length;) {
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
let end = Math.min(text.length, at + 32768);
|
|
135
|
+
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 &&
|
|
136
|
+
(text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
137
|
+
write(text.slice(at, end)); at = end;
|
|
138
|
+
const pending = yieldWork?.(); if (pending) await pending;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (high) write(high);
|
|
142
|
+
if (bytes && length !== bytes.length) throw new RangeError('SHA-256 byte length differs');
|
|
143
|
+
bytes ||= new Uint8Array(length);
|
|
144
|
+
let at = 0;
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
signal?.throwIfAborted(); bytes.set(chunk, at); at += chunk.length;
|
|
147
|
+
const pending = yieldWork?.(); if (pending) await pending;
|
|
148
|
+
}
|
|
149
|
+
chunks.length = 0;
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
152
|
+
signal?.throwIfAborted();
|
|
153
|
+
return Array.from(new Uint8Array(digest), value => value.toString(16).padStart(2, '0')).join('');
|
|
154
|
+
}
|
|
155
|
+
const LONG_TEXT = 65536, DOCUMENT_TEXT = 1048576, DIGESTS = new Map();
|
|
115
156
|
export function sha256(text) {
|
|
116
|
-
if (typeof text !== 'string' || text.length < LONG_TEXT) return createSHA256().update(text).digest();
|
|
157
|
+
if (typeof text !== 'string' || text.length < LONG_TEXT || text.length >= DOCUMENT_TEXT) return createSHA256().update(text).digest();
|
|
117
158
|
let digest = DIGESTS.get(text);
|
|
118
159
|
if (digest === undefined) {
|
|
119
160
|
digest = createSHA256().update(text).digest();
|
|
@@ -5,6 +5,7 @@ import {sourceView, HistorySource} from './source.mjs';
|
|
|
5
5
|
import {textRootParts, rootAfter} from './hash.mjs';
|
|
6
6
|
import {authoredHistory, authoredPlacements} from './transport.mjs';
|
|
7
7
|
import {plainData, canonicalJSON} from './data.mjs';
|
|
8
|
+
import {evaluateWill} from '../../agent/will.mjs';
|
|
8
9
|
|
|
9
10
|
const historyFailure = reason => ({ok: false, reason});
|
|
10
11
|
const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
@@ -323,6 +324,8 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
323
324
|
(options.splitSplice !== undefined && typeof options.splitSplice !== 'function')) return historyFailure('act_invalid');
|
|
324
325
|
const requested = new Set(actIds), available = prepared.byId;
|
|
325
326
|
if ([...requested].some(id => !available.has(id))) return historyFailure('act_unavailable');
|
|
327
|
+
if (options.actor?.kind === 'agent' && [...requested].some(id => available.get(id).record.transaction.actor.kind !== 'agent'))
|
|
328
|
+
return historyFailure('act_forbidden');
|
|
326
329
|
const selected = new Set([...requested].filter(id => prepared.effects.active.has(id)));
|
|
327
330
|
const records = prepared.records;
|
|
328
331
|
let inverseId = 'planned-inverse'; while (available.has(inverseId)) inverseId += ':';
|
|
@@ -359,6 +362,9 @@ export function selectiveUndo(input, actIds, options = {}) {
|
|
|
359
362
|
sourceIndices: transported.sourceIndices.get(record.transaction.id)})) : prepared.rows;
|
|
360
363
|
const result = projectInverseSource(transported ? {...prepared, rows, records: transported.records} : prepared, selected, projectedEffects, options);
|
|
361
364
|
if (!result.ok) return result;
|
|
365
|
+
if (options.actor?.kind === 'agent' && prepared.metadata.docKind === 'markdown' &&
|
|
366
|
+
evaluateWill(prepared.source, result.splices, {restores: result.splices}).outcome !== 'applied')
|
|
367
|
+
return historyFailure('will_refused');
|
|
362
368
|
return {...result, actIds: [...requested], sourceTransactionIds: [...selected], metadata, afterMetadata,
|
|
363
369
|
effectChanged, unchanged: result.after === prepared.source && !metadata && !effectChanged};
|
|
364
370
|
} catch (_) { return historyFailure('history_invalid'); }
|
|
@@ -10,7 +10,7 @@ const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !
|
|
|
10
10
|
!(at > 0 && at < source.length && (source.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
|
|
11
11
|
(source.charCodeAt(at) & 0xFC00) === 0xDC00);
|
|
12
12
|
|
|
13
|
-
function minimalHistorySplice(before, after, pos = 0) {
|
|
13
|
+
export function minimalHistorySplice(before, after, pos = 0) {
|
|
14
14
|
let first = 0, a = before.length, b = after.length;
|
|
15
15
|
while (first < a && first < b && before[first] === after[first]) first++;
|
|
16
16
|
if (!historyBoundary(before, first) || !historyBoundary(after, first)) first--;
|
|
@@ -32,6 +32,8 @@ function historyHunks(row) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
const differentActors = (a, b) => !!a && !!b && (a.kind !== b.kind || a.id !== b.id);
|
|
36
|
+
|
|
35
37
|
export function projectInverseSource(prepared, selected, projectedEffects, options = {}) {
|
|
36
38
|
const records = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
37
39
|
if (!options.origins && !records.some(record => !prepared.effects.references.get(record.transaction.id)?.length &&
|
|
@@ -40,6 +42,7 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
40
42
|
try {
|
|
41
43
|
let pieces = prepared.initialSource ? [{text: prepared.initialSource, owner: null, lineage: []}] : [];
|
|
42
44
|
const all = [], byId = new Map(), byAct = new Map(), processed = [], keptInverseAncestors = new Set();
|
|
45
|
+
const actorOf = new Map(records.map(record => [record.transaction.id, record.transaction.actor]));
|
|
43
46
|
const recordsForCover = prepared.records || prepared.rows.map(({record, splices}) => record.splices === splices ? record : {...record, splices});
|
|
44
47
|
const coverGroups = new Map();
|
|
45
48
|
let coverOrigins = null;
|
|
@@ -262,7 +265,11 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
262
265
|
const start = boundary(row.pos), end = boundary(row.pos + row.removed.length);
|
|
263
266
|
let at = start.last, count = Math.max(0, end.first - at);
|
|
264
267
|
if (!row.removed.length) {
|
|
265
|
-
const
|
|
268
|
+
const run = pieces.slice(start.first, start.last), next = pieces[start.last];
|
|
269
|
+
let before = start.first - 1; while (before >= 0 && !pieces[before].text) before--;
|
|
270
|
+
const leftAct = before >= 0 ? byId.get(pieces[before].owner)?.actId : undefined;
|
|
271
|
+
const sameAct = leftAct !== undefined && run.some(item => item.mark?.actId === leftAct);
|
|
272
|
+
const marker = run.findIndex(item => item.mark?.inserted.length && (next && owns(next, item.mark) || !sameAct));
|
|
266
273
|
at = marker < 0 ? start.last : start.first + marker; count = 0;
|
|
267
274
|
}
|
|
268
275
|
const removed = pieces.slice(at, at + count);
|
|
@@ -276,6 +283,12 @@ export function projectInverseSource(prepared, selected, projectedEffects, optio
|
|
|
276
283
|
const id = tx.id + '#' + rowIndex + ':' + index;
|
|
277
284
|
const lineage = new Set([id]);
|
|
278
285
|
for (const piece of removed) for (const parent of piece.lineage || piece.mark?.lineage || []) lineage.add(parent);
|
|
286
|
+
if (!row.removed.length && !count && at > 0 && /[\p{L}\p{N}\p{M}]/u.test(row.inserted)) {
|
|
287
|
+
const left = pieces[at - 1], right = pieces[at];
|
|
288
|
+
const inner = left?.text && right?.text && left.owner === right.owner ? byId.get(left.owner) : null;
|
|
289
|
+
if (inner && inner.actId !== tx.id && inner.removed && differentActors(actorOf.get(inner.actId), tx.actor) && /[\p{L}\p{N}\p{M}]$/u.test(left.text) && /^[\p{L}\p{N}\p{M}]/u.test(right.text))
|
|
290
|
+
for (const parent of [inner.id, ...(left.lineage || [])]) lineage.add(parent);
|
|
291
|
+
}
|
|
279
292
|
const hunk = {id, actId: tx.id, removed: row.removed, inserted: row.inserted, before: removed, derived,
|
|
280
293
|
chosen, kept: selected.has(tx.id) && !chosen, lineage: [...lineage], supersededBy: new Set(), active: true,
|
|
281
294
|
...(coverIdsFor(tx.id) ? {coverIds: coverIdsFor(tx.id)} : {})};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {fields, scalar, plainPrototype} from './data.mjs';
|
|
3
3
|
import {transformText} from './source.mjs';
|
|
4
|
+
import {dataImage} from '../../spec/md-assets.mjs';
|
|
4
5
|
|
|
5
6
|
const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
|
|
6
7
|
const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
|
|
@@ -75,7 +76,7 @@ function _rapierValidAuthored(origin) {
|
|
|
75
76
|
!/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted));
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function _rapierRecordIndex(records) {
|
|
79
|
+
function _rapierRecordIndex(records, shared = false) {
|
|
79
80
|
const byId = new Map(), splices = new Map(), metadata = new Map();
|
|
80
81
|
for (const record of records) {
|
|
81
82
|
const id = record?.transaction?.id;
|
|
@@ -99,9 +100,12 @@ function _rapierRecordIndex(records) {
|
|
|
99
100
|
}
|
|
100
101
|
cache.set(current, value);
|
|
101
102
|
for (let i = path.length - 1; i >= 0; i--) {
|
|
102
|
-
if (path[i].transaction.reverts && value != null)
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
if (path[i].transaction.reverts && value != null) {
|
|
104
|
+
value = effects
|
|
105
|
+
? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
|
|
106
|
+
: value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
|
|
107
|
+
if (shared) { for (const row of Object.values(value)) Object.freeze(row); Object.freeze(value); }
|
|
108
|
+
}
|
|
105
109
|
cache.set(path[i], value);
|
|
106
110
|
}
|
|
107
111
|
return value;
|
|
@@ -109,10 +113,27 @@ function _rapierRecordIndex(records) {
|
|
|
109
113
|
return {byId, splices: record => resolve(record, false), metadata: record => resolve(record, true)};
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
const _rapierLedgerIndexes = new WeakMap();
|
|
117
|
+
function _rapierLedgerIndex(ledger) {
|
|
118
|
+
const records = ledger || [];
|
|
119
|
+
if (!Array.isArray(records)) return _rapierRecordIndex(records);
|
|
120
|
+
const held = _rapierLedgerIndexes.get(records);
|
|
121
|
+
if (held && held.records.length === records.length && held.records.at(-1) === records.at(-1) &&
|
|
122
|
+
(held.checked || held.records.every((record, at) => record === records[at]))) {
|
|
123
|
+
if (!held.checked) { held.checked = true; queueMicrotask(() => { held.checked = false; }); }
|
|
124
|
+
return held.index;
|
|
125
|
+
}
|
|
126
|
+
if (!records.every(record => record && typeof record === 'object' && Object.isFrozen(record) && Object.isFrozen(record.transaction ?? {})))
|
|
127
|
+
return _rapierRecordIndex(records);
|
|
128
|
+
const index = _rapierRecordIndex(records, true);
|
|
129
|
+
_rapierLedgerIndexes.set(records, {records: records.slice(), index});
|
|
130
|
+
return index;
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
function _rapierRecordSplices(entry, ledger) {
|
|
113
134
|
try {
|
|
114
135
|
if (entry && Array.isArray(entry.splices)) return entry.splices;
|
|
115
|
-
return
|
|
136
|
+
return _rapierLedgerIndex(ledger).splices(entry);
|
|
116
137
|
} catch (_) { return null; }
|
|
117
138
|
}
|
|
118
139
|
|
|
@@ -162,7 +183,7 @@ function _rapierRecordMetadata(entry, ledger) {
|
|
|
162
183
|
try {
|
|
163
184
|
if (Object.hasOwn(entry, 'metadata')) return _rapierValidMetadataEffect(entry.metadata) ? entry.metadata : undefined;
|
|
164
185
|
if (Array.isArray(entry.splices)) return null;
|
|
165
|
-
return
|
|
186
|
+
return _rapierLedgerIndex(ledger).metadata(entry);
|
|
166
187
|
} catch (_) { return undefined; }
|
|
167
188
|
}
|
|
168
189
|
|
|
@@ -175,10 +196,10 @@ function _rapierPictureFinish(record, original, earlier, lookup) {
|
|
|
175
196
|
if (!reference || !/^\]\[[a-z0-9-]+\]$/i.test(reference.inserted) || reference.removed === reference.inserted) return false;
|
|
176
197
|
const definition = rows.find(row => row !== reference), oldLabel = reference.removed.slice(2, -1), newLabel = reference.inserted.slice(2, -1);
|
|
177
198
|
const body = (text, label) => {
|
|
178
|
-
const prefix = '[' + label + ']:
|
|
199
|
+
const prefix = '[' + label + ']: ';
|
|
179
200
|
if (typeof text !== 'string' || !text.startsWith(prefix)) return null;
|
|
180
201
|
const value = text.slice(prefix.length), end = value.indexOf(' '), bytes = end < 0 ? value : value.slice(0, end), title = end < 0 ? '' : value.slice(end);
|
|
181
|
-
return
|
|
202
|
+
return dataImage(bytes)?.codec === 'image/svg+xml' && !/[\r\n]/.test(title) ? title : null;
|
|
182
203
|
};
|
|
183
204
|
const title = body(definition.removed, oldLabel);
|
|
184
205
|
if (title === null || body(definition.inserted, newLabel) !== title) return false;
|
|
@@ -292,6 +313,7 @@ function _rapierWalkHistoryEffects(records, lookup, visit = null, observe = null
|
|
|
292
313
|
}
|
|
293
314
|
}
|
|
294
315
|
if (refs.some(id => typeof id !== 'string' || !nodes.has(id) || pictures.has(id))) return {ok: false};
|
|
316
|
+
if (tx.actor?.kind === 'agent' && refs.some(id => byId.get(id).transaction.actor?.kind !== 'agent')) return {ok: false};
|
|
295
317
|
nodes.set(tx.id, {active: true, blockers: 0, references: refs, dependents: []}); references.set(tx.id, refs.slice());
|
|
296
318
|
byId.set(tx.id, record); earlier.push(record);
|
|
297
319
|
for (const ref of refs) {
|
|
@@ -505,9 +527,10 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
505
527
|
|
|
506
528
|
const _rapierJournalRecord = (() => {
|
|
507
529
|
const copies = new WeakMap();
|
|
530
|
+
const fixed = value => { if (value && typeof value === 'object') { for (const key of Object.keys(value)) fixed(value[key]); Object.freeze(value); } return value; };
|
|
508
531
|
return record => {
|
|
509
532
|
let copy = copies.get(record);
|
|
510
|
-
if (!copy) { copy = JSON.parse(JSON.stringify(record)); copies.set(record, copy); }
|
|
533
|
+
if (!copy) { copy = fixed(JSON.parse(JSON.stringify(record))); copies.set(record, copy); }
|
|
511
534
|
return copy;
|
|
512
535
|
};
|
|
513
536
|
})();
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {readLedger, exportLedger, rootAfter} from './format.mjs';
|
|
3
3
|
import {_rapierTransformSplices, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState} from './journal-records.mjs';
|
|
4
|
-
import {mergeText} from './text-merge.mjs';
|
|
4
|
+
import {mergeText, willMergeConflict} from './text-merge.mjs';
|
|
5
5
|
import {transposeAuthored, authoredPlacements} from './transport.mjs';
|
|
6
|
-
import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
|
|
7
6
|
import {sha256} from './hash.mjs';
|
|
8
7
|
import {canonicalJSON, fault} from './data.mjs';
|
|
9
8
|
export function transportInterval(start, end, splices) {
|
|
@@ -58,17 +57,6 @@ function checkpointList(read) {
|
|
|
58
57
|
}
|
|
59
58
|
return list;
|
|
60
59
|
}
|
|
61
|
-
function kept(text) {
|
|
62
|
-
const will = parseWill(text);
|
|
63
|
-
if (will.fault) return {fault: will.fault, text};
|
|
64
|
-
return will.regions.filter(region => region.law !== 'edit').map(({law, intent, start, end}) =>
|
|
65
|
-
law === 'keep' ? {law, intent, text: text.slice(start, end)} : {law, intent});
|
|
66
|
-
}
|
|
67
|
-
function appended(from, to) {
|
|
68
|
-
const regions = text => parseWill(text).regions.filter(region => region.law === 'append');
|
|
69
|
-
const was = regions(from), now = regions(to), core = (text, region) => stripOneTerminator(text.slice(region.start, region.end));
|
|
70
|
-
return was.length === now.length && was.every((region, index) => core(to, now[index]).startsWith(core(from, region)));
|
|
71
|
-
}
|
|
72
60
|
function expandRetainedStart(shorter, longer) {
|
|
73
61
|
if (shorter.ledger.documentAuthority !== longer.ledger.documentAuthority ||
|
|
74
62
|
shorter.ledger.start.revision <= longer.ledger.start.revision) return null;
|
|
@@ -191,10 +179,8 @@ export function merge(first, second) {
|
|
|
191
179
|
const local = localIds.get(record.transaction.id);
|
|
192
180
|
if (local) mappedIds.set(record.transaction.id, local.transaction.id);
|
|
193
181
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
!appended(common.text, theirs.text) && !appended(ours.text, theirs.text);
|
|
197
|
-
let conflicted = keepDiffers && unseen.length > 0, incomingSource = common.text;
|
|
182
|
+
const keepDiffers = willMergeConflict(common.text, ours.text, theirs.text);
|
|
183
|
+
let conflicted = keepDiffers, incomingSource = common.text;
|
|
198
184
|
if (!conflicted) for (const record of incoming) {
|
|
199
185
|
const beforeSource = incomingSource;
|
|
200
186
|
incomingSource = _rapierTransformSplices(incomingSource, record.splices);
|
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
|
|
3
|
+
|
|
4
|
+
export function willMergeConflict(base, ours, theirs) {
|
|
5
|
+
const snapshot = text => {
|
|
6
|
+
const will = parseWill(text);
|
|
7
|
+
return {fault: will.fault, regions: will.regions.filter(region => region.law !== 'edit').map(region => ({
|
|
8
|
+
law: region.law, intent: region.intent, text: text.slice(region.start, region.end),
|
|
9
|
+
}))};
|
|
10
|
+
};
|
|
11
|
+
const a = snapshot(ours), b = snapshot(theirs);
|
|
12
|
+
if (a.fault || b.fault) return ours !== theirs;
|
|
13
|
+
const kept = value => JSON.stringify(value.regions.map(({law, intent, text}) => ({law, intent, ...(law === 'keep' ? {text} : {})})));
|
|
14
|
+
if (kept(a) !== kept(b)) return true;
|
|
15
|
+
if (base === null) return false;
|
|
16
|
+
const prior = snapshot(base);
|
|
17
|
+
if (prior.fault) return base !== ours || base !== theirs;
|
|
18
|
+
const appended = value => {
|
|
19
|
+
const before = prior.regions.filter(region => region.law === 'append'), after = value.regions.filter(region => region.law === 'append');
|
|
20
|
+
return before.length === after.length && before.every((region, index) =>
|
|
21
|
+
stripOneTerminator(after[index].text).startsWith(stripOneTerminator(region.text)));
|
|
22
|
+
};
|
|
23
|
+
return !appended(a) || !appended(b);
|
|
24
|
+
}
|
|
25
|
+
|
|
2
26
|
function textFault(code, message) { return Object.assign(new Error(message), {code}); }
|
|
3
27
|
export function canonicalNote(text) {
|
|
4
28
|
if (typeof text !== 'string') throw textFault('note_text', 'A note must be text');
|
|
@@ -430,8 +454,9 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
430
454
|
canonicalNote(ours); canonicalNote(theirs); if (base !== null) canonicalNote(base);
|
|
431
455
|
const unchanged = value => ({text: value, clean: !value.includes(TEXT_CONFLICT), conflicts: value.includes(TEXT_CONFLICT) ? [{kind: 'existing-conflict', advisory: true}] : []});
|
|
432
456
|
if (ours === theirs) return unchanged(ours);
|
|
433
|
-
|
|
434
|
-
if (base ===
|
|
457
|
+
const willConflict = willMergeConflict(base, ours, theirs);
|
|
458
|
+
if (!willConflict && base === ours) return unchanged(theirs);
|
|
459
|
+
if (!willConflict && base === theirs) return unchanged(ours);
|
|
435
460
|
const conflicts = []; let text = '', a, b;
|
|
436
461
|
const conflict = (before, a, b, kind) => {
|
|
437
462
|
const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);
|
|
@@ -468,6 +493,7 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
468
493
|
const [units, oursUnits, theirsUnits] = base === null ? [] : [base, ours, theirs].map(textUnits);
|
|
469
494
|
const declared = base !== null && textDeclarations(units);
|
|
470
495
|
if (base === null) conflict(null, ours, theirs, 'no-ancestor');
|
|
496
|
+
else if (willConflict) conflict(base, ours, theirs, 'will');
|
|
471
497
|
else if (textDeclarations(oursUnits) !== declared || textDeclarations(theirsUnits) !== declared) conflict(base, ours, theirs, 'document-bindings');
|
|
472
498
|
else {
|
|
473
499
|
const reordered = textPermuted(units, oursUnits, theirsUnits);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
import {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
3
|
-
export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
|
|
2
|
+
import {exportLedger, prepareLedgerAsync, replaceLedgerText, readLedger, readLedgerAsync, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
3
|
+
export {exportLedger, prepareLedgerAsync, replaceLedgerText, readLedger, readLedgerAsync, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId};
|
|
4
4
|
import {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
|
|
5
5
|
export {authorship, readAuthorship, ledgerFromAuthorship};
|
|
6
6
|
import {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
|