rapier-markdown-kit 1.2.32 → 1.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/kit/ledger/text-merge.mjs +163 -26
- package/package.json +1 -1
- package/style.css +12 -5
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
|
|
|
57
57
|
## Choose a Rapier workflow
|
|
58
58
|
|
|
59
59
|
Rapier is a phone-first Markdown editor for notes, diagrams, drawing and watercolor painting. It runs offline
|
|
60
|
-
as one HTML file on Android, Web and
|
|
60
|
+
as one HTML file on Android, Web, Windows and Linux.
|
|
61
61
|
|
|
62
62
|
1. **Work together live.** Connect to `https://mcp.rapier.website/door/mcp`, then call `open` to share one document with the person.
|
|
63
63
|
2. **Deliver an offline editor.** Run `npx rapier-html notes.md` to put the document and editor in one HTML file ([rapier-html](https://github.com/jackskip22/rapier-plugins/tree/main/npm/rapier-html)).
|
|
@@ -272,13 +272,167 @@ function textDeclarations(units) {
|
|
|
272
272
|
return [...definitions, ...continuations, ...(text.match(/<!--[\s\S]*?(?:-->|$)/g) || [])].join('\0');
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function* textGroups(units, a, b) {
|
|
276
|
+
const changes = [...a.map(e => ({...e, side: 0})), ...b.map(e => ({...e, side: 1}))].sort((x, y) => x.start - y.start || x.end - y.end || x.side - y.side);
|
|
277
|
+
for (let i = 0; i < changes.length;) {
|
|
278
|
+
const group = [changes[i++]], start = group[0].start; let end = group[0].end;
|
|
279
|
+
while (i < changes.length && (changes[i].start < end || group.some(e => {
|
|
280
|
+
const next = changes[i];
|
|
281
|
+
const afterBlock = (edit, insert) => insert.start === insert.end && insert.start === edit.end && edit.start < edit.end && units.slice(edit.start, edit.end).every(unit => textEnvelopeAt(unit));
|
|
282
|
+
if (afterBlock(e, next) || afterBlock(next, e)) return false;
|
|
283
|
+
if (e.start === e.end && next.start <= e.start && e.start <= next.end) return true;
|
|
284
|
+
if (next.start === next.end && e.start <= next.start && next.start <= e.end) return true;
|
|
285
|
+
return next.start >= e.end && units.slice(e.start, e.end).every(textSpace) &&
|
|
286
|
+
units.slice(e.end, next.start).every(textSpace) && units.slice(next.start, next.end).every(textSpace);
|
|
287
|
+
}))) {
|
|
288
|
+
group.push(changes[i]); end = Math.max(end, changes[i++].end);
|
|
289
|
+
}
|
|
290
|
+
const before = units.slice(start, end).join('');
|
|
291
|
+
const version = side => {
|
|
292
|
+
let value = '', at = start;
|
|
293
|
+
for (const e of group.filter(e => e.side === side)) { value += units.slice(at, e.start).join('') + e.text; at = e.end; }
|
|
294
|
+
return value + units.slice(at, end).join('');
|
|
295
|
+
};
|
|
296
|
+
yield {start, end, before, av: version(0), bv: version(1)};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function textMoves(groups) {
|
|
301
|
+
const blank = value => !value.trim();
|
|
302
|
+
const side = (g, which) => which === 0 ? g.av : g.bv;
|
|
303
|
+
for (const g of groups) {
|
|
304
|
+
if (g.resolved !== undefined || blank(g.before)) continue;
|
|
305
|
+
for (const gone of [0, 1]) {
|
|
306
|
+
const removed = side(g, gone), other = side(g, 1 - gone);
|
|
307
|
+
if (!blank(removed) || other === g.before) continue;
|
|
308
|
+
if (other.split(g.before).length === 2) { g.resolved = other.replace(g.before, ''); break; }
|
|
309
|
+
const homes = groups.filter(h => h !== g && h.resolved === undefined && blank(h.before) && blank(side(h, 1 - gone)) && side(h, gone).trim() === g.before.trim());
|
|
310
|
+
if (homes.length === 1) {
|
|
311
|
+
const trailing = side(homes[0], gone).match(/\s*$/)[0], edited = other.replace(/\s*$/, '');
|
|
312
|
+
g.resolved = ''; homes[0].resolved = side(homes[0], gone).match(/^\s*/)[0] + edited + trailing; break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return groups;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const textLines = text => text.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) || [];
|
|
320
|
+
const textWords = text => text.match(/\s+|\S+/g) || [];
|
|
321
|
+
const TEXT_ITEM = /^( {0,3})(?:[-+*]|\d{1,9}[.)])[ \t]+\S/;
|
|
322
|
+
const TEXT_TAGS = /^(tags|aliases):[ \t]*\[([^\[\]\r\n]*)\][ \t]*(\r\n|\r|\n|$)/;
|
|
323
|
+
function textCounts(text) {
|
|
324
|
+
const counts = new Map();
|
|
325
|
+
for (const word of text.match(/[\p{L}\p{N}_]+/gu) || []) counts.set(word, (counts.get(word) || 0) + 1);
|
|
326
|
+
return counts;
|
|
327
|
+
}
|
|
328
|
+
function textConserved(before, av, bv, merged) {
|
|
329
|
+
const [b, a, t, m] = [before, av, bv, merged].map(textCounts);
|
|
330
|
+
for (const word of new Set([...b.keys(), ...a.keys(), ...t.keys(), ...m.keys()])) {
|
|
331
|
+
const base = b.get(word) || 0, da = (a.get(word) || 0) - base, dt = (t.get(word) || 0) - base, got = (m.get(word) || 0) - base;
|
|
332
|
+
const both = [da + dt, da, dt];
|
|
333
|
+
if (da * dt > 0 ? got < Math.min(...both) || got > Math.max(...both) : got !== da + dt) return false;
|
|
334
|
+
}
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
function textRefine(before, av, bv, options, level = 'line') {
|
|
338
|
+
const merged = textRefineLevel(before, av, bv, options, level);
|
|
339
|
+
return merged !== null && textConserved(before, av, bv, merged) ? merged : null;
|
|
340
|
+
}
|
|
341
|
+
function textRefineLevel(before, av, bv, options, level) {
|
|
342
|
+
const split = level === 'line' ? textLines : textWords;
|
|
343
|
+
const units = split(before), oursUnits = split(av), theirsUnits = split(bv);
|
|
344
|
+
const reordered = textPermuted(units, oursUnits, theirsUnits);
|
|
345
|
+
if (reordered !== null) return reordered;
|
|
346
|
+
const a = textEdits(units, oursUnits), b = textEdits(units, theirsUnits);
|
|
347
|
+
if (!a || !b) return null;
|
|
348
|
+
let out = '', pos = 0;
|
|
349
|
+
for (const g of textGroups(units, a, b)) {
|
|
350
|
+
out += units.slice(pos, g.start).join('');
|
|
351
|
+
if (g.av === g.bv) out += g.av;
|
|
352
|
+
else if (g.av === g.before) out += g.bv;
|
|
353
|
+
else if (g.bv === g.before) out += g.av;
|
|
354
|
+
else {
|
|
355
|
+
const fine = level === 'line' ? textRefineGroup(g, options) : null;
|
|
356
|
+
if (fine === null) return null;
|
|
357
|
+
out += fine;
|
|
358
|
+
}
|
|
359
|
+
pos = g.end;
|
|
360
|
+
}
|
|
361
|
+
return out + units.slice(pos).join('');
|
|
362
|
+
}
|
|
363
|
+
function textPermuted(units, ours, theirs) {
|
|
364
|
+
const solid = list => list.filter(unit => !textSpace(unit));
|
|
365
|
+
const baseSolid = solid(units), known = new Map(baseSolid.map((unit, k) => [unit, k]));
|
|
366
|
+
if (known.size !== baseSolid.length) return null;
|
|
367
|
+
const reorders = side => { const x = solid(side); return x.length === baseSolid.length && x.every(unit => known.has(unit)) && new Set(x).size === x.length && side.join('') !== units.join(''); };
|
|
368
|
+
const [first, second] = [reorders(ours), reorders(theirs)];
|
|
369
|
+
if (first === second) return null;
|
|
370
|
+
const [moved, changed] = first ? [ours, theirs] : [theirs, ours];
|
|
371
|
+
const edits = textEdits(units, changed);
|
|
372
|
+
if (!edits) return null;
|
|
373
|
+
const replace = new Map(), after = new Map();
|
|
374
|
+
for (const e of edits) {
|
|
375
|
+
const inner = []; for (let i = e.start; i < e.end; i++) if (!textSpace(units[i])) inner.push(i);
|
|
376
|
+
if (!inner.length) {
|
|
377
|
+
if (!e.text.trim()) continue;
|
|
378
|
+
let k = -1; for (let i = e.start - 1; i >= 0; i--) if (!textSpace(units[i])) { k = known.get(units[i]); break; }
|
|
379
|
+
const gap = e.start > 0 && textSpace(units[e.start - 1]);
|
|
380
|
+
after.set(k, [...(after.get(k) || []), {text: e.text, gap}]);
|
|
381
|
+
} else if (inner.length === 1) {
|
|
382
|
+
if (!e.text.trim()) replace.set(known.get(units[inner[0]]), units.slice(e.start, e.end).some(textSpace) ? ' ' : '');
|
|
383
|
+
else if (units.slice(e.start, e.end).some(textSpace)) return null;
|
|
384
|
+
else replace.set(known.get(units[inner[0]]), e.text);
|
|
385
|
+
} else return null;
|
|
386
|
+
}
|
|
387
|
+
const out = [], place = (k, from) => {
|
|
388
|
+
const rows = after.get(k) || [];
|
|
389
|
+
return rows.filter(row => row.gap === from).map(row => row.text);
|
|
390
|
+
};
|
|
391
|
+
out.push(...place(-1, false));
|
|
392
|
+
for (let i = 0; i < moved.length; i++) {
|
|
393
|
+
const unit = moved[i];
|
|
394
|
+
if (textSpace(unit)) { out.push(unit); continue; }
|
|
395
|
+
const k = known.get(unit);
|
|
396
|
+
if (replace.get(k) === '' || replace.get(k) === ' ') {
|
|
397
|
+
if (replace.get(k) === ' ') { if (i + 1 < moved.length && textSpace(moved[i + 1])) i++; else if (out.length && textSpace(out[out.length - 1])) out.pop(); }
|
|
398
|
+
} else out.push(replace.has(k) ? replace.get(k) : unit);
|
|
399
|
+
out.push(...place(k, false));
|
|
400
|
+
if (after.get(k)?.some(row => row.gap)) { if (i + 1 < moved.length && textSpace(moved[i + 1])) out.push(moved[++i]); out.push(...place(k, true)); }
|
|
401
|
+
}
|
|
402
|
+
return out.join('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function textRefineGroup({before, av, bv}, options) {
|
|
406
|
+
const [first, second] = textDeviceOptions(options), ids = first < second ? [0, 1] : [1, 0];
|
|
407
|
+
const added = (side, base) => side.startsWith(base) ? side.slice(base.length) : null;
|
|
408
|
+
const oursAdded = added(av, before), theirsAdded = added(bv, before);
|
|
409
|
+
if (oursAdded !== null && theirsAdded !== null && textLines(oursAdded + theirsAdded).every(line => TEXT_ITEM.test(line))) {
|
|
410
|
+
const tail = value => value && !/[\r\n]$/.test(value) ? value + (before.includes('\r\n') ? '\r\n' : '\n') : value;
|
|
411
|
+
const parts = [tail(oursAdded), tail(theirsAdded)];
|
|
412
|
+
const last = tail(before) === before ? '' : (before.includes('\r\n') ? '\r\n' : '\n');
|
|
413
|
+
return before + last + parts[ids[0]] + parts[ids[1]];
|
|
414
|
+
}
|
|
415
|
+
const lines = textLines(before);
|
|
416
|
+
if (lines.length === 1) {
|
|
417
|
+
const tags = TEXT_TAGS.exec(before), x = TEXT_TAGS.exec(av), y = TEXT_TAGS.exec(bv);
|
|
418
|
+
if (tags && x && y && x[1] === tags[1] && y[1] === tags[1]) {
|
|
419
|
+
const list = row => row[2].split(',').map(item => item.trim()).filter(Boolean);
|
|
420
|
+
const base = list(tags), ours = list(x), theirs = list(y);
|
|
421
|
+
const kept = base.filter(item => ours.includes(item) && theirs.includes(item));
|
|
422
|
+
const added = [ours, theirs].map(list => list.filter(item => !base.includes(item)));
|
|
423
|
+
const joined = [...new Set([...kept, ...added[ids[0]], ...added[ids[1]]])];
|
|
424
|
+
return tags[1] + ': [' + joined.join(', ') + ']' + tags[3];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return textRefineLevel(before, av, bv, options, 'word');
|
|
428
|
+
}
|
|
275
429
|
export function mergeText(base, ours, theirs, options = {}) {
|
|
276
430
|
canonicalNote(ours); canonicalNote(theirs); if (base !== null) canonicalNote(base);
|
|
277
431
|
const unchanged = value => ({text: value, clean: !value.includes(TEXT_CONFLICT), conflicts: value.includes(TEXT_CONFLICT) ? [{kind: 'existing-conflict', advisory: true}] : []});
|
|
278
432
|
if (ours === theirs) return unchanged(ours);
|
|
279
433
|
if (base === ours) return unchanged(theirs);
|
|
280
434
|
if (base === theirs) return unchanged(ours);
|
|
281
|
-
const conflicts = []; let text = '';
|
|
435
|
+
const conflicts = []; let text = '', a, b;
|
|
282
436
|
const conflict = (before, a, b, kind) => {
|
|
283
437
|
const ab = inspectTextConflicts(a), bb = inspectTextConflicts(b);
|
|
284
438
|
const au = textUnits(a), bu = textUnits(b); let first = 0, last = 0;
|
|
@@ -316,38 +470,21 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
316
470
|
if (base === null) conflict(null, ours, theirs, 'no-ancestor');
|
|
317
471
|
else if (textDeclarations(oursUnits) !== declared || textDeclarations(theirsUnits) !== declared) conflict(base, ours, theirs, 'document-bindings');
|
|
318
472
|
else {
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
473
|
+
const reordered = textPermuted(units, oursUnits, theirsUnits);
|
|
474
|
+
if (reordered !== null && textConserved(base, ours, theirs, reordered)) text += reordered;
|
|
475
|
+
else if (!(a = textEdits(units, oursUnits)) || !(b = textEdits(units, theirsUnits))) conflict(base, ours, theirs, 'ambiguous-alignment-or-budget');
|
|
321
476
|
else {
|
|
322
|
-
const changes = [...a.map(e => ({...e, side: 0})), ...b.map(e => ({...e, side: 1}))].sort((x, y) => x.start - y.start || x.end - y.end || x.side - y.side);
|
|
323
477
|
let pos = 0;
|
|
324
|
-
for (
|
|
325
|
-
const group = [changes[i++]], start = group[0].start; let end = group[0].end;
|
|
326
|
-
while (i < changes.length && (changes[i].start < end || group.some(e => {
|
|
327
|
-
const next = changes[i];
|
|
328
|
-
const afterBlock = (edit, insert) => insert.start === insert.end && insert.start === edit.end && edit.start < edit.end && units.slice(edit.start, edit.end).every(unit => textEnvelopeAt(unit));
|
|
329
|
-
if (afterBlock(e, next) || afterBlock(next, e)) return false;
|
|
330
|
-
if (e.start === e.end && next.start <= e.start && e.start <= next.end) return true;
|
|
331
|
-
if (next.start === next.end && e.start <= next.start && next.start <= e.end) return true;
|
|
332
|
-
return next.start >= e.end && units.slice(e.start, e.end).every(textSpace) &&
|
|
333
|
-
units.slice(e.end, next.start).every(textSpace) && units.slice(next.start, next.end).every(textSpace);
|
|
334
|
-
}))) {
|
|
335
|
-
group.push(changes[i]); end = Math.max(end, changes[i++].end);
|
|
336
|
-
}
|
|
478
|
+
for (const {start, end, before, av, bv, resolved} of textMoves([...textGroups(units, a, b)])) {
|
|
337
479
|
text += units.slice(pos, start).join('');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
let value = '', at = start;
|
|
341
|
-
for (const e of group.filter(e => e.side === side)) { value += units.slice(at, e.start).join('') + e.text; at = e.end; }
|
|
342
|
-
return value + units.slice(at, end).join('');
|
|
343
|
-
};
|
|
344
|
-
const av = version(0), bv = version(1);
|
|
345
|
-
if (av === bv) text += av;
|
|
480
|
+
if (resolved !== undefined) text += resolved;
|
|
481
|
+
else if (av === bv) text += av;
|
|
346
482
|
else if (av === before) text += bv;
|
|
347
483
|
else if (bv === before) text += av;
|
|
348
484
|
else {
|
|
349
485
|
const tick = end === start + 1 ? textTick(before, av, bv) : null;
|
|
350
|
-
|
|
486
|
+
const fine = tick === null ? textRefine(before, av, bv, options) : null;
|
|
487
|
+
if (tick !== null) text += tick; else if (fine !== null) text += fine; else conflict(before, av, bv, 'text');
|
|
351
488
|
}
|
|
352
489
|
pos = end;
|
|
353
490
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.33",
|
|
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
|
@@ -418,6 +418,7 @@
|
|
|
418
418
|
min-height:var(--md-ol-circle);
|
|
419
419
|
padding-top:min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line)));
|
|
420
420
|
}
|
|
421
|
+
.md-render{--md-ol-dot-line:radial-gradient(circle closest-side, var(--md-color-line) calc(100% - 0.5px), transparent 100%) center top / 3px 6px repeat-y}
|
|
421
422
|
.md-render ol>li::before{
|
|
422
423
|
content:counter(rapier-ol);
|
|
423
424
|
position:absolute;
|
|
@@ -441,16 +442,21 @@
|
|
|
441
442
|
|
|
442
443
|
inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
|
|
443
444
|
/* From just below this circle into the next one: at least to the next item's top (--md-item-gap is the gap between
|
|
444
|
-
items the spacing settings give), and at least a quarter line, which the next circle covers.
|
|
445
|
+
items the spacing settings give), and at least a quarter line, which the next circle covers. Where the circles
|
|
446
|
+
nearly touch (one-line items) there is no room for dots, so none are drawn: the line shows only when it spans at
|
|
447
|
+
least half a line (100% is this item's height). */
|
|
445
448
|
top:calc(var(--md-ol-top) + 1.0875 * var(--md-ol-circle));
|
|
446
|
-
|
|
447
|
-
|
|
449
|
+
--md-ol-dots:calc(100% - var(--md-ol-top) - 1.0875 * var(--md-ol-circle) + max(var(--md-item-gap, 0px), var(--md-line) / 4));
|
|
450
|
+
height:clamp(0px, (var(--md-ol-dots) - var(--md-line) / 2) * 9999, var(--md-ol-dots));
|
|
451
|
+
/* Round dots, 3px across every 6px: a dotted border draws squares in Chrome. */
|
|
452
|
+
width:3px;
|
|
453
|
+
background:var(--md-ol-dot-line);
|
|
448
454
|
z-index:0;
|
|
449
455
|
}
|
|
450
456
|
/* A heading between two parts of one numbered list (the second part's start follows the first's last number;
|
|
451
457
|
_rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
|
|
452
458
|
line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
|
|
453
|
-
.md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
|
|
459
|
+
.md-render ol[data-rapier-list-continues]>li:last-child::after{height:auto;bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
|
|
454
460
|
/* A wrapped picture set the next circle aside: the dotted line ends with this item, as at a list's last. */
|
|
455
461
|
.md-render ol>li[data-rapier-wrap-step]::after{content:none!important}
|
|
456
462
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
|
|
@@ -460,7 +466,8 @@
|
|
|
460
466
|
inset-inline-start:calc(calc(1 * var(--md-unit)) - 1.5px);
|
|
461
467
|
top:0;
|
|
462
468
|
bottom:calc(-1 * var(--md-list-heading-after,var(--md-gap-heading-after)));
|
|
463
|
-
|
|
469
|
+
width:3px;
|
|
470
|
+
background:var(--md-ol-dot-line);
|
|
464
471
|
z-index:0;
|
|
465
472
|
}
|
|
466
473
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]:has(+:is(h1,h2,h3,h4,h5,h6)){--md-list-heading-after:var(--md-gap-heading-before)}
|