rapier-markdown-kit 1.2.8 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/agent/will.mjs +0 -6
  2. package/dist/images/gif.mjs +0 -3
  3. package/dist/images/playback.mjs +0 -10
  4. package/dist/kit/assets.mjs +0 -1
  5. package/dist/kit/conformance/measurer.mjs +0 -0
  6. package/dist/kit/conformance/run.mjs +1 -8
  7. package/dist/kit/export-fonts.mjs +0 -14
  8. package/dist/kit/ledger/authored-origin.mjs +0 -5
  9. package/dist/kit/ledger/authorship.mjs +0 -2
  10. package/dist/kit/ledger/carried.mjs +4 -7
  11. package/dist/kit/ledger/data.mjs +0 -8
  12. package/dist/kit/ledger/format.mjs +0 -25
  13. package/dist/kit/ledger/hash.mjs +11 -7
  14. package/dist/kit/ledger/history.mjs +10 -29
  15. package/dist/kit/ledger/inverse-source.mjs +0 -46
  16. package/dist/kit/ledger/journal-records.mjs +24 -56
  17. package/dist/kit/ledger/merge.mjs +0 -27
  18. package/dist/kit/ledger/text-merge.mjs +0 -20
  19. package/dist/kit/ledger/transport.mjs +0 -47
  20. package/dist/kit/ledger.mjs +0 -4
  21. package/dist/kit/markdown-it.mjs +0 -3
  22. package/dist/kit/pdf-glyphs.mjs +1 -10
  23. package/dist/kit/pdf-source.mjs +5 -5
  24. package/dist/kit/pdf-vectors.mjs +0 -3
  25. package/dist/kit/print-capitals.mjs +0 -4
  26. package/dist/kit/render-entry.mjs +0 -1
  27. package/dist/kit/render-ink.mjs +0 -5
  28. package/dist/kit/render-markdown.mjs +12 -117
  29. package/dist/kit/render-print.mjs +1 -47
  30. package/dist/kit/render-sanitize.mjs +7 -59
  31. package/dist/kit/render-styles.mjs +0 -7
  32. package/dist/kit/render-work.mjs +0 -12
  33. package/dist/kit/render.mjs +18 -173
  34. package/dist/layout/hyphenation.mjs +0 -23
  35. package/dist/layout/line-plan.mjs +0 -20
  36. package/dist/layout/model.mjs +0 -55
  37. package/dist/layout/paragraph-fit.mjs +50 -60
  38. package/dist/layout/preparation.mjs +23 -56
  39. package/dist/layout/prepared-paragraph.mjs +0 -25
  40. package/dist/layout/typography.mjs +103 -41
  41. package/dist/packages/rapier-diff/concerns.mjs +0 -16
  42. package/dist/packages/rapier-diff/diff.mjs +0 -7
  43. package/dist/packages/rapier-diff/index.mjs +0 -1
  44. package/dist/packages/rapier-diff/matching.mjs +0 -19
  45. package/dist/packages/rapier-diff/merge.mjs +0 -9
  46. package/dist/packages/rapier-diff/moves.mjs +0 -7
  47. package/dist/packages/rapier-diff/structure.mjs +0 -2
  48. package/dist/packages/rapier-diff/tokens.mjs +0 -6
  49. package/dist/packages/rapier-diff/walk.mjs +0 -12
  50. package/dist/spec/document-fonts.mjs +0 -9
  51. package/dist/spec/document-settings.mjs +4 -23
  52. package/dist/spec/frontmatter.mjs +0 -1
  53. package/dist/spec/html-blocks.mjs +0 -36
  54. package/dist/spec/md-assets.mjs +2 -61
  55. package/dist/spec/md-layout.mjs +0 -34
  56. package/dist/spec/md-marks.mjs +0 -51
  57. package/dist/spec/md-source.mjs +0 -16
  58. package/dist/spec/word-sidecar.mjs +0 -2
  59. package/package.json +1 -1
  60. package/style.css +32 -16
@@ -1,8 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The one owner of inline typography, ink and page breaks.
3
2
  import {markdownSourcePositions} from './md-source.mjs';
4
3
 
5
- // The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
6
4
  const inkReader = () => {
7
5
  // SPDX-License-Identifier: MIT
8
6
  const TEXT_COLOR_NAMES = Object.freeze({ green: '#1c7547', red: '#b32034', blue: '#175dc4', gold: '#865818', purple: '#7924d7' });
@@ -10,16 +8,6 @@ const inkReader = () => {
10
8
  const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
11
9
  const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
12
10
 
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
- // opener holds the stroke:
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 color span's value, red when absent; box is the frame's
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
- // 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
- // 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,
20
- // one space between, integers only. A comment that does not read this way is ordinary comment text and marks nothing. Ink pairs nest:
21
- // each closer takes the innermost open ink opener. Arrow/end alone take a pairing number, an end takes no other data; under, strike and arrow may omit a path for a line
22
- // derived from their words.
23
11
  const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
24
12
 
25
13
  const INK_PATH_MAX = 160;
@@ -88,7 +76,6 @@ export const FONT_STACKS = Object.freeze({
88
76
  hand: Object.freeze({stack: 'hand', css: 'cursive', word: 'Segoe Print'}),
89
77
  });
90
78
 
91
- // A family is one font-table name, never a CSS list or an HTML fragment.
92
79
  export function normalizeFontFamily(value) {
93
80
  if (typeof value !== 'string') return null;
94
81
  const family = value.trim();
@@ -106,8 +93,6 @@ export function formatFontRun(family, content) {
106
93
  return typography.wrap({family}, content);
107
94
  }
108
95
 
109
- // One typed interpretation for source, DOM and native-file adapters. The worker carries this
110
- // exact factory and its explicit grammar inputs; no DOM, CSS evaluator or resource loader.
111
96
  export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACKS}) {
112
97
  const LIMIT = 4096, DEPTH = 64;
113
98
  const generics = {serif: 'serif', 'sans-serif': 'sans', monospace: 'mono', cursive: 'hand',
@@ -127,7 +112,6 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACK
127
112
  function color(value) {
128
113
  if (typeof value !== 'string') return null;
129
114
  if (/^#[0-9a-f]{6}$/i.test(value.trim())) return value.trim().toLowerCase();
130
- // CSSOM serializes admitted hex to opaque rgb(). Native HTML import uses this same boundary.
131
115
  const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*1\s*)?\)$/i.exec(value.trim());
132
116
  return rgb && rgb.slice(1).every(channel => Number(channel) <= 255)
133
117
  ? '#' + rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, '0')).join('') : null;
@@ -154,7 +138,6 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACK
154
138
  }
155
139
  } else if (!/^[\p{L}_-][\p{L}\p{N}_ -]*$/u.test(css) || /^(?:inherit|initial|unset|revert|revert-layer)$/i.test(css)) return null;
156
140
  name = normalizeFontFamily(name);
157
- // These five names are the existing family-choice aliases, not custom font identities.
158
141
  if (name && Object.hasOwn(FONT_STACKS, name.toLowerCase())) return null;
159
142
  return name ? Object.freeze({name, css: '"' + name.replace(/[\\"]/g, '\\$&') + '"', custom: true}) : null;
160
143
  }
@@ -178,8 +161,6 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACK
178
161
  }
179
162
  return Object.freeze(result);
180
163
  }
181
- // Rendering drops unsupported declarations; rich editing admits only wholly owned
182
- // spans so unrelated HTML and rejected attributes keep their exact-source editor.
183
164
  function parseStyle(value, editable = false) {
184
165
  if (typeof value !== 'string' || value.length > LIMIT) return editable ? null : Object.freeze({});
185
166
  const parts = []; let quote = '', start = 0;
@@ -212,15 +193,11 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACK
212
193
  const wrap = (value, content) => {
213
194
  const start = open(value);
214
195
  if (!start || !content) return content;
215
- // Importers can supply a styled break by itself, or before/after words. Keep
216
- // those exact line-edge bytes outside the new span so its opener has words.
217
196
  const prefix = /^(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+/.exec(content)?.[0] || '';
218
197
  let body = content.slice(prefix.length);
219
198
  const suffix = /(?:[ \t]*(?:\r\n|\r(?!\n)|\n))+[ \t]*$/.exec(body)?.[0] || '';
220
199
  body = body.slice(0, body.length - suffix.length);
221
200
  if (!body) return content;
222
- // Soft/hard line breaks can belong to one Markdown paragraph. A blank line or
223
- // an opener on a line of its own would instead change the block grammar.
224
201
  if (/^\s*\n|\n[ \t]*\n/.test(body.replace(/\r\n?/g, '\n')))
225
202
  throw new TypeError('Typography wraps one inline paragraph at a time.');
226
203
  return prefix + start + body + '</span>' + suffix;
@@ -323,8 +300,6 @@ const inkPair = (value, name, nonNegative = false) => {
323
300
  return pair;
324
301
  };
325
302
 
326
- // {kind, id?, hex?, width?, box?, at?, path?} to the opener; the path's points are absolute in the frame and become moves after the
327
- // first. Refuses with a TypeError rather than writing a spelling the reader would not read.
328
303
  export function formatInkOpen(mark) {
329
304
  if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
330
305
  if (!INK_KINDS.includes(mark.kind)) throw new TypeError('ink: the kind is one of ' + INK_KINDS.join(', '));
@@ -358,7 +333,6 @@ export function formatInkRun(mark, content) {
358
333
  return formatInkOpen(mark) + content + INK_CLOSE;
359
334
  }
360
335
 
361
- // Returns {mark, length in UTF-16 units} or null; not end-anchored.
362
336
  export function matchInkOpen(text) {
363
337
  if (typeof text !== 'string') return null;
364
338
  const match = new RegExp('^' + INK_OPEN_PATTERN).exec(text);
@@ -367,10 +341,6 @@ export function matchInkOpen(text) {
367
341
  return mark && { mark, length: match[0].length };
368
342
  }
369
343
 
370
- // The opener's body is what a rendered span carries (a renderer's sanitizer drops an attribute whose value holds the
371
- // comment's own close): the opener after `<!--ink ` and before `-->`, so the attribute reads `under red box=… 0,95 …`,
372
- // and only a body that reads as a mark goes back. Read and write through the same grammar, so an invalid body cannot
373
- // invent a mark.
374
344
  export function inkOpenBody(opener) {
375
345
  if (typeof opener !== 'string') return null;
376
346
  const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
@@ -384,10 +354,6 @@ export function isInkClose(comment) {
384
354
  return comment === INK_CLOSE;
385
355
  }
386
356
 
387
- // Every ink marker in order: {start, end, mark} for an opener (mark null for a closer, and an opener whose path
388
- // overflows is not a marker at all).
389
- // Without a prior arrow or adjacent marker bytes, only an edit boundary can empty a span.
390
- // Read those boundaries through the planned rows before asking the full grammar to retire ink.
391
357
  export function mayRetireInk(source, rows) {
392
358
  if (source.contains('<!--ink arrow ') || source.contains('<!--ink end ') || source.contains('--><!--')) return true;
393
359
  let pieces = [{from: 0, length: source.length}], length = source.length;
@@ -430,20 +396,14 @@ export function scanInkMarkers(source) {
430
396
  return markers;
431
397
  }
432
398
 
433
- // Ink nests so independent strokes can share the same words. A marker that pairs with nothing is text
434
- // (it shows nothing and marks nothing) and is listed under strays.
435
399
  export function pairInkMarkers(source, markers = null) {
436
400
  return pairMarkers(source, 'ink', markers);
437
401
  }
438
402
 
439
- // A document parser may supply its semantic markers, leaving literal code comments untouched.
440
- // Local inline parsing and source planners need an endpoint's span even when its mate is in another block.
441
403
  export function pairInkSpans(source, markers = null) {
442
404
  return pairSpanMarkers(source, 'ink', markers);
443
405
  }
444
406
 
445
- // Color keeps its first opener; ink pairs each closer with the innermost opener. The shared range
446
- // shape lets custody owners keep every independent stroke, including strokes on identical words.
447
407
  export function pairMarkers(source, kind, markers = null) {
448
408
  const paired = pairSpanMarkers(source, kind, markers);
449
409
  if (kind !== 'ink') return paired;
@@ -470,9 +430,6 @@ export function pairMarkers(source, kind, markers = null) {
470
430
  return { runs, strays, arrows };
471
431
  }
472
432
 
473
- // Where a Will marker may stand (docs/will.md, Conversion). A pair inside a fence, a raw HTML block
474
- // (until its blank line) or the opening front matter is text: the line is quoted,
475
- // and a Word or PDF writer that met it as a marker would govern the wrong region. The same frame
476
433
  // as spec/frontmatter.mjs, copied here so this grammar stays inside the MIT kit's closure.
477
434
  const WILL_BLOCK_TAGS = new Set('address article aside base basefont blockquote body caption center col colgroup dd details dialog dir div dl dt fieldset figcaption figure footer form frame frameset h1 h2 h3 h4 h5 h6 head header hr html iframe legend li link main menu menuitem nav noframes ol optgroup option p param search section summary table tbody td tfoot th thead title tr track ul'.split(' '));
478
435
  const WILL_VOID_TAGS = new Set('area base br col embed hr img input link meta param source track wbr'.split(' '));
@@ -620,9 +577,6 @@ function willLeaf(stack) {
620
577
  return kind === 'fence' || kind === 'pre' || kind === 'html' || kind === 'raw';
621
578
  }
622
579
 
623
- // `spans` are [start, end) of marker lines in `source` (the Will reader's own offsets).
624
- // Returns the first quoted span, {start, end, where}, or null when every span may stand.
625
- // `where` is `fence`, `raw-pre`, `raw-div`, `raw-table`, `raw-html` (any other raw HTML block) or `frontmatter`.
626
580
  export function quotedWillSpan(source, spans) {
627
581
  const text = String(source ?? '');
628
582
  const marks = Array.isArray(spans) ? spans : [];
@@ -703,8 +657,6 @@ function pairSpanMarkers(source, kind, markers = null) {
703
657
  return { runs, strays };
704
658
  }
705
659
 
706
- // Copy carries an arrow only when both complete anchor words are selected. A partial selection removes just
707
- // its endpoint comments from the copied slice, including a comment clipped at the selection's boundary.
708
660
  export function sliceInkArrows(source, start = 0, end = String(source ?? '').length, markers = null) {
709
661
  const text = String(source ?? '');
710
662
  if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > text.length) throw new RangeError('ink: invalid source slice');
@@ -728,8 +680,6 @@ export function sliceInkArrows(source, start = 0, end = String(source ?? '').len
728
680
  return result + text.slice(at, end);
729
681
  }
730
682
 
731
- // The paste owns only its incoming markers: an orphan cannot adopt an existing document's endpoint, and a
732
- // complete pair with an occupied number gets a new number in both openers without rewriting its stroke bytes.
733
683
  export function prepareInkPaste(source, existing = '', markers = null) {
734
684
  const text = String(source ?? ''), paired = pairInkMarkers(text, markers), edits = [];
735
685
  for (const marker of paired.strays) if (marker.mark?.id != null || marker.arrowId != null) edits.push({ start: marker.start, end: marker.end, text: '' });
@@ -767,7 +717,6 @@ export function hasInkMarker(text) {
767
717
  return scanInkMarkers(text).length > 0;
768
718
  }
769
719
 
770
- // The only spelling.
771
720
  export function formatPageBreak() {
772
721
  return PAGE_BREAK_MARKER;
773
722
  }
@@ -1,9 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import {splitOpeningFrontmatter} from './frontmatter.mjs';
3
3
 
4
- // The standard parser owns literal regions and comment boundaries. Offsets are original UTF-16,
5
- // including BOM/CRLF and list/quote prefixes. A private parser view observes rules; it changes no
6
- // live rule or token. Call once per source, then ask isText(position) or isComment(start, end).
7
4
  export function markdownSourcePositions(source, parser) {
8
5
  if (typeof source !== 'string' || !parser) throw new TypeError('Source positions require text and its Markdown parser');
9
6
  const {body, bodyOffset} = splitOpeningFrontmatter(source), lines = [bodyOffset];
@@ -17,8 +14,6 @@ export function markdownSourcePositions(source, parser) {
17
14
  ruler.getRules = chain => parser.inline.ruler.getRules(chain).map(rule => (state, silent) => {
18
15
  const start = state.pos, first = state.tokens.length;
19
16
  const matched = rule(state, silent), token = state.tokens.at(-1);
20
- // A pending text token may flush before this token. Never overwrite a nested rule's span
21
- // with its enclosing link's range.
22
17
  if (matched && !silent && state.tokens.length > first && !observed.has(token) &&
23
18
  ['code_inline', 'html_inline', 'image', 'footnote_ref'].includes(token?.type)) {
24
19
  observed.set(token, {start, end: state.pos, source: state.src,
@@ -28,8 +23,6 @@ export function markdownSourcePositions(source, parser) {
28
23
  }
29
24
  return matched;
30
25
  });
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
26
  const read = new WeakMap();
34
27
  inline.parse = function (text, md, env, children) { read.set(children, text); return parser.inline.parse.call(this, text, md, env, children); };
35
28
  const {tokens, cells} = markdownTableSources(body, owner, {}, lines);
@@ -37,8 +30,6 @@ export function markdownSourcePositions(source, parser) {
37
30
  for (const child of children || []) {
38
31
  const span = observed.get(child);
39
32
  if (!span || span.source !== content) continue;
40
- // Inline footnotes are moved to a generated footer without line maps. Their parser-owned
41
- // child tokens still stand inside the original ^[...] carrier, not in that footer.
42
33
  if (span.footnote?.tokens) {
43
34
  readInline(span.footnote.tokens, span.footnote.content, pos => rawOffset(span.start + 2 + pos));
44
35
  } else if (child.type !== 'footnote_ref') {
@@ -58,9 +49,6 @@ export function markdownSourcePositions(source, parser) {
58
49
  if (token.map && ['fence', 'code_block', 'html_block', 'reference_definition'].includes(token.type)) {
59
50
  const start = lineAt(token.map[0]), end = lineAt(token.map[1]);
60
51
  add(start, end);
61
- // A comment-carrier block can contain several adjacent comments. Keep each carrier,
62
- // including a closer surviving damage to its opener. Never promote nested comments or
63
- // comment-shaped text in a raw tagged HTML block (pre, script, div, ...).
64
52
  if (token.type === 'html_block' && /^\s*<!--/.test(token.content)) {
65
53
  let at = source.indexOf('<!--', start);
66
54
  while (at >= start && at < end) {
@@ -81,8 +69,6 @@ export function markdownSourcePositions(source, parser) {
81
69
  for (let i = 0; i < rows.length; i++) {
82
70
  const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1), line = source.slice(start, end).replace(/\0/g, '\ufffd');
83
71
  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
72
  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; }
87
73
  if (at < 0) throw new Error('Markdown inline source mapping failed');
88
74
  starts.push(local); spaces.push(lead); offsets.push(start + at - lead); local += rows[i].length + 1;
@@ -113,8 +99,6 @@ export function markdownSourcePositions(source, parser) {
113
99
  return {isText, isComment: (start, end) => comments.get(start) === end, typography: typography.sort((a, b) => a.start - b.start)};
114
100
  }
115
101
 
116
- // Capture only table-cell provenance while the parser still owns container-adjusted row
117
- // bounds. Looking for cell text in a whole row aliases literal pipes and quote/list markers.
118
102
  export function markdownTableSources(source, parser, environment, lineStarts = null, parse = null) {
119
103
  const cells = new WeakMap();
120
104
  const read = owner => {
@@ -1,6 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // An inert document asset, kept in Source and excluded from the reading surface.
3
- // This is recognition only: interchange/docx.mjs admits the bounded JSON and native XML.
4
2
  export const WORD_SIDECAR_OPEN = '<!-- rapier-word:v1\n';
5
3
  export function isWordSidecarBlock(raw) {
6
4
  return typeof raw === 'string' && /^<!-- rapier-word:v1\r?\n[^\r\n]*\r?\n-->[ \t\r\n]*$/.test(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
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
@@ -6,6 +6,7 @@
6
6
  /* The reference scale's unit (docs/markdown-standard.md, "The reference scale"): 16 px at M, a length every reader can read
7
7
  back as pixels (a planner scales its own lengths by it). */
8
8
  @property --md-unit{syntax:'<length>';inherits:true;initial-value:16px}
9
+ @property --md-item-line{syntax:'<length>';inherits:true;initial-value:0px}
9
10
  :root,.md-render{
10
11
  --md-unit:1rem;
11
12
  --md-hair:calc(var(--md-unit) / 16);
@@ -362,21 +363,28 @@
362
363
  position:relative;
363
364
  --md-marker:0px;
364
365
  counter-increment:rapier-ol;
365
- min-height:0;
366
- padding-top:0;
366
+ /* The circle's size follows the item's own line (--md-item-line is registered, so 1lh resolves here on the li,
367
+ not in the marker's font): 2 units from the house line of 1.75 units up, never taller than the line at 1.48 units
368
+ or less, growing in between. The li makes room for it: at the house line exactly 0.15 units above
369
+ and 2 units' height, none once the circle fits the line. */
370
+ --md-item-line:1lh;
371
+ --md-ol-circle:min(2 * var(--md-unit), max(var(--md-item-line), 2 * var(--md-item-line) - 1.48 * var(--md-unit)));
372
+ --md-ol-top:calc(min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line))) - min(0.125 * var(--md-unit), max(0px, var(--md-ol-circle) - var(--md-item-line)) / 2) + max(0px, var(--md-item-line) - var(--md-ol-circle)) / 2);
373
+ min-height:var(--md-ol-circle);
374
+ padding-top:min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line)));
367
375
  }
368
376
  .md-render ol>li::before{
369
377
  content:counter(rapier-ol);
370
378
  position:absolute;
371
- /* 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. */
372
- inset-inline-start:calc(-2.75 * var(--md-unit) + var(--md-marker, 0px)); top:0;
373
- width:min(2 * var(--md-unit), 1lh); height:min(2 * var(--md-unit), 1lh);
379
+ /* Centered on the item's first line and on the dotted line's axis, 1.75 units before the item's text. */
380
+ inset-inline-start:calc(-1.75 * var(--md-unit) - var(--md-ol-circle) / 2 + var(--md-marker, 0px)); top:var(--md-ol-top);
381
+ width:var(--md-ol-circle); height:var(--md-ol-circle);
374
382
  display:flex; align-items:center; justify-content:center;
375
383
  border:2px solid var(--md-color-line);
376
384
  border-radius:999px;
377
385
  background:var(--md-color-bg);
378
386
  color:var(--md-color-line);
379
- font:var(--md-fw-semibold) calc(0.95 * var(--md-unit))/1 var(--md-font-sans);
387
+ font:var(--md-fw-semibold) min(0.95 * var(--md-unit), 0.55 * var(--md-ol-circle))/1 var(--md-font-sans);
380
388
  font-variant-numeric:tabular-nums;
381
389
  box-sizing:border-box;
382
390
  z-index:1;
@@ -387,8 +395,10 @@
387
395
  position:absolute;
388
396
 
389
397
  inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
390
- top:calc(2.2 * var(--md-unit));
391
- bottom:calc(var(--md-line) / -4);
398
+ /* From just below this circle into the next one: at least to the next item's top (--md-item-gap is the gap between
399
+ items the spacing settings give), and at least a quarter line, which the next circle covers. */
400
+ top:calc(var(--md-ol-top) + 1.0875 * var(--md-ol-circle));
401
+ bottom:calc(-1 * max(var(--md-item-gap, 0px), var(--md-line) / 4));
392
402
  border-inline-start:3px dotted var(--md-color-line);
393
403
  z-index:0;
394
404
  }
@@ -426,37 +436,43 @@
426
436
  .md-render .rapier-jump-label strong{font:inherit}
427
437
  .md-render ul.rapier-jump-list{
428
438
  --md-jump-gap:calc(.9 * var(--md-unit));
439
+ --md-jump-pad:calc(.18 * var(--md-unit));
440
+ --md-jump-circle:10px;
429
441
  --md-jump-marker-inline-start:calc(-1.18 * var(--md-unit));
430
- --md-jump-marker-center:1em;
431
442
  list-style:none;
432
443
  padding-inline-start:calc(1.85 * var(--md-unit));
433
444
  margin:0 0 var(--md-gap-block);
434
445
  }
435
446
  .md-render ul.rapier-jump-list>li{
436
447
  position:relative;
448
+ /* The circle sits on the first line's center: --md-item-line is registered, so 1lh resolves here on the li. */
449
+ --md-item-line:1lh;
450
+ --md-jump-circle-top:calc(var(--md-jump-pad) + (var(--md-item-line) - var(--md-jump-circle)) / 2);
437
451
  margin:0 0 var(--md-jump-gap);
438
- padding:calc(.18 * var(--md-unit)) 0 calc(.18 * var(--md-unit)) calc(.15 * var(--md-unit));
452
+ padding:var(--md-jump-pad) 0 var(--md-jump-pad) calc(.15 * var(--md-unit));
439
453
  }
440
454
  .md-render ul.rapier-jump-list>li:last-child{margin-bottom:0}
441
455
  .md-render ul.rapier-jump-list>li::before{
442
456
  content:'';
443
457
  position:absolute;
444
458
  inset-inline-start:var(--md-jump-marker-inline-start);
445
- top:calc(var(--md-jump-marker-center) - 5px);
446
- width:10px;height:10px;
459
+ top:var(--md-jump-circle-top);
460
+ width:var(--md-jump-circle);height:var(--md-jump-circle);
447
461
  box-sizing:border-box;
448
462
  border:2px solid var(--md-color-accent-text);
449
463
  background:var(--md-color-bg);
450
464
  border-radius:50%;
451
465
  z-index:1;
452
466
  }
467
+ /* The dotted line is a numbered list's (3px, the text's black or white): from below this circle to the next circle's
468
+ center, which covers its end; none after the last item. --md-item-gap is the gap the spacing settings give. */
453
469
  .md-render ul.rapier-jump-list>li:not(:last-child)::after{
454
470
  content:'';
455
471
  position:absolute;
456
- inset-inline-start:calc(var(--md-jump-marker-inline-start) + 4px);
457
- top:var(--md-jump-marker-center);
458
- bottom:calc(0px - var(--md-jump-gap) - var(--md-jump-marker-center));
459
- border-inline-start:2px dotted color-mix(in srgb,var(--md-color-accent-text) 42%,transparent);
472
+ inset-inline-start:calc(var(--md-jump-marker-inline-start) + var(--md-jump-circle) / 2 - 1.5px);
473
+ top:calc(var(--md-jump-circle-top) + var(--md-jump-circle));
474
+ bottom:calc(-1 * (var(--md-item-gap, var(--md-jump-gap)) + var(--md-jump-circle-top) + var(--md-jump-circle) / 2));
475
+ border-inline-start:3px dotted var(--md-color-line);
460
476
  z-index:0;
461
477
  }
462
478
  .md-render ul.rapier-jump-list>li>a{