rapier-markdown-kit 1.1.104 → 1.2.5

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/LICENSE +8 -4
  2. package/README.md +50 -7
  3. package/dist/images/gif.mjs +49 -0
  4. package/dist/images/playback.mjs +103 -0
  5. package/dist/kit/conformance/03-wrap-around-silhouette.md +1 -1
  6. package/dist/kit/conformance/04-wrap-box.md +1 -1
  7. package/dist/kit/conformance/05-behind.md +1 -1
  8. package/dist/kit/conformance/06-front.md +1 -1
  9. package/dist/kit/conformance/07-rotate-raster.md +1 -1
  10. package/dist/kit/conformance/08-drawing-rotation.md +1 -1
  11. package/dist/kit/conformance/09-drawing-ring-interior.expected.json +18 -18
  12. package/dist/kit/conformance/09-drawing-ring-interior.md +1 -1
  13. package/dist/kit/conformance/10-both-sides.md +1 -1
  14. package/dist/kit/conformance/11-neighbour-skips-image.md +1 -1
  15. package/dist/kit/conformance/{12-heading-barrier.expected.json → 12-rule-barrier.expected.json} +1 -1
  16. package/dist/kit/conformance/{12-heading-barrier.md → 12-rule-barrier.md} +2 -2
  17. package/dist/kit/conformance/README.md +19 -28
  18. package/dist/kit/conformance/run.mjs +10 -8
  19. package/dist/kit/ledger/authored-origin.mjs +49 -0
  20. package/dist/kit/ledger/authorship.mjs +18 -9
  21. package/dist/kit/ledger/carried.mjs +10 -5
  22. package/dist/kit/ledger/data.mjs +7 -4
  23. package/dist/kit/ledger/format.mjs +37 -11
  24. package/dist/kit/ledger/hash.mjs +31 -15
  25. package/dist/kit/ledger/history.mjs +48 -10
  26. package/dist/kit/ledger/inverse-source.mjs +111 -49
  27. package/dist/kit/ledger/journal-records.mjs +24 -29
  28. package/dist/kit/ledger/merge.mjs +51 -15
  29. package/dist/kit/ledger/text-merge.mjs +75 -29
  30. package/dist/kit/ledger/transport.mjs +30 -45
  31. package/dist/kit/pdf-vectors.mjs +6 -2
  32. package/dist/kit/print-capitals.mjs +0 -1
  33. package/dist/kit/rapier-ledger.mjs +4 -2
  34. package/dist/kit/render-ink.mjs +1 -1
  35. package/dist/kit/render-markdown.mjs +155 -31
  36. package/dist/kit/render-print.mjs +173 -61
  37. package/dist/kit/render-sanitize.mjs +25 -15
  38. package/dist/kit/render-styles.mjs +3 -3
  39. package/dist/kit/render.mjs +37 -115
  40. package/dist/layout/model.mjs +10 -8
  41. package/dist/layout/paragraph-fit.mjs +1 -15
  42. package/dist/layout/typography.mjs +2 -1
  43. package/dist/packages/rapier-diff/concerns.mjs +163 -0
  44. package/dist/packages/rapier-diff/diff.mjs +68 -0
  45. package/dist/packages/rapier-diff/index.mjs +4 -701
  46. package/dist/packages/rapier-diff/matching.mjs +157 -0
  47. package/dist/packages/rapier-diff/merge.mjs +226 -0
  48. package/dist/packages/rapier-diff/moves.mjs +130 -0
  49. package/dist/packages/rapier-diff/structure.mjs +50 -0
  50. package/dist/packages/rapier-diff/tokens.mjs +72 -0
  51. package/dist/packages/rapier-diff/walk.mjs +219 -0
  52. package/dist/spec/document-fonts.mjs +1 -1
  53. package/dist/spec/document-settings.mjs +11 -164
  54. package/dist/spec/html-blocks.mjs +618 -79
  55. package/dist/spec/md-assets.mjs +29 -6
  56. package/dist/spec/md-layout.mjs +9 -1
  57. package/dist/spec/md-marks.mjs +25 -147
  58. package/dist/spec/md-source.mjs +69 -26
  59. package/package.json +1 -1
  60. package/style.css +7 -51
@@ -22,7 +22,7 @@ export function installMarkdownImages(md) {
22
22
  const normalize = md.normalizeLink;
23
23
  // An exact base64 data URL contains no URI escapes or host name to normalize.
24
24
  // Preserve its spelling and avoid allocating one URI-normalizer item per raster byte.
25
- md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
25
+ md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|gif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
26
26
  const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
27
27
  md.block.ruler.at('reference', (state, start, end, silent) => {
28
28
  if (silent) return reference(state, start, end, silent);
@@ -199,7 +199,7 @@ export function dataImage(url) {
199
199
  if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
200
200
  const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
201
201
  if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1};
202
- const match = /^data:(image\/(?:jxl|png|jpeg|webp|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
202
+ const match = /^data:(image\/(?:jxl|png|jpeg|webp|gif|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
203
203
  if (!match || match[2].length % 4) return null;
204
204
  const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
205
205
  const byteLength = match[2].length / 4 * 3 - padding;
@@ -451,7 +451,7 @@ function occurrenceChange(image, splices) {
451
451
 
452
452
  // A permissive gate keeps ordinary typing out of full-document image parsing.
453
453
  // Recognition and retirement proof still belong to Markdown-it below.
454
- export function mayRetireImageDefinitions(source, splices, factory) {
454
+ export function mayRetireImageDefinitions(source, splices, factory, hint) {
455
455
  const store = typeof source === 'string' ? null : source;
456
456
  // A definition's closing bracket is immediately followed by its colon (parseAssets uses the same necessary condition).
457
457
  if (store ? !store.contains(']:') || !store.contains('data:image/', true)
@@ -468,13 +468,36 @@ export function mayRetireImageDefinitions(source, splices, factory) {
468
468
  if (!row.removed && !row.inserted) return false;
469
469
  const start = original(row.pos, false, index), end = original(row.pos + row.removed.length, true, index);
470
470
  if (store) {
471
+ // No image use exists without this marker. A definition appendix alone must
472
+ // not make a prose keystroke join the document to prove that.
473
+ if (!store.contains('![')) return false;
471
474
  const from = Math.max(0, start - 8192);
472
475
  const local = store.readSlice(from, Math.min(store.length, end + 4));
473
476
  const image = local.lastIndexOf('![', end - from);
474
477
  const separator = Math.max(local.lastIndexOf('\n\n', start - from),
475
478
  local.lastIndexOf('\r\n\r\n', start - from), local.lastIndexOf('\r\r', start - from));
476
479
  if (image >= 0) return image + from >= start || separator < image;
477
- return from > 0 && separator < 0;
480
+ const exclude = left => {
481
+ if (hint && splices.length === 1) hint.excluded = {start: left, end};
482
+ return false;
483
+ };
484
+ if (separator >= 0 || from === 0) return exclude(separator >= 0 ? from + separator + 4 : 0);
485
+ // Still inside a longer paragraph. Walk back in slices to the blank line.
486
+ // Finding an image there is the same "possible" answer as the 8KiB window;
487
+ // finding the break, or none, does not read the joined document.
488
+ let cursor = from;
489
+ while (cursor > 0) {
490
+ const sliceFrom = Math.max(0, cursor - 8192);
491
+ const chunk = store.readSlice(sliceFrom, Math.min(store.length, cursor + 3));
492
+ const limit = cursor - sliceFrom;
493
+ const sep = Math.max(chunk.lastIndexOf('\n\n', limit), chunk.lastIndexOf('\r\n\r\n', limit),
494
+ chunk.lastIndexOf('\r\r', limit));
495
+ const imageAt = chunk.lastIndexOf('![', limit);
496
+ if (imageAt >= 0 && (sep < 0 || imageAt > sep)) return true;
497
+ if (sep >= 0 || sliceFrom === 0) return exclude(sep >= 0 ? sliceFrom + sep + 4 : 0);
498
+ cursor = sliceFrom;
499
+ }
500
+ return false;
478
501
  }
479
502
  const image = source.lastIndexOf('![', end);
480
503
  return image >= 0 && (image >= start || Math.max(source.lastIndexOf('\n\n', start),
@@ -562,10 +585,10 @@ export function serializeAsset(asset) {
562
585
  }
563
586
  // Pure: re-derived at commit against the moved document; appends only at the text's end.
564
587
  // A replay supplies the retained definition's line ending instead of choosing one from later prose.
565
- export function appendAssetText(source, asset, lineEnding) {
588
+ export function appendAssetText(source, asset, lineEnding, {preserveLabel = false} = {}) {
566
589
  if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
567
590
  const parsed = documentAssets(source), title = assetTitle(asset.title);
568
- const existing = [...parsed.assets.values()].find(row => row.url === asset.url && row.title === title);
591
+ const existing = !preserveLabel && [...parsed.assets.values()].find(row => row.url === asset.url && row.title === title);
569
592
  if (existing) return {source, id: existing.id, reference: existing.label, added: false, suffix: ''};
570
593
  const previous = parsed.references[asset.id];
571
594
  const block = serializeAsset(asset);
@@ -152,6 +152,14 @@ export function imageStyle(value) {
152
152
 
153
153
  export const linesHeightCss = lines => lines === 1 ? '1cap' : 'calc(' + (lines - 1) + ' * var(--md-line, 1.6em) + 1cap)';
154
154
 
155
+ // An HTML object has an outer clip box and an independent browser viewport.
156
+ // SCALE keeps that authored viewport; REFLOW keeps measured content height.
157
+ export function objectHeight(width, naturalHeight, attrs, line) {
158
+ if (attrs.height && attrs.height !== 'auto') return Math.min(4096, Number(attrs.height) * line);
159
+ const [w, h] = (attrs.viewport || '640x360').split('x').map(Number);
160
+ return attrs.resize === 'scale' ? Math.min(4096, width * h / w) : naturalHeight;
161
+ }
162
+
155
163
  // Controls remain real DOM nodes while their surrounding text flows. A checkbox, link or
156
164
  // disclosure is not a reason to exclude its entire block from picture placement.
157
165
  export function wrapTextBlock(element) {
@@ -159,7 +167,7 @@ export function wrapTextBlock(element) {
159
167
  element.textContent.trim() && !element.querySelector('img,table,pre,figure,math,.math-rendered') ? element : null;
160
168
  }
161
169
 
162
- // The one wrap-owner rule (layout/browser.js, layout/interchange.js, editor/share.js): text after, else before, skipping pictures,
170
+ // The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
163
171
  // metadata and empty paragraphs; any other block is a barrier in that direction.
164
172
  export function wrapNeighbour(node, classify) {
165
173
  for (const step of [n => n && n.nextElementSibling, n => n && n.previousElementSibling]) {
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // The one owner of inline typography, authored colour/font comments, ink and page breaks.
2
+ // The one owner of inline typography, ink and page breaks.
3
3
  import {markdownSourcePositions} from './md-source.mjs';
4
4
 
5
5
  // The exported page retains this reader alone. The editor uses the same closure, then adds its writers and scans.
@@ -17,8 +17,8 @@ const inkReader = () => {
17
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
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
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 do not nest
21
- // in each other. Arrow/end alone take a pairing number, an end takes no other data; under, strike and arrow may omit a path for a line
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
22
  // derived from their words.
23
23
  const INK_KINDS = Object.freeze(['under', 'strike', 'ring', 'bracket', 'free', 'arrow', 'end']);
24
24
 
@@ -29,9 +29,6 @@ const inkReader = () => {
29
29
  + '(?: (' + INK_INT + ',' + INK_INT + '(?: ' + INK_INT + ',' + INK_INT + ')*))?';
30
30
  const INK_OPEN_PATTERN = '<!--ink ' + INK_OPEN_BODY + '-->';
31
31
 
32
- // Relocated into the parse Worker by .toString() with parseInkOpen and isInkClose (engine.js _buildParseWorkerSource):
33
- // reads only its parameter and the planted INK_PATH_MAX, INK_INT_MAX and TEXT_COLOR_NAMES; parseInkOpen reads
34
- // INK_OPEN_PATTERN and this function; isInkClose reads INK_CLOSE.
35
32
  function readInkMatch(match) {
36
33
  const linked = match[1] === 'arrow' || match[1] === 'end';
37
34
  if (linked !== (match[2] != null) || (!linked && match[1] !== 'under' && match[1] !== 'strike' && match[9] == null)) return null;
@@ -69,73 +66,20 @@ const inkReader = () => {
69
66
  }
70
67
  return { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody };
71
68
  };
72
- const { TEXT_COLOR_NAMES, COLOR_NAME_ALT, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody } = inkReader();
69
+ const { TEXT_COLOR_NAMES, COLOR_VALUE, INK_KINDS, INK_PATH_MAX, INK_INT_MAX, INK_OPEN_PATTERN, readInkMatch, parseInkOpen, parseInkBody } = inkReader();
73
70
  export { readInkMatch, parseInkOpen, parseInkBody };
74
71
 
75
72
  const TEXT_COLOR_HEX_TO_NAME = Object.freeze(
76
73
  Object.fromEntries(Object.entries(TEXT_COLOR_NAMES).map(([name, hex]) => [hex, name]))
77
74
  );
78
- const COLOR_CLOSE = '<!--/c-->';
79
75
  const PAGE_BREAK_MARKER = '<!--md-break:v1 page-->';
80
76
 
81
- export { TEXT_COLOR_NAMES, TEXT_COLOR_HEX_TO_NAME, COLOR_CLOSE, PAGE_BREAK_MARKER };
82
-
83
- export function formatColorOpen(hex) {
84
- return '<!--c ' + (TEXT_COLOR_HEX_TO_NAME[hex] || hex) + '-->';
85
- }
77
+ export { TEXT_COLOR_NAMES, PAGE_BREAK_MARKER };
86
78
 
87
79
  export function formatColorRun(hex, content) {
88
80
  return typography.wrap({color: hex}, content);
89
81
  }
90
82
 
91
- // Relocated into the parse Worker by .toString() (engine.js _buildParseWorkerSource): read only parameters, TEXT_COLOR_NAMES and COLOR_CLOSE.
92
-
93
- // Returns {hex, length in UTF-16 units} or null; not end-anchored.
94
- export function matchColorOpen(text) {
95
- if (typeof text !== 'string') return null;
96
- const match = new RegExp('^<!--c (#[0-9a-f]{6}|' + Object.keys(TEXT_COLOR_NAMES).join('|') + ')-->').exec(text);
97
- if (!match) return null;
98
- const value = match[1];
99
- return { hex: value.charCodeAt(0) === 0x23 /* '#' */ ? value : TEXT_COLOR_NAMES[value], length: match[0].length };
100
- }
101
-
102
- export function parseColorOpen(comment) {
103
- if (typeof comment !== 'string') return null;
104
- const match = new RegExp('^<!--c (#[0-9a-f]{6}|' + Object.keys(TEXT_COLOR_NAMES).join('|') + ')-->$').exec(comment);
105
- if (!match) return null;
106
- const value = match[1];
107
- return value.charCodeAt(0) === 0x23 /* '#' */ ? value : TEXT_COLOR_NAMES[value];
108
- }
109
-
110
- export function isColorClose(comment) {
111
- return comment === COLOR_CLOSE;
112
- }
113
-
114
- export function scanColorMarkers(source) {
115
- const text = String(source == null ? '' : source);
116
- const scan = new RegExp('<!--c (?:(#[0-9a-f]{6})|(' + COLOR_NAME_ALT + '))-->|<!--/c-->', 'g');
117
- const markers = [];
118
- let match;
119
- while ((match = scan.exec(text))) {
120
- const hex = match[1] || (match[2] ? TEXT_COLOR_NAMES[match[2]] : null) || null;
121
- markers.push({ start: match.index, end: match.index + match[0].length, hex });
122
- }
123
- return markers;
124
- }
125
-
126
- export function pairColorMarkers(source) {
127
- return pairMarkers(source, 'color');
128
- }
129
-
130
- export function stripColorMarkers(text) {
131
- return String(text == null ? '' : text).replace(new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->', 'g'), '');
132
- }
133
-
134
- export function hasColorMarker(text) {
135
- return new RegExp('<!--(?:c ' + COLOR_VALUE + '|/c)-->').test(String(text == null ? '' : text));
136
- }
137
-
138
- export const FONT_CLOSE = '<!--/f-->';
139
83
  export const FONT_STACKS = Object.freeze({
140
84
  sans: Object.freeze({stack: 'sans', css: '', word: ''}),
141
85
  serif: Object.freeze({stack: 'serif', css: "Georgia,'Palatino Linotype',Palatino,'Times New Roman',Times,serif", word: 'Georgia'}),
@@ -144,8 +88,7 @@ export const FONT_STACKS = Object.freeze({
144
88
  hand: Object.freeze({stack: 'hand', css: 'cursive', word: 'Segoe Print'}),
145
89
  });
146
90
 
147
- // A family is one font-table name, never a CSS list or an HTML fragment. These readers are also
148
- // relocated into the parse worker with FONT_CLOSE and their original function names.
91
+ // A family is one font-table name, never a CSS list or an HTML fragment.
149
92
  export function normalizeFontFamily(value) {
150
93
  if (typeof value !== 'string') return null;
151
94
  const family = value.trim();
@@ -159,60 +102,13 @@ export function fontFamilyCss(value) {
159
102
  return generic ? generic.css || "'Geist',system-ui,sans-serif" : '"' + family.replace(/[\\"]/g, '\\$&') + '"';
160
103
  }
161
104
 
162
- export function formatFontOpen(family) {
163
- const name = normalizeFontFamily(family);
164
- if (!name) throw new TypeError('font: use one family name');
165
- return '<!--f ' + name + '-->';
166
- }
167
-
168
105
  export function formatFontRun(family, content) {
169
106
  return typography.wrap({family}, content);
170
107
  }
171
108
 
172
- export function matchFontOpen(text) {
173
- if (typeof text !== 'string') return null;
174
- const match = /^<!--f ([^<>\r\n]{1,256})-->/.exec(text);
175
- const family = match && normalizeFontFamily(match[1]);
176
- return family ? {family, length: match[0].length} : null;
177
- }
178
-
179
- export function parseFontOpen(comment) {
180
- const match = matchFontOpen(comment);
181
- return match && match.length === comment.length ? match.family : null;
182
- }
183
-
184
- export function isFontClose(comment) {
185
- return comment === FONT_CLOSE;
186
- }
187
-
188
- export function scanFontMarkers(source) {
189
- const text = String(source ?? ''), markers = [];
190
- const scan = /<!--(?:f [^<>\r\n]{1,256}|\/f)-->/g;
191
- for (const match of text.matchAll(scan)) {
192
- const family = match[0] === FONT_CLOSE ? null : parseFontOpen(match[0]);
193
- if (family || match[0] === FONT_CLOSE) markers.push({start: match.index, end: match.index + match[0].length, family});
194
- }
195
- return markers;
196
- }
197
-
198
- export function pairFontMarkers(source, markers = null) {
199
- return pairMarkers(source, 'font', markers);
200
- }
201
-
202
- export function stripFontMarkers(source) {
203
- const text = String(source ?? '');
204
- let result = '', at = 0;
205
- for (const marker of scanFontMarkers(text)) { result += text.slice(at, marker.start); at = marker.end; }
206
- return result + text.slice(at);
207
- }
208
-
209
- export function hasFontMarker(source) {
210
- return scanFontMarkers(source).length > 0;
211
- }
212
-
213
109
  // One typed interpretation for source, DOM and native-file adapters. The worker carries this
214
110
  // exact factory and its explicit grammar inputs; no DOM, CSS evaluator or resource loader.
215
- export function createTypography({normalizeFontFamily, fontFamilyCss, parseColorOpen, parseFontOpen, FONT_STACKS}) {
111
+ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACKS}) {
216
112
  const LIMIT = 4096, DEPTH = 64;
217
113
  const generics = {serif: 'serif', 'sans-serif': 'sans', monospace: 'mono', cursive: 'hand',
218
114
  fantasy: 'fantasy', 'system-ui': 'system', 'ui-serif': 'ui-serif', 'ui-sans-serif': 'ui-sans-serif',
@@ -383,34 +279,23 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, parseColor
383
279
  const stack = []; let depth = 0, overflow = 0;
384
280
  for (const token of block.children) {
385
281
  if (token.type !== 'html_inline') { depth += token.nesting || 0; continue; }
386
- const source = token.content, htmlClose = /^<\/span\s*>$/i.test(source);
387
- const kind = htmlClose || /^<span(?=[\s>])/i.test(source) ? 'span'
388
- : source === '<!--/c-->' || parseColorOpen(source) ? 'color'
389
- : source === '<!--/f-->' || parseFontOpen(source) ? 'font' : '';
390
- if (!kind) continue;
391
- const close = htmlClose || source === '<!--/c-->' || source === '<!--/f-->';
282
+ const source = token.content, close = /^<\/span\s*>$/i.test(source);
283
+ if (!close && !/^<span(?=[\s>])/i.test(source)) continue;
392
284
  if (overflow) { overflow += close ? -1 : 1; continue; }
393
285
  if (!close) {
394
- // Authored comment marks retain their original first-opener rule. Ordinary
395
- // canonical spans nest; a repeated old opener remains an inert stray.
396
- if (kind !== 'span' && stack.some(held => held.kind === kind)) continue;
397
286
  if (stack.length === DEPTH) { overflow = 1; continue; }
398
- const facts = kind === 'span' ? parseOpen(source, value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\')))
399
- : kind === 'color' ? normalize({color: parseColorOpen(source)}) : normalize({family: parseFontOpen(source)});
400
- stack.push({token, kind, facts, depth, source}); continue;
287
+ const facts = parseOpen(source, value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\')));
288
+ stack.push({token, facts, depth, source}); continue;
401
289
  }
402
- const held = stack.at(-1);
403
- if (!held || held.kind !== kind) continue;
404
- stack.pop();
405
- if (!held.facts || depth !== held.depth) continue;
406
- const type = kind === 'span' ? 'typography' : kind, opener = held.token;
290
+ const held = stack.pop();
291
+ if (!held || !held.facts || depth !== held.depth) continue;
292
+ const type = 'typography', opener = held.token;
407
293
  opener.type = 'rapier_' + type + '_open'; opener.tag = 'span'; opener.nesting = 1; opener.content = '';
408
294
  (opener.meta ||= {}).typography = Object.freeze({style: held.facts, open: held.source, close: source});
409
295
  opener.attrSet('style', style(held.facts));
410
296
  if (held.facts.color) opener.attrSet('data-md-color', held.facts.color);
411
297
  if (held.facts.size) opener.attrSet('data-md-size', held.facts.size);
412
298
  if (held.facts.family) opener.attrSet('data-md-font', held.facts.family.name);
413
- if (kind === 'font') { opener.attrSet('data-md-font-open', encodeURIComponent(held.source)); opener.meta.mdFont = {source: held.source, family: held.facts.family.name}; }
414
299
  token.type = 'rapier_' + type + '_close'; token.tag = 'span'; token.nesting = -1; token.content = '';
415
300
  (token.meta ||= {}).typography = opener.meta.typography;
416
301
  }
@@ -421,7 +306,7 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, parseColor
421
306
  return Object.freeze({size, color, family, familyCss, normalize, parseStyle, style, open, wrap, parseOpen, nodeStyle, runs, install});
422
307
  }
423
308
 
424
- export const typography = createTypography({normalizeFontFamily, fontFamilyCss, parseColorOpen, parseFontOpen, FONT_STACKS});
309
+ export const typography = createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACKS});
425
310
  export function typographySourceRanges(source, parser) { return markdownSourcePositions(source, parser).typography; }
426
311
 
427
312
  const INK_CLOSE = '<!--/ink-->';
@@ -485,7 +370,7 @@ export function matchInkOpen(text) {
485
370
  // The opener's body is what a rendered span carries (a renderer's sanitizer drops an attribute whose value holds the
486
371
  // comment's own close): the opener after `<!--ink ` and before `-->`, so the attribute reads `under red box=… 0,95 …`,
487
372
  // and only a body that reads as a mark goes back. Read and write through the same grammar, so an invalid body cannot
488
- // invent a mark. Relocated into the parse Worker with the three above: reads INK_OPEN_PATTERN and readInkMatch alone.
373
+ // invent a mark.
489
374
  export function inkOpenBody(opener) {
490
375
  if (typeof opener !== 'string') return null;
491
376
  const match = new RegExp('^' + INK_OPEN_PATTERN + '$').exec(opener);
@@ -557,7 +442,7 @@ export function pairInkSpans(source, markers = null) {
557
442
  return pairSpanMarkers(source, 'ink', markers);
558
443
  }
559
444
 
560
- // Colour and font keep their first opener; ink pairs each closer with the innermost opener. The shared range
445
+ // Colour keeps its first opener; ink pairs each closer with the innermost opener. The shared range
561
446
  // shape lets custody owners keep every independent stroke, including strokes on identical words.
562
447
  export function pairMarkers(source, kind, markers = null) {
563
448
  const paired = pairSpanMarkers(source, kind, markers);
@@ -585,8 +470,8 @@ export function pairMarkers(source, kind, markers = null) {
585
470
  return { runs, strays, arrows };
586
471
  }
587
472
 
588
- // Where a Will marker may stand (docs/will.md, Conversion). A pair inside a fence, a raw block
589
- // (`<pre>`, `<div>`, a raw table cell) or the opening front matter is text: the line is quoted,
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,
590
475
  // and a Word or PDF writer that met it as a marker would govern the wrong region. The same frame
591
476
  // as spec/frontmatter.mjs, copied here so this grammar stays inside the MIT kit's closure.
592
477
  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(' '));
@@ -724,7 +609,7 @@ function willQuotedWhere(stack) {
724
609
  if (item.tags.includes('pre')) return 'raw-pre';
725
610
  if (item.tags.includes('td') || item.tags.includes('th')) return 'raw-table';
726
611
  if (item.tags.includes('div')) return 'raw-div';
727
- return null;
612
+ return 'raw-html';
728
613
  }
729
614
  }
730
615
  return null;
@@ -737,7 +622,7 @@ function willLeaf(stack) {
737
622
 
738
623
  // `spans` are [start, end) of marker lines in `source` (the Will reader's own offsets).
739
624
  // Returns the first quoted span, {start, end, where}, or null when every span may stand.
740
- // `where` is `fence`, `raw-pre`, `raw-div`, `raw-table` or `frontmatter`.
625
+ // `where` is `fence`, `raw-pre`, `raw-div`, `raw-table`, `raw-html` (any other raw HTML block) or `frontmatter`.
741
626
  export function quotedWillSpan(source, spans) {
742
627
  const text = String(source ?? '');
743
628
  const marks = Array.isArray(spans) ? spans : [];
@@ -804,19 +689,13 @@ export function quotedWillSpan(source, spans) {
804
689
  }
805
690
 
806
691
  function pairSpanMarkers(source, kind, markers = null) {
807
- if (kind !== 'ink' && kind !== 'color' && kind !== 'font') throw new TypeError('Unknown paired marker kind');
808
- const color = kind === 'color', font = kind === 'font';
692
+ if (kind !== 'ink') throw new TypeError('Unknown paired marker kind');
809
693
  const runs = [], strays = [], stack = [];
810
- for (const marker of markers || (color ? scanColorMarkers(source) : font ? scanFontMarkers(source) : scanInkMarkers(source))) {
811
- if (color ? marker.hex !== null : font ? marker.family !== null : !marker.close) {
812
- if ((color || font) && stack.length) { strays.push(marker); continue; }
813
- stack.push(marker);
814
- continue;
815
- }
694
+ for (const marker of markers || scanInkMarkers(source)) {
695
+ if (!marker.close) { stack.push(marker); continue; }
816
696
  const open = stack.pop();
817
697
  if (!open) { strays.push(marker); continue; }
818
- runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start,
819
- ...(color ? { hex: open.hex } : font ? {family: open.family} : { mark: open.mark }) });
698
+ runs.push({ start: open.start, end: marker.end, innerStart: open.end, innerEnd: marker.start, mark: open.mark });
820
699
  }
821
700
  strays.push(...stack);
822
701
  runs.sort((a, b) => a.start - b.start);
@@ -897,7 +776,6 @@ export function isPageBreakLine(text) {
897
776
  return typeof text === 'string' && /^<!--md-break:v1 page-->\r?$/.test(text);
898
777
  }
899
778
 
900
- // Also relocated by .toString(); reads only its own regex.
901
779
  export function isPageBreakBlock(text) {
902
780
  return typeof text === 'string' && /^<!--md-break:v1 page-->\r?\n?$/.test(text);
903
781
  }
@@ -28,7 +28,11 @@ export function markdownSourcePositions(source, parser) {
28
28
  }
29
29
  return matched;
30
30
  });
31
- const tokens = owner.parse(body, {});
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
+ const read = new WeakMap();
34
+ inline.parse = function (text, md, env, children) { read.set(children, text); return parser.inline.parse.call(this, text, md, env, children); };
35
+ const {tokens, cells} = markdownTableSources(body, owner, {}, lines);
32
36
  const readInline = (children, content, rawOffset) => {
33
37
  for (const child of children || []) {
34
38
  const span = observed.get(child);
@@ -50,20 +54,7 @@ export function markdownSourcePositions(source, parser) {
50
54
  }
51
55
  }
52
56
  };
53
- let table = null;
54
57
  for (const token of tokens) {
55
- if (token.type === 'tr_open' && token.map) {
56
- // GFM unescapes a pipe before parsing a cell. Preserve that one-character projection and
57
- // consume cells in order, so repeated cells and backticks across a separator never alias.
58
- const start = lineAt(token.map[0]), end = lineAt(token.map[0] + 1), offsets = [];
59
- let text = '';
60
- for (let at = start; at < end; at++) {
61
- if (source[at] === '\\' && source[at + 1] === '|') at++;
62
- offsets.push(at); text += source[at] === '\0' ? '\ufffd' : source[at];
63
- }
64
- offsets.push(end); table = {text, offsets, at: 0};
65
- }
66
- if (token.type === 'tr_close') table = null;
67
58
  if (token.map && ['fence', 'code_block', 'html_block', 'reference_definition'].includes(token.type)) {
68
59
  const start = lineAt(token.map[0]), end = lineAt(token.map[1]);
69
60
  add(start, end);
@@ -81,29 +72,30 @@ export function markdownSourcePositions(source, parser) {
81
72
  }
82
73
  continue;
83
74
  }
84
- if (token.type !== 'inline' || !token.content) continue;
75
+ const content = read.get(token.children) ?? token.content;
76
+ if (token.type !== 'inline' || !content) continue;
85
77
  let rawOffset;
86
78
  if (token.map) {
87
- const offsets = [], starts = [], rows = token.content.split('\n');
79
+ const offsets = [], starts = [], spaces = [], rows = content.split('\n');
88
80
  let local = 0;
89
81
  for (let i = 0; i < rows.length; i++) {
90
- const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1);
91
- const at = source.slice(start, end).replace(/\0/g, '\ufffd').indexOf(rows[i]);
82
+ const start = lineAt(token.map[0] + i), end = lineAt(token.map[0] + i + 1), line = source.slice(start, end).replace(/\0/g, '\ufffd');
83
+ 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
+ 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; }
92
87
  if (at < 0) throw new Error('Markdown inline source mapping failed');
93
- starts.push(local); offsets.push(start + at); local += rows[i].length + 1;
88
+ starts.push(local); spaces.push(lead); offsets.push(start + at - lead); local += rows[i].length + 1;
94
89
  }
95
90
  rawOffset = pos => {
96
91
  let row = starts.length - 1;
97
92
  while (row > 0 && starts[row] > pos) row--;
98
- return offsets[row] + pos - starts[row];
93
+ return offsets[row] + Math.max(pos - starts[row], spaces[row] - 1);
99
94
  };
100
- } else if (table) {
101
- const at = table.text.indexOf(token.content, table.at);
102
- if (at < 0) throw new Error('Markdown table source mapping failed');
103
- table.at = at + token.content.length;
104
- rawOffset = pos => table.offsets[at + pos];
95
+ } else if (cells.has(token)) {
96
+ rawOffset = cells.get(token);
105
97
  } else continue;
106
- readInline(token.children, token.content, rawOffset);
98
+ readInline(token.children, content, rawOffset);
107
99
  }
108
100
  literal.sort((a, b) => a.start - b.start || b.end - a.end);
109
101
  const merged = [];
@@ -120,3 +112,54 @@ export function markdownSourcePositions(source, parser) {
120
112
  };
121
113
  return {isText, isComment: (start, end) => comments.get(start) === end, typography: typography.sort((a, b) => a.start - b.start)};
122
114
  }
115
+
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
+ export function markdownTableSources(source, parser, environment, lineStarts = null) {
119
+ const cells = new WeakMap();
120
+ if (!source.includes('|')) return {tokens: parser.parse(source, environment), cells};
121
+ const lines = lineStarts || [0];
122
+ if (!lineStarts) for (const row of source.matchAll(/\r\n?|\n/g)) lines.push(row.index + row[0].length);
123
+ const owner = Object.create(parser), block = Object.create(parser.block), ruler = Object.create(parser.block.ruler);
124
+ owner.block = block; block.ruler = ruler;
125
+ ruler.getRules = chain => parser.block.ruler.getRules(chain).map(rule => (state, start, end, silent) => {
126
+ const first = state.tokens.length, matched = rule(state, start, end, silent);
127
+ if (matched && !silent && state.tokens[first]?.type === 'table_open') {
128
+ let cell;
129
+ for (let at = first; at < state.tokens.length; at++) {
130
+ const token = state.tokens[at];
131
+ if (token.type === 'tr_open') {
132
+ const row = token.map[0], from = state.bMarks[row] + state.tShift[row];
133
+ const line = state.src.lastIndexOf('\n', from - 1) + 1;
134
+ cell = tableSource(state.src.slice(from, state.eMarks[row]), lines[row] + from - line);
135
+ } else if (token.type === 'inline' && cell) cells.set(token, cell(token.content));
136
+ else if (token.type === 'tr_close') cell = null;
137
+ }
138
+ }
139
+ return matched;
140
+ });
141
+ return {tokens: owner.parse(source, environment), cells};
142
+ }
143
+
144
+ function tableSource(row, base) {
145
+ const trim = row.trimStart();
146
+ base += row.length - trim.length; row = trim.trimEnd();
147
+ const offsets = [], stops = [];
148
+ let text = '', cursor = 0;
149
+ for (let at = 0; at < row.length; at++) {
150
+ if (row[at] === '\\' && row[at + 1] === '|') at++;
151
+ else if (row[at] === '|') stops.push(text.length);
152
+ offsets.push(base + at); text += row[at];
153
+ }
154
+ offsets.push(base + row.length);
155
+ let next = stops[0] === 0 ? 1 : 0;
156
+ if (next) cursor = 1;
157
+ if (stops.at(-1) !== text.length - 1) stops.push(text.length);
158
+ return content => {
159
+ const stop = stops[next++] ?? text.length, raw = text.slice(cursor, stop);
160
+ const start = Math.min(text.length, cursor + raw.length - raw.trimStart().length);
161
+ if (raw.trim() !== content) throw new Error('Markdown table source mapping failed');
162
+ cursor = stop + 1;
163
+ return pos => offsets[start + pos];
164
+ };
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.1.104",
3
+ "version": "1.2.5",
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
@@ -407,7 +407,7 @@
407
407
  border-inline-start:3px dotted var(--md-color-line);
408
408
  z-index:0;
409
409
  }
410
- .md-render:not([data-md-space]):not([data-md-space] *) :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)}
410
+ .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)}
411
411
  .md-render li{margin:0 0 calc(var(--md-line) / 4)}
412
412
  .md-render li:last-child{margin-bottom:0}
413
413
  .md-render li>ul,.md-render li>ol{margin:calc(var(--md-line) / 4) 0 0}
@@ -499,8 +499,8 @@
499
499
  vertical-align:middle;
500
500
  }
501
501
  .md-render p>.math-display-wrap{margin:calc(var(--md-line) / 4) 0 var(--md-gap-block)}
502
- .md-render:not([data-md-space]):not([data-md-space] *) p:has(>:is(.math-display-wrap,.rapier-hscroll--math):only-child){margin-bottom:var(--md-gap-block)}
503
- .md-render:not([data-md-space]):not([data-md-space] *) p>:is(.math-display-wrap,.rapier-hscroll--math):only-child{margin:0}
502
+ .md-render p:has(>:is(.math-display-wrap,.rapier-hscroll--math):only-child){margin-bottom:var(--md-gap-block)}
503
+ .md-render p>:is(.math-display-wrap,.rapier-hscroll--math):only-child{margin:0}
504
504
  /* Diagram MathML keeps the metrics used by its renderer. */
505
505
  .md-render math[display="block"]:not(svg *){font-size:1.12em}
506
506
  .md-render math:not([display="block"]):not(svg *){font-size:1em}
@@ -650,6 +650,9 @@ html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowcha
650
650
  vertical-align:middle;
651
651
  }
652
652
  .md-render img[data-rapier-image-size]{width:min(var(--md-image-width),100%);height:auto}
653
+ /* AUTO stays relative to its column. Authored and gesture-owned inline sizing wins;
654
+ Plain reads the intrinsic attributes without rerendering ordinary image blocks. */
655
+ html:not([data-layout="plain"]) .md-render img[data-rapier-image-geometry="auto"]{width:min(var(--rapier-image-auto-width),var(--rapier-image-natural-width));height:auto}
653
656
  .md-render img:is([data-rapier-asset],[data-rapier-image-url]):not([data-rapier-asset-state="ready"]){background:color-mix(in srgb,var(--md-color-text) 5%,transparent)}
654
657
 
655
658
  .md-render dl{margin:0 0 var(--md-gap-block)}
@@ -697,7 +700,7 @@ html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowcha
697
700
  .md-render [data-md-color="#be185d"]{--md-color-dark:#f9a8d4}
698
701
  .md-render [data-md-color="#57606a"]{--md-color-dark:#aab1bb}
699
702
  .md-render abbr[title]{text-decoration:underline dotted;text-decoration-color:var(--md-color-text-muted);text-underline-offset:2px;cursor:help}
700
- .md-render ins{text-decoration:underline;color:var(--md-color-success)}
703
+ .md-render :is(ins,u){text-decoration:underline;color:var(--md-color-success)}
701
704
  /* The document owns its selection accent, including on an exported page. */
702
705
  .md-render ::selection{background:color-mix(in srgb,var(--md-color-accent) 48%,transparent)}
703
706
  .md-render del,.md-render s{color:var(--md-color-text-muted);text-decoration:line-through}
@@ -747,52 +750,5 @@ html[data-invert-image-alpha="off"] :is(img,.md-render svg.rapier-native-flowcha
747
750
  .md-render [data-md-break]::after{content:none!important}
748
751
  }
749
752
 
750
- /* Authored spacing: one body line, native collapsed gaps, and whole-line block boxes. */
751
- :is(.md-render[data-md-space],[data-md-space] .md-render){
752
- --md-list-heading-before:var(--md-line);
753
- --md-list-heading-after:calc(var(--md-line) / 2);
754
- --md-gap-paragraph:calc(var(--md-space-paragraph,.5) * var(--md-line));
755
- --md-gap-heading-before:calc(var(--md-space-heading-before,1.5) * var(--md-line));
756
- --md-gap-heading-after:calc(var(--md-space-heading-after,.5) * var(--md-line));
757
- --md-gap-block:calc(var(--md-space-block,1) * var(--md-line));
758
- }
759
- :is(.md-render[data-md-space],[data-md-space] .md-render) :where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,details,dl,figure,.diagram-block,.math-display-wrap,.table-scroll-wrap,.rapier-hscroll,.md-space-block){
760
- margin-block:var(--md-gap-block);
761
- height:calc-size(auto,round(up,size,var(--md-line)));
762
- }
763
- :is(.md-render[data-md-space],[data-md-space] .md-render) .md-space-block{display:flow-root}
764
- :is(.md-render[data-md-space],[data-md-space] .md-render) p{margin-block:0 var(--md-gap-paragraph)}
765
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(h1,h2,h3,h4,h5,h6){margin-block:var(--md-gap-heading-before) var(--md-gap-heading-after)}
766
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(h2,h3){line-height:calc(2 * var(--md-line))}
767
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(p[data-md-picture],p:has(>.math-display-wrap:only-child)){margin-block:var(--md-gap-block)}
768
- :is(.md-render[data-md-space],[data-md-space] .md-render) p[data-md-picture]{line-height:0}
769
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(li,dd){margin-block:0 var(--md-gap-paragraph)}
770
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(li:last-child,dd:last-child){margin-block-end:0}
771
- :is(.md-render[data-md-space],[data-md-space] .md-render) dt{margin-block:var(--md-gap-paragraph) 0}
772
- :is(.md-render[data-md-space],[data-md-space] .md-render) blockquote{padding-block:calc(var(--md-line) / 2)}
773
- :is(.md-render[data-md-space],[data-md-space] .md-render) .callout__label{margin-block:0 var(--md-gap-paragraph);height:auto}
774
- :is(.md-render[data-md-space],[data-md-space] .md-render) pre{padding-block:calc(var(--md-line) / 2 - 1px);line-height:var(--md-line)}
775
- :is(.md-render[data-md-space],[data-md-space] .md-render) pre code{display:block;line-height:var(--md-line)}
776
- :is(.md-render[data-md-space],[data-md-space] .md-render) pre[data-rapier-code-lines]{padding-block-start:calc(var(--md-line) * 1.5 - 1px)}
777
- :is(.md-render[data-md-space],[data-md-space] .md-render) details{padding-block:calc(var(--md-line) / 2 - var(--md-hair));}
778
- :is(.md-render[data-md-space],[data-md-space] .md-render) summary{padding-block:0;line-height:var(--md-line)}
779
- :is(.md-render[data-md-space],[data-md-space] .md-render) details[open]>summary{margin-block-end:var(--md-gap-paragraph)}
780
- :is(.md-render[data-md-space],[data-md-space] .md-render) details[open]:not(:has(>:not(summary))){padding-block-end:calc(var(--md-line) * 1.5 - var(--md-hair))}
781
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(th,td){line-height:var(--md-line);padding-block:calc(var(--md-line) / 2 - var(--md-hair) / 2);
782
- border:0;border-inline-end:var(--md-hair) solid color-mix(in srgb,var(--md-color-line) 74%,transparent);border-block-end:var(--md-hair) solid color-mix(in srgb,var(--md-color-line) 74%,transparent)}
783
- :is(.md-render[data-md-space],[data-md-space] .md-render) thead th{border-block-end:2px solid var(--md-color-line);padding-block-end:calc(var(--md-line) / 2 + var(--md-hair) / 2 - 2px)}
784
- :is(.md-render[data-md-space],[data-md-space] .md-render) table{margin-block:var(--md-gap-block);border:0;border-collapse:separate;border-spacing:0;outline:calc(2 * var(--md-hair)) solid var(--md-color-line);outline-offset:calc(-2 * var(--md-hair))}
785
- :is(.md-render[data-md-space],[data-md-space] .md-render) thead{outline:calc(2 * var(--md-hair)) solid var(--md-color-line);outline-offset:calc(-2 * var(--md-hair))}
786
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(.table-scroll-wrap,.rapier-hscroll--table){padding-block:0}
787
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(.table-scroll-wrap,.rapier-hscroll)>:is(table,pre,.math-display-wrap),
788
- :is(.md-render[data-md-space],[data-md-space] .md-render) p>.math-display-wrap{margin-block:0}
789
- :is(.md-render[data-md-space],[data-md-space] .md-render) hr{margin-block:var(--md-gap-block);height:var(--md-line);box-sizing:border-box}
790
- :is(.md-render[data-md-space],[data-md-space] .md-render)>:first-child,
791
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(blockquote,li,td,th,dd,details,.md-space-block)>:first-child{margin-block-start:0}
792
- :is(.md-render[data-md-space],[data-md-space] .md-render) :is(blockquote,td,th,dd,details,.md-space-block)>:last-child{margin-block-end:0}
793
- @media print{
794
- :is(.md-render[data-md-space],[data-md-space] .md-render) pre[data-rapier-code-lines]{padding-block-start:calc(var(--md-line) / 2 - 1px)!important}
795
- }
796
-
797
753
  /* Authored run sizes supply their own line box; surrounding block rhythm is unchanged. */
798
754
  .md-render [data-md-size]{line-height:normal}