rapier-markdown-kit 1.2.8 → 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.
Files changed (60) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +1 -47
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +18 -173
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +4 -23
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/word-sidecar.mjs +0 -2
  59. package/package.json +1 -1
  60. package/style.css +32 -16
@@ -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;
@@ -1,6 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The public edit ledger (rapier-markdown-kit/rapier-ledger): the history, its carried parts and three-way text merge.
3
- // The editor binds kit/rapier-ledger.mjs, which also carries its private record helpers.
4
2
  import {exportLedger} from './ledger/format.mjs';
5
3
  export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
6
4
  export {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
@@ -11,8 +9,6 @@ export {transposeSource, authoredBasis, authoredPlacement, authoredHistory, tran
11
9
  export {writeDocument, readDocument, writeParts, readParts} from './ledger/carried.mjs';
12
10
  export {mergeText, resolveTextConflict, inspectTextConflicts, mapTextConflictVariants} from './ledger/text-merge.mjs';
13
11
 
14
- // A new ledger for one text, with no history yet. documentAuthority names the document across its copies:
15
- // pass the same one to every copy that may later merge.
16
12
  export function openLedger(text, {filename = 'document.md', docKind = 'markdown', documentAuthority = globalThis.crypto.randomUUID()} = {}) {
17
13
  return exportLedger({text, metadata: {filename, docKind}, records: [], documentAuthority, complete: true});
18
14
  }
@@ -1,6 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // rapier-markdown-kit/markdown-it: md.use(rapierMarkdown) renders Self-contained Markdown with the reference style's markup
3
- // (wrap the output in <main class="md-render"> with rapier-markdown-kit/style.css). Create markdown-it with {html: true}.
4
2
  import {parseLayout, imageStyle} from './layout.mjs';
5
3
  import {typography, isPageBreakBlock} from './marks.mjs';
6
4
  import {installMarkdownImages} from './assets.mjs';
@@ -15,7 +13,6 @@ export function rapierMarkdown(md) {
15
13
  '<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
16
14
  md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
17
15
  renderer.renderToken(tokens, index, options) + '</div>\n';
18
- // A paragraph's or heading's closing layout comment: its alignment, and a lone picture's width.
19
16
  md.core.ruler.after('inline', 'rapier-layout', state => {
20
17
  for (let i = 1; i < state.tokens.length; i++) {
21
18
  const inline = state.tokens[i], owner = state.tokens[i - 1];
@@ -1,6 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // The licensed static faces retain the shipped variable font's exact glyphs and metrics.
3
- // Browser ranges own placement; unsupported shaping stays in the original page paint.
4
3
 
5
4
  const familyName = value => value.trim().replace(/^['"]|['"]$/g, '');
6
5
 
@@ -80,7 +79,6 @@ function shape(value, font, preserve, ligatures) {
80
79
  if (code === 0x00ad) {
81
80
  at += character.length;
82
81
  const own = font.glyph(code), hyphen = font.glyph(0x2d), glyph = own || hyphen;
83
- // A discretionary hyphen with no glyph stays in the page paint. Do not skip the lookup.
84
82
  if (!glyph || glyph >= font.glyphCount) return null;
85
83
  result.push({start, end: at, text: own ? character : '-', glyph, soft: true});
86
84
  continue;
@@ -101,7 +99,7 @@ function shape(value, font, preserve, ligatures) {
101
99
  }
102
100
 
103
101
  export async function collectPdfGlyphs(main, {width, check, faces = []}) {
104
- const fontCache = new Map(); // Parsed byte copies belong to this PDF operation.
102
+ const fontCache = new Map();
105
103
  const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
106
104
  const nodes = new Map(), hidden = [], canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
107
105
  const context = canvas.getContext('2d', {willReadFrequently: true});
@@ -130,9 +128,6 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
130
128
  let visited = 0;
131
129
  for (const element of main.querySelectorAll('*')) {
132
130
  if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
133
- // Leaving the existing element in place preserves selectors, counters, line breaking and inline marks.
134
- // Keep preformatted blocks, their inline struts and syntax paint together.
135
- // A paragraph's own words beside a mark, a link or code are drawn too: each text node is placed and hidden on its own.
136
131
  if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.closest('svg,pre,.rapier-will-carrier')) continue;
137
132
  const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3 && node.data.trim());
138
133
  if (!textNodes.length || !safe(element)) continue;
@@ -162,8 +157,6 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
162
157
  for (const item of shaped) {
163
158
  range.setStart(node, item.start); range.setEnd(node, item.end);
164
159
  const ink = Array.from(range.getClientRects()).filter(rect => rect.height > 0);
165
- // A break after a soft hyphen can leave the next letter with a phantom box on the
166
- // hyphen's line. Keep the line that actually holds the ink.
167
160
  const lines = [];
168
161
  for (const rect of ink) {
169
162
  const line = lines.find(row => Math.abs(row[0].top - rect.top) <= .25);
@@ -174,7 +167,6 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
174
167
  if (lines.length > 1 && !item.soft && !previous?.soft) {valid = false; break;}
175
168
  const rects = lines[0] || [];
176
169
  const first = (item.soft ? rects.find(rect => rect.width > 0) : null) || rects[0];
177
- // An unbroken soft hyphen has no ink. Leave it out of the vector run.
178
170
  if (item.soft && !(first?.width > 0)) continue;
179
171
  if (!first) {valid = false; break;}
180
172
  const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
@@ -184,7 +176,6 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
184
176
  if (!valid || !placed.length) continue;
185
177
  nodes.set(node, placed);
186
178
  if (element.localName === 'li' || element.children.length || textNodes.length > 1) {
187
- // A list item's marker and a paragraph's marks are not this node's words: hide only the words.
188
179
  const wrap = document.createElement('span');
189
180
  node.replaceWith(wrap); wrap.append(node);
190
181
  wrap.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Add an associated source file to Rapier's fresh PDF without rewriting its pages.
3
2
  function pdfStructure(input) {
4
3
  const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
5
4
  const fail = () => { throw Object.assign(new Error('The PDF source could not be attached.'), {code: 'pdf_source_invalid'}); };
@@ -93,14 +92,15 @@ export async function attachPdfSource(input, {source, filename, maxBytes = Infin
93
92
  const payload = encode.encode(source); check();
94
93
  const compressed = new Uint8Array(await new Response(new Blob([payload]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()); check();
95
94
  const fileId = size, streamId = size + 1, infoId = size + 2, totalSize = size + 3;
96
- let infoBody = '<< /Producer (Rapier) >>';
95
+ let infoBody = '<< /Producer (Rapier) /Creator (Rapier) >>';
97
96
  const existingInfo = trailer.fields.get('Info');
98
97
  if (existingInfo) {
99
98
  const ref = /^(\d+)\s+(\d+)\s+R$/.exec(existingInfo.value);
100
99
  const held = ref ? readDictionary(Number(ref[1]), Number(ref[2])) : dictionary(existingInfo.from);
101
- const producer = held.fields.get('Producer');
102
- const kept = producer ? text.slice(held.start, producer.start) + text.slice(producer.end, held.end - 2) : text.slice(held.start, held.end - 2);
103
- infoBody = kept + ' /Producer (Rapier) >>';
100
+ const named = ['Producer', 'Creator'].map(key => held.fields.get(key)).filter(Boolean).sort((a, b) => a.start - b.start);
101
+ let kept = '', at = held.start;
102
+ for (const field of named) { kept += text.slice(at, field.start); at = field.end; }
103
+ infoBody = kept + text.slice(at, held.end - 2) + ' /Producer (Rapier) /Creator (Rapier) >>';
104
104
  }
105
105
  const escaped = '(' + filename.replace(/[^\x20-\x7e]/g, '_').replace(/[\\()]/g, '\\$&') + ')';
106
106
  const parts = [bytes, ascii('\n')], merged = new Map(offsets);
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // SVG keeps its geometry in PDF; only paint operations that need browser compositing are rasterized.
3
2
  const SVG = 'http://www.w3.org/2000/svg';
4
3
  const number = value => Number(Number(value).toFixed(6));
5
4
  const nums = values => values.map(number).join(' ');
@@ -132,7 +131,6 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
132
131
  root = document.importNode(parsed.documentElement, true);
133
132
  root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
134
133
  mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
135
- // A shadow root keeps the picture's own <style> on the picture, as an <img> does.
136
134
  mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
137
135
  textRoots.push(root);
138
136
  }
@@ -177,7 +175,6 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
177
175
  let path, fill, stroke;
178
176
  try {
179
177
  path = geometry(node); fill = color(style.fill);
180
- // SVG zero-width strokes are absent; PDF's zero width instead paints a hairline.
181
178
  stroke = Number.parseFloat(style.strokeWidth) === 0 ? null : color(style.stroke);
182
179
  if (style.vectorEffect !== 'none' || style.markerStart !== 'none' || style.markerMid !== 'none' || style.markerEnd !== 'none' || style.paintOrder !== 'normal') unsupported();
183
180
  } catch (_) { sequence.push({raster: node}); return; }
@@ -9,8 +9,6 @@ export function followingPrintProse(paragraph) {
9
9
  return next;
10
10
  }
11
11
 
12
- // The print compositor owns pagination. Only a normal-flow picture initial moves into
13
- // its adjacent prose, so a native float can remain with the lines it introduces.
14
12
  export function markPrintCapitals(root) {
15
13
  for (const image of root.querySelectorAll('img[data-rapier-image-layout]')) {
16
14
  const layout = parseLayoutAttribute(image.getAttribute('data-rapier-image-layout'));
@@ -23,7 +21,6 @@ export function markPrintCapitals(root) {
23
21
  }
24
22
  }
25
23
 
26
- // This factory is also embedded in the offline page, with the same line metrics owner.
27
24
  export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
28
25
  const selector = 'img[data-rapier-print-lines]', states = new WeakMap();
29
26
  const restoreStyle = (node, value) => value == null ? node.removeAttribute('style') : node.setAttribute('style', value);
@@ -55,7 +52,6 @@ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
55
52
  const availableWidth = owner.getBoundingClientRect().width -
56
53
  ['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'].reduce((sum, name) => sum + (parseFloat(style[name]) || 0), 0);
57
54
  if (!(availableWidth > 0)) continue;
58
- // As imageBox does, cap the width first and retain the picture's natural ratio.
59
55
  const width = Math.min(availableWidth, height * naturalWidth / naturalHeight);
60
56
  const occurrence = state?.occurrence || (image.parentElement.tagName === 'A' ? image.parentElement : image);
61
57
  if (!state) {
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The public renderer door; each factory binds explicit host ports once per document realm.
3
2
  export {createRenderer} from './render.mjs';
4
3
  export {createMarkdownRenderer} from './render-markdown.mjs';
5
4
  export {createRenderStyles} from './render-styles.mjs';