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.
- package/LICENSE +6 -5
- package/README.md +28 -16
- package/dist/images/gif.mjs +3 -1
- package/dist/images/playback.mjs +6 -4
- package/dist/kit/conformance/run.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +25 -8
- package/dist/kit/ledger/history.mjs +31 -5
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +48 -13
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +2 -1
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +2 -0
- package/dist/kit/render-markdown.mjs +12 -3
- package/dist/kit/render-print.mjs +43 -7
- package/dist/kit/render-sanitize.mjs +19 -12
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render-work.mjs +1 -1
- package/dist/kit/render.mjs +6 -41
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -744
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/html-blocks.mjs +3 -3
- package/dist/spec/md-layout.mjs +1 -1
- package/dist/spec/md-marks.mjs +5 -5
- package/dist/spec/md-source.mjs +15 -7
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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');
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -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,
|
|
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]) {
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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);
|
package/dist/spec/md-source.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
+
"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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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))}
|