rapier-markdown-kit 1.1.87 → 1.1.88
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 +5 -5
- package/dist/kit/render.mjs +67 -34
- package/dist/layout/model.mjs +100 -0
- package/package.json +7 -6
- package/style.css +4 -2
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ and colour.
|
|
|
11
11
|
- Will/1 markers say which regions an agent may edit, only add to, or must keep.
|
|
12
12
|
- MIT, no dependencies, Node 22 or newer.
|
|
13
13
|
|
|
14
|
-
To
|
|
15
|
-
|
|
14
|
+
To show documents in your app without writing a renderer, frame the read-only reader with
|
|
15
|
+
`npm install rapier-embed` (`build: 'reader'`). To hand a person the whole editor around their document as one
|
|
16
|
+
offline file, run `npx rapier-html notes.md`.
|
|
16
17
|
|
|
17
18
|
```sh
|
|
18
19
|
npm install rapier-markdown-kit
|
|
@@ -302,7 +303,7 @@ const together = merge(ledger, reopened.ledger);
|
|
|
302
303
|
console.assert(together.text === 'Hello');
|
|
303
304
|
```
|
|
304
305
|
|
|
305
|
-
## The format
|
|
306
|
+
## The format
|
|
306
307
|
|
|
307
308
|
`rapier-ledger/1` is a plain JSON object:
|
|
308
309
|
|
|
@@ -334,8 +335,7 @@ actual document. `historyEnvelope` additionally proves the editor's Undo/Redo br
|
|
|
334
335
|
`chars:fnv-base36:adler-base36` initial root. Each splice advances it with
|
|
335
336
|
`rootAfter(priorRoot, splice)`, matching the source store. A later root is **not**
|
|
336
337
|
a fresh hash of the whole current text. SHA-256 of the exact checkpoint text is
|
|
337
|
-
separate.
|
|
338
|
-
earlier design shorthand. A nonzero start names a retained checkpoint; it does not
|
|
338
|
+
separate. A nonzero start names a retained checkpoint; it does not
|
|
339
339
|
claim to reconstruct discarded events. `complete: false` also permits a stated
|
|
340
340
|
revision gap from the editor's pruning of cancelling navigation pairs; each
|
|
341
341
|
remaining splice must still replay exactly. It is never a claim about missing edits.
|
package/dist/kit/render.mjs
CHANGED
|
@@ -1030,7 +1030,7 @@ function _rapierArtifactFactoryModules(dependencies, group) {
|
|
|
1030
1030
|
function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
1031
1031
|
if (!root || !metadata || !geometry || !pretext) return null;
|
|
1032
1032
|
const doc = root.ownerDocument, view = doc.defaultView;
|
|
1033
|
-
const styles = new Map(), originals = new Map(), spacers = [], floats = [], profiles = new WeakMap();
|
|
1033
|
+
const styles = new Map(), originals = new Map(), spacers = [], floats = [], steps = [], profiles = new WeakMap();
|
|
1034
1034
|
const css = node => view.getComputedStyle(node);
|
|
1035
1035
|
const box = node => node.getBoundingClientRect();
|
|
1036
1036
|
const pixels = value => (Number.isFinite(value) ? Math.round(value * 100) / 100 : 0) + 'px';
|
|
@@ -1053,6 +1053,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1053
1053
|
spacers.length = 0;
|
|
1054
1054
|
for (const float of floats) float.remove();
|
|
1055
1055
|
floats.length = 0;
|
|
1056
|
+
for (const item of steps) item.removeAttribute('data-rapier-wrap-step');
|
|
1057
|
+
steps.length = 0;
|
|
1056
1058
|
}
|
|
1057
1059
|
|
|
1058
1060
|
// Plain inline text uses Pretext. Complex blocks keep their original controls through floats.
|
|
@@ -1210,14 +1212,14 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1210
1212
|
}
|
|
1211
1213
|
}
|
|
1212
1214
|
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1215
|
+
// A block that cannot wrap stands with its box just below the outline, as in the editor. Its own margin moves it, so
|
|
1216
|
+
// nothing is inserted between a picture and its words (a reader and the witnesses read nextElementSibling).
|
|
1217
|
+
function clearTo(element, bottom, origin) {
|
|
1218
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
1219
|
+
const distance = bottom - (box(element).top - origin);
|
|
1220
|
+
if (Math.abs(distance) <= 0.5) return;
|
|
1221
|
+
style(element, {'margin-top': pixels(Math.max(0, (parseFloat(css(element).marginTop) || 0) + distance))});
|
|
1222
|
+
}
|
|
1221
1223
|
}
|
|
1222
1224
|
|
|
1223
1225
|
const imageOnly = geometry.imageOnly;
|
|
@@ -1234,7 +1236,27 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1234
1236
|
// Float fallback as the editor's floatAround.
|
|
1235
1237
|
const FLOAT_BLOCK = /^(P|H[1-6]|UL|OL|BLOCKQUOTE|DETAILS|SUMMARY|DL)$/;
|
|
1236
1238
|
function floatAround(element, width, height, top, obstacles) {
|
|
1237
|
-
|
|
1239
|
+
// As the editor: a paragraph or heading floats whatever it holds; a list, quote or disclosure holding a rigid block does not.
|
|
1240
|
+
if (!FLOAT_BLOCK.test(element.tagName) || !/^(P|H[1-6])$/.test(element.tagName) && element.querySelector('table, pre, figure, img, .math-rendered')) return false;
|
|
1241
|
+
// As the editor: a disclosure's box ends where the outline begins (layout/model.mjs boxClearance).
|
|
1242
|
+
if (element.tagName === 'DETAILS') {
|
|
1243
|
+
const computed = css(element), outer = box(element), floor = metadata.wrapColumnFloor(parseFloat(computed.fontSize) || 16);
|
|
1244
|
+
const offset = (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0);
|
|
1245
|
+
const own = obstacles.map(obstacle => ({...obstacle, x: obstacle.x + offset}));
|
|
1246
|
+
const marginLeft = parseFloat(computed.marginLeft) || 0, marginRight = parseFloat(computed.marginRight) || 0;
|
|
1247
|
+
let extent = height, clear = null;
|
|
1248
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
1249
|
+
const next = geometry.boxClearance(own, outer.width, top, extent, floor);
|
|
1250
|
+
if (!next) break;
|
|
1251
|
+
if (!next.fits) return false;
|
|
1252
|
+
clear = {left: Math.max(clear?.left || 0, next.left), right: Math.max(clear?.right || 0, next.right)};
|
|
1253
|
+
style(element, {'margin-left': pixels(marginLeft + clear.left), 'margin-right': pixels(marginRight + clear.right)});
|
|
1254
|
+
const grown = box(element).height;
|
|
1255
|
+
if (Math.abs(grown - extent) < .5) break;
|
|
1256
|
+
extent = grown;
|
|
1257
|
+
}
|
|
1258
|
+
if (clear) return true;
|
|
1259
|
+
}
|
|
1238
1260
|
if (element.tagName === 'DETAILS' && !element.open) {
|
|
1239
1261
|
const summary = element.querySelector(':scope > summary');
|
|
1240
1262
|
if (!summary) return false;
|
|
@@ -1246,21 +1268,26 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1246
1268
|
obstacles.map(obstacle => ({...obstacle, x: obstacle.x - bounds.left - inset + outer.left + parentInset})));
|
|
1247
1269
|
}
|
|
1248
1270
|
const computed = css(element);
|
|
1249
|
-
|
|
1271
|
+
const inset = (parseFloat(computed.paddingTop) || 0) + (parseFloat(computed.borderTopWidth) || 0);
|
|
1272
|
+
top += inset;
|
|
1250
1273
|
const bottom = top + height;
|
|
1251
1274
|
const inside = obstacles.filter(obstacle => obstacle.y < bottom + 4096 && obstacle.y + obstacle.height > top &&
|
|
1252
1275
|
obstacle.x < width && obstacle.x + obstacle.width > 0);
|
|
1253
1276
|
if (!inside.length) return false;
|
|
1254
|
-
// layout/model.mjs wrapShape; `geometry` is modules["layout/model.mjs"].
|
|
1277
|
+
// layout/model.mjs wrapShape; `geometry` is modules["layout/model.mjs"]. As the editor, the start side reaches from
|
|
1278
|
+
// the box's own start edge.
|
|
1279
|
+
const start = computed.direction === 'rtl' ? 'right' : 'left';
|
|
1280
|
+
const startInset = start === 'left' ? (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0)
|
|
1281
|
+
: -((parseFloat(computed.paddingRight) || 0) + (parseFloat(computed.borderRightWidth) || 0));
|
|
1255
1282
|
const shapeFor = side => {
|
|
1256
|
-
const shape = geometry.wrapShape(side, inside, width, top);
|
|
1283
|
+
const shape = geometry.wrapShape(side, side !== start ? inside : inside.map(obstacle => ({...obstacle, x: obstacle.x + startInset})), width, top);
|
|
1257
1284
|
if (!shape) return null;
|
|
1258
1285
|
return {...shape, shape: 'polygon(' + shape.points.map(point => pixels(point[0]) + ' ' + pixels(point[1])).join(',') + ') border-box'};
|
|
1259
1286
|
};
|
|
1260
1287
|
const shapes = {left: shapeFor('left'), right: shapeFor('right')};
|
|
1261
1288
|
const spent = (shapes.left?.boxWidth || 0) + (shapes.right?.boxWidth || 0);
|
|
1262
1289
|
if (!spent || spent > width - metadata.wrapColumnFloor(parseFloat(css(element).fontSize) || 16)) return false;
|
|
1263
|
-
const placed = [];
|
|
1290
|
+
const before = geometry.edgeLines(element), placed = [];
|
|
1264
1291
|
for (const side of ['left', 'right']) {
|
|
1265
1292
|
const shape = shapes[side];
|
|
1266
1293
|
if (!shape) continue;
|
|
@@ -1269,25 +1296,39 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1269
1296
|
float.style.cssText = `float:${side};width:${pixels(shape.boxWidth)};height:${pixels(shape.boxHeight)};margin-top:${pixels(shape.startY)};shape-outside:${shape.shape};pointer-events:none`;
|
|
1270
1297
|
element.prepend(float); floats.push(float); placed.push([float, shape]);
|
|
1271
1298
|
}
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
style(element, {display: computed.display.includes('list-item') ? 'flow-root list-item' : 'flow-root'});
|
|
1299
|
+
// As the editor: each float ends at the block's own content bottom and the block keeps its own display, so its
|
|
1300
|
+
// margins collapse as they do with no picture. A float the content does not reach goes.
|
|
1275
1301
|
const contentBottom = () => {
|
|
1276
1302
|
let first = element.firstChild;
|
|
1277
1303
|
while (first && placed.some(([float]) => float === first)) first = first.nextSibling;
|
|
1278
|
-
if (!first || !element.lastChild) return box(element).height;
|
|
1304
|
+
if (!first || !element.lastChild) return box(element).height - inset;
|
|
1279
1305
|
const range = doc.createRange(); range.setStartBefore(first); range.setEndAfter(element.lastChild);
|
|
1280
|
-
return range.getBoundingClientRect().bottom - box(element).top;
|
|
1306
|
+
return range.getBoundingClientRect().bottom - box(element).top - inset;
|
|
1281
1307
|
};
|
|
1282
1308
|
for (let pass = 0; pass < 4; pass++) {
|
|
1283
1309
|
const content = contentBottom();
|
|
1284
1310
|
let changed = false;
|
|
1285
|
-
for (const [float, shape] of placed) {
|
|
1286
|
-
const bounded = Math.
|
|
1287
|
-
if (
|
|
1311
|
+
for (const [float, shape] of [...placed]) {
|
|
1312
|
+
const bounded = Math.min(shape.boxHeight, content - shape.startY);
|
|
1313
|
+
if (bounded <= 0) { float.remove(); placed.splice(placed.findIndex(([value]) => value === float), 1); changed = true; }
|
|
1314
|
+
else if (Math.abs((parseFloat(float.style.height) || 0) - bounded) > 0.5) { float.style.height = pixels(bounded); changed = true; }
|
|
1288
1315
|
}
|
|
1289
1316
|
if (!changed) break;
|
|
1290
1317
|
}
|
|
1318
|
+
// As the editor: a list item's marker and a quote's bar move with the words the floats moved.
|
|
1319
|
+
const {markers, bars, steps: breaks} = geometry.edgeMarks(element, before);
|
|
1320
|
+
for (const {item, shift} of markers) style(item, {'--md-marker': 'calc(' + (item.style.getPropertyValue('--md-marker') || '0px') + ' + ' + pixels(shift) + ')'});
|
|
1321
|
+
for (const item of breaks) { item.setAttribute('data-rapier-wrap-step', ''); steps.push(item); }
|
|
1322
|
+
for (const {quote, runs} of bars) {
|
|
1323
|
+
const look = css(quote), edge = box(quote).top + (parseFloat(look.borderTopWidth) || 0), barWidth = parseFloat(look.borderInlineStartWidth), color = look.borderInlineStartColor;
|
|
1324
|
+
style(quote, {'border-inline-start-color': 'transparent', ...(look.position === 'static' ? {position: 'relative'} : {})});
|
|
1325
|
+
for (const run of runs) {
|
|
1326
|
+
const bar = doc.createElement('span');
|
|
1327
|
+
bar.setAttribute('aria-hidden', 'true');
|
|
1328
|
+
bar.style.cssText = `position:absolute;inset-inline-start:${pixels(run.shift - barWidth)};top:${pixels(run.top - edge)};width:${pixels(barWidth)};height:${pixels(run.bottom - run.top)};background:${color};pointer-events:none`;
|
|
1329
|
+
quote.prepend(bar); floats.push(bar);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1291
1332
|
return true;
|
|
1292
1333
|
}
|
|
1293
1334
|
|
|
@@ -1322,13 +1363,11 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1322
1363
|
syncInlineRotatedPictures(positionedImages);
|
|
1323
1364
|
for (const element of children) {
|
|
1324
1365
|
if (wrapped.has(element)) continue;
|
|
1325
|
-
|
|
1326
|
-
let bounds = box(element);
|
|
1366
|
+
const bounds = box(element);
|
|
1327
1367
|
if (!bounds.width || !bounds.height) continue;
|
|
1328
1368
|
const computed = css(element), inset = (parseFloat(computed.paddingLeft) || 0) + (parseFloat(computed.borderLeftWidth) || 0);
|
|
1329
1369
|
const width = bounds.width - inset - (parseFloat(computed.paddingRight) || 0) - (parseFloat(computed.borderRightWidth) || 0);
|
|
1330
1370
|
|
|
1331
|
-
let ownerLed = false;
|
|
1332
1371
|
for (const {element: source, image, layout, width: imageWidth} of anchors.get(element) || []) {
|
|
1333
1372
|
// A picture sized in lines (`lines`) reads its anchor's line and stands on the first line's cap height, as the live editor.
|
|
1334
1373
|
const lines = layout.lines != null ? geometry.lineMetrics(element) : null;
|
|
@@ -1347,19 +1386,12 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1347
1386
|
// The standoff and the room between pictures, in the reference scale's pixels, as the live editor.
|
|
1348
1387
|
const scale = geometry.frameScale(image), room = 10 * scale;
|
|
1349
1388
|
if (!outOfFlow(layout)) {
|
|
1389
|
+
// A picture an earlier one stands in moves down alone; its owner keeps its place, as in the editor.
|
|
1350
1390
|
for (const obstacle of [...obstacles].sort((left, right) => left.y - right.y)) {
|
|
1351
1391
|
if (obstacle.x < left + rectangle.width + room && obstacle.x + obstacle.width > left - room &&
|
|
1352
1392
|
obstacle.y + obstacle.height > y - room && obstacle.y < y + rectangle.height + room)
|
|
1353
1393
|
y = obstacle.y + obstacle.height + room;
|
|
1354
1394
|
}
|
|
1355
|
-
// A picture pushed down by an earlier one takes its owner with it. A deliberate `y` stays; only the collision push moves the paragraph.
|
|
1356
|
-
const ownerTop = bounds.top - origin + topInset + geometry.linesTop(layout, lines) + Math.min(layout.y || 0, height / em) * em;
|
|
1357
|
-
// Only the first picture of an owner takes it down.
|
|
1358
|
-
if (!ownerLed && y > ownerTop + 0.5) {
|
|
1359
|
-
clearTo(element, (bounds.top - origin) + (y - ownerTop), origin, source);
|
|
1360
|
-
bounds = box(element);
|
|
1361
|
-
}
|
|
1362
|
-
ownerLed = true;
|
|
1363
1395
|
obstacles.push(...geometry.linesSlices(geometry.pictureSlices(profile(image, layout), left, y, rectangle.width, rectangle.height, scale), layout, lines, y, rectangle.height));
|
|
1364
1396
|
}
|
|
1365
1397
|
placed.push({source, image, rectangle, left, y, wrap: layout.wrap,
|
|
@@ -1371,7 +1403,8 @@ function _rapierProjectArtifactLayout(root, metadata, geometry, pretext) {
|
|
|
1371
1403
|
const own = active.map(obstacle => ({...obstacle, x: obstacle.x - bounds.left - inset}));
|
|
1372
1404
|
const prepared = prepare(element);
|
|
1373
1405
|
if (!(prepared && width > 0 && project(prepared, width, top, own)) && !(width > 0 && floatAround(element, width, bounds.height, top, own))) {
|
|
1374
|
-
|
|
1406
|
+
// As the editor: only an outline that meets the block's box moves it.
|
|
1407
|
+
const overlapping = active.filter(obstacle => obstacle.y < top + bounds.height && obstacle.x < bounds.right && obstacle.x + obstacle.width > bounds.left);
|
|
1375
1408
|
if (overlapping.length) clearTo(element, Math.max(...overlapping.map(obstacle => obstacle.y + obstacle.height)), origin);
|
|
1376
1409
|
}
|
|
1377
1410
|
}
|
package/dist/layout/model.mjs
CHANGED
|
@@ -411,6 +411,106 @@ export function wrapShape(side, obstacles, width, top) {
|
|
|
411
411
|
return {boxWidth, boxHeight, startY, points};
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
// ---- What a block draws at its edge moves with its words (live and export alike) ----
|
|
415
|
+
// A bordered box (a disclosure) cannot follow an outline line by line: it stands clear of it. `obstacles` in the box's
|
|
416
|
+
// border-box coordinates, its band [top, top + height]: the insets that end the box where the outline begins on each
|
|
417
|
+
// side, `fits` false when they leave less than `floor`; null when the outline does not meet the box.
|
|
418
|
+
export function boxClearance(obstacles, width, top, height, floor) {
|
|
419
|
+
let left = 0, right = 0;
|
|
420
|
+
for (const row of obstacles) {
|
|
421
|
+
if (row.y >= top + height || row.y + row.height <= top || row.x >= width || row.x + row.width <= 0) continue;
|
|
422
|
+
if (row.x > width - (row.x + row.width)) right = Math.max(right, width - row.x);
|
|
423
|
+
else left = Math.max(left, row.x + row.width);
|
|
424
|
+
}
|
|
425
|
+
return left || right ? {left, right, fits: width - left - right >= floor} : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Where an element's content begins at the inline start (a right edge counts negative in a right-to-left block).
|
|
429
|
+
function contentStart(element) {
|
|
430
|
+
const style = element.ownerDocument.defaultView.getComputedStyle(element), box = element.getBoundingClientRect();
|
|
431
|
+
return style.direction === 'rtl' ? -(box.right - (parseFloat(style.paddingRight) || 0) - (parseFloat(style.borderRightWidth) || 0))
|
|
432
|
+
: box.left + (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.borderLeftWidth) || 0);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// The lines of words in `root` (text outside `skip`), each with its block, where it starts and where its block's content starts.
|
|
436
|
+
export function edgeLines(root, skip = () => false) {
|
|
437
|
+
const doc = root.ownerDocument, view = doc.defaultView, lines = [], starts = new Map();
|
|
438
|
+
const walker = doc.createTreeWalker(root, 4);
|
|
439
|
+
for (let node; (node = walker.nextNode());) {
|
|
440
|
+
if (!node.data.trim() || skip(node)) continue;
|
|
441
|
+
let block = node.parentElement;
|
|
442
|
+
while (block !== root && view.getComputedStyle(block).display.startsWith('inline')) block = block.parentElement;
|
|
443
|
+
if (!starts.has(block)) starts.set(block, contentStart(block));
|
|
444
|
+
const rtl = view.getComputedStyle(block).direction === 'rtl', range = doc.createRange();
|
|
445
|
+
range.selectNodeContents(node);
|
|
446
|
+
for (const box of range.getClientRects()) {
|
|
447
|
+
if (!(box.width > 0)) continue;
|
|
448
|
+
const line = lines.find(row => row.block === block && Math.abs(row.top - box.top) < 2);
|
|
449
|
+
const start = rtl ? -box.right : box.left;
|
|
450
|
+
if (line) { line.start = Math.min(line.start, start); line.bottom = Math.max(line.bottom, box.bottom); }
|
|
451
|
+
else lines.push({block, top: box.top, bottom: box.bottom, start, content: starts.get(block)});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return lines;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// After a picture's floats settle: how far each line moved from where its block's lines began without them (`before`,
|
|
458
|
+
// edgeLines read first). A list item's marker moves as far as its first line; a quote's bar moves to stand beside its
|
|
459
|
+
// moved lines as it stood beside them before, in runs of one shift from the quote's top to its bottom.
|
|
460
|
+
export function edgeMarks(root, before, skip = () => false) {
|
|
461
|
+
const view = root.ownerDocument.defaultView, home = new Map();
|
|
462
|
+
const byBlock = rows => {
|
|
463
|
+
const blocks = new Map();
|
|
464
|
+
for (const row of rows) { if (!blocks.has(row.block)) blocks.set(row.block, []); blocks.get(row.block).push(row); }
|
|
465
|
+
for (const list of blocks.values()) list.sort((a, b) => a.top - b.top);
|
|
466
|
+
return blocks;
|
|
467
|
+
};
|
|
468
|
+
for (const [block, rows] of byBlock(before)) {
|
|
469
|
+
const content = rows[0].content;
|
|
470
|
+
home.set(block, {content, first: rows[0].start, rest: rows.length > 1 ? Math.min(...rows.slice(1).map(row => row.start)) : content});
|
|
471
|
+
}
|
|
472
|
+
const lines = [];
|
|
473
|
+
for (const [block, rows] of byBlock(edgeLines(root, skip))) rows.forEach((row, index) => {
|
|
474
|
+
const natural = home.get(block);
|
|
475
|
+
if (natural) lines.push({...row, content: natural.content, moved: Math.max(0, row.start - (index ? natural.rest : natural.first))});
|
|
476
|
+
});
|
|
477
|
+
const within = selector => [...(root.matches(selector) ? [root] : []), ...root.querySelectorAll(selector)];
|
|
478
|
+
const shiftOf = item => lines.filter(line => line.block.closest('li') === item).sort((a, b) => a.top - b.top)[0]?.moved || 0;
|
|
479
|
+
const markers = [];
|
|
480
|
+
for (const item of within('li')) {
|
|
481
|
+
if (item.classList.contains('task-list-item')) continue;
|
|
482
|
+
const shift = shiftOf(item);
|
|
483
|
+
if (shift > .5) markers.push({item, shift});
|
|
484
|
+
}
|
|
485
|
+
// A numbered item's dotted line runs to the next circle: where the two circles moved apart it is not drawn (`steps`).
|
|
486
|
+
const steps = [];
|
|
487
|
+
for (const list of within('ol')) {
|
|
488
|
+
const items = [...list.children].filter(child => child.tagName === 'LI');
|
|
489
|
+
items.forEach((item, index) => {
|
|
490
|
+
const next = items[index + 1];
|
|
491
|
+
if (next ? Math.abs(shiftOf(item) - shiftOf(next)) > .5 : list.hasAttribute('data-rapier-list-continues') && shiftOf(item) > .5) steps.push(item);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const bars = [];
|
|
495
|
+
for (const quote of within('blockquote')) {
|
|
496
|
+
if (!(parseFloat(view.getComputedStyle(quote).borderInlineStartWidth) > 0)) continue;
|
|
497
|
+
const start = contentStart(quote);
|
|
498
|
+
const own = lines.filter(line => quote.contains(line.block)).sort((a, b) => a.top - b.top)
|
|
499
|
+
.map(line => ({...line, shift: line.moved > .5 ? line.moved + line.content - start : 0}));
|
|
500
|
+
if (!own.some(line => line.shift > .5)) continue;
|
|
501
|
+
const box = quote.getBoundingClientRect(), runs = [];
|
|
502
|
+
for (const line of own) {
|
|
503
|
+
const run = runs.at(-1);
|
|
504
|
+
if (run && Math.abs(run.shift - line.shift) <= .5) { run.bottom = line.bottom; continue; }
|
|
505
|
+
if (run) run.end = (run.bottom + line.top) / 2;
|
|
506
|
+
runs.push({shift: line.shift, top: run ? run.end : box.top, bottom: line.bottom});
|
|
507
|
+
}
|
|
508
|
+
runs.at(-1).end = box.bottom;
|
|
509
|
+
bars.push({quote, runs: runs.map(run => ({shift: run.shift, top: run.top, bottom: run.end}))});
|
|
510
|
+
}
|
|
511
|
+
return {markers, bars, steps};
|
|
512
|
+
}
|
|
513
|
+
|
|
414
514
|
// ---- The one "paragraph holds only the picture": one meaningful child, the image or an <a> wrapping only it ----
|
|
415
515
|
export function imageOnly(paragraph, image) {
|
|
416
516
|
const meaningful = node => [...node.childNodes].filter(child => child.nodeType !== 8 &&
|
package/package.json
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Read, render
|
|
3
|
+
"version": "1.1.88",
|
|
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",
|
|
7
7
|
"self-contained",
|
|
8
8
|
"commonmark",
|
|
9
9
|
"gfm",
|
|
10
|
+
"renderer",
|
|
10
11
|
"stylesheet",
|
|
11
12
|
"css",
|
|
12
|
-
"layout",
|
|
13
13
|
"text-layout",
|
|
14
|
-
"text-wrap",
|
|
15
14
|
"line-breaking",
|
|
16
15
|
"images",
|
|
16
|
+
"svg",
|
|
17
17
|
"document",
|
|
18
18
|
"will",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"authorship",
|
|
21
|
+
"merge",
|
|
21
22
|
"rapier"
|
|
22
23
|
],
|
|
23
24
|
"author": "Jack Skipworth",
|
package/style.css
CHANGED
|
@@ -234,7 +234,7 @@
|
|
|
234
234
|
/* The line model (the way a Word document reads). One line, --md-line, is the body's line box: 1.75rem, 1.6 lines
|
|
235
235
|
of the 1.1rem body, a whole 28 px at the browser's 16 px root so baselines land on pixels. Every block ends one
|
|
236
236
|
line below its last line; a heading ends half a line below. A heading's own box is the whole or half lines its
|
|
237
|
-
size fills: h1 at 2.
|
|
237
|
+
size fills: h1 at 2.6 x the body on two lines (medium weight, lighter than the bold smaller headings), h2 and h3
|
|
238
238
|
on one and a half, h4 to h6 on one. Nothing sets a top
|
|
239
239
|
margin, so a plain page (sibling margins collapsing) and the editor (a wrapper per block) space alike, and the
|
|
240
240
|
space before any block is the line its predecessor leaves. */
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
.md-render h1,.md-render h2,.md-render h3,.md-render h4,.md-render h5,.md-render h6{
|
|
247
247
|
font-family:var(--md-font-sans);letter-spacing:-0.01em;
|
|
248
248
|
line-height:var(--md-line);margin:0 0 calc(var(--md-line) / 2);color:var(--md-heading-color);text-wrap:balance}
|
|
249
|
-
.md-render h1{font-size:calc(var(--md-text-body) * 2.
|
|
249
|
+
.md-render h1{font-size:calc(var(--md-text-body) * 2.6);line-height:calc(var(--md-line) * 2);font-weight:500}
|
|
250
250
|
.md-render h2{font-size:calc(var(--md-text-body) * 1.9);line-height:calc(var(--md-line) * 1.5);font-weight:600}
|
|
251
251
|
.md-render h3{font-size:calc(var(--md-text-body) * 1.5);line-height:calc(var(--md-line) * 1.5);font-weight:600}
|
|
252
252
|
.md-render h4{font-size:calc(var(--md-text-body) * 1.25);font-weight:500}
|
|
@@ -377,6 +377,8 @@
|
|
|
377
377
|
_rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
|
|
378
378
|
line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
|
|
379
379
|
.md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-line))}
|
|
380
|
+
/* A wrapped picture set the next circle aside: the dotted line ends with this item, as at a list's last. */
|
|
381
|
+
.md-render ol>li[data-rapier-wrap-step]::after{content:none!important}
|
|
380
382
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
|
|
381
383
|
.md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]::before{
|
|
382
384
|
content:'';
|