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.
Files changed (45) hide show
  1. package/README.md +15 -9
  2. package/dist/agent/vendor/pretext/NOTICE +2 -2
  3. package/dist/agent/vendor/pretext/SOURCE.json +62 -78
  4. package/dist/agent/vendor/pretext/analysis.js +24 -6
  5. package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
  6. package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
  7. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
  8. package/dist/agent/vendor/pretext/graphemes.js +1 -1
  9. package/dist/agent/vendor/pretext/han-kerning.js +1 -1
  10. package/dist/agent/vendor/pretext/layout.js +14 -11
  11. package/dist/agent/vendor/pretext/line-break.js +170 -195
  12. package/dist/agent/vendor/pretext/line-breaks.js +1 -7
  13. package/dist/agent/vendor/pretext/line-text.js +2 -2
  14. package/dist/agent/vendor/pretext/measurement.js +454 -113
  15. package/dist/agent/vendor/pretext/prepare.js +164 -244
  16. package/dist/agent/vendor/pretext/rich-inline.js +57 -37
  17. package/dist/agent/will.mjs +73 -0
  18. package/dist/kit/ledger/data.mjs +57 -4
  19. package/dist/kit/ledger/format.mjs +114 -47
  20. package/dist/kit/ledger/hash.mjs +43 -2
  21. package/dist/kit/ledger/history.mjs +6 -0
  22. package/dist/kit/ledger/inverse-source.mjs +15 -2
  23. package/dist/kit/ledger/journal-records.mjs +32 -9
  24. package/dist/kit/ledger/merge.mjs +3 -17
  25. package/dist/kit/ledger/text-merge.mjs +28 -2
  26. package/dist/kit/rapier-ledger.mjs +2 -2
  27. package/dist/kit/render-markdown.mjs +170 -12
  28. package/dist/kit/render-print.mjs +3 -3
  29. package/dist/kit/render-sanitize.mjs +66 -23
  30. package/dist/kit/render.mjs +10 -10
  31. package/dist/kit/wiki.mjs +34 -0
  32. package/dist/kit/will.d.mts +3 -0
  33. package/dist/layout/hyphenation.mjs +6 -4
  34. package/dist/layout/line-plan.mjs +96 -0
  35. package/dist/layout/model.mjs +2 -1
  36. package/dist/layout/paragraph-fit.mjs +4 -4
  37. package/dist/layout/prepared-paragraph.mjs +19 -2
  38. package/dist/layout/typography.mjs +57 -63
  39. package/dist/spec/document-fonts.mjs +1 -0
  40. package/dist/spec/document-settings.mjs +1 -0
  41. package/dist/spec/html-blocks.mjs +10 -7
  42. package/dist/spec/md-assets.mjs +52 -12
  43. package/package.json +1 -1
  44. package/style.css +33 -23
  45. 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, sha256Parts, createSHA256} from './hash.mjs';
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 readLedgerState(input, expectedText, expectedMetadata) {
30
- const ledger = plainData(input);
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 = canonicalJSON({...ledger, start: {...ledger.start, text: ''}});
36
- let proof = typeof input === 'object' ? proofs.get(input) : undefined;
37
- if (!proof || proof.shell !== shell || proof.startText !== ledger.start.text) {
38
- proof = {shell, startText: ledger.start.text, ...proveLedger(ledger)};
39
- if (typeof input === 'object') proofs.set(input, proof);
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 !== digest(payload)) throw fault('envelope checksum differs');
51
- let text = documentText(ledger.start.text), root = ledger.start.root, revision = ledger.start.revision;
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 !== textRoot(text)) || ledger.start.sha256 !== sha256(text) ||
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 = utf8Length(text);
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
- if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text)) throw fault('history does not describe this document');
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* checksum(parts) {
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.update(text.slice(start, end)); yield;
163
+ hash?.update(text.slice(start, end)); yield;
116
164
  }
117
- return {utf8Bytes: bytes, sha256: hash.digest(), contentRoot: text.length + ':' +
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
- function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
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
- expanded.push({transaction: plainData(record.transaction), splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
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(canonicalParts(payload))};
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
- export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {}) {
178
- const producer = produceLedger(input);
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 && yieldWork) {
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 historyEnvelope(input, segmentIdentity = [], expectedText, expectedMetadata) {
192
- const {value: {ledger, text, metadata}, prepared, source} = readLedgerState(input, expectedText, expectedMetadata);
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
- let start = 0, a = current.text.length, b = text.length;
253
- while (start < a && start < b && current.text[start] === text[start]) start++;
254
- if (start && /[\uDC00-\uDFFF]/.test(current.text[start] || text[start] || '')) start--;
255
- while (a > start && b > start && current.text[a - 1] === text[b - 1]) { a--; b--; }
256
- if (/[\uDC00-\uDFFF]/.test(current.text[a] || text[b] || '')) { a++; b++; }
257
- const splices = [{pos: start, removed: current.text.slice(start, a), inserted: text.slice(start, b)}];
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:' + revision.toString(36),
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,
@@ -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
- const LONG_TEXT = 65536, DIGESTS = new Map();
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 marker = pieces.slice(start.first, start.last).findIndex(item => item.mark?.inserted.length);
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) value = effects
103
- ? Object.fromEntries(Object.entries(value).map(([key, pair]) => [key, {before: pair.after, after: pair.before}]))
104
- : value.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
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 _rapierRecordIndex(ledger || []).splices(entry);
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 _rapierRecordIndex(ledger || []).metadata(entry);
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 + ']: data:image/svg+xml;base64,';
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 /^[A-Za-z0-9+/]+={0,2}$/.test(bytes) && !/[\r\n]/.test(title) ? title : null;
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 incomingKept = canonicalJSON(kept(theirs.text));
195
- const keepDiffers = incomingKept !== canonicalJSON(kept(ours.text)) && incomingKept !== canonicalJSON(kept(common.text)) ||
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
- if (base === ours) return unchanged(theirs);
434
- if (base === theirs) return unchanged(ours);
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';