rapier-markdown-kit 1.1.104 → 1.2.3
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 +5 -2
- package/README.md +34 -3
- package/dist/images/gif.mjs +47 -0
- package/dist/images/playback.mjs +101 -0
- package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
- package/dist/kit/conformance/04-wrap-box.md +1 -1
- package/dist/kit/conformance/05-behind.md +1 -1
- package/dist/kit/conformance/06-front.md +1 -1
- package/dist/kit/conformance/07-rotate-raster.md +1 -1
- package/dist/kit/conformance/08-drawing-rotation.md +1 -1
- package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
- package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
- package/dist/kit/conformance/10-both-sides.md +1 -1
- package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
- package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
- package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
- package/dist/kit/conformance/README.md +19 -28
- package/dist/kit/conformance/run.mjs +9 -7
- package/dist/kit/ledger/carried.mjs +10 -5
- package/dist/kit/ledger/format.mjs +12 -3
- package/dist/kit/ledger/hash.mjs +31 -15
- package/dist/kit/ledger/history.mjs +17 -5
- package/dist/kit/ledger/merge.mjs +4 -3
- package/dist/kit/pdf-vectors.mjs +4 -1
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-ink.mjs +1 -1
- package/dist/kit/render-markdown.mjs +146 -31
- package/dist/kit/render-print.mjs +131 -55
- package/dist/kit/render-sanitize.mjs +6 -3
- package/dist/kit/render.mjs +34 -77
- package/dist/layout/model.mjs +10 -8
- package/dist/packages/rapier-diff/index.mjs +75 -32
- package/dist/spec/document-fonts.mjs +1 -1
- package/dist/spec/document-settings.mjs +11 -164
- package/dist/spec/html-blocks.mjs +618 -79
- package/dist/spec/md-assets.mjs +29 -6
- package/dist/spec/md-layout.mjs +8 -0
- package/dist/spec/md-marks.mjs +25 -147
- package/dist/spec/md-source.mjs +54 -19
- package/package.json +1 -1
- package/style.css +7 -51
package/dist/layout/model.mjs
CHANGED
|
@@ -251,13 +251,11 @@ export function bandsProfile(bands) {
|
|
|
251
251
|
export function serializeProfile(profile, count = shapeGrid) {
|
|
252
252
|
if (!profile) return '';
|
|
253
253
|
const bands = [];
|
|
254
|
-
let any = false;
|
|
255
254
|
for (let index = 0; index < count; index++) {
|
|
256
255
|
const t = (index + .5) / count;
|
|
257
256
|
let runs = profile.runsAt ? profile.runsAt(t) : null;
|
|
258
257
|
if (!runs) { const span = profile.spanAt ? profile.spanAt(t) : null; runs = span ? [span] : []; }
|
|
259
258
|
const clean = (runs || []).map(([a, b]) => [Math.max(0, Math.min(1, +a)), Math.max(0, Math.min(1, +b))]).filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b) && b > a);
|
|
260
|
-
if (clean.length) any = true;
|
|
261
259
|
bands.push(clean.map(([a, b]) => {
|
|
262
260
|
let left = Math.round(a * 1e4), right = Math.round(b * 1e4);
|
|
263
261
|
// A narrow occupied run must not round to an invalid zero-width descriptor.
|
|
@@ -265,13 +263,13 @@ export function serializeProfile(profile, count = shapeGrid) {
|
|
|
265
263
|
return (left / 1e4) + '-' + (right / 1e4);
|
|
266
264
|
}).join(','));
|
|
267
265
|
}
|
|
268
|
-
return
|
|
266
|
+
return bands.join('|');
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
export function parseProfile(text) {
|
|
272
|
-
if (typeof text !== 'string' || !text) return null;
|
|
270
|
+
if (typeof text !== 'string' || !text || text.length > 262144) return null;
|
|
273
271
|
const bands = text.split('|').map(band => band ? band.split(',').map(run => run.split('-').map(Number)) : []);
|
|
274
|
-
if (bands.length < 2 || bands.some(band => band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
|
|
272
|
+
if (bands.length < 2 || bands.length > 192 || bands.some(band => band.length > 192 || band.some(run => run.length !== 2 || !run.every(Number.isFinite) || run[0] < 0 || run[1] > 1 || run[1] <= run[0]))) return null;
|
|
275
273
|
return bandsProfile(bands);
|
|
276
274
|
}
|
|
277
275
|
|
|
@@ -309,9 +307,13 @@ export function pictureSlices(profile, x, y, width, height, scale = 1) {
|
|
|
309
307
|
return span ? [span] : [];
|
|
310
308
|
}).filter(run => Number.isFinite(run[0]) && Number.isFinite(run[1]) && run[1] > run[0])
|
|
311
309
|
.sort((a, b) => a[0] - b[0]);
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
const
|
|
310
|
+
// Preserve transparent holes and gaps; only overlapping runs share an obstacle.
|
|
311
|
+
const merged = [];
|
|
312
|
+
for (const run of runs) {
|
|
313
|
+
const tail = merged.at(-1);
|
|
314
|
+
if (tail && run[0] <= tail[1]) tail[1] = Math.max(tail[1], run[1]);
|
|
315
|
+
else merged.push([...run]);
|
|
316
|
+
}
|
|
315
317
|
const current = new Map();
|
|
316
318
|
for (const [left, right] of merged) {
|
|
317
319
|
const key = left + ':' + right;
|
|
@@ -68,7 +68,7 @@ export function diff(before, after, options = {}) {
|
|
|
68
68
|
const initial = budget.left, raw = options._unit;
|
|
69
69
|
let result = [], code = options.as === 'code', structures, pa, pb, la, lb, aliases;
|
|
70
70
|
const spend = (n = 1) => (budget.left -= n) >= 0;
|
|
71
|
-
const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c);
|
|
71
|
+
const letter = c => c && /[\p{L}\p{M}\p{N}_]/u.test(c), within = (text, at) => letter(text[at - 1]) && letter(text[at]);
|
|
72
72
|
const overlaps = (a, b) => a[0] < b[1] && a[1] > b[0];
|
|
73
73
|
const inside = (c, a, b) => [a, b].every((range, side) => c[side][0] === c[side][1] ||
|
|
74
74
|
c[side][0] >= range[0] && c[side][1] <= range[1]);
|
|
@@ -146,7 +146,11 @@ export function diff(before, after, options = {}) {
|
|
|
146
146
|
if (!count) continue;
|
|
147
147
|
// A chain is admitted only after visiting every candidate. Reject a
|
|
148
148
|
// search whose mandatory visits cannot fit before allocating its nodes.
|
|
149
|
-
if ((unique || count) > Math.min(131072 - nodes, budget.left)) {
|
|
149
|
+
if ((unique || count) > Math.min(131072 - nodes, budget.left)) {
|
|
150
|
+
// Repetitive text has too many equal pairs; a shortest script needs no anchors.
|
|
151
|
+
if (!shortest(a, b, a0, a1, b0, b1, found) && budget.left >= 0) (found.coarse ||= new Set()).add(a0);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
150
154
|
}
|
|
151
155
|
search: for (let i = a0; i < a1; i++) {
|
|
152
156
|
const list = positions[a[i]];
|
|
@@ -178,6 +182,27 @@ export function diff(before, after, options = {}) {
|
|
|
178
182
|
return found;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
// Myers' greedy search: O((N + M) D) steps; each frontier is kept for the backward pass.
|
|
186
|
+
function shortest(a, b, a0, a1, b0, b1, found) {
|
|
187
|
+
const n = a1 - a0, m = b1 - b0, v = new Int32Array(2 * (n + m) + 3), o = n + m + 1, trace = [];
|
|
188
|
+
for (let d = 0; d <= 2048 && spend(2 * d + 1); d++) {
|
|
189
|
+
trace.push(v.slice(o - d, o + d + 1));
|
|
190
|
+
for (let k = -d; k <= d; k += 2) {
|
|
191
|
+
let x = k === -d || k !== d && v[o + k - 1] < v[o + k + 1] ? v[o + k + 1] : v[o + k - 1] + 1, y = x - k;
|
|
192
|
+
while (x < n && y < m && a[a0 + x] === b[b0 + y] && spend()) { x++; y++; }
|
|
193
|
+
v[o + k] = x;
|
|
194
|
+
if (x < n || y < m) continue;
|
|
195
|
+
for (x = n, y = m; d >= 0; d--) {
|
|
196
|
+
const w = trace[d], k = x - y, prev = k === -d || k !== d && w[d + k - 1] < w[d + k + 1] ? k + 1 : k - 1;
|
|
197
|
+
const px = d ? w[d + prev] : 0, py = px - prev;
|
|
198
|
+
while (x > px && y > py) if (a[a0 + --x] === b[b0 + --y]) found[a0 + x] = b0 + y;
|
|
199
|
+
x = px; y = py;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
181
206
|
// A search step scans at most one fixed-size source chunk.
|
|
182
207
|
function common(a, b, end, step) {
|
|
183
208
|
let n = 0;
|
|
@@ -195,14 +220,18 @@ export function diff(before, after, options = {}) {
|
|
|
195
220
|
const structural = level <= 0 && level > -128 && (ap?.length || bp?.length);
|
|
196
221
|
if (!structural && raw !== 'word') {
|
|
197
222
|
let n = common(a0, b0, Math.min(a1 - a0, b1 - b0), 1);
|
|
198
|
-
while (n && (!safe(before, a0 + n) || !safe(after, b0 + n)
|
|
223
|
+
while (n && (!safe(before, a0 + n) || !safe(after, b0 + n) || !raw && level < 3 && (within(before, a0 + n) || within(after, b0 + n)))) {
|
|
224
|
+
if (!spend()) { n = 0; break; } n--;
|
|
225
|
+
}
|
|
199
226
|
if (level === 1 && n) {
|
|
200
227
|
const next = Math.max(0, before.lastIndexOf('\n', a0 + n - 1) + 1 - a0);
|
|
201
228
|
spend(Math.ceil((n - next) / 256)); n = next;
|
|
202
229
|
}
|
|
203
230
|
a0 += n; b0 += n;
|
|
204
231
|
n = common(a1 - 1, b1 - 1, Math.min(a1 - a0, b1 - b0), -1);
|
|
205
|
-
while (n && (!safe(before, a1 - n) || !safe(after, b1 - n)
|
|
232
|
+
while (n && (!safe(before, a1 - n) || !safe(after, b1 - n) || !raw && level < 3 && (within(before, a1 - n) || within(after, b1 - n)))) {
|
|
233
|
+
if (!spend()) { n = 0; break; } n--;
|
|
234
|
+
}
|
|
206
235
|
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--; }
|
|
207
236
|
a1 -= n; b1 -= n;
|
|
208
237
|
if (budget.left < 0) { record(a0, a1, b0, b1, 'block', context, true); return; }
|
|
@@ -220,7 +249,7 @@ export function diff(before, after, options = {}) {
|
|
|
220
249
|
if (j === undefined) continue;
|
|
221
250
|
const x0 = x[ai], x1 = x[i], y0 = y[bi], y1 = y[j];
|
|
222
251
|
if (ai < i || bi < j) {
|
|
223
|
-
if (budget.left < 0) record(x0, x1, y0, y1, unit, context, true);
|
|
252
|
+
if (budget.left < 0 || pairs.coarse?.has(ai)) record(x0, x1, y0, y1, unit, context, true);
|
|
224
253
|
else if (structural && (ai === i || bi === j)) {
|
|
225
254
|
for (let k = ai; k < i; k++) record(x[k], x[k + 1], y0, y0, unit, ownersA[k] || context);
|
|
226
255
|
for (let k = bi; k < j; k++) record(x1, x1, y[k], y[k + 1], unit, ownersB[k] || context);
|
|
@@ -233,12 +262,9 @@ export function diff(before, after, options = {}) {
|
|
|
233
262
|
const start = result.length;
|
|
234
263
|
walk(x0, x1, y0, y1, 3, null, null, context);
|
|
235
264
|
const edits = result.slice(start);
|
|
265
|
+
// A letter-sized correction stays a letter; any other word change marks the whole word.
|
|
236
266
|
if (edits.length !== 1 || edits[0][0][1] - edits[0][0][0] + edits[0][1][1] - edits[0][1][0] > Math.max(2, (x1 - x0 + y1 - y0) / 3)) {
|
|
237
|
-
|
|
238
|
-
for (const edit of edits) {
|
|
239
|
-
if (group[0] !== edit[3][0]) group[0] = 1;
|
|
240
|
-
edit[3] = group;
|
|
241
|
-
}
|
|
267
|
+
result.length = start; record(x0, x1, y0, y1, unit, context);
|
|
242
268
|
}
|
|
243
269
|
} else record(x0, x1, y0, y1, unit, context);
|
|
244
270
|
}
|
|
@@ -328,6 +354,23 @@ export function diff(before, after, options = {}) {
|
|
|
328
354
|
else walk(0, before.length, 0, after.length, structures ? 0 : raw === 'char' ? 3 : raw === 'word' ? 2 : 1,
|
|
329
355
|
structures?.[0]?.parts, structures?.[1]?.parts);
|
|
330
356
|
|
|
357
|
+
// A separator-only piece beside another edit joins it; equal separators at its ends stay unchanged.
|
|
358
|
+
if (!raw) for (let i = result.length - 1; i > 0; i--) {
|
|
359
|
+
const p = result[i - 1], q = result[i], side = p[0][0] === p[0][1] ? 1 : p[1][0] === p[1][1] ? 0 : -1, text = side ? after : before;
|
|
360
|
+
if (side >= 0 && !/\S/.test(text.slice(...p[side]))) {
|
|
361
|
+
let n = 0;
|
|
362
|
+
while (p[0][1] + n < q[0][0] && text[p[side][0] + n] === text[p[side][1] + n]) n++;
|
|
363
|
+
if (p[0][1] + n === q[0][0] && p.slice(0, 2).every((span, k) => safe(k ? after : before, span[0] + n) && safe(k ? after : before, span[1] + n)))
|
|
364
|
+
for (const span of p.slice(0, 2)) { span[0] += n; span[1] += n; }
|
|
365
|
+
}
|
|
366
|
+
if (p[2] || q[2] || p[0][1] !== q[0][0] || p[1][1] !== q[1][0] ||
|
|
367
|
+
![p, q].some(r => !/\S/.test(before.slice(...r[0]) + after.slice(...r[1])))) continue;
|
|
368
|
+
const x = [p[0][0], q[0][1]], y = [p[1][0], q[1][1]], keep = /\S/.test(before.slice(...q[0]) + after.slice(...q[1])) ? q : p;
|
|
369
|
+
while (x[0] < x[1] && y[0] < y[1] && /\s/.test(before[x[1] - 1]) && before[x[1] - 1] === after[y[1] - 1] &&
|
|
370
|
+
safe(before, x[1] - 1) && safe(after, y[1] - 1)) { x[1]--; y[1]--; }
|
|
371
|
+
keep[3][0] = x[0] === x[1] ? 2 : y[0] === y[1] ? 0 : 1;
|
|
372
|
+
result.splice(i - 1, 2, [x, y, false, keep[3]]);
|
|
373
|
+
}
|
|
331
374
|
try { if (!raw && budget.left >= 0) {
|
|
332
375
|
const check = n => { if (!spend(n)) throw null; };
|
|
333
376
|
// Split transported text at wording boundaries; positions and words own distinct edits.
|
|
@@ -353,11 +396,14 @@ export function diff(before, after, options = {}) {
|
|
|
353
396
|
if (n * 5 > Math.max(prior.length, key.length) * 3 && n >= score) { old = n === score ? null : candidate; score = n; }
|
|
354
397
|
}
|
|
355
398
|
if (!old || old[4]) continue;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
399
|
+
// Separators that only one end carries stay outside the transported text.
|
|
400
|
+
const x = [...old[0]], y = [...item[1]];
|
|
401
|
+
for (const end of [0, 1]) {
|
|
402
|
+
const pattern = end ? /\s*$/ : /^\s*/, left = before.slice(...x).match(pattern)[0], right = after.slice(...y).match(pattern)[0];
|
|
403
|
+
let common = 0;
|
|
404
|
+
while (common < Math.min(left.length, right.length) && left.at(end ? common : -1 - common) === right.at(end ? common : -1 - common)) common++;
|
|
405
|
+
const dx = (left.length - common) * (end ? -1 : 1), dy = (right.length - common) * (end ? -1 : 1);
|
|
406
|
+
if (safe(before, x[end] + dx) && safe(after, y[end] + dy)) { x[end] += dx; y[end] += dy; }
|
|
361
407
|
}
|
|
362
408
|
const outer = result; result = [];
|
|
363
409
|
walk(...x, ...y, 1);
|
|
@@ -371,7 +417,8 @@ export function diff(before, after, options = {}) {
|
|
|
371
417
|
if (a0 < a1) old[4].push([[a0, a1], [old[1][0], old[1][0]], false, group]);
|
|
372
418
|
if (b0 < b1) item[4].push([[item[0][0], item[0][0]], [b0, b1], false, group]);
|
|
373
419
|
};
|
|
374
|
-
|
|
420
|
+
fragment(old[0][0], x[0], 0, 0, [0, 'char']); fragment(0, 0, item[1][0], y[0], [2, 'char']);
|
|
421
|
+
let a = x[0], b = y[0], wording;
|
|
375
422
|
for (const edit of inner) {
|
|
376
423
|
if (edit[3][3]) wording = edit[3];
|
|
377
424
|
else if (a !== edit[0][0] || b !== edit[1][0]) wording = [edit[3][0], edit[3][1]];
|
|
@@ -428,6 +475,14 @@ export function diff(before, after, options = {}) {
|
|
|
428
475
|
result = result.flatMap(item => item[4] || [item]);
|
|
429
476
|
}
|
|
430
477
|
|
|
478
|
+
// Whole-word replacements separated only by spaces read and restore as one phrase.
|
|
479
|
+
if (!raw && !code) for (let i = result.length - 1; i > 0; i--) {
|
|
480
|
+
const p = result[i - 1], q = result[i], gap = before.slice(p[0][1], q[0][0]);
|
|
481
|
+
if (p[3][0] !== 1 || q[3][0] !== 1 || p[3][3] || q[3][3] || p[2] || q[2] || !/^[^\S\r\n]+$/.test(gap) ||
|
|
482
|
+
gap !== after.slice(p[1][1], q[1][0]) || within(before, p[0][0]) || within(after, p[1][0]) ||
|
|
483
|
+
within(before, q[0][1]) || within(after, q[1][1])) continue;
|
|
484
|
+
p[0][1] = q[0][1]; p[1][1] = q[1][1]; result.splice(i, 1);
|
|
485
|
+
}
|
|
431
486
|
const changes = [], edits = [];
|
|
432
487
|
let normalized;
|
|
433
488
|
const meaning = structures?.[0]?.meaning;
|
|
@@ -453,9 +508,11 @@ export function diff(before, after, options = {}) {
|
|
|
453
508
|
if (!raw && !group[3]) group[3] = [[...item[0]], [...item[1]]];
|
|
454
509
|
if (!group[4]) {
|
|
455
510
|
const context = group[3];
|
|
511
|
+
// Context completes a word only where an edge falls inside it.
|
|
456
512
|
if (!raw) for (const [text, span] of [[before, context[0]], [after, context[1]]]) {
|
|
457
|
-
|
|
458
|
-
while (span[
|
|
513
|
+
const start = within(text, span[0]), end = within(text, span[1]);
|
|
514
|
+
while (start && span[0] && spend() && letter(text[span[0] - 1]) && safe(text, span[0] - 1)) span[0]--;
|
|
515
|
+
while (end && span[1] < text.length && spend() && letter(text[span[1]]) && safe(text, span[1] + 1)) span[1]++;
|
|
459
516
|
}
|
|
460
517
|
group[4] = []; changes.push(group);
|
|
461
518
|
if (format) group[0] = 5;
|
|
@@ -463,20 +520,6 @@ export function diff(before, after, options = {}) {
|
|
|
463
520
|
group[4].push(edits.length);
|
|
464
521
|
edits.push({before: item[0], after: item[1], ...(item[2] ? {coarse: true} : {})});
|
|
465
522
|
}
|
|
466
|
-
if (!raw && !structures && !code && !evidence) for (let i = 0; i < changes.length; i++) {
|
|
467
|
-
let end = i + 1;
|
|
468
|
-
for (; end < changes.length; end++) {
|
|
469
|
-
const a = changes[end - 1], b = changes[end];
|
|
470
|
-
const gap = before.slice(a[3][0][1], b[3][0][0]);
|
|
471
|
-
if (a[0] !== 1 || b[0] !== 1 || a[1] !== 'word' || b[1] !== 'word' ||
|
|
472
|
-
/\S|\r|\n/.test(gap) || gap !== after.slice(a[3][1][1], b[3][1][0])) break;
|
|
473
|
-
}
|
|
474
|
-
if (end - i < 4) continue;
|
|
475
|
-
const group = changes[i]; group[1] = 'sentence';
|
|
476
|
-
for (const next of changes.splice(i + 1, end - i - 1)) {
|
|
477
|
-
group[4].push(...next[4]); group[3][0][1] = next[3][0][1]; group[3][1][1] = next[3][1][1];
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
523
|
if (structures && !raw && budget.left >= 0) {
|
|
481
524
|
const oldNames = new Set(pa.filter(p => p.nameAt).map(p => p.name)), newNames = new Set(pb.filter(p => p.nameAt).map(p => p.name));
|
|
482
525
|
for (const change of changes) {
|
|
@@ -9,7 +9,7 @@ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS, typography} from './md-
|
|
|
9
9
|
export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
10
10
|
'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'initial', 'revert', 'revert-layer']);
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// No decoding or external resource loading happens here.
|
|
13
13
|
export function fontDataUrl(url) {
|
|
14
14
|
if (typeof url !== 'string') return null;
|
|
15
15
|
const match = /^data:font\/(woff2|woff|ttf|otf);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// One reader of document-wide settings and the inherited font snapshot.
|
|
3
3
|
// Framing stays in spec/frontmatter.mjs. Reading never rewrites source.
|
|
4
|
-
// The live view reads fontsize, the three font scopes
|
|
4
|
+
// The live view reads fontsize, the three font scopes and linestretch. It ignores papersize,
|
|
5
5
|
// geometry and pagestyle: a scroll has no page. Word and the PDF read all of them.
|
|
6
6
|
import {frontMatterFrame, frontMatterLine} from './frontmatter.mjs';
|
|
7
7
|
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
8
8
|
|
|
9
9
|
// Twelve points are one reference unit (16 px at the default reading scale).
|
|
10
10
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
|
-
const SPACE = Object.freeze({paragraph: .5, heading: Object.freeze([1.5, .5]), block: 1});
|
|
12
11
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
13
12
|
|
|
14
13
|
function measure(value, positive = false) {
|
|
@@ -41,7 +40,7 @@ const PAPERS = Object.freeze({
|
|
|
41
40
|
});
|
|
42
41
|
const EMPTY = Object.freeze({
|
|
43
42
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
44
|
-
papersize: null, geometry: null, pagestyle: null,
|
|
43
|
+
papersize: null, geometry: null, pagestyle: null,
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
function unquote(value) {
|
|
@@ -53,7 +52,7 @@ function marginOf(value) {
|
|
|
53
52
|
const match = /^margin\s*=\s*(\d+(?:\.\d+)?)(cm|mm|in|pt)$/i.exec(unquote(value));
|
|
54
53
|
if (!match) return null;
|
|
55
54
|
const number = Number(match[1]);
|
|
56
|
-
if (!Number.isFinite(number) || number < 0
|
|
55
|
+
if (!Number.isFinite(number) || number < 0) return null;
|
|
57
56
|
const unit = match[2].toLowerCase();
|
|
58
57
|
const twips = Math.round(unit === 'in' ? number * 1440 : unit === 'pt' ? number * 20 : unit === 'cm' ? number * 1440 / 2.54 : number * 1440 / 25.4);
|
|
59
58
|
if (!Number.isSafeInteger(twips) || twips < 0 || twips > 1440 * 6) return null;
|
|
@@ -214,97 +213,6 @@ function fieldsOf(source) {
|
|
|
214
213
|
return {frame, fields: parsed.fields, invalid: parsed.invalid || !structureIsClosed(source, lines)};
|
|
215
214
|
}
|
|
216
215
|
|
|
217
|
-
function flowItems(source, start, stop) {
|
|
218
|
-
const items = [];
|
|
219
|
-
for (let at = start; at < stop;) {
|
|
220
|
-
if (!source.slice(at, stop).trim()) break;
|
|
221
|
-
let end = at;
|
|
222
|
-
for (; end < stop; end++) {
|
|
223
|
-
const char = source[end];
|
|
224
|
-
if ((char === '"' || char === "'") && (end === at || /[\s:[{,]/.test(source[end - 1]))) {
|
|
225
|
-
const next = quotedEnd(source, end, stop);
|
|
226
|
-
if (next === null) return null;
|
|
227
|
-
end = next - 1;
|
|
228
|
-
} else if (char === '[' || char === '{') {
|
|
229
|
-
const next = flowEnd(source, end, stop);
|
|
230
|
-
if (next === null) return null;
|
|
231
|
-
end = next - 1;
|
|
232
|
-
} else if (char === ',') break;
|
|
233
|
-
}
|
|
234
|
-
if (!source.slice(at, end).trim()) return null;
|
|
235
|
-
items.push({start: at, end, comma: end < stop ? end : null});
|
|
236
|
-
at = end + 1;
|
|
237
|
-
}
|
|
238
|
-
return items;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function spaceFields(source, field) {
|
|
242
|
-
if (!field || field.opaque || field.invalid || field.multiline) return null;
|
|
243
|
-
const value = source.slice(field.start, field.end);
|
|
244
|
-
if (value.startsWith('{') && value.endsWith('}') && !field.children.length) {
|
|
245
|
-
const chunks = flowItems(source, field.start + 1, field.end - 1), fields = [];
|
|
246
|
-
if (!chunks) return null;
|
|
247
|
-
for (const chunk of chunks) {
|
|
248
|
-
const start = chunk.start + source.slice(chunk.start, chunk.end).match(/^\s*/)[0].length;
|
|
249
|
-
const line = {start, end: chunk.end, next: chunk.end, text: source.slice(start, chunk.end), eol: ''};
|
|
250
|
-
const item = fieldLine(line);
|
|
251
|
-
if (!item) return null;
|
|
252
|
-
valueSpan(source, item, chunk.end);
|
|
253
|
-
if (item.invalid || item.key === null) return null;
|
|
254
|
-
fields.push({...item, entryStart: chunk.start, entryEnd: chunk.end, comma: chunk.comma});
|
|
255
|
-
}
|
|
256
|
-
return {fields, flow: true};
|
|
257
|
-
}
|
|
258
|
-
if (value) return null;
|
|
259
|
-
if (!field.children.length) return {fields: [], flow: false};
|
|
260
|
-
const indent = /^ */.exec(field.children[0].text)[0].length;
|
|
261
|
-
const parsed = mapFields(source, field.children, indent);
|
|
262
|
-
return parsed.invalid ? null : {...parsed, flow: false, indent};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function headingParts(source, field) {
|
|
266
|
-
if (field.opaque || field.invalid || field.multiline) return null;
|
|
267
|
-
let spans;
|
|
268
|
-
if (field.children.length) {
|
|
269
|
-
if (field.start !== field.end) return null;
|
|
270
|
-
let indent = null;
|
|
271
|
-
spans = field.children.map(line => {
|
|
272
|
-
const match = /^( +)-[ \t]+/.exec(line.text);
|
|
273
|
-
if (!match || indent !== null && match[1].length !== indent) return null;
|
|
274
|
-
indent = match[1].length;
|
|
275
|
-
const span = valueSpan(source, {start: line.start + match[0].length, line, children: []}, line.end);
|
|
276
|
-
return span.invalid || span.opaque ? null : span;
|
|
277
|
-
});
|
|
278
|
-
} else {
|
|
279
|
-
if (source[field.start] !== '[' || source[field.end - 1] !== ']') return null;
|
|
280
|
-
spans = flowItems(source, field.start + 1, field.end - 1);
|
|
281
|
-
}
|
|
282
|
-
if (!spans || spans.length !== 2 || spans.some(span => !span)) return null;
|
|
283
|
-
const values = spans.map(span => measure(source.slice(span.start, span.end)));
|
|
284
|
-
return values.includes(null) ? null : {values, spans};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function spaceOf(source, field) {
|
|
288
|
-
const parsed = spaceFields(source, field);
|
|
289
|
-
if (!parsed) return null;
|
|
290
|
-
const value = {...SPACE}, seen = new Set();
|
|
291
|
-
for (const item of parsed.fields) {
|
|
292
|
-
if (seen.has(item.key)) return null;
|
|
293
|
-
seen.add(item.key);
|
|
294
|
-
if (!Object.hasOwn(SPACE, item.key)) continue;
|
|
295
|
-
if (item.key === 'heading') {
|
|
296
|
-
const pair = headingParts(source, item);
|
|
297
|
-
if (!pair) return null;
|
|
298
|
-
value.heading = Object.freeze(pair.values);
|
|
299
|
-
} else {
|
|
300
|
-
if (item.children.length || item.opaque || item.invalid || item.multiline) return null;
|
|
301
|
-
value[item.key] = measure(source.slice(item.start, item.end));
|
|
302
|
-
if (value[item.key] === null) return null;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return Object.keys(SPACE).some(key => seen.has(key)) ? Object.freeze(value) : null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
216
|
export function documentFont(value) {
|
|
309
217
|
const family = normalizeFontFamily(value);
|
|
310
218
|
if (!family) return null;
|
|
@@ -348,94 +256,37 @@ export function readDocumentSettings(markdown) {
|
|
|
348
256
|
const page = String(take('pagestyle') || '').toLowerCase();
|
|
349
257
|
return Object.freeze({
|
|
350
258
|
title: take('title') || null, subtitle: take('subtitle') || null, lang: languageTag(take('lang')),
|
|
351
|
-
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch,
|
|
259
|
+
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
|
|
352
260
|
pagestyle: page === 'plain' || page === 'empty' ? page : null,
|
|
353
261
|
});
|
|
354
262
|
}
|
|
355
263
|
|
|
356
|
-
function spaceValue(value) {
|
|
357
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !Object.hasOwn(SPACE, key))) return null;
|
|
358
|
-
const result = {...SPACE, ...value};
|
|
359
|
-
if (!Array.isArray(result.heading) || result.heading.length !== 2) return null;
|
|
360
|
-
return [result.paragraph, ...result.heading, result.block].every(number => typeof number === 'number' && Number.isFinite(number) && number >= 0) ? result : null;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
264
|
// Change only supplied settings. Unrelated YAML, comments, line endings, BOM and body are never reprinted.
|
|
364
265
|
export function updateDocumentSettings(markdown, patch) {
|
|
365
266
|
const source = String(markdown ?? ''), parsed = fieldsOf(source), {frame, fields} = parsed, before = readDocumentSettings(source);
|
|
366
267
|
const changes = [], append = [];
|
|
367
268
|
const eol = frame.closing ? frame.opening.eol : (source.match(/\r\n|\r|\n/) || ['\n'])[0];
|
|
368
|
-
const encodeSpace = value => '{ paragraph: ' + value.paragraph + ', heading: [' + value.heading.join(', ') + '], block: ' + value.block + ' }';
|
|
369
269
|
const same = (left, right) => Math.abs(left - right) <= Number.EPSILON * Math.max(Math.abs(left), Math.abs(right)) * 4;
|
|
370
270
|
const refuse = key => { throw new RangeError('Edit this setting in source: ' + key); };
|
|
371
|
-
const removeLine = (line, end
|
|
271
|
+
const removeLine = (line, end) => {
|
|
372
272
|
const suffix = source.slice(end, line.end);
|
|
373
|
-
changes.push({start: line.start, end: line.next, text: suffix.trimStart().startsWith('#') ?
|
|
273
|
+
changes.push({start: line.start, end: line.next, text: suffix.trimStart().startsWith('#') ? suffix + line.eol : ''});
|
|
374
274
|
};
|
|
375
275
|
const replaceValue = (field, text) => changes.push({start: field.start, end: field.end,
|
|
376
276
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
377
277
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
378
278
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
379
|
-
if (!font && !['fontsize', 'linestretch'
|
|
279
|
+
if (!font && !['fontsize', 'linestretch'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
380
280
|
const matches = fields.filter(item => item.key === key);
|
|
381
281
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
382
282
|
const field = matches[0];
|
|
383
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) :
|
|
283
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : leading(supplied);
|
|
384
284
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
385
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step :
|
|
386
|
-
if (
|
|
285
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : String(value.factor);
|
|
286
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : before.linestretch.factor === value.factor)) continue;
|
|
287
|
+
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
387
288
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
388
289
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
|
389
|
-
if (key === 'space') {
|
|
390
|
-
const map = spaceFields(source, field);
|
|
391
|
-
if (!map) refuse(key);
|
|
392
|
-
const known = map.fields.filter(item => Object.hasOwn(SPACE, item.key));
|
|
393
|
-
if (new Set(map.fields.map(item => item.key)).size !== map.fields.length) throw new RangeError('Duplicate document setting: space');
|
|
394
|
-
if (known.length && !before.space) refuse(key);
|
|
395
|
-
if (value === null) {
|
|
396
|
-
const unknown = map.fields.length !== known.length;
|
|
397
|
-
for (const item of known) {
|
|
398
|
-
if (map.flow) {
|
|
399
|
-
if (unknown) changes.push({start: item.entryStart, end: item.comma === null ? item.entryEnd : item.comma + 1, text: ''});
|
|
400
|
-
} else {
|
|
401
|
-
for (const child of item.children) {
|
|
402
|
-
const own = headingParts(source, item)?.spans.find(span => span.line === child);
|
|
403
|
-
if (!own) refuse(key);
|
|
404
|
-
removeLine(child, own.end, /^ */.exec(child.text)[0].length);
|
|
405
|
-
}
|
|
406
|
-
removeLine(item.line, item.end, item.indent);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (!unknown) removeLine(field.line, field.end);
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
if (before.space && encodeSpace(before.space) === encoded) continue;
|
|
413
|
-
if (!map.fields.length) { replaceValue(field, encoded); continue; }
|
|
414
|
-
const missing = [];
|
|
415
|
-
for (const name of Object.keys(SPACE)) {
|
|
416
|
-
if (before.space && JSON.stringify(before.space[name]) === JSON.stringify(value[name])) continue;
|
|
417
|
-
const item = map.fields.find(item => item.key === name);
|
|
418
|
-
const text = name === 'heading' ? '[' + value.heading.join(', ') + ']' : String(value[name]);
|
|
419
|
-
if (!item) { missing.push(name + ': ' + text); continue; }
|
|
420
|
-
if (name === 'heading') {
|
|
421
|
-
const pair = headingParts(source, item);
|
|
422
|
-
for (let i = 0; i < 2; i++) if (pair.values[i] !== value.heading[i]) {
|
|
423
|
-
const span = pair.spans[i], raw = source.slice(span.start, span.end);
|
|
424
|
-
changes.push({start: span.start + raw.length - raw.trimStart().length, end: span.end - raw.length + raw.trimEnd().length, text: String(value.heading[i])});
|
|
425
|
-
}
|
|
426
|
-
} else replaceValue(item, text);
|
|
427
|
-
}
|
|
428
|
-
if (missing.length) {
|
|
429
|
-
if (map.flow) {
|
|
430
|
-
const last = map.fields.at(-1);
|
|
431
|
-
changes.push({start: field.end - 1, end: field.end - 1, text: (last.comma === null ? ', ' : ' ') + missing.join(', ')});
|
|
432
|
-
} else {
|
|
433
|
-
const last = field.children.at(-1), indent = ' '.repeat(map.indent);
|
|
434
|
-
changes.push({start: last.next, end: last.next, text: missing.map(item => indent + item + eol).join('')});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
290
|
if (field.children.length) refuse(key);
|
|
440
291
|
if (value === null) removeLine(field.line, field.end);
|
|
441
292
|
else replaceValue(field, encoded);
|
|
@@ -472,10 +323,6 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
472
323
|
const decls = [];
|
|
473
324
|
if (settings.fontsize) decls.push('--md-text-body:' + settings.fontsize.css);
|
|
474
325
|
if (settings.linestretch || settings.fontsize) decls.push('--md-line:calc(var(--md-text-body) * ' + (settings.linestretch?.factor ?? DOCUMENT_LEADING) + ')');
|
|
475
|
-
if (settings.space) {
|
|
476
|
-
decls.push('--md-space-paragraph:' + settings.space.paragraph, '--md-space-heading-before:' + settings.space.heading[0],
|
|
477
|
-
'--md-space-heading-after:' + settings.space.heading[1], '--md-space-block:' + settings.space.block);
|
|
478
|
-
}
|
|
479
326
|
const {main, sans, mono} = documentSettingsFonts(settings, defaultFont);
|
|
480
327
|
if (sans) decls.push('--md-font-sans:' + (sans.css || fontFamilyCss(sans.family)));
|
|
481
328
|
if (main) decls.push('font-family:' + (main.css || fontFamilyCss(main.family)));
|