rapier-markdown-kit 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -30
- package/dist/agent/will.mjs +1 -6
- package/dist/images/gif.mjs +0 -3
- package/dist/images/playback.mjs +1 -10
- package/dist/kit/assets.mjs +0 -1
- package/dist/kit/conformance/measurer.mjs +0 -0
- package/dist/kit/conformance/run.mjs +1 -8
- package/dist/kit/export-fonts.mjs +0 -14
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +0 -5
- package/dist/kit/ledger/authorship.mjs +0 -2
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger/carried.mjs +4 -7
- package/dist/kit/ledger/data.mjs +0 -8
- package/dist/kit/ledger/format.mjs +0 -25
- package/dist/kit/ledger/hash.mjs +11 -7
- package/dist/kit/ledger/history.mjs +10 -29
- package/dist/kit/ledger/inverse-source.mjs +0 -46
- package/dist/kit/ledger/journal-records.mjs +24 -56
- package/dist/kit/ledger/merge.mjs +0 -27
- package/dist/kit/ledger/text-merge.mjs +0 -20
- package/dist/kit/ledger/transport.mjs +0 -47
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +14 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +36 -0
- package/dist/kit/pdf-glyphs.mjs +39 -45
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/pdf-vectors.mjs +0 -3
- package/dist/kit/print-capitals.mjs +0 -4
- package/dist/kit/render-entry.mjs +0 -1
- package/dist/kit/render-ink.mjs +0 -5
- package/dist/kit/render-markdown.mjs +15 -119
- package/dist/kit/render-print.mjs +4 -49
- package/dist/kit/render-sanitize.mjs +22 -58
- package/dist/kit/render-styles.mjs +0 -7
- package/dist/kit/render-work.mjs +0 -12
- package/dist/kit/render.mjs +18 -173
- package/dist/kit/will.d.mts +42 -0
- package/dist/layout/hyphenation.mjs +0 -23
- package/dist/layout/line-plan.mjs +0 -20
- package/dist/layout/model.mjs +0 -55
- package/dist/layout/paragraph-fit.mjs +50 -60
- package/dist/layout/preparation.mjs +23 -56
- package/dist/layout/prepared-paragraph.mjs +0 -25
- package/dist/layout/typography.mjs +103 -41
- package/dist/packages/rapier-diff/concerns.mjs +0 -16
- package/dist/packages/rapier-diff/diff.mjs +0 -7
- package/dist/packages/rapier-diff/index.mjs +0 -1
- package/dist/packages/rapier-diff/matching.mjs +0 -19
- package/dist/packages/rapier-diff/merge.mjs +0 -9
- package/dist/packages/rapier-diff/moves.mjs +0 -7
- package/dist/packages/rapier-diff/structure.mjs +0 -2
- package/dist/packages/rapier-diff/tokens.mjs +0 -6
- package/dist/packages/rapier-diff/walk.mjs +0 -12
- package/dist/spec/document-fonts.mjs +0 -9
- package/dist/spec/document-settings.mjs +6 -25
- package/dist/spec/frontmatter.mjs +0 -1
- package/dist/spec/html-blocks.mjs +3 -37
- package/dist/spec/md-assets.mjs +2 -61
- package/dist/spec/md-layout.mjs +0 -34
- package/dist/spec/md-marks.mjs +0 -51
- package/dist/spec/md-source.mjs +0 -16
- package/dist/spec/word-sidecar.mjs +0 -2
- package/package.json +21 -4
- package/style.css +38 -21
|
@@ -6,7 +6,6 @@ import {transposeAuthored, authoredPlacements} from './transport.mjs';
|
|
|
6
6
|
import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
|
|
7
7
|
import {sha256} from './hash.mjs';
|
|
8
8
|
import {canonicalJSON, fault} from './data.mjs';
|
|
9
|
-
// The same interval transport used by the agent kernel, now shared with the public ledger.
|
|
10
9
|
export function transportInterval(start, end, splices) {
|
|
11
10
|
let a = start, b = end;
|
|
12
11
|
for (const row of splices) {
|
|
@@ -17,14 +16,11 @@ export function transportInterval(start, end, splices) {
|
|
|
17
16
|
}
|
|
18
17
|
return {start: a, end: b};
|
|
19
18
|
}
|
|
20
|
-
// Follow the current extent of a touched range for disclosure or the person's focus.
|
|
21
|
-
// Unlike transportInterval, this does not prove unchanged source and never grants edit authority.
|
|
22
19
|
export function transportTouchedInterval(start, end, splices) {
|
|
23
20
|
const unchanged = transportInterval(start, end, splices);
|
|
24
21
|
if (unchanged) return unchanged;
|
|
25
22
|
let range = {start, end};
|
|
26
23
|
for (const row of splices) {
|
|
27
|
-
// A split replacement can insert at a boundary before another splice touches the range.
|
|
28
24
|
const boundaryInsertion = !row.removed.length && (row.pos === range.start || row.pos === range.end);
|
|
29
25
|
const exact = boundaryInsertion ? null : transportInterval(range.start, range.end, [row]);
|
|
30
26
|
range = exact || {start: Math.min(range.start, row.pos),
|
|
@@ -33,21 +29,15 @@ export function transportTouchedInterval(start, end, splices) {
|
|
|
33
29
|
return range;
|
|
34
30
|
}
|
|
35
31
|
const recordKey = record => sha256(canonicalJSON(record));
|
|
36
|
-
// Identity survives rebasing. Position, roots, revision and parent are local placement,
|
|
37
|
-
// while authorial choices and semantic inverse targets belong to the original act.
|
|
38
32
|
function actFingerprint(record, effects) {
|
|
39
33
|
const tx = record.transaction, references = effects.references.get(tx.id) || [];
|
|
40
34
|
const {id, documentAuthority, baseRevision, revision, parent, reverts, reapplies,
|
|
41
35
|
sourceTransactionId, sourceTransactionIds, remoteTransactionId, ...authored} = tx;
|
|
42
36
|
return sha256(canonicalJSON({transaction: authored, references, picture: effects.pictures.get(tx.id) || null,
|
|
43
37
|
changeSet: record.changeSet ?? null, derivedCommentIndex: record.derivedCommentIndex ?? null,
|
|
44
|
-
// Applied source placement is proved below, never inferred from this fingerprint.
|
|
45
|
-
// An inverse's physical metadata is projected from its targets at the destination.
|
|
46
38
|
metadata: references.length ? null : Object.fromEntries(Object.entries(record.metadata || {}).map(([key, pair]) => [key, pair.after]))}));
|
|
47
39
|
}
|
|
48
40
|
const mergeIdentity = (localDigest, incomingDigest) => 'merge:' + sha256(canonicalJSON([localDigest, incomingDigest]));
|
|
49
|
-
// One physical replacement can carry two authored acts when both removed the same
|
|
50
|
-
// units and wrote the same text. An empty list is the act that does not repeat it.
|
|
51
41
|
function coincidentEffect(origin, left, right) {
|
|
52
42
|
const authored = origin?.splices;
|
|
53
43
|
if (!authored?.some(row => row.removed.length)) return false;
|
|
@@ -68,22 +58,17 @@ function checkpointList(read) {
|
|
|
68
58
|
}
|
|
69
59
|
return list;
|
|
70
60
|
}
|
|
71
|
-
// Keep and append regions with their intents, and the kept text: what the other copy may not change unseen.
|
|
72
61
|
function kept(text) {
|
|
73
62
|
const will = parseWill(text);
|
|
74
63
|
if (will.fault) return {fault: will.fault, text};
|
|
75
64
|
return will.regions.filter(region => region.law !== 'edit').map(({law, intent, start, end}) =>
|
|
76
65
|
law === 'keep' ? {law, intent, text: text.slice(start, end)} : {law, intent});
|
|
77
66
|
}
|
|
78
|
-
// Every append region of `from` still begins its counterpart in `to`.
|
|
79
67
|
function appended(from, to) {
|
|
80
68
|
const regions = text => parseWill(text).regions.filter(region => region.law === 'append');
|
|
81
69
|
const was = regions(from), now = regions(to), core = (text, region) => stripOneTerminator(text.slice(region.start, region.end));
|
|
82
70
|
return was.length === now.length && was.every((region, index) => core(to, now[index]).startsWith(core(from, region)));
|
|
83
71
|
}
|
|
84
|
-
// A retained suffix can reuse a prefix only when an existing ledger proves its
|
|
85
|
-
// exact source, metadata, revision and path root. Export then rechecks the whole
|
|
86
|
-
// chain, including the boundary parent. No missing act is invented.
|
|
87
72
|
function expandRetainedStart(shorter, longer) {
|
|
88
73
|
if (shorter.ledger.documentAuthority !== longer.ledger.documentAuthority ||
|
|
89
74
|
shorter.ledger.start.revision <= longer.ledger.start.revision) return null;
|
|
@@ -104,7 +89,6 @@ export function merge(first, second) {
|
|
|
104
89
|
const expandedOurs = expandRetainedStart(ours, theirs);
|
|
105
90
|
if (expandedOurs) {
|
|
106
91
|
const result = merge(expandedOurs, theirs.ledger);
|
|
107
|
-
// A refused join still returns the caller's exact retained envelope.
|
|
108
92
|
return !result.clean && result.ledger.sha256 === expandedOurs.sha256
|
|
109
93
|
? {...result, ledger: ours.ledger} : result;
|
|
110
94
|
}
|
|
@@ -129,8 +113,6 @@ export function merge(first, second) {
|
|
|
129
113
|
}
|
|
130
114
|
}
|
|
131
115
|
const a = checkpointList(ours), b = checkpointList(theirs);
|
|
132
|
-
// A synthetic conflict act has one identity derived from both original envelopes.
|
|
133
|
-
// Reconstruct its exact local prefix to recognize a repeat without aliasing act IDs.
|
|
134
116
|
for (const [index, record] of ours.ledger.records.entries()) {
|
|
135
117
|
if (record.transaction.operation !== 'document.merge') continue;
|
|
136
118
|
const point = a[index], prefix = exportLedger({text: point.text, metadata: point.metadata,
|
|
@@ -148,8 +130,6 @@ export function merge(first, second) {
|
|
|
148
130
|
if (found) common = {ours: found.index, theirs: b[i].index, text: found.text, metadata: found.metadata};
|
|
149
131
|
}
|
|
150
132
|
if (!common) throw fault('copies have no proven common start');
|
|
151
|
-
// A prior join can contain our acts in a different transported order. After
|
|
152
|
-
// proving their placement below, keep the complete superset's representation.
|
|
153
133
|
const incomingIds = new Set(theirs.ledger.records.map(row => row.transaction.id));
|
|
154
134
|
const sameStart = ours.complete && theirs.complete && ours.ledger.documentAuthority === theirs.ledger.documentAuthority &&
|
|
155
135
|
canonicalJSON(ours.ledger.start) === canonicalJSON(theirs.ledger.start);
|
|
@@ -195,7 +175,6 @@ export function merge(first, second) {
|
|
|
195
175
|
};
|
|
196
176
|
let against = ours.ledger.records.slice(common.ours);
|
|
197
177
|
const incoming = theirs.ledger.records.slice(common.theirs);
|
|
198
|
-
// The same original ID is a repeat only after its immutable fingerprint agrees.
|
|
199
178
|
const unseen = incoming.filter(record => !imported.has(record.transaction.id));
|
|
200
179
|
const refused = (kind, code, details = {}) => ({text: ours.text, metadata: ours.metadata, ledger: ours.ledger,
|
|
201
180
|
clean: false, conflicts: [{kind, code, ...details}],
|
|
@@ -212,7 +191,6 @@ export function merge(first, second) {
|
|
|
212
191
|
const local = localIds.get(record.transaction.id);
|
|
213
192
|
if (local) mappedIds.set(record.transaction.id, local.transaction.id);
|
|
214
193
|
}
|
|
215
|
-
// Kept text arriving from the other copy waits for the person; this copy's own kept edits merge like any other.
|
|
216
194
|
const incomingKept = canonicalJSON(kept(theirs.text));
|
|
217
195
|
const keepDiffers = incomingKept !== canonicalJSON(kept(ours.text)) && incomingKept !== canonicalJSON(kept(common.text)) ||
|
|
218
196
|
!appended(common.text, theirs.text) && !appended(ours.text, theirs.text);
|
|
@@ -253,7 +231,6 @@ export function merge(first, second) {
|
|
|
253
231
|
if (!conflicted) {
|
|
254
232
|
if (unproved.size) return refused('identity', 'act_order_conflict', {actId: unproved.values().next().value});
|
|
255
233
|
if (theirsContained) {
|
|
256
|
-
// Only equivalent proved states choose a representation, never competing authored values.
|
|
257
234
|
return settled(oursContained && theirs.ledger.sha256 < ours.ledger.sha256 ? theirs : ours);
|
|
258
235
|
}
|
|
259
236
|
if (oursContained) {
|
|
@@ -265,8 +242,6 @@ export function merge(first, second) {
|
|
|
265
242
|
if (conflicted) {
|
|
266
243
|
if (unseen.length !== incoming.length) return refused('history', 'record_transport_conflict');
|
|
267
244
|
if (incomingFields.size) return metadataConflict('record_transport_conflict', {fields: [...incomingFields]});
|
|
268
|
-
// A display of both alternatives is not an accepted history. The caller's envelope
|
|
269
|
-
// stays whole; neither original act is replaced by a synthetic copy.
|
|
270
245
|
const merged = mergeText(keepDiffers ? null : common.text, ours.text, theirs.text, {oursId: 'copy-' + ours.ledger.sha256.slice(0, 16), theirsId: 'copy-' + theirs.ledger.sha256.slice(0, 16)});
|
|
271
246
|
if (!merged.conflicts.length) return refused('history', 'record_transport_conflict');
|
|
272
247
|
return {text: ours.text, metadata: ours.metadata, ledger: ours.ledger, clean: false,
|
|
@@ -274,8 +249,6 @@ export function merge(first, second) {
|
|
|
274
249
|
review: {required: true, base: ours.text, proposed: theirs.text}};
|
|
275
250
|
}
|
|
276
251
|
let conflicts = [];
|
|
277
|
-
// The act that keeps the physical splice needs the same authored origin as its
|
|
278
|
-
// witness. Copy it onto this result only; the caller's record stays untouched.
|
|
279
252
|
if (records.some(row => !row.splices.length && row.authored?.splices?.some(splice => splice.removed.length))) {
|
|
280
253
|
const origins = authoredPlacements(ours.ledger.start.text, records);
|
|
281
254
|
for (let index = 0; index < records.length; index++) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Shared conflict envelopes and exact three-way text merge; Notes and the ledger import this owner.
|
|
3
2
|
function textFault(code, message) { return Object.assign(new Error(message), {code}); }
|
|
4
3
|
export function canonicalNote(text) {
|
|
5
4
|
if (typeof text !== 'string') throw textFault('note_text', 'A note must be text');
|
|
@@ -24,7 +23,6 @@ const textHeading = line => /^ {0,3}#{1,6}(?:[ \t]|$)/.test(line);
|
|
|
24
23
|
const textDefinition = line => /^ {0,3}\[(?:\\.|[^\]\\\r\n])+\]:/.test(line);
|
|
25
24
|
const textStart = line => textHeading(line) || textRule(line) || textFence(line) || /^ {0,3}(?:[-+*]|1[.)])[ \t]+/.test(line) || /^ {0,3}(?:>|<!--|<(?:script|style|pre|textarea)(?:\s|>))/i.test(line);
|
|
26
25
|
|
|
27
|
-
// Only the exact public envelope is structural; malformed comments and fenced examples stay text.
|
|
28
26
|
function textEnvelopeAt(text, start = 0) {
|
|
29
27
|
if (!text.startsWith(TEXT_CONFLICT, start)) return null;
|
|
30
28
|
const headerEnd = text.indexOf(' -->\n\n', start), variants = [];
|
|
@@ -60,8 +58,6 @@ export function inspectTextConflicts(text, {nested = false} = {}) {
|
|
|
60
58
|
const found = textEnvelopeAt(unit);
|
|
61
59
|
if (found && found.end === unit.length) {
|
|
62
60
|
out.push({...found, start: at, end: at + unit.length});
|
|
63
|
-
// Nested offsets belong to their alternative's source; custody callers use
|
|
64
|
-
// the exact block bytes. Ordinary fenced examples remain opaque at every level.
|
|
65
61
|
if (nested) for (const variant of found.variants) out.push(...inspectTextConflicts(variant.text, {nested}));
|
|
66
62
|
}
|
|
67
63
|
at += unit.length;
|
|
@@ -80,7 +76,6 @@ export function mapTextConflictVariants(text, mapper) {
|
|
|
80
76
|
return out + text.slice(at);
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
// This is a conservative edit boundary scan, never the document's rendering grammar.
|
|
84
79
|
function textUnits(text) {
|
|
85
80
|
const lines = text.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) || [], out = [], offsets = [0];
|
|
86
81
|
for (const line of lines) offsets.push(offsets[offsets.length - 1] + line.length);
|
|
@@ -152,11 +147,9 @@ function textUnits(text) {
|
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
const TEXT_WINDOW_CELLS = 1000000, TEXT_CELLS = 4000000;
|
|
155
|
-
// One window's authored matches, or false when two optimal alignments disagree on any of them.
|
|
156
150
|
function textWindowMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
157
151
|
const n = b1 - b0, m = s1 - s0, cells = (n + 1) * (m + 1);
|
|
158
152
|
if (cells > TEXT_WINDOW_CELLS || (budget.cells -= cells) < 0) return false;
|
|
159
|
-
// Only authored units score; a blank line matches only beside a matched neighbour.
|
|
160
153
|
const width = m + 1, score = new Uint32Array(cells), content = i => !textSpace(base[b0 + i]), same = (i, j) => base[b0 + i] === side[s0 + j];
|
|
161
154
|
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) {
|
|
162
155
|
const p = i * width + j;
|
|
@@ -168,7 +161,6 @@ function textWindowMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
|
168
161
|
if (content(i) && same(i, j) && score[p] === score[p + width + 1] + 1) meaningful.push([i++, j++]);
|
|
169
162
|
else if (score[p + width] >= score[p + 1]) i++; else j++;
|
|
170
163
|
}
|
|
171
|
-
// Every optimal path must agree on every authored anchor, not just two greedy walks.
|
|
172
164
|
const seen = new Uint32Array(cells); seen.fill(0xffffffff); seen[0] = 0;
|
|
173
165
|
for (let i = 0; i <= n; i++) for (let j = 0; j <= m; j++) {
|
|
174
166
|
const p = i * width + j, at = seen[p]; if (at === 0xffffffff) continue;
|
|
@@ -185,8 +177,6 @@ function textWindowMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
|
185
177
|
return true;
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
// A window too large to prove whole splits only at authored units found once on each side of it:
|
|
189
|
-
// such a unit names its own occurrence. The strict rule above still decides every window between them.
|
|
190
180
|
function textMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
191
181
|
if ((b1 - b0 + 1) * (s1 - s0 + 1) <= TEXT_WINDOW_CELLS) return textWindowMatches(base, side, b0, b1, s0, s1, out, budget);
|
|
192
182
|
const once = (units, from, to) => {
|
|
@@ -196,7 +186,6 @@ function textMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
|
196
186
|
};
|
|
197
187
|
const inBase = once(base, b0, b1), pairs = [];
|
|
198
188
|
for (const [unit, j] of once(side, s0, s1)) { const i = inBase.get(unit); if (j >= 0 && i >= 0) pairs.push([i, j]); }
|
|
199
|
-
// The longest run of them in the same order on both sides.
|
|
200
189
|
const tails = [], prev = new Int32Array(pairs.length);
|
|
201
190
|
for (let k = 0; k < pairs.length; k++) {
|
|
202
191
|
let lo = 0, hi = tails.length;
|
|
@@ -239,7 +228,6 @@ function textTick(base, a, b) {
|
|
|
239
228
|
return m && {at: m[1].length, tick: m[2], body: value.slice(0, m[1].length) + ' ' + value.slice(m[1].length + 1)};
|
|
240
229
|
};
|
|
241
230
|
const c = box(base); if (!c) return null;
|
|
242
|
-
// One side ticked, the other added lines: the tick lands on the item found whole among the other side's units. Ambiguity is a conflict.
|
|
243
231
|
const au = textUnits(a), bu = textUnits(b);
|
|
244
232
|
if (au.length !== 1 || bu.length !== 1) {
|
|
245
233
|
const [one, many] = au.length === 1 ? [a, bu] : bu.length === 1 ? [b, au] : [null, null];
|
|
@@ -276,11 +264,9 @@ function textEnvelope(id, variants) {
|
|
|
276
264
|
return out + '<!-- /note-conflict:v1 -->\n';
|
|
277
265
|
}
|
|
278
266
|
|
|
279
|
-
// Takes the text's units, scanned once by the caller; a text without conflict envelopes is not scanned again.
|
|
280
267
|
function textDeclarations(units) {
|
|
281
268
|
const kept = units.filter(unit => !textEnvelopeAt(unit)).map(unit =>
|
|
282
269
|
textFence(unit.split(/\r\n|\r|\n/, 1)[0].replace(/^\ufeff/, '')) ? '\n' : unit), text = kept.join('');
|
|
283
|
-
// A definition can move a destination/title onto the next unindented line, or span its label.
|
|
284
270
|
const definitions = text.match(/^\ufeff? {0,3}\[(?:\\.|[^\]\\])+\]:[^\r\n]*(?:(?:\r\n|\r|\n)(?![ ]*(?:\r\n|\r|\n|$))[^\r\n]*)*/gm) || [];
|
|
285
271
|
const continuations = (kept.length === units.length ? kept : textUnits(text)).filter(unit => /^\ufeff? {0,3}\[\^/.test(unit));
|
|
286
272
|
return [...definitions, ...continuations, ...(text.match(/<!--[\s\S]*?(?:-->|$)/g) || [])].join('\0');
|
|
@@ -294,7 +280,6 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
294
280
|
if (base === theirs) return unchanged(ours);
|
|
295
281
|
const conflicts = []; let text = '';
|
|
296
282
|
const conflict = (before, a, b, kind) => {
|
|
297
|
-
// Shared framing remains ordinary text; only the changed region is a choice.
|
|
298
283
|
const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);
|
|
299
284
|
const au = textUnits(a), bu = textUnits(b); let first = 0, last = 0;
|
|
300
285
|
while (first < Math.min(au.length, bu.length) && au[first] === bu[first]) first++;
|
|
@@ -305,8 +290,6 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
305
290
|
conflict(null, au.slice(first, last ? -last : undefined).join(''), bu.slice(first, last ? -last : undefined).join(''), kind);
|
|
306
291
|
text += suffix; return;
|
|
307
292
|
}
|
|
308
|
-
// A shared unresolved region is an anchor even when its alternatives have grown. Splitting
|
|
309
|
-
// around it keeps surrounding edits in their own choices instead of quoting an old choice.
|
|
310
293
|
const overlaps = (x, y) => x.variants.some(v => y.variants.some(w => v.device === w.device && v.text === w.text));
|
|
311
294
|
const anchor = ab.map(x => ({x, matches: bb.filter(y => overlaps(x, y))})).find(({x, matches}) =>
|
|
312
295
|
matches.length === 1 && ab.filter(y => overlaps(y, matches[0])).length === 1 &&
|
|
@@ -340,10 +323,8 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
340
323
|
let pos = 0;
|
|
341
324
|
for (let i = 0; i < changes.length;) {
|
|
342
325
|
const group = [changes[i++]], start = group[0].start; let end = group[0].end;
|
|
343
|
-
// An insertion can anchor on either side of a blank run; that is still one gap.
|
|
344
326
|
while (i < changes.length && (changes[i].start < end || group.some(e => {
|
|
345
327
|
const next = changes[i];
|
|
346
|
-
// Appending after an unresolved block does not edit any of its alternatives.
|
|
347
328
|
const afterBlock = (edit, insert) => insert.start === insert.end && insert.start === edit.end && edit.start < edit.end && units.slice(edit.start, edit.end).every(unit => textEnvelopeAt(unit));
|
|
348
329
|
if (afterBlock(e, next) || afterBlock(next, e)) return false;
|
|
349
330
|
if (e.start === e.end && next.start <= e.start && e.start <= next.end) return true;
|
|
@@ -376,7 +357,6 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
376
357
|
return {text, clean: conflicts.length === 0 && !text.includes(TEXT_CONFLICT), conflicts};
|
|
377
358
|
}
|
|
378
359
|
|
|
379
|
-
// A marker is advisory data. The caller supplies its trusted descriptor and current revision.
|
|
380
360
|
export function resolveTextConflict(text, conflict, device) {
|
|
381
361
|
canonicalNote(text);
|
|
382
362
|
if (!conflict || !Number.isInteger(conflict.start) || !Number.isInteger(conflict.end) || conflict.start < 0 || conflict.end < conflict.start || text.slice(conflict.start, conflict.end) !== conflict.block) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Paired source placement, shared by live editing and canonical-history proof.
|
|
3
2
|
import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
|
|
4
3
|
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
4
|
import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
|
|
@@ -25,8 +24,6 @@ function emit(rows, at, remove, insert = '') {
|
|
|
25
24
|
} else rows.push({at, remove, insert});
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
// Sweep original-source boundaries. Each counter names a coordinate in one input's
|
|
29
|
-
// resulting document; concurrent insertions never enter the other input's removals.
|
|
30
27
|
export function transformPair(left, right, leftFirst) {
|
|
31
28
|
admitted(left); admitted(right);
|
|
32
29
|
const points = [...new Set([0, ...left.flatMap(row => [row.at, row.at + row.remove]),
|
|
@@ -62,9 +59,6 @@ function applyBatch(text, rows) {
|
|
|
62
59
|
return {text: after, splices: exact};
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
// Transport one sequential canonical act and every crossed record together. The
|
|
66
|
-
// reciprocal path is checked against exact source, rather than only equal end text.
|
|
67
|
-
// Canonical transport refuses deleting any authored unit already removed elsewhere.
|
|
68
62
|
export function transposeRecord(base, splices, id, records) {
|
|
69
63
|
if (typeof base !== 'string' || !Array.isArray(splices) || typeof id !== 'string' || !Array.isArray(records))
|
|
70
64
|
throw new RangeError('canonical_transport_invalid');
|
|
@@ -75,10 +69,6 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
75
69
|
if (next === null) throw new RangeError('canonical_transport_invalid');
|
|
76
70
|
current = next;
|
|
77
71
|
}
|
|
78
|
-
// A concurrent act that already removed these same authored units and wrote the
|
|
79
|
-
// same replacement is one physical edit. This act stays in history with no second
|
|
80
|
-
// deletion. Pure insertions are never collapsed: each one is its own unit. An
|
|
81
|
-
// inverse of that act means the replacement is not still in force.
|
|
82
72
|
const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
|
|
83
73
|
row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
|
|
84
74
|
const matched = crossed.filter(record => sameRows(record.splices, splices));
|
|
@@ -122,8 +112,6 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
122
112
|
return {splices: moved, sourceIndices, against: crossed, text: current};
|
|
123
113
|
}
|
|
124
114
|
|
|
125
|
-
// Hosted and editor callers already own a reciprocal transport log. Convert only
|
|
126
|
-
// its admitted exact source to records, then prove canonical-unit preservation.
|
|
127
115
|
export function transposeSource(base, splices, client, log) {
|
|
128
116
|
let source = base;
|
|
129
117
|
const records = log.map(entry => {
|
|
@@ -135,8 +123,6 @@ export function transposeSource(base, splices, client, log) {
|
|
|
135
123
|
|
|
136
124
|
export {authoredBasis, authoredPlacement, authoredPlacements};
|
|
137
125
|
|
|
138
|
-
// Reconstruct an authored view from the same records, then prove its exact physical
|
|
139
|
-
// placement. Equality of resulting text alone cannot prove a repeated-text anchor.
|
|
140
126
|
function authoredReplay(base, records, append = null) {
|
|
141
127
|
const origins = new Map(), byId = new Map(), originalOrder = [], cache = new Map(), frontier = new Set();
|
|
142
128
|
const effects = _rapierHistoryEffects(append ? [...records, append] : records);
|
|
@@ -156,8 +142,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
156
142
|
if (!result.ok) throw new RangeError('canonical_origin_invalid');
|
|
157
143
|
return result;
|
|
158
144
|
};
|
|
159
|
-
// Only immutable text digests are reused, within this one replay. Every
|
|
160
|
-
// authored-source comparison and physical placement proof still runs.
|
|
161
145
|
const textHashes = new Map();
|
|
162
146
|
const hashText = text => {
|
|
163
147
|
if (typeof text !== 'string') return sha256(text);
|
|
@@ -166,7 +150,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
166
150
|
return digest;
|
|
167
151
|
};
|
|
168
152
|
const same = (a, b) => canonicalJSON(a) === canonicalJSON(b);
|
|
169
|
-
// Only owner-created ordered views of already admitted string IDs use this.
|
|
170
153
|
const sameIds = (a, b) => {
|
|
171
154
|
if (a.length !== b.length) return false;
|
|
172
155
|
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
@@ -192,8 +175,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
192
175
|
})()))) throw new RangeError('canonical_origin_invalid');
|
|
193
176
|
const refs = effects.references.get(id) || [];
|
|
194
177
|
if (refs.some(id => !seen.has(id))) throw new RangeError('canonical_origin_invalid');
|
|
195
|
-
// An explicit origin can prove the same linear placement as an implicit one.
|
|
196
|
-
// Preserve the origin, but do not reconstruct every growing prefix to prove it again.
|
|
197
178
|
if (linearPrefix === originalOrder.length && same(origin.basis, [...frontier].sort()) &&
|
|
198
179
|
same(origin.splices, record.splices) && (!Object.hasOwn(record, 'authored') || origin.source === sha256(source)))
|
|
199
180
|
linearPrefix++;
|
|
@@ -212,7 +193,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
212
193
|
const key = coverKey(origin);
|
|
213
194
|
if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
|
|
214
195
|
}
|
|
215
|
-
// Two inverses can name different acts and still withdraw one physical replacement.
|
|
216
196
|
const sharesCover = id => (effects.references.get(id) || []).some(ref => {
|
|
217
197
|
const key = coverKey(origins.get(ref));
|
|
218
198
|
return !!key && coverCounts.get(key) > 1;
|
|
@@ -250,14 +230,9 @@ function authoredReplay(base, records, append = null) {
|
|
|
250
230
|
return closure;
|
|
251
231
|
};
|
|
252
232
|
const canonicalOrder = ordered(originalOrder);
|
|
253
|
-
// Only the three immediate fresh-spread call sites use this entry. Nothing
|
|
254
|
-
// retains allocation trust after an array has crossed an observable method.
|
|
255
233
|
const literalKey = order => {
|
|
256
|
-
// An input iterator can supply arbitrary values. Preserve the whole-array
|
|
257
|
-
// depth/refusal path before any per-string encoding in that case.
|
|
258
234
|
for (let index = 0; index < order.length; index++)
|
|
259
235
|
if (typeof order[index] !== 'string') return canonicalJSON(order);
|
|
260
|
-
// Keep each canonical primitive encoder call and its serializer behavior.
|
|
261
236
|
return '[' + Array.from({length: order.length}, (_, index) => canonicalJSON(order[index])).join(',') + ']';
|
|
262
237
|
};
|
|
263
238
|
const materialize = (order, rawSubmitted = false) => materializeKey(order, canonicalJSON(order), rawSubmitted);
|
|
@@ -266,14 +241,10 @@ function authoredReplay(base, records, append = null) {
|
|
|
266
241
|
if (cache.has(key)) return cache.get(key);
|
|
267
242
|
let prefixEnds = null;
|
|
268
243
|
const prefixKey = length => {
|
|
269
|
-
// The original submitted array keeps its existing slice/species/data gate.
|
|
270
|
-
// Only this module's own order allocations use the validated key string.
|
|
271
244
|
if (rawSubmitted) return canonicalJSON(order.slice(0, length));
|
|
272
245
|
if (!prefixEnds) {
|
|
273
246
|
prefixEnds = [1];
|
|
274
247
|
let quoted = false, depth = 0;
|
|
275
|
-
// Scan canonical JSON only: an escaped code unit cannot end a string,
|
|
276
|
-
// and commas inside strings or nested values cannot split this array.
|
|
277
248
|
for (let at = 1; at < key.length - 1; at++) {
|
|
278
249
|
const code = key.charCodeAt(at);
|
|
279
250
|
if (quoted) {
|
|
@@ -290,11 +261,7 @@ function authoredReplay(base, records, append = null) {
|
|
|
290
261
|
};
|
|
291
262
|
let placed = [], ids = new Set(), sourceIndices = new Map(), text = base;
|
|
292
263
|
if (!order.length) { const empty = {text, records: [], sourceIndices}; cache.set(key, empty); return empty; }
|
|
293
|
-
// A proved linear prefix already is its authored view. Replay it once;
|
|
294
|
-
// reconstructing every growing prefix adds no evidence.
|
|
295
264
|
let prefix = 0;
|
|
296
|
-
// Resume only an exact ordered prefix already proved in this replay. Copy its
|
|
297
|
-
// mutable containers before extending it; transported rows retain their placement.
|
|
298
265
|
for (let length = order.length - 1; length > 0; length--) {
|
|
299
266
|
const prior = cache.get(prefixKey(length));
|
|
300
267
|
if (!prior) continue;
|
|
@@ -314,8 +281,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
314
281
|
throw new RangeError('canonical_origin_invalid');
|
|
315
282
|
const closure = closureOf(origin.basis);
|
|
316
283
|
if (closure.size === ids.size && [...closure].every(prior => ids.has(prior))) {
|
|
317
|
-
// The current view already is the complete authored context. There are no
|
|
318
|
-
// independent rows to cross, even when earlier acts were concurrent.
|
|
319
284
|
if (hashText(text) !== origin.source) throw new RangeError('canonical_origin_invalid');
|
|
320
285
|
const next = _rapierTransformSplices(text, origin.splices);
|
|
321
286
|
if (next === null) throw new RangeError('canonical_origin_invalid');
|
|
@@ -326,8 +291,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
326
291
|
}
|
|
327
292
|
const basisOrder = canonicalOrder.filter(prior => closure.has(prior)), basis = materialize(basisOrder);
|
|
328
293
|
if (hashText(basis.text) !== origin.source) throw new RangeError('canonical_origin_invalid');
|
|
329
|
-
// Reconstruct causal context before crossing it. Physical gaps may already
|
|
330
|
-
// have collapsed under a deletion; they cannot choose concurrent ordering.
|
|
331
294
|
const contextOrder = canonicalOrder.filter(prior => ids.has(prior)), context = sameIds(contextOrder, [...ids]) ? {text, records: placed} : materialize(contextOrder);
|
|
332
295
|
if (context.text !== text) throw new RangeError('canonical_origin_invalid');
|
|
333
296
|
const alignedOrder = [...basisOrder, ...contextOrder.filter(prior => !closure.has(prior))];
|
|
@@ -336,10 +299,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
336
299
|
const crossed = aligned.records.slice(basisOrder.length);
|
|
337
300
|
let moved;
|
|
338
301
|
if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
|
|
339
|
-
// Each inverse retains its own targets. First prove the exact intention
|
|
340
|
-
// in its causal source, then place that choice in the accepted effect
|
|
341
|
-
// graph. Another inverse may already suppress the same original bytes,
|
|
342
|
-
// including when the two inverses name different authors of one replacement.
|
|
343
302
|
const authored = projectInverse(basis.text, basis.records, id);
|
|
344
303
|
if (!same(origin.splices, authored.splices) ||
|
|
345
304
|
_rapierTransformSplices(basis.text, origin.splices) !== authored.after)
|
|
@@ -357,7 +316,6 @@ function authoredReplay(base, records, append = null) {
|
|
|
357
316
|
const physicalOrder = append ? originalOrder.slice(0, -1) : originalOrder, physical = materialize(physicalOrder);
|
|
358
317
|
if (physical.text !== source || physical.records.some((row, index) => !same(row.splices, records[index].splices)))
|
|
359
318
|
throw new RangeError('canonical_origin_invalid');
|
|
360
|
-
// Raw submission is selected here by a private call site, never an input flag.
|
|
361
319
|
return {source, origins, originalOrder, materialize, materializeLiteral,
|
|
362
320
|
materializeSubmitted: order => materialize(order, true), ordered};
|
|
363
321
|
}
|
|
@@ -365,14 +323,11 @@ function authoredReplay(base, records, append = null) {
|
|
|
365
323
|
export function authoredHistory(base, records, selected = null) {
|
|
366
324
|
const replay = authoredReplay(base, records);
|
|
367
325
|
if (!selected) return {text: replay.source, records, origins: replay.origins};
|
|
368
|
-
// Targets follow independent work only in this transient Undo projection.
|
|
369
326
|
const projected = replay.materialize(replay.ordered(replay.originalOrder, selected));
|
|
370
327
|
if (projected.text !== replay.source) throw new RangeError('canonical_origin_invalid');
|
|
371
328
|
return {...projected, origins: replay.origins};
|
|
372
329
|
}
|
|
373
330
|
|
|
374
|
-
// Semantic annotations use the original splice coordinates, even when an accepted
|
|
375
|
-
// physical placement split those rows. The same replay proves the causal basis first.
|
|
376
331
|
export function authoredSourceReader(base, records) {
|
|
377
332
|
const replay = authoredReplay(base, records);
|
|
378
333
|
return actId => {
|
|
@@ -390,8 +345,6 @@ export function authoredSourceReader(base, records) {
|
|
|
390
345
|
};
|
|
391
346
|
}
|
|
392
347
|
|
|
393
|
-
// The original view and the accepted view are both reconstructed from the same
|
|
394
|
-
// immutable acts. The reciprocal rows are an exact ACK, not another history.
|
|
395
348
|
export function transposeAuthored(base, records, incoming, submitted) {
|
|
396
349
|
if (!incoming.authored || !Array.isArray(submitted)) throw new RangeError('canonical_origin_invalid');
|
|
397
350
|
const replay = authoredReplay(base, records, incoming), id = incoming.transaction.id;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// rapier-markdown-kit/rapier-ledger: a document's edit history with authorship, Undo of any act and offline merge.
|
|
3
|
+
// Offsets count UTF-16 code units; splices run in their recorded order.
|
|
4
|
+
export type ActorKind = 'human' | 'agent' | 'system';
|
|
5
|
+
export interface Actor { kind: ActorKind; id: string; }
|
|
6
|
+
export type DocKind = 'markdown' | 'text' | 'code';
|
|
7
|
+
export interface Metadata { filename: string; docKind: DocKind; }
|
|
8
|
+
export interface Splice { pos: number; removed: string; inserted: string; }
|
|
9
|
+
export interface Transaction {
|
|
10
|
+
id: string; documentAuthority: string; baseRevision: number; revision: number; actor: Actor;
|
|
11
|
+
transport: string; operation: string; requestId: string | null; sourceTransactionId: string | null;
|
|
12
|
+
affectedBlockIds: string[]; parent: string | null; reverts: string | null; reapplies: string | null;
|
|
13
|
+
createdAt: string | null; turnId?: string; sourceTransactionIds?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface LedgerRecord { transaction: Transaction; splices: Splice[]; beforeHash: string; afterHash: string; metadata?: Record<string, unknown>; authored?: unknown; }
|
|
16
|
+
/** rapier-ledger/2: plain JSON. */
|
|
17
|
+
export interface LedgerEnvelope {
|
|
18
|
+
format: 'rapier-ledger/2';
|
|
19
|
+
documentAuthority: string;
|
|
20
|
+
start: {text: string; revision: number; root: string; sha256: string; metadata: Metadata};
|
|
21
|
+
records: LedgerRecord[];
|
|
22
|
+
head: {revision: number; root: string; sha256: string; metadata: Metadata};
|
|
23
|
+
complete: boolean;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
/** Runs of [start, end, actor kind, actor id, createdAt] over the current text. */
|
|
27
|
+
export interface AuthorshipEnvelope { format: 'rapier-authorship/1'; sha256: string; runs: [number, number, ActorKind, string, string | number | null][]; }
|
|
28
|
+
export interface ReadLedger { ledger: LedgerEnvelope; text: string; metadata: Metadata; root: string; revision: number; complete: boolean; }
|
|
29
|
+
export interface LedgerInput { text: string; metadata: Metadata; records: LedgerRecord[]; documentAuthority?: string; revision?: number; root?: string; complete?: boolean; }
|
|
30
|
+
|
|
31
|
+
/** A new ledger with no history. Give every copy that may later merge the same documentAuthority. */
|
|
32
|
+
export function openLedger(text: string, options?: {filename?: string; docKind?: DocKind; documentAuthority?: string}): LedgerEnvelope;
|
|
33
|
+
export function exportLedger(input: LedgerInput): LedgerEnvelope;
|
|
34
|
+
/** Verifies the envelope (and, when given, that it describes this text and metadata); throws when it does not. */
|
|
35
|
+
export function readLedger(input: LedgerEnvelope, expectedText?: string, expectedMetadata?: Metadata): ReadLedger;
|
|
36
|
+
/** Records one replacement of the whole text as an act by `actor`. */
|
|
37
|
+
export function replaceLedgerText(input: LedgerEnvelope, text: string, options?: {actor?: Actor; operation?: string; at?: string}): LedgerEnvelope;
|
|
38
|
+
export function ledgerWithStart(input: LedgerEnvelope, text: string): LedgerEnvelope;
|
|
39
|
+
export function historyEnvelope(input: LedgerEnvelope, segmentIdentity?: unknown[], expectedText?: string, expectedMetadata?: Metadata): Record<string, unknown>;
|
|
40
|
+
/** The actor id an agent writes under, for a door and an agent name. */
|
|
41
|
+
export function agentActorId(door: string, agent: string): string;
|
|
42
|
+
export function textRoot(text: string): string;
|
|
43
|
+
export function rootAfter(root: string, splice: Splice): string;
|
|
44
|
+
export function sha256State(): {update(text: string): unknown; digest(): string};
|
|
45
|
+
|
|
46
|
+
export function authorship(input: LedgerEnvelope): AuthorshipEnvelope;
|
|
47
|
+
export function readAuthorship(input: AuthorshipEnvelope, text: string): AuthorshipEnvelope;
|
|
48
|
+
export function ledgerFromAuthorship(input: AuthorshipEnvelope, text: string, documentAuthority: string, metadata: Metadata): LedgerEnvelope;
|
|
49
|
+
|
|
50
|
+
export interface MergeConflict { kind: string; code?: string; actId?: string; [key: string]: unknown; }
|
|
51
|
+
export interface MergeResult {
|
|
52
|
+
text: string; metadata: Metadata; ledger: LedgerEnvelope;
|
|
53
|
+
/** False when a person must review: conflicts, or Will keep regions that differ. */
|
|
54
|
+
clean: boolean;
|
|
55
|
+
conflicts: MergeConflict[];
|
|
56
|
+
review: {required: boolean; base: string; proposed: string};
|
|
57
|
+
}
|
|
58
|
+
/** Merges two copies of one document (same documentAuthority) without a server. */
|
|
59
|
+
export function merge(first: LedgerEnvelope, second: LedgerEnvelope): MergeResult;
|
|
60
|
+
export function transportInterval(start: number, end: number, splices: Splice[]): {start: number; end: number} | null;
|
|
61
|
+
export function transportTouchedInterval(start: number, end: number, splices: Splice[]): {start: number; end: number};
|
|
62
|
+
|
|
63
|
+
export interface HistoryInput { source: string; metadata: Metadata; records: LedgerRecord[]; revision: number; earliestRevision: number; complete: boolean; }
|
|
64
|
+
export interface HistoryFailure { ok?: false; code: string; [key: string]: unknown; }
|
|
65
|
+
/** Reverses chosen acts while later work stays. */
|
|
66
|
+
export function selectiveUndo(input: HistoryInput, actIds: string[], options?: {actor?: Actor; splitSplice?: (...args: unknown[]) => unknown}): Record<string, unknown> | HistoryFailure;
|
|
67
|
+
export function replayHistory(input: HistoryInput): unknown;
|
|
68
|
+
export function sourceBefore(input: HistoryInput, actId: string): unknown;
|
|
69
|
+
export function historyProjection(input: HistoryInput, range?: {from?: number; to?: number; actId?: string}): unknown;
|
|
70
|
+
export function historyProjections(input: HistoryInput, ranges: unknown[]): unknown;
|
|
71
|
+
export function historyPlaces(input: HistoryInput, blocks: unknown[]): unknown;
|
|
72
|
+
export function groupHistoryActs(acts: unknown[]): unknown;
|
|
73
|
+
export function pictureHistorySplices(input: unknown, target: unknown, redo: boolean): Splice[] | null;
|
|
74
|
+
export function transposeSource(base: string, splices: Splice[], client: unknown, log: unknown): unknown;
|
|
75
|
+
export function authoredBasis(...args: unknown[]): unknown;
|
|
76
|
+
export function authoredPlacement(...args: unknown[]): unknown;
|
|
77
|
+
export function authoredHistory(base: string, records: LedgerRecord[], selected?: unknown): unknown;
|
|
78
|
+
export function transposeAuthored(base: string, records: LedgerRecord[], incoming: unknown, submitted: unknown): unknown;
|
|
79
|
+
|
|
80
|
+
export {writeDocument, readDocument, writeParts, readParts} from './ledger/carried.mjs';
|
|
81
|
+
|
|
82
|
+
export interface TextConflict { kind: 'text'; id: string; start: number; end: number; block: string; variants: string[]; }
|
|
83
|
+
export interface TextMerge { text: string; clean: boolean; conflicts: (TextConflict | {kind: string; advisory?: boolean})[]; }
|
|
84
|
+
/** Exact three-way text merge; a region both sides changed becomes a marked choice in the text. */
|
|
85
|
+
export function mergeText(base: string | null, ours: string, theirs: string, options?: Record<string, unknown>): TextMerge;
|
|
86
|
+
export function inspectTextConflicts(text: string, options?: {nested?: boolean}): TextConflict[];
|
|
87
|
+
export function resolveTextConflict(text: string, conflict: TextConflict | string, device: unknown): string;
|
|
88
|
+
export function mapTextConflictVariants(text: string, mapper: (variant: string) => string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {exportLedger} from './ledger/format.mjs';
|
|
3
|
+
export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
4
|
+
export {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
|
|
5
|
+
export {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
|
|
6
|
+
export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
|
|
7
|
+
export {sha256State} from './ledger/hash.mjs';
|
|
8
|
+
export {transposeSource, authoredBasis, authoredPlacement, authoredHistory, transposeAuthored} from './ledger/transport.mjs';
|
|
9
|
+
export {writeDocument, readDocument, writeParts, readParts} from './ledger/carried.mjs';
|
|
10
|
+
export {mergeText, resolveTextConflict, inspectTextConflicts, mapTextConflictVariants} from './ledger/text-merge.mjs';
|
|
11
|
+
|
|
12
|
+
export function openLedger(text, {filename = 'document.md', docKind = 'markdown', documentAuthority = globalThis.crypto.randomUUID()} = {}) {
|
|
13
|
+
return exportLedger({text, metadata: {filename, docKind}, records: [], documentAuthority, complete: true});
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// md.use(rapierMarkdown): Self-contained Markdown in markdown-it, with the reference style's markup.
|
|
3
|
+
interface MarkdownItLike { use(plugin: (md: any, ...params: any[]) => unknown, ...params: any[]): any; renderer: {rules: Record<string, unknown>}; core: {ruler: {after(after: string, name: string, rule: (state: any) => unknown): void}}; }
|
|
4
|
+
export function rapierMarkdown<T extends MarkdownItLike>(md: T): T;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {parseLayout, imageStyle} from './layout.mjs';
|
|
3
|
+
import {typography, isPageBreakBlock} from './marks.mjs';
|
|
4
|
+
import {installMarkdownImages} from './assets.mjs';
|
|
5
|
+
|
|
6
|
+
const PAGE_BREAK = '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n';
|
|
7
|
+
|
|
8
|
+
export function rapierMarkdown(md) {
|
|
9
|
+
if (md.__rapierMarkdownInstalled) return md;
|
|
10
|
+
md.__rapierMarkdownInstalled = true;
|
|
11
|
+
typography.install(installMarkdownImages(md));
|
|
12
|
+
md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
|
|
13
|
+
'<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
|
|
14
|
+
md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
|
|
15
|
+
renderer.renderToken(tokens, index, options) + '</div>\n';
|
|
16
|
+
md.core.ruler.after('inline', 'rapier-layout', state => {
|
|
17
|
+
for (let i = 1; i < state.tokens.length; i++) {
|
|
18
|
+
const inline = state.tokens[i], owner = state.tokens[i - 1];
|
|
19
|
+
if (inline.type !== 'inline' || !['paragraph_open', 'heading_open'].includes(owner.type)) continue;
|
|
20
|
+
const children = inline.children, marker = children.at(-1);
|
|
21
|
+
const layout = marker?.type === 'html_inline' ? parseLayout(marker.content) : null;
|
|
22
|
+
if (!layout) continue;
|
|
23
|
+
children.pop();
|
|
24
|
+
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
25
|
+
const visible = children.filter(token => token.type !== 'text' || token.content.trim());
|
|
26
|
+
if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
|
|
27
|
+
visible[0].attrSet('style', imageStyle(layout));
|
|
28
|
+
visible[0].attrSet('data-md-image-width', String(layout.width));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const htmlBlock = md.renderer.rules.html_block || ((tokens, index) => tokens[index].content);
|
|
33
|
+
md.renderer.rules.html_block = (tokens, index, options, env, renderer) =>
|
|
34
|
+
isPageBreakBlock(tokens[index].content) ? PAGE_BREAK : htmlBlock(tokens, index, options, env, renderer);
|
|
35
|
+
return md;
|
|
36
|
+
}
|