rapier-markdown-kit 1.2.3 → 1.2.6

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 (41) hide show
  1. package/LICENSE +6 -5
  2. package/README.md +28 -16
  3. package/dist/images/gif.mjs +3 -1
  4. package/dist/images/playback.mjs +6 -4
  5. package/dist/kit/conformance/run.mjs +1 -1
  6. package/dist/kit/ledger/authored-origin.mjs +49 -0
  7. package/dist/kit/ledger/authorship.mjs +18 -9
  8. package/dist/kit/ledger/data.mjs +7 -4
  9. package/dist/kit/ledger/format.mjs +25 -8
  10. package/dist/kit/ledger/history.mjs +31 -5
  11. package/dist/kit/ledger/inverse-source.mjs +111 -49
  12. package/dist/kit/ledger/journal-records.mjs +24 -29
  13. package/dist/kit/ledger/merge.mjs +48 -13
  14. package/dist/kit/ledger/text-merge.mjs +75 -29
  15. package/dist/kit/ledger/transport.mjs +30 -45
  16. package/dist/kit/pdf-vectors.mjs +2 -1
  17. package/dist/kit/print-capitals.mjs +0 -1
  18. package/dist/kit/rapier-ledger.mjs +2 -0
  19. package/dist/kit/render-markdown.mjs +12 -3
  20. package/dist/kit/render-print.mjs +43 -7
  21. package/dist/kit/render-sanitize.mjs +19 -12
  22. package/dist/kit/render-styles.mjs +3 -3
  23. package/dist/kit/render-work.mjs +1 -1
  24. package/dist/kit/render.mjs +6 -41
  25. package/dist/layout/paragraph-fit.mjs +1 -15
  26. package/dist/layout/typography.mjs +2 -1
  27. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  28. package/dist/packages/rapier-diff/diff.mjs +68 -0
  29. package/dist/packages/rapier-diff/index.mjs +4 -744
  30. package/dist/packages/rapier-diff/matching.mjs +157 -0
  31. package/dist/packages/rapier-diff/merge.mjs +226 -0
  32. package/dist/packages/rapier-diff/moves.mjs +130 -0
  33. package/dist/packages/rapier-diff/structure.mjs +50 -0
  34. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  35. package/dist/packages/rapier-diff/walk.mjs +219 -0
  36. package/dist/spec/html-blocks.mjs +3 -3
  37. package/dist/spec/md-layout.mjs +1 -1
  38. package/dist/spec/md-marks.mjs +5 -5
  39. package/dist/spec/md-source.mjs +15 -7
  40. package/package.json +1 -1
  41. package/style.css +11 -11
@@ -0,0 +1,219 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {words, letter, within, prev, next} from './tokens.mjs';
3
+
4
+ // Exact source records for a range: lines, then words, then characters, or structural parts when supplied.
5
+ export function walker(c) {
6
+ const {before, after, spend, safe, budget, raw, chars, code, segmenter, tokens, matches} = c;
7
+ function record(a0, a1, b0, b1, unit, part, coarse = false) {
8
+ if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
9
+ // Internal records: before, after, coarse, concern, transported fragments.
10
+ c.result.push([[a0, a1], [b0, b1], coarse, [a0 === a1 ? 2 : b0 === b1 ? 0 : 1, part?.kind || unit, part?.name]]);
11
+ }
12
+
13
+ // A line deletion or insertion holding whole named parts and only blank text besides is one record per part,
14
+ // cut at line starts, so each keeps its own name (and its callers' breaks).
15
+ function name(from, ownersA, ownersB) {
16
+ for (let r = from; r < c.result.length; r++) {
17
+ const [[a0, a1], [b0, b1]] = c.result[r], side = a0 === a1 ? 1 : b0 === b1 ? 0 : -1;
18
+ if (side < 0) continue;
19
+ const [s0, s1] = c.result[r][side], text = side ? after : before;
20
+ const parts = (side ? ownersB : ownersA).filter(part => part?.name && part.start >= s0 && part.end <= s1);
21
+ if (!parts.length || !spend(s1 - s0)) continue;
22
+ let blank = text.slice(s0, parts[0].start);
23
+ for (let k = 1; k <= parts.length; k++) blank += text.slice(parts[k - 1].end, parts[k]?.start ?? s1);
24
+ if (/\S/.test(blank)) continue;
25
+ const rows = parts.map((part, k) => {
26
+ const start = k ? Math.max(parts[k - 1].end, text.lastIndexOf('\n', part.start - 1) + 1) : s0;
27
+ return [start, part];
28
+ });
29
+ c.result.splice(r, 1, ...rows.map(([start, part], k) => {
30
+ const end = rows[k + 1]?.[0] ?? s1, here = [side ? [a0, a0] : [start, end], side ? [start, end] : [b0, b0]];
31
+ return [here[0], here[1], false, [side ? 2 : 0, part.kind, part.name]];
32
+ }));
33
+ r += rows.length - 1;
34
+ }
35
+ }
36
+
37
+ // A search step scans at most one fixed-size source chunk.
38
+ function common(a, b, end, step) {
39
+ let n = 0;
40
+ while (n < end && spend()) {
41
+ const stop = Math.min(end, n + 256);
42
+ while (n < stop && before.charCodeAt(a) === after.charCodeAt(b)) { n++; a += step; b += step; }
43
+ if (n < stop) break;
44
+ }
45
+ return n;
46
+ }
47
+
48
+ function walk(a0, a1, b0, b1, level, ap, bp, context) {
49
+ if (a1 - a0 === b1 - b0 && before.slice(a0, a1) === after.slice(b0, b1)) return;
50
+ if (!spend()) { record(a0, a1, b0, b1, 'block', context, true); return; }
51
+ const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
52
+ if (!structural && raw !== 'word') {
53
+ let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
54
+ while (n && (!safe(before, a0 + n) || !safe(after, b0 + n) || !raw && level < 3 && (within(before, a0 + n) || within(after, b0 + n)))) {
55
+ if (!spend()) { n = 0; break; } n--;
56
+ }
57
+ if (level === 1 && n) {
58
+ const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
59
+ spend(Math.ceil((n - next) / 256)); n = next;
60
+ }
61
+ a0 += n; b0 += n;
62
+ n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
63
+ while (n && (!safe(before, a1 - n) || !safe(after, b1 - n) || !raw && level < 3 && (within(before, a1 - n) || within(after, b1 - n)))) {
64
+ if (!spend()) { n = 0; break; } n--;
65
+ }
66
+ if (level === 1) while (n && (before[a1 - n - 1] !== '\n' || after[b1 - n - 1] !== '\n' || before[a1 - n] === '\n' || after[b1 - n] === '\n')) { if (!spend()) { n = 0; break; } n--; }
67
+ a1 -= n; b1 -= n;
68
+ if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
69
+ }
70
+ let unit = level >= 3 ? 'char' : level === 2 ? 'word' : 'line';
71
+ if (!raw && level === 2 && a1 - a0 + b1 - b0 <= 4 && [...before.slice(a0, a1), ...after.slice(b0, b1)].length <= 2 &&
72
+ letter(prev(before, a0) || prev(after, b0)) && letter(next(before, a1) || next(after, b1))) unit = 'char';
73
+ if (!structural && (a0 === a1 || b0 === b1)) { record(a0, a1, b0, b1, unit, context); return; }
74
+ const left = tokens(before, a0, a1, structural ? 0 : Math.max(1, level), ap);
75
+ const right = tokens(after, b0, b1, structural ? 0 : Math.max(1, level), bp);
76
+ if (!left || !right) { record(a0, a1, b0, b1, unit, context, true); return; }
77
+ const [a, x, ownersA] = left, [b, y, ownersB] = right, pairs = matches(a, b);
78
+ let ai = 0, bi = 0;
79
+ for (let i = 0; i <= a.length; i++) {
80
+ const j = i === a.length ? b.length : pairs[i];
81
+ if (j === undefined) continue;
82
+ const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
83
+ if (ai < i || bi < j) {
84
+ if (budget.left < 0 || pairs.coarse?.has(ai)) record(x0, x1, y0, y1, unit, context, true);
85
+ else if (structural && (ai === i || bi === j)) {
86
+ for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
87
+ for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
88
+ } else if (structural) {
89
+ const p = ownersA[ai], q = ownersB[bi];
90
+ if (i - ai === 1 && j - bi === 1 && p && q && p.kind === q.kind) walk(x0, x1, y0, y1, level - 1, p.children, q.children, q);
91
+ else {
92
+ const from = c.result.length;
93
+ walk(x0, x1, y0, y1, 1, null, null, context);
94
+ name(from, ownersA.slice(ai, i), ownersB.slice(bi, j));
95
+ }
96
+ } else if (level < 2 && raw !== 'line') lines(x.slice(ai, i + 1), y.slice(bi, j + 1), context);
97
+ else if (level === 2 && chars && !raw && i - ai <= 1 && j - bi <= 1 && x1 - x0 + y1 - y0 < 100) {
98
+ // A changed token is replaced whole unless the caller asks for the characters inside it.
99
+ walk(x0, x1, y0, y1, 3, null, null, context);
100
+ } else record(x0, x1, y0, y1, unit, context);
101
+ }
102
+ if (i < a.length) {
103
+ const p = ownersA[i], q = ownersB[j];
104
+ if ((p || q || level <= 1 && !raw) && before.slice(x[i], x[i + 1]) !== after.slice(y[j], y[j + 1]))
105
+ walk(x[i], x[i + 1], y[j], y[j + 1], p || q ? level - 1 : 3, p?.children, q?.children, q || p || context);
106
+ }
107
+ ai = i + 1; bi = j + 1;
108
+ }
109
+ }
110
+
111
+ // Unequal lines pair in order by shared words. A line sharing under a quarter of its words with every candidate is
112
+ // replaced whole: an inserted line never borrows a neighbor's words, and a rewrite never reads as stray fragments.
113
+ function lines(x, y, context) {
114
+ const n = x.length - 1, m = y.length - 1;
115
+ if (n * m > 4096 || !spend(n * m + x[n] - x[0] + y[m] - y[0])) return row(x[0], x[n], y[0], y[m], context);
116
+ // A renamed name's sites count as shared words.
117
+ if (!c.renames) {
118
+ c.renames = [[], []];
119
+ for (const [p, old] of c.aliases || []) if (before.slice(...old.nameAt) !== after.slice(...p.nameAt)) {
120
+ c.renames[0].push(old.nameAt, ...c.la.filter(l => l.key === old.key).map(l => l.at));
121
+ c.renames[1].push(p.nameAt, ...c.lb.filter(l => l.key === p.key).map(l => l.at));
122
+ }
123
+ }
124
+ const weight = (side, a, b) => {
125
+ const counts = new Map(), text = side ? after : before;
126
+ for (const [c, d] of words(segmenter, text.slice(a, b))) counts.set(text.slice(a + c, a + d), (counts.get(text.slice(a + c, a + d)) || 0) + d - c);
127
+ return [counts, [...counts.values()].reduce((n, v) => n + v, 0), c.renames[side].reduce((n, at) => n + Math.max(0, Math.min(b, at[1]) - Math.max(a, at[0])), 0)];
128
+ };
129
+ const left = x.slice(0, -1).map((at, k) => weight(0, at, x[k + 1])), right = y.slice(0, -1).map((at, k) => weight(1, at, y[k + 1]));
130
+ const best = Array.from({length: n + 1}, () => new Float64Array(m + 1)), pairs = [];
131
+ for (let k = 1; k <= n; k++) for (let l = 1; l <= m; l++) {
132
+ const [a, sa, ra] = left[k - 1], [b, sb, rb] = right[l - 1];
133
+ let shared = sa || sb ? Math.max(ra, rb) : 0.5;
134
+ for (const [key, v] of a) shared += Math.min(v, b.get(key) || 0);
135
+ best[k][l] = Math.max(best[k - 1][l], best[k][l - 1], shared * 4 > Math.max(sa, sb) && best[k - 1][l - 1] + shared);
136
+ }
137
+ for (let k = n, l = m; k && l;) {
138
+ if (best[k][l] === best[k - 1][l]) k--;
139
+ else if (best[k][l] === best[k][l - 1]) l--;
140
+ else pairs.unshift([--k, --l]);
141
+ }
142
+ let k = 0, l = 0;
143
+ for (const [p, q] of [...pairs, [n, m]]) {
144
+ // A merge compares one line against one line by character, whatever they share.
145
+ if (raw === 'char' && p - k === 1 && q - l === 1) row(x[k], x[p], y[l], y[q], context);
146
+ else if (k < p || l < q) record(x[k], x[p], y[l], y[q], 'line', context);
147
+ if (p < n) row(x[p], x[p + 1], y[q], y[q + 1], context);
148
+ k = p + 1; l = q + 1;
149
+ }
150
+ }
151
+
152
+ // A table row with the same number of cells on both sides changes cell by cell; a character diff stays in its line.
153
+ function row(a0, a1, b0, b1, context) {
154
+ if (raw === 'char') return walk(a0, a1, b0, b1, 3, null, null, context);
155
+ const cells = (text, a, b) => /^[ \t]*\|/.test(text.slice(a, b)) && [...text.slice(a, b).matchAll(/(?<!\\)\|/g)].map(m => a + m.index);
156
+ const p = !code && cells(before, a0, a1), q = p && cells(after, b0, b1);
157
+ if (!q || p.length !== q.length || !spend(p.length)) return walk(a0, a1, b0, b1, 2, null, null, context);
158
+ for (let k = 0; k <= p.length; k++) walk(k ? p[k - 1] + 1 : a0, p[k] ?? a1, k ? q[k - 1] + 1 : b0, q[k] ?? b1, 2, null, null, context);
159
+ }
160
+
161
+ // Surviving source pieces keep the occurrence witnessed by the host.
162
+ function operations(rows) {
163
+ if (!Array.isArray(rows)) return null;
164
+ const pieces = [[0, before.length]];
165
+ let length = before.length, steps = 1e7;
166
+ const verify = n => {
167
+ if ((steps -= n) >= 0) return true;
168
+ budget.left = -1; return false;
169
+ };
170
+ const split = at => {
171
+ let position = 0;
172
+ for (let i = 0; i < pieces.length; i++) {
173
+ const [start, end, text] = pieces[i], size = end - start;
174
+ if (position === at) return i;
175
+ if (position + size > at) {
176
+ const n = at - position;
177
+ pieces.splice(i, 1, [start, start + n, text], [start + n, end, text]);
178
+ return i + 1;
179
+ }
180
+ position += size;
181
+ }
182
+ return pieces.length;
183
+ };
184
+ for (const row of rows) {
185
+ if (!row || !Number.isSafeInteger(row.pos) || typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
186
+ row.pos < 0 || row.pos + row.removed.length > length || !verify(pieces.length * 2 + 1)) return null;
187
+ const left = split(row.pos), right = split(row.pos + row.removed.length);
188
+ let at = 0;
189
+ for (let i = left; i < right; i++) {
190
+ const [start, end, text = before] = pieces[i];
191
+ if (!row.removed.startsWith(text.slice(start, end), at)) return null;
192
+ at += end - start;
193
+ }
194
+ if (row.removed === row.inserted) continue;
195
+ pieces.splice(left, right - left, [0, row.inserted.length, row.inserted]);
196
+ length += row.inserted.length - row.removed.length;
197
+ }
198
+ if (length !== after.length) return null;
199
+ const spans = [];
200
+ let a = 0, b = 0, at = 0;
201
+ for (const [start, end, text] of pieces) {
202
+ const size = end - start;
203
+ if (!verify(1) || !after.startsWith((text ?? before).slice(start, end), at)) return null;
204
+ if (text === undefined) {
205
+ let x = start, y = at, tail = end;
206
+ while (x < tail && (!safe(before, x) || !safe(after, y))) { x++; y++; if (!verify(1)) return null; }
207
+ while (tail > x && (!safe(before, tail) || !safe(after, at + tail - start))) { tail--; if (!verify(1)) return null; }
208
+ if (tail > x) {
209
+ if (a !== x || b !== y) spans.push([a, x, b, y]);
210
+ a = tail; b = at + tail - start;
211
+ }
212
+ }
213
+ at += size;
214
+ }
215
+ if (a !== before.length || b !== after.length) spans.push([a, before.length, b, after.length]);
216
+ return spans;
217
+ }
218
+ return {record, name, walk, operations};
219
+ }
@@ -73,8 +73,8 @@ export function htmlImageRecord(raw) {
73
73
  tokenEnd: raw.length, source: raw, placement: fence.layout, standalone: true, imageOnly: true};
74
74
  }
75
75
 
76
- // The editor's bounded worker splits before layout annotation. Join only the
77
- // exact adjacent marker proven by this same source grammar, preserving offsets.
76
+ // The first phase of the editor's bounded worker splits before layout annotation. Join only
77
+ // the exact adjacent marker proven by this same source grammar, preserving offsets.
78
78
  export function mergeHtmlObjectBlocks(blocks, source) {
79
79
  for (let index = 0; index + 1 < blocks.length; index++) {
80
80
  const block = blocks[index], next = blocks[index + 1];
@@ -104,7 +104,7 @@ export function htmlLayoutEdit(record, value) {
104
104
  }
105
105
 
106
106
  // Change only owned header tokens. Body, fence spelling, class/id and unrelated attributes stay exact.
107
- export function htmlInfoEdit(info, changes) {
107
+ function htmlInfoEdit(info, changes) {
108
108
  let text = info;
109
109
  for (const [key, value] of Object.entries(changes)) {
110
110
  const pattern = new RegExp('(^|[\\s{])\\s*' + key + '=(?:"[^"]*"|\'[^\']*\'|[^\\s}]+)', 'g');
@@ -167,7 +167,7 @@ export function wrapTextBlock(element) {
167
167
  element.textContent.trim() && !element.querySelector('img,table,pre,figure,math,.math-rendered') ? element : null;
168
168
  }
169
169
 
170
- // The one wrap-owner rule (layout/browser.js, layout/interchange.js, editor/share.js): text after, else before, skipping pictures,
170
+ // The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
171
171
  // metadata and empty paragraphs; any other block is a barrier in that direction.
172
172
  export function wrapNeighbour(node, classify) {
173
173
  for (const step of [n => n && n.nextElementSibling, n => n && n.previousElementSibling]) {
@@ -10,10 +10,10 @@ const inkReader = () => {
10
10
  const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
11
11
  const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
12
12
 
13
- // Ink (docs/markdown-standard.md, "Ink"): a pen stroke that stays with the words it marks, a paired comment with the colour span's shape. The
13
+ // Ink (docs/markdown-standard.md, "Ink"): a pen stroke that stays with the words it marks, a paired comment with the color span's shape. The
14
14
  // opener holds the stroke:
15
15
  // <!--ink KIND [NUMBER] [COLOUR] [w=N] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
16
- // KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when absent; box is the frame's
16
+ // KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the color span's value, red when absent; box is the frame's
17
17
  // size when the stroke was drawn and at the frame's offset from the words' box, both in hundredths of an em; w is the pen's width as Draw
18
18
  // counts a nib (2 to 24; 9 is the default and is never written): the stroke is drawn 0.11 em wide at 9 and in proportion to w otherwise; PATH
19
19
  // is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an em. One spelling: the fields in this order,
@@ -74,7 +74,7 @@ const TEXT_COLOR_HEX_TO_NAME = Object.freeze(
74
74
  );
75
75
  const PAGE_BREAK_MARKER = '<!--md-break:v1 page-->';
76
76
 
77
- export { TEXT_COLOR_NAMES, TEXT_COLOR_HEX_TO_NAME, PAGE_BREAK_MARKER };
77
+ export { TEXT_COLOR_NAMES, PAGE_BREAK_MARKER };
78
78
 
79
79
  export function formatColorRun(hex, content) {
80
80
  return typography.wrap({color: hex}, content);
@@ -338,7 +338,7 @@ export function formatInkOpen(mark) {
338
338
  let colour = '';
339
339
  if (mark.hex != null) {
340
340
  const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
341
- if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the colour is the colour span\'s value');
341
+ if (!new RegExp('^' + COLOR_VALUE + '$').test(value)) throw new TypeError('ink: the color is the color span\'s value');
342
342
  colour = ' ' + value;
343
343
  }
344
344
  if (mark.width != null && (!Number.isInteger(mark.width) || mark.width < 2 || mark.width > 24)) throw new TypeError('ink: the width is an integer from 2 to 24, and 9 is spelled by leaving it out');
@@ -442,7 +442,7 @@ export function pairInkSpans(source, markers = null) {
442
442
  return pairSpanMarkers(source, 'ink', markers);
443
443
  }
444
444
 
445
- // Colour keeps its first opener; ink pairs each closer with the innermost opener. The shared range
445
+ // Color keeps its first opener; ink pairs each closer with the innermost opener. The shared range
446
446
  // shape lets custody owners keep every independent stroke, including strokes on identical words.
447
447
  export function pairMarkers(source, kind, markers = null) {
448
448
  const paired = pairSpanMarkers(source, kind, markers);
@@ -28,6 +28,10 @@ export function markdownSourcePositions(source, parser) {
28
28
  }
29
29
  return matched;
30
30
  });
31
+ // A core rule may rewrite an inline token's content after its children were parsed (a task item drops
32
+ // its `[ ] `). Map rows from the text the inline rules read.
33
+ const read = new WeakMap();
34
+ inline.parse = function (text, md, env, children) { read.set(children, text); return parser.inline.parse.call(this, text, md, env, children); };
31
35
  const {tokens, cells} = markdownTableSources(body, owner, {}, lines);
32
36
  const readInline = (children, content, rawOffset) => {
33
37
  for (const child of children || []) {
@@ -68,26 +72,30 @@ export function markdownSourcePositions(source, parser) {
68
72
  }
69
73
  continue;
70
74
  }
71
- if (token.type !== 'inline' || !token.content) continue;
75
+ const content = read.get(token.children) ?? token.content;
76
+ if (token.type !== 'inline' || !content) continue;
72
77
  let rawOffset;
73
78
  if (token.map) {
74
- const offsets = [], starts = [], rows = token.content.split('\n');
79
+ const offsets = [], starts = [], spaces = [], rows = content.split('\n');
75
80
  let local = 0;
76
81
  for (let i = 0; i < rows.length; i++) {
77
- const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1);
78
- const at = source.slice(start, end).replace(/\0/g, '\ufffd').indexOf(rows[i]);
82
+ const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1), line = source.slice(start, end).replace(/\0/g, '\ufffd');
83
+ let at = line.indexOf(rows[i]), lead = 0;
84
+ // A container that consumes part of a tab leaves the rest as spaces the source does not hold.
85
+ // They all stand for that tab.
86
+ if (at < 0) { const rest = rows[i].replace(/^ +/, ''); lead = rows[i].length - rest.length; at = line.indexOf(rest); if (!lead || line[at - 1] !== '\t') at = -1; }
79
87
  if (at < 0) throw new Error('Markdown inline source mapping failed');
80
- starts.push(local); offsets.push(start + at); local += rows[i].length + 1;
88
+ starts.push(local); spaces.push(lead); offsets.push(start + at - lead); local += rows[i].length + 1;
81
89
  }
82
90
  rawOffset = pos => {
83
91
  let row = starts.length - 1;
84
92
  while (row > 0 && starts[row] > pos) row--;
85
- return offsets[row] + pos - starts[row];
93
+ return offsets[row] + Math.max(pos - starts[row], spaces[row] - 1);
86
94
  };
87
95
  } else if (cells.has(token)) {
88
96
  rawOffset = cells.get(token);
89
97
  } else continue;
90
- readInline(token.children, token.content, rawOffset);
98
+ readInline(token.children, content, rawOffset);
91
99
  }
92
100
  literal.sort((a, b) => a.start - b.start || b.end - a.end);
93
101
  const merged = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.3",
3
+ "version": "1.2.6",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -267,9 +267,9 @@
267
267
  .md-render h2{font-size:calc(var(--md-text-body) * 1.9);line-height:calc(var(--md-line) * 1.5);font-weight:var(--md-fw-semibold)}
268
268
  .md-render h3{font-size:calc(var(--md-text-body) * 1.5);line-height:calc(var(--md-line) * 1.5);font-weight:var(--md-fw-semibold)}
269
269
  .md-render h4{font-size:calc(var(--md-text-body) * 1.25);font-weight:var(--md-fw-semibold)}
270
- /* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/colour), never
270
+ /* h5/h6 complete the six-level hierarchy: modest but owned (explicit size/weight/color), never
271
271
  the browser default. Size stays strictly monotonic below h4; h6 sits at the body's own size and
272
- firms up weight and dims colour, since its size alone would read as body text. */
272
+ firms up weight and dims color, since its size alone would read as body text. */
273
273
  .md-render h5{font-size:calc(var(--md-text-body) * 1.1);font-weight:var(--md-fw-semibold)}
274
274
  .md-render h6{font-size:var(--md-text-body);font-weight:var(--md-fw-semibold);color:var(--md-heading-muted-color)}
275
275
  .md-render p{margin:0 0 var(--md-gap-paragraph)}
@@ -300,9 +300,9 @@
300
300
 
301
301
  .md-render .task-list-item input[type="checkbox"] {
302
302
  --md-task-box: clamp(14px, .92em, 24px);
303
- /* A form control does not inherit colour on its own; the shell's own app sheet gives it one
303
+ /* A form control does not inherit color on its own; the shell's own app sheet gives it one
304
304
  (`input,textarea,select{...color:inherit}`) and an exported page does not carry that sheet, so
305
- it says so here. The drawn box names its own colours, so this changes no pixel today -- it
305
+ it says so here. The drawn box names its own colors, so this changes no pixel today -- it
306
306
  stops the two surfaces disagreeing about a property, which is how the next rule that does
307
307
  depend on it would go wrong quietly. */
308
308
  color: inherit;
@@ -321,10 +321,10 @@
321
321
  touch-action: manipulation;
322
322
  }
323
323
  /* The box and its tick are Feather's square and check-square, with squared corners: drawn on Feather's
324
- 24-unit grid with its 2-unit stroke, the box's corners mitred square, and a ticked box opens at its
324
+ 24-unit grid with its 2-unit stroke, the box's corners mitered square, and a ticked box opens at its
325
325
  top right where the tick breaks out through it. The grid is a third wider than the box itself (the
326
326
  box is units 3 to 21), so the painted square keeps exactly the place and size --rapier-task-box gave
327
- it; the ink is --color-line through a mask, so the theme recolours it and an export needs nothing
327
+ it; the ink is --color-line through a mask, so the theme recolors it and an export needs nothing
328
328
  more than the data: images its policy already admits. The three glyphs are written here once, and a
329
329
  Notes card's check rows read these same three (rapier-notes.css: one SVG per icon across Rapier). */
330
330
  :root,.md-render {
@@ -367,7 +367,7 @@
367
367
  .md-render ol>li::before{
368
368
  content:counter(rapier-ol);
369
369
  position:absolute;
370
- /* The circle is centred on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
370
+ /* The circle is centered on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
371
371
  inset-inline-start:calc(-2.75 * var(--md-unit) + var(--md-marker, 0px)); top:calc(calc(0.15 * var(--md-unit)) + (var(--md-line) - calc(2 * var(--md-unit))) / 2);
372
372
  width:calc(2 * var(--md-unit)); height:calc(2 * var(--md-unit));
373
373
  display:flex; align-items:center; justify-content:center;
@@ -581,7 +581,7 @@
581
581
  }
582
582
 
583
583
  /* Page break: unobtrusive by design, the same weight as .section-fold-btn (rapier-editor.css) --
584
- muted colour and .37 opacity, never the stronger --color-line the horizontal-rule variants use.
584
+ muted color and .37 opacity, never the stronger --color-line the horizontal-rule variants use.
585
585
  Invisible on paper: this sheet's print rule zeros this out and applies the actual break instead. */
586
586
  .md-render [data-md-break]{
587
587
  position:relative;
@@ -642,7 +642,7 @@
642
642
  font-size: var(--md-text-sm);
643
643
  color: var(--md-color-text-secondary);
644
644
  }
645
- /* A view preference can keep embedded SVGs on their original light-paper colours. */
645
+ /* A view preference can keep embedded SVGs on their original light-paper colors. */
646
646
  html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowchart){color-scheme:light}
647
647
  .md-render img{
648
648
  max-width:100%;height:auto;
@@ -675,9 +675,9 @@ html:not([data-layout="plain"]) .md-render img[data-rapier-image-geometry="auto"
675
675
  .md-render mark[data-rapier-highlight="blue"]{--md-highlight-color:#4f91f7}
676
676
  .md-render mark[data-rapier-highlight="yellow"]{--md-highlight-color:#e6b91e}
677
677
  .md-render mark[data-rapier-highlight="purple"]{--md-highlight-color:#9b6dea}
678
- /* Text colour: the canonical (light) hex and its OKLCH-derived dark pair both ship as inline
678
+ /* Text color: the canonical (light) hex and its OKLCH-derived dark pair both ship as inline
679
679
  custom properties on the run itself (editor/engine.js, §b) — this just picks between them the
680
- same way as the document palette: the document colour scheme chooses the matching value. The five named swatches also carry a literal,
680
+ same way as the document palette: the document color scheme chooses the matching value. The five named swatches also carry a literal,
681
681
  pre-computed --md-color-dark here, so the common case survives even a round-trip that kept
682
682
  data-md-color but dropped style, exactly like highlight's own swatch constants. */
683
683
  .md-render [data-md-color]{--md-color-dark:var(--md-color);color:light-dark(var(--md-color),var(--md-color-dark))}