rapier-markdown-kit 1.1.32 → 1.1.49

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.
@@ -27,10 +27,12 @@ function normalize(raw) {
27
27
  return {text: pieces.join(''), offsets, trimStart: start, trimEnd: end};
28
28
  }
29
29
 
30
- export function prepareRun(raw, font, letterSpacing = 0) {
30
+ export function prepareRun(raw, font, letterSpacing = 0, measured = null) {
31
31
  if (typeof raw !== 'string' || typeof font !== 'string' || !font.trim() || !finite(letterSpacing)) return null;
32
32
  const normalized = normalize(raw);
33
- const prepared = prepareWithSegments(normalized.text, font, {letterSpacing});
33
+ // A paragraph's rich-inline plan has already measured this run. Map those same segments to source
34
+ // instead of measuring and segmenting it again; standalone callers still prepare their own run.
35
+ const prepared = measured || prepareWithSegments(normalized.text, font, {letterSpacing});
34
36
  if (!Array.isArray(prepared.segments) || prepared.segments.join('') !== normalized.text) return null;
35
37
  const segmentOffsets = [0];
36
38
  for (const segment of prepared.segments) segmentOffsets.push(segmentOffsets.at(-1) + segment.length);
@@ -1,12 +1,75 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // The one place these literal grammars are written (docs/markdown-standard.md, "Text colour", "Page break").
3
3
 
4
- const TEXT_COLOR_NAMES = Object.freeze({ green: '#1c7547', red: '#b32034', blue: '#175dc4', gold: '#865818', purple: '#7924d7' });
4
+ // The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
5
+ const inkReader = () => {
6
+ // SPDX-License-Identifier: MIT
7
+ const TEXT_COLOR_NAMES = Object.freeze({ green: '#1c7547', red: '#b32034', blue: '#175dc4', gold: '#865818', purple: '#7924d7' });
8
+
9
+ const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
10
+ const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
11
+
12
+ // Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
13
+ // marks, a paired comment with the colour span's shape. The opener holds the stroke:
14
+ // <!--ink KIND [NUMBER] [COLOUR] [box=W,H] [at=X,Y] [PATH]-->words<!--/ink-->
15
+ // KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
16
+ // absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
17
+ // hundredths of an em; PATH is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an
18
+ // em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
19
+ // is ordinary comment text and marks nothing. Ink pairs do not nest in each other. Arrow/end alone take a pairing
20
+ // number, an end takes no other data; under, strike and arrow may omit a path for a line derived from their words.
21
+ const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
22
+
23
+ const INK_PATH_MAX = 160;
24
+ const INK_INT_MAX = 999999;
25
+ const INK_INT = '-?\\d{1,6}';
26
+ const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: ([1-9]\\d{0,5}))?(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
27
+ + '(?: (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*))?';
28
+ const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
29
+
30
+ // Relocated into the parse Worker by .toString() with parseInkOpen and isInkClose (engine.js _buildParseWorkerSource):
31
+ // reads only its parameter and the planted INK_PATH_MAX, INK_INT_MAX and TEXT_COLOR_NAMES; parseInkOpen reads
32
+ // INK_OPEN_PATTERN and this function; isInkClose reads INK_CLOSE.
33
+ function readInkMatch(match) {
34
+ const linked = match[1] === 'arrow' || match[1] === 'end';
35
+ if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[8] == null)) return null;
36
+ if (match[1] === 'end' && match.slice(3, 9).some(value => value != null)) return null;
37
+ const moves = match[8] == null ? [] : match[8].split(' ').map(pair => pair.split(',').map(Number));
38
+ if (moves.length > INK_PATH_MAX) return null;
39
+ const path = moves.length ? [moves[0]] : [];
40
+ for (let i = 1; i < moves.length; i++) {
41
+ const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
42
+ if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
43
+ path.push(point);
44
+ }
45
+ const colour = match[3] || null;
46
+ return Object.freeze({
47
+ kind: match[1],
48
+ ...(linked ? { id: Number(match[2]) } : {}),
49
+ hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
50
+ box: match[4] == null ? null : [Number(match[4]), Number(match[5])],
51
+ at: match[6] == null ? null : [Number(match[6]), Number(match[7])],
52
+ path,
53
+ });
54
+ }
55
+
56
+ function parseInkOpen(comment) {
57
+ if (typeof comment !== 'string') return null;
58
+ const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
59
+ return match && readInkMatch(match);
60
+ }
61
+
62
+ function parseInkBody(body) {
63
+ return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
64
+ }
65
+ return { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody };
66
+ };
67
+ const { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody } = inkReader();
68
+ export { readInkMatch, parseInkOpen, parseInkBody };
69
+
5
70
  const TEXT_COLOR_HEX_TO_NAME = Object.freeze(
6
71
  Object.fromEntries(Object.entries(TEXT_COLOR_NAMES).map(([name, hex]) => [hex, name]))
7
72
  );
8
- const COLOR_NAME_ALT = Object.keys(TEXT_COLOR_NAMES).join('|');
9
- const COLOR_VALUE = '(?:#[0-9a-f]{6}|' + COLOR_NAME_ALT + ')';
10
73
  const COLOR_CLOSE = '<!--/c-->';
11
74
  const PAGE_BREAK_MARKER = '<!--md-break:v1 page-->';
12
75
 
@@ -67,24 +130,8 @@ export function hasColorMarker(text) {
67
130
  return new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->').test(String(text == null ? '' : text));
68
131
  }
69
132
 
70
- // Ink (docs/markdown-standard.md, "Ink"; the design docs/briefs/ink.md): a pen stroke that stays with the words it
71
- // marks, a paired comment with the colour span's shape. The opener holds the stroke:
72
- // <!--ink KIND [COLOUR] [box=W,H] [at=X,Y] PATH-->words<!--/ink-->
73
- // KIND is the stroke's kind, decided once when it was lifted; COLOUR is exactly the colour span's value, red when
74
- // absent; box is the frame's size when the stroke was drawn and at the frame's offset from the words' box, both in
75
- // hundredths of an em; PATH is the stroke, its first point absolute in the frame and the rest as moves, hundredths of an
76
- // em. One spelling: the fields in this order, one space between, integers only. A comment that does not read this way
77
- // is ordinary comment text and marks nothing. Ink pairs do not nest in each other.
78
- const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free']);
79
133
  const INK_CLOSE = '<!--/ink-->';
80
- const INK_PATH_MAX = 160;
81
- const INK_INT_MAX = 999999;
82
- const INK_INT = '-?\\d{1,6}';
83
- const INK_OPEN_BODY = '(' + INK_KINDS.join('|') + ')(?: (' + COLOR_VALUE + '))?(?: box=(\\d{1,6}),(\\d{1,6}))?(?: at=(' + INK_INT + '),(' + INK_INT + '))?'
84
- + ' (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*)';
85
- const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
86
-
87
- export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX };
134
+ export { INK_KINDS, INK_CLOSE, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN };
88
135
 
89
136
  const inkInt = (value, name) => {
90
137
  if (!Number.isInteger(value) || Math.abs(value) > INK_INT_MAX) throw new TypeError('ink: ' + name + ' must be an integer within ' + INK_INT_MAX);
@@ -97,11 +144,18 @@ const inkPair = (value, name, nonNegative = false) => {
97
144
  return pair;
98
145
  };
99
146
 
100
- // {kind, hex?, box?, at?, path} to the opener; the path's points are absolute in the frame and become moves after the
147
+ // {kind, id?, hex?, box?, at?, path?} to the opener; the path's points are absolute in the frame and become moves after the
101
148
  // first. Refuses with a TypeError rather than writing a spelling the reader would not read.
102
149
  export function formatInkOpen(mark) {
103
150
  if (!mark || typeof mark !== 'object') throw new TypeError('ink: a mark is an object');
104
151
  if (!INK_KINDS.includes(mark.kind)) throw new TypeError('ink: the kind is one of ' + INK_KINDS.join(', '));
152
+ const linked = mark.kind === 'arrow' || mark.kind === 'end';
153
+ if (linked ? !Number.isInteger(mark.id) || mark.id <= 0 || mark.id > INK_INT_MAX : mark.id != null) throw new TypeError('ink: only an arrow or its end has a positive pairing number');
154
+ const id = linked ? ' ' + mark.id : '';
155
+ if (mark.kind === 'end') {
156
+ if (mark.hex != null || mark.box != null || mark.at != null || (mark.path != null && (!Array.isArray(mark.path) || mark.path.length))) throw new TypeError('ink: an arrow end holds only its pairing number');
157
+ return '<!--ink end' + id + '-->';
158
+ }
105
159
  let colour = '';
106
160
  if (mark.hex != null) {
107
161
  const value = TEXT_COLOR_HEX_TO_NAME[mark.hex] || mark.hex;
@@ -110,36 +164,19 @@ export function formatInkOpen(mark) {
110
164
  }
111
165
  const box = mark.box == null ? '' : ' box=' + inkPair(mark.box, 'box', true).join(',');
112
166
  const at = mark.at == null ? '' : ' at=' + inkPair(mark.at, 'at').join(',');
113
- if (!Array.isArray(mark.path) || !mark.path.length || mark.path.length > INK_PATH_MAX) throw new TypeError('ink: the path holds 1 to ' + INK_PATH_MAX + ' points');
114
- const points = mark.path.map(point => inkPair(point, 'path'));
115
- const moves = [points[0].join(',')];
167
+ const optionalPath = linked || mark.kind === 'under' || mark.kind === 'strike';
168
+ const path = mark.path == null && optionalPath ? [] : mark.path;
169
+ if (!Array.isArray(path) || (!path.length && !optionalPath) || path.length > INK_PATH_MAX) throw new TypeError('ink: the path holds 1 to ' + INK_PATH_MAX + ' points, or is absent on an arrow, underline or strike');
170
+ const points = path.map(point => inkPair(point, 'path'));
171
+ const moves = points.length ? [points[0].join(',')] : [];
116
172
  for (let i = 1; i < points.length; i++) moves.push([inkInt(points[i][0] - points[i - 1][0], 'path move'), inkInt(points[i][1] - points[i - 1][1], 'path move')].join(','));
117
- return '<!--ink ' + mark.kind + colour + box + at + ' ' + moves.join(' ') + '-->';
173
+ return '<!--ink ' + mark.kind + id + colour + box + at + (moves.length ? ' ' + moves.join(' ') : '') + '-->';
118
174
  }
119
175
 
120
176
  export function formatInkRun(mark, content) {
121
177
  return formatInkOpen(mark) + content + INK_CLOSE;
122
178
  }
123
179
 
124
- function readInkMatch(match) {
125
- const moves = match[7].split(' ').map(pair => pair.split(',').map(Number));
126
- if (moves.length > INK_PATH_MAX) return null;
127
- const path = [moves[0]];
128
- for (let i = 1; i < moves.length; i++) {
129
- const point = [path[i - 1][0] + moves[i][0], path[i - 1][1] + moves[i][1]];
130
- if (Math.abs(point[0]) > INK_INT_MAX || Math.abs(point[1]) > INK_INT_MAX) return null;
131
- path.push(point);
132
- }
133
- const colour = match[2] || null;
134
- return Object.freeze({
135
- kind: match[1],
136
- hex: colour == null ? null : (colour.charCodeAt(0) === 0x23 ? colour : TEXT_COLOR_NAMES[colour]),
137
- box: match[3] == null ? null : [Number(match[3]), Number(match[4])],
138
- at: match[5] == null ? null : [Number(match[5]), Number(match[6])],
139
- path,
140
- });
141
- }
142
-
143
180
  // Returns {mark, length in UTF-16 units} or null; not end-anchored.
144
181
  export function matchInkOpen(text) {
145
182
  if (typeof text !== 'string') return null;
@@ -149,12 +186,6 @@ export function matchInkOpen(text) {
149
186
  return mark && { mark, length: match[0].length };
150
187
  }
151
188
 
152
- export function parseInkOpen(comment) {
153
- if (typeof comment !== 'string') return null;
154
- const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(comment);
155
- return match && readInkMatch(match);
156
- }
157
-
158
189
  // The opener's body is what a rendered span carries (a renderer's sanitizer drops an attribute whose value holds the
159
190
  // comment's own close): the opener after `<!--ink ` and before `-->`, so the attribute reads `under red box=… 0,95 …`,
160
191
  // and only a body that reads as a mark goes back. Read and write through the same grammar, so an invalid body cannot
@@ -164,9 +195,6 @@ export function inkOpenBody(opener) {
164
195
  const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
165
196
  return match && readInkMatch(match) ? opener.slice(8, -3) : null;
166
197
  }
167
- export function parseInkBody(body) {
168
- return typeof body === 'string' ? parseInkOpen('<!--ink ' + body + '-->') : null;
169
- }
170
198
  export function formatInkOpenFromBody(body) {
171
199
  return parseInkBody(body) ? '<!--ink ' + body + '-->' : null;
172
200
  }
@@ -190,43 +218,129 @@ export function scanInkMarkers(source) {
190
218
  return markers;
191
219
  }
192
220
 
193
- // The runs: an opener and the next closer with no opener between them. A marker that pairs with nothing is text
221
+ // Ink nests so independent strokes can share the same words. A marker that pairs with nothing is text
194
222
  // (it shows nothing and marks nothing) and is listed under strays.
195
- export function pairInkMarkers(source) {
196
- return pairMarkers(source, 'ink');
223
+ export function pairInkMarkers(source, markers = null) {
224
+ return pairMarkers(source, 'ink', markers);
197
225
  }
198
226
 
199
- // Colour keeps the first opener, as its inline parser does; ink replaces an unclosed opener.
200
- // Neither kind nests in itself. The shared range shape lets custody owners treat both alike.
201
- export function pairMarkers(source, kind) {
227
+ // A document parser may supply its semantic markers, leaving literal code comments untouched.
228
+ // Local inline parsing and source planners need an endpoint's span even when its mate is in another block.
229
+ export function pairInkSpans(source, markers = null) {
230
+ return pairSpanMarkers(source, 'ink', markers);
231
+ }
232
+
233
+ // Colour keeps its first opener; ink pairs each closer with the innermost opener. The shared range
234
+ // shape lets custody owners keep every independent stroke, including strokes on identical words.
235
+ export function pairMarkers(source, kind, markers = null) {
236
+ const paired = pairSpanMarkers(source, kind, markers);
237
+ if (kind !== 'ink') return paired;
238
+ const groups = new Map();
239
+ const groupFor = id => {
240
+ if (!groups.has(id)) groups.set(id, { id, tail: null, head: null, count: 0 });
241
+ return groups.get(id);
242
+ };
243
+ for (const run of paired.runs) if (run.mark.id != null) {
244
+ const group = groupFor(run.mark.id);
245
+ group.count++;
246
+ group[run.mark.kind === 'arrow' ? 'tail' : 'head'] = run;
247
+ }
248
+ for (const marker of paired.strays) if (marker.mark?.id != null) groupFor(marker.mark.id).count++;
249
+ const arrows = [...groups.values()].filter(group => group.count === 2 && group.tail && group.head)
250
+ .map(({id, tail, head}) => ({id, tail, head}));
251
+ const complete = new Set(arrows.map(arrow => arrow.id)), runs = [], strays = paired.strays.slice();
252
+ for (const run of paired.runs) {
253
+ if (run.mark.id == null || complete.has(run.mark.id)) { runs.push(run); continue; }
254
+ strays.push({ start: run.start, end: run.innerStart, mark: run.mark, close: false },
255
+ { start: run.innerEnd, end: run.end, mark: null, close: true, arrowId: run.mark.id });
256
+ }
257
+ strays.sort((a, b) => a.start - b.start);
258
+ return { runs, strays, arrows };
259
+ }
260
+
261
+ function pairSpanMarkers(source, kind, markers = null) {
202
262
  if (kind !== 'ink' && kind !== 'color') throw new TypeError('Unknown paired marker kind');
203
263
  const color = kind === 'color';
204
- const runs = [], strays = [];
205
- let open = null;
206
- for (const marker of color ? scanColorMarkers(source) : scanInkMarkers(source)) {
264
+ const runs = [], strays = [], stack = [];
265
+ for (const marker of markers || (color ? scanColorMarkers(source) : scanInkMarkers(source))) {
207
266
  if (color ? marker.hex !== null : !marker.close) {
208
- if (open) {
209
- strays.push(color ? marker : open);
210
- if (color) continue;
211
- }
212
- open = marker;
267
+ if (color && stack.length) { strays.push(marker); continue; }
268
+ stack.push(marker);
213
269
  continue;
214
270
  }
271
+ const open = stack.pop();
215
272
  if (!open) { strays.push(marker); continue; }
216
273
  runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start,
217
274
  ...(color ? { hex: open.hex } : { mark: open.mark }) });
218
- open = null;
219
275
  }
220
- if (open) strays.push(open);
276
+ strays.push(...stack);
277
+ runs.sort((a, b) => a.start - b.start);
278
+ strays.sort((a, b) => a.start - b.start);
221
279
  return { runs, strays };
222
280
  }
223
281
 
282
+ // Copy carries an arrow only when both complete anchor words are selected. A partial selection removes just
283
+ // its endpoint comments from the copied slice, including a comment clipped at the selection's boundary.
284
+ export function sliceInkArrows(source, start = 0, end = String(source ?? '').length, markers = null) {
285
+ const text = String(source ?? '');
286
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > text.length) throw new RangeError('ink: invalid source slice');
287
+ const kept = new Set();
288
+ for (const arrow of pairInkMarkers(text, markers).arrows) {
289
+ if (![arrow.tail, arrow.head].every(run => run.innerStart >= start && run.innerEnd <= end && run.innerStart < run.innerEnd)) continue;
290
+ kept.add(arrow.id);
291
+ start = Math.min(start, arrow.tail.start, arrow.head.start);
292
+ end = Math.max(end, arrow.tail.end, arrow.head.end);
293
+ }
294
+ const spans = pairInkSpans(text, markers), cuts = [];
295
+ for (const run of spans.runs) if (run.mark.id != null && !kept.has(run.mark.id)) cuts.push(
296
+ { start: run.start, end: run.innerStart }, { start: run.innerEnd, end: run.end });
297
+ for (const marker of spans.strays) if (marker.mark?.id != null) cuts.push(marker);
298
+ let result = '', at = start;
299
+ for (const cut of cuts.sort((a, b) => a.start - b.start)) {
300
+ if (cut.end <= at || cut.start >= end) continue;
301
+ result += text.slice(at, Math.max(at, cut.start));
302
+ at = Math.min(end, cut.end);
303
+ }
304
+ return result + text.slice(at, end);
305
+ }
306
+
307
+ // The paste owns only its incoming markers: an orphan cannot adopt an existing document's endpoint, and a
308
+ // complete pair with an occupied number gets a new number in both openers without rewriting its stroke bytes.
309
+ export function prepareInkPaste(source, existing = '', markers = null) {
310
+ const text = String(source ?? ''), paired = pairInkMarkers(text, markers), edits = [];
311
+ for (const marker of paired.strays) if (marker.mark?.id != null || marker.arrowId != null) edits.push({ start: marker.start, end: marker.end, text: '' });
312
+ const occupied = new Set(scanInkMarkers(existing).filter(marker => marker.mark?.id != null).map(marker => marker.mark.id));
313
+ const reserved = new Set([...occupied, ...paired.arrows.map(arrow => arrow.id)]);
314
+ let next = 1;
315
+ for (const arrow of paired.arrows) {
316
+ const runs = [arrow.tail, arrow.head];
317
+ if (runs.some(run => run.innerStart === run.innerEnd)) {
318
+ for (const run of runs) edits.push({ start: run.start, end: run.innerStart, text: '' }, { start: run.innerEnd, end: run.end, text: '' });
319
+ continue;
320
+ }
321
+ if (!occupied.has(arrow.id)) continue;
322
+ while (reserved.has(next) && next <= INK_INT_MAX) next++;
323
+ if (next > INK_INT_MAX) throw new RangeError('ink: no unused arrow pairing number');
324
+ reserved.add(next);
325
+ for (const run of runs) {
326
+ const prefix = '<!--ink ' + run.mark.kind + ' ';
327
+ edits.push({ start: run.start + prefix.length, end: run.start + prefix.length + String(arrow.id).length, text: String(next) });
328
+ }
329
+ }
330
+ let result = text;
331
+ for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
332
+ return result;
333
+ }
334
+
224
335
  export function stripInkMarkers(text) {
225
- return String(text == null ? '' : text).replace(new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-'), 'g'), '');
336
+ const source = String(text == null ? '' : text);
337
+ let result = '', at = 0;
338
+ for (const marker of scanInkMarkers(source)) { result += source.slice(at, marker.start); at = marker.end; }
339
+ return result + source.slice(at);
226
340
  }
227
341
 
228
342
  export function hasInkMarker(text) {
229
- return new RegExp(INK_OPEN_PATTERN + '|' + INK_CLOSE.replace(/[-]/g, '\\-')).test(String(text == null ? '' : text));
343
+ return scanInkMarkers(text).length > 0;
230
344
  }
231
345
 
232
346
  // The only spelling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.32",
3
+ "version": "1.1.49",
4
4
  "description": "Read, write and style Self-contained Markdown: the universal stylesheet, picture layout, text marks, picture appendix, line planner and Will/1 markers. MIT, no dependencies.",
5
5
  "author": "Jack Skipworth",
6
6
  "license": "MIT",
package/style.css CHANGED
@@ -471,7 +471,8 @@
471
471
  .md-render ol>li::before{
472
472
  content:counter(rapier-ol);
473
473
  position:absolute;
474
- inset-inline-start:-2.75rem; top:0;
474
+ /* The circle is centred on the item's first line: the li's own padding, plus half of what the 2rem circle is taller than one line. */
475
+ inset-inline-start:-2.75rem; top:calc(0.15rem + (var(--md-line) - 2rem) / 2);
475
476
  width:2rem; height:2rem;
476
477
  display:flex; align-items:center; justify-content:center;
477
478
  border:2px solid var(--md-color-line);
@@ -483,7 +484,8 @@
483
484
  box-sizing:border-box;
484
485
  z-index:1;
485
486
  }
486
- .md-render ol>li:not(:last-child)::after{
487
+ .md-render ol>li:not(:last-child)::after,
488
+ .md-render ol[data-rapier-list-continues]>li:last-child::after{
487
489
  content:'';
488
490
  position:absolute;
489
491
 
@@ -493,6 +495,20 @@
493
495
  border-inline-start:3px dotted var(--md-color-line);
494
496
  z-index:0;
495
497
  }
498
+ /* A heading between two parts of one numbered list (the second part's start follows the first's last number;
499
+ _rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
500
+ line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
501
+ .md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-line))}
502
+ .md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:2.75rem}
503
+ .md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]::before{
504
+ content:'';
505
+ position:absolute;
506
+ inset-inline-start:calc(1rem - 1.5px);
507
+ top:0;
508
+ bottom:calc(var(--md-line) / -2);
509
+ border-inline-start:3px dotted var(--md-color-line);
510
+ z-index:0;
511
+ }
496
512
  .md-render li{margin:0 0 calc(var(--md-line) / 4)}
497
513
  .md-render li:last-child{margin-bottom:0}
498
514
  .md-render li>ul,.md-render li>ol{margin:calc(var(--md-line) / 4) 0 0}
@@ -556,6 +572,7 @@
556
572
  }
557
573
  .md-render strong{font-weight:var(--md-fw-semibold)}
558
574
  .md-render em{font-style:italic}
575
+ .md-render :not(pre)>code{line-height:1}
559
576
  .md-render code{font-family:var(--md-font-mono);font-size:0.85em;
560
577
  background:var(--md-color-inset);border:1px solid var(--md-color-border);padding:1px 5px}
561
578
  .md-render pre{margin:0 0 var(--md-line);padding:var(--md-space-3) var(--md-space-4);
@@ -755,6 +772,12 @@
755
772
  pre-computed --md-color-dark here, so the common case survives even a round-trip that kept
756
773
  data-md-color but dropped style, exactly like highlight's own swatch constants. */
757
774
  .md-render [data-md-color]{--md-color-dark:var(--md-color);color:light-dark(var(--md-color),var(--md-color-dark))}
775
+ /* Ink (docs/markdown-standard.md, "Ink"): the span shows nothing; the ink layer draws the stroke over the words it marks. */
776
+ .md-render span.rapier-ink-mark{all:unset;display:inline}
777
+ .rapier-ink-layer{--md-ink:#b32034;--md-ink-dark:#b32034;position:relative;display:block;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;pointer-events:none;z-index:3}
778
+ .rapier-ink-layer svg{position:absolute;display:block;margin:0;padding:0;overflow:visible}
779
+ .rapier-ink-layer path{fill:light-dark(var(--md-ink),var(--md-ink-dark));stroke:none;opacity:.92}
780
+ @media print{.rapier-ink-layer path{fill:var(--md-ink)}}
758
781
  .md-render [data-md-color="#1c7547"]{--md-color-dark:#70c390}
759
782
  .md-render [data-md-color="#b32034"]{--md-color-dark:#ff8385}
760
783
  .md-render [data-md-color="#175dc4"]{--md-color-dark:#7bafff}
@@ -799,6 +822,13 @@
799
822
  /* A wide table scrolls inside its wrap; the wrap carries the block's line, so a scrollbar sits under the table, not a line below it. */
800
823
  .md-render .table-scroll-wrap{width:100%;margin:0 0 var(--md-line);overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:2px}
801
824
  .md-render .table-scroll-wrap table{min-width:100%;margin-bottom:0}
825
+ /* Single-axis overflow leaves vertical sticky positioning with the document's scroller.
826
+ Older engines keep the ordinary sideways wrapper; screen-only rules leave print unchanged. */
827
+ @media screen{
828
+ .md-render{--md-table-header-top:0px}
829
+ .md-render .table-scroll-wrap{overflow:auto clip}
830
+ .md-render thead{position:sticky;top:var(--md-table-header-top,0px);z-index:2;background:var(--md-color-bg)}
831
+ }
802
832
  @media print{
803
833
  .md-render [data-md-break]{border:0!important;height:0!important;margin:0!important;break-before:page;page-break-before:always}
804
834
  .md-render [data-md-break]::after{content:none!important}