@mirrordown/core 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2270 @@
1
+ // src/escapes.ts
2
+ const ASCII_PUNCTUATION = /^[!-/:-@[-`{-~]$/;
3
+ const ENTITY = /^&(?:#\d+|#x[\da-f]+|[a-z][\da-z]*);/i;
4
+ const MASK_BASE = 983040;
5
+ const MASKED = /[\u{F0021}-\u{F007E}]/gu;
6
+ /**
7
+ * Indices of `value` whose character was backslash-escaped in `source`, where
8
+ * `start`–`end` is the source range the text came from. Characters the parser
9
+ * dropped (backslashes, container prefixes such as `> `, trailing spaces) are
10
+ * skipped, and a character reference stands for the character it decodes to.
11
+ */
12
+ const escapedIndices = (value, source, start, end) => {
13
+ const escaped = /* @__PURE__ */ new Set();
14
+ let j = start;
15
+ for (let i = 0; i < value.length && j < end; i++) {
16
+ const char = value.charAt(i);
17
+ let entity = null;
18
+ while (j < end && source[j] !== char) {
19
+ entity = source[j] === `&` ? ENTITY.exec(source.slice(j, end)) : null;
20
+ if (entity) break;
21
+ j++;
22
+ }
23
+ if (j >= end) break;
24
+ if (entity) {
25
+ j += entity[0].length;
26
+ continue;
27
+ }
28
+ let slashes = 0;
29
+ for (let k = j - 1; k >= start && source[k] === `\\`; k--) slashes++;
30
+ if (slashes % 2 === 1 && ASCII_PUNCTUATION.test(char)) escaped.add(i);
31
+ j++;
32
+ }
33
+ return escaped;
34
+ };
35
+ /** Replace the characters at `escaped` with stand-ins no delimiter matches. */
36
+ const maskEscaped = (value, escaped) => {
37
+ if (escaped.size === 0) return value;
38
+ let out = ``;
39
+ for (let i = 0; i < value.length; i++) out += escaped.has(i) ? String.fromCodePoint(MASK_BASE + value.charCodeAt(i)) : value[i];
40
+ return out;
41
+ };
42
+ /** Restore characters replaced by {@link maskEscaped}. */
43
+ const unmaskEscaped = (value) => value.replace(MASKED, (c) => String.fromCharCode(c.codePointAt(0) - MASK_BASE));
44
+ /**
45
+ * Mask escaped characters in every positioned `text` node of a unist tree, so
46
+ * a delimiter scan skips them. Returns a function that restores them in every
47
+ * node afterwards, including nodes the scan created from masked text.
48
+ */
49
+ const maskTreeEscapes = (tree, source) => {
50
+ const walk = (node, visit) => {
51
+ visit(node);
52
+ node.children?.forEach((child) => walk(child, visit));
53
+ };
54
+ walk(tree, (node) => {
55
+ const start = node.position?.start.offset;
56
+ const end = node.position?.end.offset;
57
+ if (node.type !== `text` || typeof node.value !== `string`) return;
58
+ if (start === void 0 || end === void 0) return;
59
+ node.value = maskEscaped(node.value, escapedIndices(node.value, source, start, end));
60
+ });
61
+ return () => walk(tree, (node) => {
62
+ if (typeof node.value === `string`) node.value = unmaskEscaped(node.value);
63
+ });
64
+ };
65
+ // src/github-alerts/types.ts
66
+ /** Maps every recognized alert keyword (lowercase) to its canonical {@link AlertType}. */
67
+ const ALERT_ALIASES = {
68
+ note: "note",
69
+ tip: "tip",
70
+ important: "important",
71
+ warning: "warning",
72
+ caution: "caution",
73
+ abstract: "abstract",
74
+ summary: "abstract",
75
+ tldr: "abstract",
76
+ info: "info",
77
+ todo: "todo",
78
+ hint: "tip",
79
+ success: "success",
80
+ check: "success",
81
+ done: "success",
82
+ question: "question",
83
+ help: "question",
84
+ faq: "question",
85
+ attention: "warning",
86
+ failure: "failure",
87
+ fail: "failure",
88
+ missing: "failure",
89
+ danger: "danger",
90
+ error: "danger",
91
+ bug: "bug",
92
+ example: "example",
93
+ quote: "quote",
94
+ cite: "quote"
95
+ };
96
+ /** Default display title for each {@link AlertType}. */
97
+ const DEFAULT_TITLE = {
98
+ note: "Note",
99
+ tip: "Tip",
100
+ important: "Important",
101
+ warning: "Warning",
102
+ caution: "Caution",
103
+ abstract: "Abstract",
104
+ info: "Info",
105
+ todo: "Todo",
106
+ success: "Success",
107
+ question: "Question",
108
+ failure: "Failure",
109
+ danger: "Danger",
110
+ bug: "Bug",
111
+ example: "Example",
112
+ quote: "Quote"
113
+ };
114
+ // src/github-alerts/icons.ts
115
+ const icon = (path) => `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${path}</svg>`;
116
+ /** Inline SVG icon markup for each {@link AlertType}. */
117
+ const ICONS = {
118
+ note: icon("<path d=\"M12 20h9\"></path><path d=\"M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z\"></path>"),
119
+ tip: icon("<path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"></path><path d=\"M9 18h6\"></path><path d=\"M10 22h4\"></path>"),
120
+ important: icon("<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22z\"></path>"),
121
+ warning: icon("<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"></path><path d=\"M12 9v4\"></path><path d=\"M12 17h.01\"></path>"),
122
+ caution: icon("<circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"m4.9 4.9 14.2 14.2\"></path><path d=\"M12 8v4\"></path><path d=\"M12 16h.01\"></path>"),
123
+ abstract: icon("<rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"></rect><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><path d=\"M12 11h4\"></path><path d=\"M12 16h4\"></path><path d=\"M8 11h.01\"></path><path d=\"M8 16h.01\"></path>"),
124
+ info: icon("<circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M12 16v-4\"></path><path d=\"M12 8h.01\"></path>"),
125
+ todo: icon("<path d=\"M8 6h13\"></path><path d=\"M8 12h13\"></path><path d=\"M8 18h13\"></path><path d=\"M3 6h.01\"></path><path d=\"M3 12h.01\"></path><path d=\"M3 18h.01\"></path>"),
126
+ success: icon("<path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\"></path><path d=\"m9 11 3 3L22 4\"></path>"),
127
+ question: icon("<circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\"></path><path d=\"M12 17h.01\"></path>"),
128
+ failure: icon("<circle cx=\"12\" cy=\"12\" r=\"10\"></circle><path d=\"m15 9-6 6\"></path><path d=\"m9 9 6 6\"></path>"),
129
+ danger: icon("<path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8z\"></path>"),
130
+ bug: icon("<path d=\"m8 2 1.88 1.88\"></path><path d=\"M14.12 3.88 16 2\"></path><path d=\"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1\"></path><path d=\"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6\"></path><path d=\"M12 20v-9\"></path><path d=\"M6.53 9C4.6 8.8 3 7.1 3 5\"></path><path d=\"M6 13H2\"></path><path d=\"M3 21c0-2.1 1.7-3.9 3.8-4\"></path><path d=\"M20.97 5c0 2.1-1.6 3.8-3.5 4\"></path><path d=\"M22 13h-4\"></path><path d=\"M17.2 17c2.1.1 3.8 1.9 3.8 4\"></path>"),
131
+ example: icon("<path d=\"M8 6h13\"></path><path d=\"M8 12h13\"></path><path d=\"M8 18h13\"></path><path d=\"M3 6 4 5l1 1-1 1z\"></path><path d=\"M3 12 4 11l1 1-1 1z\"></path><path d=\"M3 18 4 17l1 1-1 1z\"></path>"),
132
+ quote: icon("<path d=\"M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z\"></path><path d=\"M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z\"></path>")
133
+ };
134
+ // src/github-alerts/index.ts
135
+ const MARKER_RE = /^\[!([^\]]+)\]([+-])?/;
136
+ /** Whether a source line opens an alert (`> [!NOTE]`), after its `>` markers and indentation, whether or not the alerts plugin reads it: a block that takes the quote after it as more of its content (a step body) leaves an alert be. */
137
+ const isAlertMarkerLine = (line) => MARKER_RE.test(line.replace(/^[ >]*/, ``));
138
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
139
+ /** Resolve alert keywords, titles and icons the same way in every engine. */
140
+ const createAlertResolver = (options = {}) => {
141
+ const { types: extraTypes = {}, titles = {}, matchCaseInsensitive = true, icons = true, containerClass = `markdown-alert` } = options;
142
+ const aliasMap = {
143
+ ...ALERT_ALIASES,
144
+ ...extraTypes
145
+ };
146
+ const customTitles = titles;
147
+ const isAlertType = (type) => type in DEFAULT_TITLE;
148
+ return {
149
+ resolve: (keyword) => aliasMap[matchCaseInsensitive ? keyword.toLowerCase() : keyword],
150
+ titleFor: (type) => customTitles[type] ?? (isAlertType(type) ? DEFAULT_TITLE[type] : void 0) ?? capitalize(type),
151
+ iconFor: (type) => icons && isAlertType(type) ? ICONS[type] : ``,
152
+ containerClass
153
+ };
154
+ };
155
+ /** Plain text of inline content: text, inline code and image alt count; raw HTML does not. */
156
+ const plainText = (nodes) => nodes.map((n) => n.type === `text` || n.type === `inlineCode` ? n.value ?? `` : n.type === `image` ? n.alt ?? `` : n.children ? plainText(n.children) : ``).join(``);
157
+ const withValue = (node, value) => ({
158
+ ...node,
159
+ value
160
+ });
161
+ /**
162
+ * Split an alert's first paragraph (an mdast `paragraph`'s children) into its marker, title and body, or return `null` when it does not open with a `[!KEYWORD]` marker in plain text. The first line is the marker plus an optional title; everything after the first line break — a newline inside a text node (soft) or a `break` node (hard) — is the body. Nothing is dropped. Text nodes are copied; other nodes are reused as they are.
163
+ */
164
+ const splitAlertParagraph = (children) => {
165
+ const [first, ...rest] = children;
166
+ if (first?.type !== `text` || first.value === void 0) return null;
167
+ const opening = first.value.trimStart();
168
+ const match = MARKER_RE.exec(opening);
169
+ if (!match) return null;
170
+ const [marker, keyword = ``, fold] = match;
171
+ const line = [];
172
+ const body = [];
173
+ for (const child of [withValue(first, opening.slice(marker.length)), ...rest]) if (body.length > 0 || line.at(-1)?.type === `break`) body.push(child);
174
+ else if (child.type === `text` && child.value?.includes(`\n`)) {
175
+ const at = child.value.indexOf(`\n`);
176
+ line.push(withValue(child, child.value.slice(0, at)));
177
+ body.push(withValue(child, child.value.slice(at + 1)));
178
+ } else line.push(child);
179
+ if (line.at(-1)?.type === `break`) line.pop();
180
+ const titleNodes = line.map((n, i) => n.type !== `text` ? n : withValue(n, i === 0 && i === line.length - 1 ? (n.value ?? ``).trim() : i === 0 ? (n.value ?? ``).trimStart() : i === line.length - 1 ? (n.value ?? ``).trimEnd() : n.value ?? ``)).filter((n) => n.type !== `text` || n.value !== ``);
181
+ const lastTitle = titleNodes.at(-1);
182
+ if (lastTitle?.type === `text`) titleNodes[titleNodes.length - 1] = withValue(lastTitle, (lastTitle.value ?? ``).trimEnd());
183
+ return {
184
+ keyword,
185
+ fold: fold === `+` || fold === `-` ? fold : void 0,
186
+ titleNodes,
187
+ titleText: plainText(titleNodes).trim(),
188
+ bodyNodes: body.filter((n) => n.type !== `text` || n.value !== ``)
189
+ };
190
+ };
191
+ // src/spans.ts
192
+ /** Wrap a scan with built-node tracking and source-based escape masking. */
193
+ const createSpanTransform = (nodes, scan) => {
194
+ const built = /* @__PURE__ */ new WeakSet();
195
+ const contents = /* @__PURE__ */ new WeakMap();
196
+ const tracked = {
197
+ text: (value) => {
198
+ const node = nodes.text(value);
199
+ built.add(node);
200
+ return node;
201
+ },
202
+ wrap: (children) => {
203
+ const node = nodes.wrap(children);
204
+ built.add(node);
205
+ contents.set(node, children);
206
+ return node;
207
+ }
208
+ };
209
+ const transform = (children, parentType = ``) => scan(children, parentType, tracked);
210
+ const restore = (list) => {
211
+ for (const node of list) {
212
+ if (!built.has(node)) continue;
213
+ if (node.type === `text` && node.value !== void 0) node.value = unmaskEscaped(node.value);
214
+ else restore(contents.get(node) ?? []);
215
+ }
216
+ };
217
+ const withSource = (children, parentType, source) => {
218
+ const masked = children.map((node) => {
219
+ const start = node.position?.start.offset;
220
+ const end = node.position?.end.offset;
221
+ if (node.type !== `text` || node.value === void 0 || start === void 0 || end === void 0) return node;
222
+ const escaped = escapedIndices(node.value, source, start, end);
223
+ return escaped.size === 0 ? node : tracked.text(maskEscaped(node.value, escaped));
224
+ });
225
+ const next = transform(masked, parentType);
226
+ if (next === masked) return void 0;
227
+ restore(next);
228
+ return [...next];
229
+ };
230
+ return Object.assign(transform, {
231
+ built: (node) => built.has(node),
232
+ withSource
233
+ });
234
+ };
235
+ // src/delimited.ts
236
+ const INLINE_PARENTS = /* @__PURE__ */ new Set([
237
+ `delete`,
238
+ `emphasis`,
239
+ `insert`,
240
+ `link`,
241
+ `linkReference`,
242
+ `mark`,
243
+ `spoiler`,
244
+ `spoilerContent`,
245
+ `strong`,
246
+ `subscript`,
247
+ `superscript`
248
+ ]);
249
+ const WHITESPACE$3 = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/u;
250
+ const PUNCTUATION$1 = /^[\p{P}\p{S}]$/u;
251
+ const classify$1 = (char) => WHITESPACE$3.test(char) ? `space` : PUNCTUATION$1.test(char) ? `punct` : `other`;
252
+ /**
253
+ * markdown-it's `balance_pairs` for a single marker. Every delimiter has `length: 0`, as in markdown-it-mark, so the emphasis "rule of 3" never applies.
254
+ */
255
+ const balancePairs = (delimiters) => {
256
+ const jumps = [];
257
+ const openersBottom = [
258
+ -1,
259
+ -1,
260
+ -1,
261
+ -1,
262
+ -1,
263
+ -1
264
+ ];
265
+ let headerIdx = 0;
266
+ let lastItem = -2;
267
+ delimiters.forEach((closer, closerIdx) => {
268
+ jumps.push(0);
269
+ if (lastItem !== closer.item - 1) headerIdx = closerIdx;
270
+ lastItem = closer.item;
271
+ if (!closer.close) return;
272
+ const bottom = closer.open ? 3 : 0;
273
+ const minOpenerIdx = openersBottom[bottom] ?? -1;
274
+ let openerIdx = headerIdx - (jumps[headerIdx] ?? 0) - 1;
275
+ let newMinOpenerIdx = openerIdx;
276
+ for (; openerIdx > minOpenerIdx; openerIdx -= (jumps[openerIdx] ?? 0) + 1) {
277
+ const opener = delimiters[openerIdx];
278
+ if (!opener?.open || opener.end >= 0) continue;
279
+ const lastJump = openerIdx > 0 && delimiters[openerIdx - 1]?.open === false ? (jumps[openerIdx - 1] ?? 0) + 1 : 0;
280
+ jumps[closerIdx] = closerIdx - openerIdx + lastJump;
281
+ jumps[openerIdx] = lastJump;
282
+ closer.open = false;
283
+ opener.end = closerIdx;
284
+ opener.close = false;
285
+ newMinOpenerIdx = -1;
286
+ lastItem = -2;
287
+ break;
288
+ }
289
+ if (newMinOpenerIdx !== -1) openersBottom[bottom] = newMinOpenerIdx;
290
+ });
291
+ };
292
+ /** Build the delimiter scan for one marker: `=` (mark), `+` (ins), `-` (del) or `|` (spoiler). */
293
+ const createDelimitedTransform = (marker, nodes) => createSpanTransform(nodes, (children, parentType, { text, wrap }) => {
294
+ const edge = INLINE_PARENTS.has(parentType) ? `punct` : `space`;
295
+ const beside = (node, side) => {
296
+ if (!node) return edge;
297
+ if (node.type === `text` && node.value) return classify$1(side === `before` ? node.value.slice(-1) : node.value.charAt(0));
298
+ return node.type === `break` ? `space` : `punct`;
299
+ };
300
+ const items = [];
301
+ const delimiters = [];
302
+ children.forEach((child, index) => {
303
+ const value = child.type === `text` ? child.value : void 0;
304
+ if (value === void 0) {
305
+ items.push({
306
+ kind: `node`,
307
+ node: child
308
+ });
309
+ return;
310
+ }
311
+ let pending = ``;
312
+ const flush = () => {
313
+ if (pending) items.push({
314
+ kind: `text`,
315
+ value: pending
316
+ });
317
+ pending = ``;
318
+ };
319
+ let i = 0;
320
+ while (i < value.length) {
321
+ let end = i;
322
+ while (value.charAt(end) === marker) end++;
323
+ if (end - i < 2) {
324
+ pending += value.slice(i, Math.max(end, i + 1));
325
+ i = Math.max(end, i + 1);
326
+ continue;
327
+ }
328
+ const last = i > 0 ? classify$1(value.charAt(i - 1)) : beside(children[index - 1], `before`);
329
+ const next = end < value.length ? classify$1(value.charAt(end)) : beside(children[index + 1], `after`);
330
+ const open = next !== `space` && (next !== `punct` || last !== `other`);
331
+ const close = last !== `space` && (last !== `punct` || next !== `other`);
332
+ flush();
333
+ let length = end - i;
334
+ if (length % 2) {
335
+ items.push({
336
+ kind: `text`,
337
+ value: marker
338
+ });
339
+ length--;
340
+ }
341
+ for (let pair = 0; pair < length; pair += 2) if (open || close) {
342
+ delimiters.push({
343
+ open,
344
+ close,
345
+ end: -1,
346
+ item: items.length
347
+ });
348
+ items.push({ kind: `delimiter` });
349
+ } else items.push({
350
+ kind: `text`,
351
+ value: marker + marker
352
+ });
353
+ i = end;
354
+ }
355
+ flush();
356
+ });
357
+ balancePairs(delimiters);
358
+ if (!delimiters.some(({ end }) => end >= 0)) return children;
359
+ const lone = [];
360
+ for (const delimiter of delimiters) {
361
+ const closer = delimiters[delimiter.end];
362
+ if (!closer) continue;
363
+ items[delimiter.item] = { kind: `opening` };
364
+ items[closer.item] = { kind: `closing` };
365
+ const previous = items[closer.item - 1];
366
+ if (previous?.kind === `text` && previous.value === marker) lone.push(closer.item - 1);
367
+ }
368
+ for (const i of lone.reverse()) {
369
+ let j = i + 1;
370
+ while (items[j]?.kind === `closing`) j++;
371
+ j--;
372
+ const moved = items[i];
373
+ const target = items[j];
374
+ if (i !== j && moved && target) {
375
+ items[i] = target;
376
+ items[j] = moved;
377
+ }
378
+ }
379
+ const root = [];
380
+ const stack = [root];
381
+ let buffer = ``;
382
+ const append = (node) => {
383
+ (stack.at(-1) ?? root).push(node);
384
+ };
385
+ const flushText = () => {
386
+ if (!buffer) return;
387
+ append(text(buffer));
388
+ buffer = ``;
389
+ };
390
+ for (const item of items) if (item.kind === `text`) buffer += item.value;
391
+ else if (item.kind === `delimiter`) buffer += marker + marker;
392
+ else if (item.kind === `node`) {
393
+ flushText();
394
+ append(item.node);
395
+ } else if (item.kind === `opening`) {
396
+ flushText();
397
+ stack.push([]);
398
+ } else {
399
+ flushText();
400
+ append(wrap(stack.pop() ?? []));
401
+ }
402
+ flushText();
403
+ return root;
404
+ });
405
+ // src/kbd.ts
406
+ const isChar = (unit, char) => unit !== void 0 && `char` in unit && unit.char === char;
407
+ /**
408
+ * The index of the `]]` that closes the `[[` at `start`, or -1. This mirrors markdown-it-kbd's loop, which looks at each character with the one after it, starting from the opener's second `[`, and skips the second character of every pair it consumes.
409
+ */
410
+ const findClose = (units, start) => {
411
+ let depth = 1;
412
+ let skipNext = false;
413
+ for (let i = start + 1; i < units.length; i++) {
414
+ if (skipNext) {
415
+ skipNext = false;
416
+ continue;
417
+ }
418
+ const unit = units[i];
419
+ const next = units[i + 1];
420
+ if (isChar(unit, `]`) && isChar(next, `]`)) {
421
+ depth--;
422
+ if (depth === 0) return i;
423
+ skipNext = true;
424
+ } else if (isChar(unit, `[`) && isChar(next, `[`)) {
425
+ depth++;
426
+ skipNext = true;
427
+ } else if (isChar(unit, `\n`) || unit && `node` in unit && unit.node.type === `break`) return -1;
428
+ }
429
+ return -1;
430
+ };
431
+ /** Build the `[[key]]` scan. */
432
+ const createKbdTransform = (nodes) => createSpanTransform(nodes, (children, _parentType, { text, wrap }) => {
433
+ const units = children.flatMap((child) => {
434
+ const value = child.type === `text` ? child.value : void 0;
435
+ return value === void 0 ? [{ node: child }] : Array.from(value, (char) => ({ char }));
436
+ });
437
+ const scan = (list) => {
438
+ const out = [];
439
+ let keyed = false;
440
+ let buffer = ``;
441
+ const flushText = () => {
442
+ if (buffer) out.push(text(buffer));
443
+ buffer = ``;
444
+ };
445
+ for (let i = 0; i < list.length;) {
446
+ const unit = list[i];
447
+ if (isChar(unit, `[`) && isChar(list[i + 1], `[`)) {
448
+ const end = findClose(list, i);
449
+ if (end >= 0) {
450
+ keyed = true;
451
+ flushText();
452
+ out.push(wrap(scan(list.slice(i + 2, end)).nodes));
453
+ i = end + 2;
454
+ continue;
455
+ }
456
+ }
457
+ if (unit && `char` in unit) buffer += unit.char;
458
+ else if (unit) {
459
+ flushText();
460
+ out.push(unit.node);
461
+ }
462
+ i++;
463
+ }
464
+ flushText();
465
+ return {
466
+ nodes: out,
467
+ keyed
468
+ };
469
+ };
470
+ const { nodes: result, keyed } = scan(units);
471
+ return keyed ? result : children;
472
+ });
473
+ // src/scripts.ts
474
+ /** The marker characters, and the micromark token types each produces. */
475
+ const SCRIPTS = {
476
+ "~": {
477
+ code: 126,
478
+ type: `subscript`
479
+ },
480
+ "^": {
481
+ code: 94,
482
+ type: `superscript`
483
+ }
484
+ };
485
+ const tokenType = (type) => type;
486
+ const temporary = (type) => tokenType(`${type}SequenceTemporary`);
487
+ const scriptOf = (tokenType) => Object.values(SCRIPTS).find(({ type }) => temporary(type) === tokenType);
488
+ const PUNCTUATION = /\p{P}|\p{S}/u;
489
+ const WHITESPACE$2 = /\s/;
490
+ const classify = (code) => code === null || code < 0 || code === 32 || WHITESPACE$2.test(String.fromCharCode(code)) ? 1 : PUNCTUATION.test(String.fromCharCode(code)) ? 2 : void 0;
491
+ const splice = (list, start, remove, items) => {
492
+ list.splice(start, remove);
493
+ for (let i = 0; i < items.length; i += 1e4) list.splice(start + i, 0, ...items.slice(i, i + 1e4));
494
+ };
495
+ const resolveAll = (constructs, events, context) => {
496
+ const called = [];
497
+ let result = events;
498
+ for (const { resolveAll: resolve } of constructs) if (resolve && !called.includes(resolve)) {
499
+ result = resolve(result, context);
500
+ called.push(resolve);
501
+ }
502
+ return result;
503
+ };
504
+ const resolveScripts = (events, context) => {
505
+ for (let index = 0; index < events.length; index++) {
506
+ const [kind, closer] = events.at(index) ?? [];
507
+ const script = closer && scriptOf(closer.type);
508
+ if (kind !== `enter` || !closer || !script || !closer._close) continue;
509
+ for (let open = index - 1; open >= 0; open--) {
510
+ const [openKind, opener] = events.at(open) ?? [];
511
+ if (openKind !== `exit` || !opener || opener.type !== closer.type || !opener._open) continue;
512
+ opener.type = tokenType(`${script.type}Sequence`);
513
+ closer.type = tokenType(`${script.type}Sequence`);
514
+ const span = {
515
+ type: tokenType(script.type),
516
+ start: { ...opener.start },
517
+ end: { ...closer.end }
518
+ };
519
+ const text = {
520
+ type: tokenType(`${script.type}Text`),
521
+ start: { ...opener.end },
522
+ end: { ...closer.start }
523
+ };
524
+ const next = [
525
+ [
526
+ `enter`,
527
+ span,
528
+ context
529
+ ],
530
+ [
531
+ `enter`,
532
+ opener,
533
+ context
534
+ ],
535
+ [
536
+ `exit`,
537
+ opener,
538
+ context
539
+ ],
540
+ [
541
+ `enter`,
542
+ text,
543
+ context
544
+ ],
545
+ ...resolveAll(context.parser.constructs.insideSpan.null ?? [], events.slice(open + 1, index), context),
546
+ [
547
+ `exit`,
548
+ text,
549
+ context
550
+ ],
551
+ [
552
+ `enter`,
553
+ closer,
554
+ context
555
+ ],
556
+ [
557
+ `exit`,
558
+ closer,
559
+ context
560
+ ],
561
+ [
562
+ `exit`,
563
+ span,
564
+ context
565
+ ]
566
+ ];
567
+ splice(events, open - 1, index - open + 3, next);
568
+ index = open + next.length - 2;
569
+ break;
570
+ }
571
+ }
572
+ for (const [, token] of events) if (scriptOf(token.type)) token.type = `data`;
573
+ return events;
574
+ };
575
+ function tokenize(effects, ok, nok) {
576
+ const { previous, events } = this;
577
+ return (start) => {
578
+ const script = Object.values(SCRIPTS).find((s) => s.code === start);
579
+ if (!script) return nok(start);
580
+ if (previous === script.code && events.at(-1)?.[1].type !== `characterEscape`) return nok(start);
581
+ effects.enter(temporary(script.type));
582
+ effects.consume(start);
583
+ return (after) => {
584
+ if (after === script.code) return nok(after);
585
+ const token = effects.exit(temporary(script.type));
586
+ const before = classify(previous);
587
+ const next = classify(after);
588
+ token._open = !next || next === 2 && Boolean(before);
589
+ token._close = !before || before === 2 && Boolean(next);
590
+ return ok(after);
591
+ };
592
+ };
593
+ }
594
+ /**
595
+ * The micromark syntax extension for one of the two markers: `~` (subscript, `subscript` tokens) or `^` (superscript, `superscript` tokens). The constructs of both markers resolve together.
596
+ */
597
+ const scriptSyntax = (marker) => {
598
+ const construct = {
599
+ name: SCRIPTS[marker].type,
600
+ tokenize,
601
+ resolveAll: resolveScripts
602
+ };
603
+ return {
604
+ text: { [SCRIPTS[marker].code]: construct },
605
+ insideSpan: { null: [construct] }
606
+ };
607
+ };
608
+ // src/cut-attributes.ts
609
+ /** Paragraphs recognition rebuilt from lines, as it does where a tab or step header lazily continued them. */
610
+ const rebuiltParagraphs = /* @__PURE__ */ new WeakSet();
611
+ /** Joins `added` into `properties`: classes accumulate, other attributes replace. */
612
+ const mergeProperties = (properties, added) => {
613
+ const merged = { ...properties };
614
+ for (const [key, value] of Object.entries(added)) {
615
+ const existing = merged[key];
616
+ merged[key] = key === "className" && Array.isArray(existing) && Array.isArray(value) ? [...existing, ...value] : value;
617
+ }
618
+ return merged;
619
+ };
620
+ const listLoose = (list) => list?.type !== "list" || Boolean(list.spread) || list.children.some((item) => item.spread ?? item.children.length > 1);
621
+ /**
622
+ * Places the attribute block now ending each paragraph recognition rebuilt (one a header was cut from), as the parser places one: a block on a line of its own goes on the block it's written after, at the level its `>` markers and indentation put it (the paragraph, its list item in a tight list, or the outermost blockquote or list item it's written outside of, a list item giving its list); a block after text goes on the paragraph, or its list item from the item's first paragraph.
623
+ */
624
+ const placeCutAttributes = (root, sourceLines, read) => {
625
+ const parents = /* @__PURE__ */ new Map();
626
+ const cut = [];
627
+ const items = [];
628
+ const walk = (node) => {
629
+ if (node.type === "paragraph" && rebuiltParagraphs.has(node)) cut.push(node);
630
+ const data = node.data;
631
+ if (typeof data === "object" && data !== null && "attrsRole" in data && data.attrsRole === "containerItem" && "attrsTitle" in data && Array.isArray(data.attrsTitle)) items.push(node);
632
+ if (!("children" in node)) return;
633
+ for (const child of node.children) {
634
+ parents.set(child, node);
635
+ walk(child);
636
+ }
637
+ };
638
+ walk(root);
639
+ for (const para of cut) {
640
+ const children = para.children;
641
+ const last = children.at(-1);
642
+ if (last?.type !== "text") continue;
643
+ const value = last.value.trimEnd();
644
+ const lineStart = value.lastIndexOf("\n");
645
+ const line = value.slice(lineStart + 1);
646
+ const open = line.lastIndexOf("{");
647
+ if (open === -1) continue;
648
+ const properties = read(line.slice(open));
649
+ if (!properties) continue;
650
+ const before = line.slice(0, open);
651
+ const ownLine = before.trim() === "" && (lineStart !== -1 || children.at(-2)?.type === "break");
652
+ if (before === "" && !ownLine) continue;
653
+ const ancestors = [];
654
+ for (let p = parents.get(para); p; p = parents.get(p)) ancestors.unshift(p);
655
+ const parent = ancestors.at(-1);
656
+ let target = para;
657
+ if (ownLine) {
658
+ const end = last.position?.end;
659
+ const written = end ? sourceLines[end.line - 1] ?? "" : "";
660
+ const prefix = /^[ \t>]*/.exec(written)?.[0] ?? "";
661
+ const markers = prefix.split(">").length - 1;
662
+ const outside = end ? ancestors.findIndex((ancestor) => {
663
+ const start = ancestor.position?.start;
664
+ if (ancestor.type === "blockquote" && start) {
665
+ const own = (sourceLines[start.line - 1] ?? "").slice(0, start.column - 1);
666
+ return markers < own.split(">").length;
667
+ }
668
+ const first = ancestor.type === "listItem" ? ancestor.children[0]?.position?.start : void 0;
669
+ return first !== void 0 && prefix.length < first.column - 1;
670
+ }) : -1;
671
+ const container = ancestors[outside];
672
+ if (container) target = container.type === "listItem" ? ancestors[outside - 1] ?? container : container;
673
+ else if (parent?.type === "listItem" && !listLoose(ancestors.at(-2))) target = parent;
674
+ } else if (parent?.type === "listItem" && parent.children[0] === para) target = parent;
675
+ const data = target.data;
676
+ target.data = {
677
+ ...data,
678
+ hProperties: mergeProperties(data?.hProperties, properties)
679
+ };
680
+ const kept = ownLine ? value.slice(0, Math.max(lineStart, 0)) : value.slice(0, lineStart + 1) + before;
681
+ if (kept.trimEnd() === "") children.pop();
682
+ else last.value = kept.trimEnd();
683
+ }
684
+ for (const item of items) {
685
+ const title = item.data.attrsTitle;
686
+ const last = title.at(-1);
687
+ if (last?.type !== "text") continue;
688
+ const value = last.value.trimEnd();
689
+ const open = value.lastIndexOf("{");
690
+ if (open === -1) continue;
691
+ const properties = read(value.slice(open));
692
+ if (!properties) continue;
693
+ item.data.hProperties = mergeProperties(item.data.hProperties, properties);
694
+ last.value = value.slice(0, open).trimEnd();
695
+ if (last.value === "" && title.length > 1) title.pop();
696
+ }
697
+ };
698
+ // src/lazy.ts
699
+ /** The `>` markers a source line starts with. */
700
+ const markerCount = (line) => (/^[ \t>]*/.exec(line)?.[0] ?? "").split(">").length - 1;
701
+ /** The `>` markers of a blockquote's own lines: those before it on its first line, and its own (that line can open nested blockquotes too). */
702
+ const blockquoteMarkers = (bq, sourceLines) => {
703
+ const start = bq.position?.start;
704
+ if (!start) return void 0;
705
+ return markerCount((sourceLines[start.line - 1] ?? "").slice(0, start.column - 1)) + 1;
706
+ };
707
+ /**
708
+ * A paragraph's phrasing content split into its lines: text at each line ending, and after a hard break, which stays at the end of its line. With the source, the text on each line keeps its place in it, as remark would give it: from after the line's container prefix to its last non-space character.
709
+ */
710
+ const paraToLines = (para, sourceLines) => {
711
+ const lines = [[]];
712
+ for (const child of para.children) if (child.type === "break") {
713
+ lines.at(-1)?.push(child);
714
+ lines.push([]);
715
+ } else if (child.type === "text") {
716
+ const parts = child.value.split("\n");
717
+ for (const [p, part] of parts.entries()) {
718
+ if (p > 0) lines.push([]);
719
+ if (part === "") continue;
720
+ const text = {
721
+ type: "text",
722
+ value: part
723
+ };
724
+ const position = child.position;
725
+ if (position) {
726
+ const line = position.start.line + p;
727
+ const source = sourceLines?.[line - 1];
728
+ const start = p === 0 ? position.start : source === void 0 ? void 0 : {
729
+ line,
730
+ column: (/^[ \t>]*/.exec(source)?.[0].length ?? 0) + 1
731
+ };
732
+ const end = p === parts.length - 1 ? position.end : source === void 0 ? void 0 : {
733
+ line,
734
+ column: source.trimEnd().length + 1
735
+ };
736
+ if (start && end) text.position = {
737
+ start,
738
+ end
739
+ };
740
+ }
741
+ lines.at(-1)?.push(text);
742
+ }
743
+ } else lines.at(-1)?.push(child);
744
+ return lines;
745
+ };
746
+ /** The attributes a paragraph carries, which a parser that reads attribute blocks before recognition (Sätteri) read off its end: they belong to its last line. */
747
+ const paragraphProperties = (para) => {
748
+ return para.data?.hProperties;
749
+ };
750
+ /** Appends `node` to `children`, merging text into the text before it, as remark keeps a run of text in one node. */
751
+ const pushPhrasing = (children, node) => {
752
+ const last = children.at(-1);
753
+ if (node.type !== "text" || last?.type !== "text") {
754
+ children.push(node);
755
+ return;
756
+ }
757
+ const start = last.position?.start ?? (last.value.trim() === "" ? node.position?.start : void 0);
758
+ const end = node.position?.end ?? last.position?.end;
759
+ const merged = {
760
+ type: "text",
761
+ value: last.value + node.value
762
+ };
763
+ if (start && end) merged.position = {
764
+ start,
765
+ end
766
+ };
767
+ children[children.length - 1] = merged;
768
+ };
769
+ /** A paragraph from lines of phrasing content, or `null` for none. Lines join with a line ending, except after a hard break, which is one. */
770
+ const linesToParagraph = (lines) => {
771
+ const children = [];
772
+ for (const [i, line] of lines.entries()) {
773
+ if (i > 0 && lines[i - 1]?.at(-1)?.type !== "break") pushPhrasing(children, {
774
+ type: "text",
775
+ value: "\n"
776
+ });
777
+ for (const node of line) pushPhrasing(children, node);
778
+ }
779
+ if (children.length === 0) return null;
780
+ const para = {
781
+ type: "paragraph",
782
+ children
783
+ };
784
+ const start = children[0]?.position?.start;
785
+ const end = children.at(-1)?.position?.end;
786
+ if (start && end) para.position = {
787
+ start,
788
+ end
789
+ };
790
+ rebuiltParagraphs.add(para);
791
+ return para;
792
+ };
793
+ /**
794
+ * The blocks lazily continued lines stand for, from their markers relative to the body they belong to (`level` deep): as such lines go on CommonMark only nests deeper, so a line with more markers opens blockquotes and a new paragraph, and a line with fewer continues the paragraph it follows.
795
+ */
796
+ const rebuildLazyLines = (lines, level) => {
797
+ const deeper = lines.findIndex(({ markers }) => markers > level);
798
+ const own = deeper === -1 ? lines : lines.slice(0, deeper);
799
+ const para = linesToParagraph(own.map(({ segs }) => segs));
800
+ const blocks = para ? [para] : [];
801
+ if (deeper !== -1) {
802
+ const quote = {
803
+ type: "blockquote",
804
+ children: rebuildLazyLines(lines.slice(deeper), level + 1)
805
+ };
806
+ blocks.push(quote);
807
+ }
808
+ return blocks;
809
+ };
810
+ /** Blocks whose lines carry one more `>` marker than the block around them: a blockquote (or an alert, Sätteri's node for one) and a tab panel or step body. */
811
+ const MARKED_BLOCKS = /* @__PURE__ */ new Set([
812
+ `blockquote`,
813
+ `alert`,
814
+ `tabsItem`,
815
+ `stepsItem`
816
+ ]);
817
+ /** Blocks a line can lazily continue into by its indentation rather than its markers: a list item or a definition. */
818
+ const INDENTED_BLOCKS = /* @__PURE__ */ new Set([
819
+ `listItem`,
820
+ `defListDescription`,
821
+ `descriptionDetails`
822
+ ]);
823
+ /**
824
+ * A paragraph split before its first line (after its first) that `opens` a group, written at the paragraph's level (`quotes` `>` markers), as markdown-it's block rules interrupt a paragraph; `null` when there's none. Attributes ending the paragraph go with the part they end.
825
+ */
826
+ const splitAtOpeningLine = (para, sourceLines, quotes, opens) => {
827
+ const start = para.position?.start.line;
828
+ if (start === void 0) return null;
829
+ const lines = paraToLines(para, sourceLines);
830
+ const at = lines.findIndex((line, index) => {
831
+ const first = line[0];
832
+ return index > 0 && first?.type === `text` && opens(first.value) && markerCount(sourceLines[start - 1 + index] ?? ``) === quotes;
833
+ });
834
+ if (at === -1) return null;
835
+ const head = linesToParagraph(lines.slice(0, at));
836
+ const tail = linesToParagraph(lines.slice(at));
837
+ if (!head || !tail) return null;
838
+ if (para.data) tail.data = para.data;
839
+ return [head, tail];
840
+ };
841
+ const lastParagraph = (node) => {
842
+ const last = node?.children?.at(-1);
843
+ if (!last) return void 0;
844
+ if (last.type === `paragraph`) return last;
845
+ return lastParagraph(last);
846
+ };
847
+ /**
848
+ * End each group of `listType` (tab groups or steps) where its last panel's `>` markers stop, as markdown-it reads a panel: a line CommonMark lazily continued into the panel's last paragraph, written with fewer markers than the panel's own lines, starts a paragraph after the group, with the lines after it. Groups nested in a panel are ended first, so a line that leaves an outer panel too moves on past its group. The paragraph's attributes (Sätteri reads them off its end) go with its last line.
849
+ */
850
+ const endGroupsAtMarkers = (root, sourceLines, listType) => {
851
+ const visit = (node, quotes) => {
852
+ const children = node.children;
853
+ if (!children) return;
854
+ for (let i = 0; i < children.length; i++) {
855
+ const child = children[i];
856
+ visit(child, quotes + (MARKED_BLOCKS.has(child.type) ? 1 : 0));
857
+ if (child.type !== listType) continue;
858
+ const para = lastParagraph(child.children?.at(-1));
859
+ const start = para?.position?.start.line;
860
+ if (!para || start === void 0) continue;
861
+ const lines = paraToLines(para, sourceLines);
862
+ const outside = lines.findIndex((_, index) => index > 0 && markerCount(sourceLines[start - 1 + index] ?? ``) < quotes + 1);
863
+ if (outside === -1) continue;
864
+ const kept = linesToParagraph(lines.slice(0, outside));
865
+ const moved = linesToParagraph(lines.slice(outside));
866
+ if (!kept || !moved) continue;
867
+ para.children = kept.children;
868
+ const data = para.data;
869
+ if (data?.hProperties) {
870
+ const { hProperties, ...rest } = data;
871
+ moved.data = { hProperties };
872
+ para.data = rest;
873
+ }
874
+ children.splice(i + 1, 0, moved);
875
+ }
876
+ };
877
+ visit(root, 0);
878
+ };
879
+ // src/line-containers.ts
880
+ /**
881
+ * How many of `containers`, outermost first, a line is written in. Tabs count as one column.
882
+ */
883
+ const containersKept = (line, containers) => {
884
+ const prefix = /^[ \t>]*/.exec(line)?.[0] ?? ``;
885
+ const markers = prefix.split(`>`).length - 1;
886
+ let kept = 0;
887
+ for (const container of containers) {
888
+ if (container.kind === `quote` ? markers < container.markers : prefix.length < container.column) break;
889
+ kept++;
890
+ }
891
+ return kept;
892
+ };
893
+ /**
894
+ * The columns the content of the list items opened on a line starts at, in the order they open, as CommonMark reads a list marker: up to four spaces after it belong to the marker, more start indented content one column after.
895
+ */
896
+ const itemColumns = (line) => {
897
+ const columns = [];
898
+ let pos = 0;
899
+ for (;;) {
900
+ while (line[pos] === ` ` || line[pos] === `\t`) pos++;
901
+ if (line[pos] === `>`) {
902
+ pos++;
903
+ continue;
904
+ }
905
+ const marker = /^(?:[-+*]|\d{1,9}[.)])(?=[ \t]|$)/.exec(line.slice(pos));
906
+ if (!marker) return columns;
907
+ const end = pos + marker[0].length;
908
+ let spaces = 0;
909
+ while (line[end + spaces] === ` ` || line[end + spaces] === `\t`) spaces++;
910
+ const column = end + spaces === line.length || spaces > 4 ? end + 1 : end + spaces;
911
+ columns.push(column);
912
+ if (spaces > 4) return columns;
913
+ pos = column;
914
+ }
915
+ };
916
+ // src/caption.ts
917
+ /** The class of the `<figure>` a caption wraps its block in. */
918
+ const CAPTION_CLASS = "markdown-caption";
919
+ /** Whether a source line is a caption line: `~:`, whitespace and the caption's first word after its `>` markers and indentation. An escaped `\~:` isn't one, nor is a marker with nothing after it. */
920
+ const isCaptionLine = (line) => /^~:[ \t]+\S/.test(line.replace(/^[ \t>]*/, ``));
921
+ /** Whether text starts with a caption marker, as a parser gives a caption line's text (inline markdown may follow in other nodes). */
922
+ const startsCaption = (text) => /^~:(?:[ \t]|$)/.test(text);
923
+ /** The text after a caption marker. */
924
+ const stripCaptionMarker = (text) => text.replace(/^~:[ \t]*/, ``);
925
+ /**
926
+ * Blocks a caption doesn't attach to, by mdast (and custom node) type: a caption after them stays text. Headings, thematic breaks, raw HTML, definitions and front matter, another caption's figure, and the parts of containers that aren't blocks of their own (titles, labels, terms).
927
+ */
928
+ const UNCAPTIONED = /* @__PURE__ */ new Set([
929
+ `heading`,
930
+ `thematicBreak`,
931
+ `html`,
932
+ `definition`,
933
+ `footnoteDefinition`,
934
+ `yaml`,
935
+ `toml`,
936
+ `figure`,
937
+ `alertTitle`,
938
+ `stepsTitle`,
939
+ `tabsLabel`,
940
+ `tabsLabels`,
941
+ `tabsInput`,
942
+ `defListTerm`,
943
+ `descriptionTerm`
944
+ ]);
945
+ /** The properties of a caption's `<figure>`: its class, then the attributes written at the end of the caption (a class joins it). */
946
+ const figureProperties = (author) => {
947
+ const { class: extraClass, className: extraClassName, ...rest } = author ?? {};
948
+ const extra = [extraClass, extraClassName].flat().flatMap((value) => typeof value === `string` ? value.split(` `) : []).filter((value) => value !== ``);
949
+ return {
950
+ className: [CAPTION_CLASS, ...extra],
951
+ ...rest
952
+ };
953
+ };
954
+ const WHITESPACE$1 = /^\s*$/;
955
+ const onlyImages = (children) => children.some((child) => child.type !== `text`) && children.every((child, index) => child.type === `image` || child.type === `text` && (WHITESPACE$1.test(child.value ?? ``) || /^\s*!$/.test(child.value ?? ``) && children[index + 1]?.type === `image`) || child.type === `link` && (child.children ?? []).length > 0 && (child.children ?? []).every((inner) => inner.type === `image`));
956
+ const figure = (content, caption, author) => ({
957
+ type: `figure`,
958
+ data: {
959
+ hName: `figure`,
960
+ hProperties: figureProperties(author)
961
+ },
962
+ children: [...content, {
963
+ type: `figcaption`,
964
+ data: { hName: `figcaption` },
965
+ children: caption
966
+ }]
967
+ });
968
+ const captionContent = (lines) => {
969
+ const first = lines[0]?.[0];
970
+ if (first?.type === `text`) {
971
+ const value = stripCaptionMarker(first.value);
972
+ lines[0] = value ? [{
973
+ ...first,
974
+ value
975
+ }, ...lines[0].slice(1)] : lines[0].slice(1);
976
+ }
977
+ return linesToParagraph(lines)?.children ?? [];
978
+ };
979
+ const takeProperties = (node) => {
980
+ const data = node.data;
981
+ if (!data?.hProperties) return void 0;
982
+ const { hProperties, ...rest } = data;
983
+ node.data = rest;
984
+ return hProperties;
985
+ };
986
+ /**
987
+ * Wrap each captioned block of an mdast tree, with its caption, in a `figure` node (`hName: "figure"`, class `markdown-caption`) whose last child is a `figcaption` node. `source` is the document the tree was parsed from: caption lines are found, and placed, by their source lines.
988
+ */
989
+ const processCaptions = (root, source) => {
990
+ if (!source.includes(`~:`)) return;
991
+ const sourceLines = source.split(/\r?\n/);
992
+ const parents = /* @__PURE__ */ new Map();
993
+ const paragraphs = [];
994
+ const tables = [];
995
+ const walk = (node) => {
996
+ if (node.type === `paragraph`) paragraphs.push(node);
997
+ else if (node.type === `table`) tables.push(node);
998
+ for (const child of node.children ?? []) {
999
+ parents.set(child, node);
1000
+ walk(child);
1001
+ }
1002
+ };
1003
+ walk(root);
1004
+ const replace = (node, replacement) => {
1005
+ const parent = parents.get(node);
1006
+ const siblings = parent?.children;
1007
+ if (!parent || !siblings) return;
1008
+ const index = siblings.indexOf(node);
1009
+ if (index === -1) return;
1010
+ if (replacement) {
1011
+ siblings.splice(index, 1, replacement);
1012
+ parents.set(replacement, parent);
1013
+ parents.set(node, replacement);
1014
+ } else siblings.splice(index, 1);
1015
+ };
1016
+ const captioned = (node) => parents.get(node)?.type === `figure`;
1017
+ const ancestorsOf = (node) => {
1018
+ const ancestors = [];
1019
+ for (let p = parents.get(node); p; p = parents.get(p)) ancestors.unshift(p);
1020
+ return ancestors;
1021
+ };
1022
+ const containersOf = (ancestors) => {
1023
+ const containers = [];
1024
+ const nearest = (index, type) => ancestors.slice(0, index).reverse().find((a) => a.type === type);
1025
+ let quotes = 0;
1026
+ for (const [index, ancestor] of ancestors.entries()) {
1027
+ const quote = (target) => {
1028
+ containers.push({
1029
+ target: target ?? ancestor,
1030
+ line: {
1031
+ kind: `quote`,
1032
+ markers: ++quotes
1033
+ }
1034
+ });
1035
+ };
1036
+ switch (ancestor.type) {
1037
+ case `blockquote`:
1038
+ case `alert`:
1039
+ quote(ancestor);
1040
+ break;
1041
+ case `tabsItem`:
1042
+ case `tabsPanel`:
1043
+ quote(nearest(index, `tabsList`));
1044
+ break;
1045
+ case `stepsItem`:
1046
+ if (!(ancestor.children ?? []).some((c) => c.type === `stepsBody`)) quote(nearest(index, `stepsList`));
1047
+ break;
1048
+ case `stepsBody`:
1049
+ quote(nearest(index, `stepsList`));
1050
+ break;
1051
+ case `listItem`:
1052
+ case `defListDescription`:
1053
+ case `descriptionDetails`: {
1054
+ const column = ancestor.children?.[0]?.position?.start.column;
1055
+ if (column === void 0) return void 0;
1056
+ containers.push({
1057
+ target: ancestors[index - 1] ?? ancestor,
1058
+ line: {
1059
+ kind: `item`,
1060
+ column: column - 1
1061
+ }
1062
+ });
1063
+ break;
1064
+ }
1065
+ }
1066
+ }
1067
+ return containers;
1068
+ };
1069
+ for (const table of tables) {
1070
+ const rows = table.children ?? [];
1071
+ const first = rows.findIndex((row, index) => {
1072
+ const line = row.position?.start.line;
1073
+ const text = row.children?.[0]?.children?.[0];
1074
+ return index > 0 && line !== void 0 && isCaptionLine(sourceLines[line - 1] ?? ``) && text?.type === `text` && startsCaption(text.value ?? ``);
1075
+ });
1076
+ if (first === -1) continue;
1077
+ const captionRows = rows.slice(first);
1078
+ const lastCell = captionRows.at(-1)?.children?.[0];
1079
+ const author = lastCell ? takeProperties(lastCell) : void 0;
1080
+ const caption = captionContent(captionRows.map((row) => row.children?.[0]?.children ?? []));
1081
+ table.children = rows.slice(0, first);
1082
+ replace(table, figure([table], caption, author));
1083
+ }
1084
+ for (const para of paragraphs.reverse()) {
1085
+ const first = para.position?.start.line;
1086
+ if (first === void 0) continue;
1087
+ const lines = paraToLines(para, sourceLines);
1088
+ const parent = parents.get(para);
1089
+ const start = (parent?.type === `alert` || parent?.type === `blockquote` && parent.data?.alertType !== void 0) && isAlertMarkerLine(sourceLines[first - 1] ?? ``) ? first + 1 : first;
1090
+ const isCaptionAt = (index) => {
1091
+ const text = lines[index]?.[0];
1092
+ return isCaptionLine(sourceLines[start + index - 1] ?? ``) && text?.type === `text` && startsCaption(text.value);
1093
+ };
1094
+ if (isCaptionAt(0)) {
1095
+ const siblings = parents.get(para)?.children ?? [];
1096
+ const before = siblings[siblings.indexOf(para) - 1];
1097
+ const above = sourceLines[start - 2];
1098
+ if (!before || UNCAPTIONED.has(before.type) || above === void 0 || above.replace(/^[ \t>]*/, ``) === ``) continue;
1099
+ const author = takeProperties(para);
1100
+ replace(para, null);
1101
+ replace(before, figure([before], captionContent(lines), author));
1102
+ continue;
1103
+ }
1104
+ let containers;
1105
+ const segments = [];
1106
+ for (let index = 1; index < lines.length; index++) {
1107
+ if (!isCaptionAt(index)) continue;
1108
+ containers ??= containersOf(ancestorsOf(para));
1109
+ if (!containers) break;
1110
+ const kept = containersKept(sourceLines[start + index - 1] ?? ``, containers.map((c) => c.line));
1111
+ const previous = segments.at(-1);
1112
+ if (previous && kept >= previous.kept) continue;
1113
+ const target = containers[kept]?.target ?? para;
1114
+ if (captioned(target)) continue;
1115
+ segments.push({
1116
+ index,
1117
+ kept,
1118
+ target
1119
+ });
1120
+ }
1121
+ if (segments.length === 0) continue;
1122
+ const head = lines.slice(0, segments[0].index);
1123
+ const lastLine = head.at(-1);
1124
+ if (lastLine?.at(-1)?.type === `break`) head[head.length - 1] = lastLine.slice(0, -1);
1125
+ para.children = linesToParagraph(head)?.children ?? [];
1126
+ const author = takeProperties(para);
1127
+ for (const [i, segment] of segments.entries()) {
1128
+ const caption = captionContent(lines.slice(segment.index, segments[i + 1]?.index));
1129
+ const properties = i === segments.length - 1 ? author : void 0;
1130
+ const { target } = segment;
1131
+ if (target === para && onlyImages(para.children ?? [])) replace(para, figure(para.children ?? [], caption, properties));
1132
+ else replace(target, figure([target], caption, properties));
1133
+ }
1134
+ }
1135
+ };
1136
+ // src/hast.ts
1137
+ const WHITESPACE = /^[ \t\n\f\r]*$/;
1138
+ const isWhitespaceText = (node) => node.type === `text` && WHITESPACE.test(node.value ?? ``);
1139
+ /** A `<p>` with no children, or only whitespace text: squeeze-paragraphs removes it. */
1140
+ const isEmptyParagraph = (node) => node.tagName === `p` && (node.children ?? []).every(isWhitespaceText);
1141
+ /**
1142
+ * A `<p>` whose content is only images (each possibly inside a link or a lightbox button) and whitespace: unwrap-images replaces it with its children.
1143
+ */
1144
+ const isImageParagraph = (node) => {
1145
+ const scan = (parent, inLink) => {
1146
+ let result = `empty`;
1147
+ for (const child of parent.children ?? []) {
1148
+ if (isWhitespaceText(child)) continue;
1149
+ if (child.type !== `element`) return `other`;
1150
+ if (child.tagName === `img`) {
1151
+ result = `image`;
1152
+ continue;
1153
+ }
1154
+ if (!inLink && (child.tagName === `a` || child.tagName === `button`)) {
1155
+ const inner = scan(child, true);
1156
+ if (inner === `other`) return `other`;
1157
+ if (inner === `image`) result = `image`;
1158
+ continue;
1159
+ }
1160
+ return `other`;
1161
+ }
1162
+ return result;
1163
+ };
1164
+ return node.tagName === `p` && scan(node, false) === `image`;
1165
+ };
1166
+ /** The rank of an `h1`–`h6` element, or `undefined`. */
1167
+ const headingRank = (node) => {
1168
+ if (node.type !== `element`) return void 0;
1169
+ const match = /^h([1-6])$/i.exec(node.tagName ?? ``);
1170
+ return match ? Number(match[1]) : void 0;
1171
+ };
1172
+ /**
1173
+ * Wrap heading-delimited runs of `nodes` into `<section data-depth>` elements, nesting deeper headings inside shallower ones. Content before the first heading stays unwrapped.
1174
+ */
1175
+ const sectionize = (nodes, section, floor = 0) => {
1176
+ let minRank = Infinity;
1177
+ for (const node of nodes) {
1178
+ const rank = headingRank(node);
1179
+ if (rank !== void 0 && rank > floor && rank < minRank) minRank = rank;
1180
+ }
1181
+ if (minRank === Infinity) return [...nodes];
1182
+ const result = [];
1183
+ let i = 0;
1184
+ for (; i < nodes.length; i++) {
1185
+ const node = nodes[i];
1186
+ if (!node || headingRank(node) === minRank) break;
1187
+ result.push(node);
1188
+ }
1189
+ while (i < nodes.length) {
1190
+ const start = i;
1191
+ for (i++; i < nodes.length; i++) {
1192
+ const node = nodes[i];
1193
+ if (!node || headingRank(node) === minRank) break;
1194
+ }
1195
+ result.push(section(minRank, sectionize(nodes.slice(start, i), section, minRank)));
1196
+ }
1197
+ return result;
1198
+ };
1199
+ /**
1200
+ * The lightbox opt-in: an image directly after a text run that ends in a `!` at a boundary (start of the run or after whitespace), which is what `!![alt](src)` leaves behind.
1201
+ */
1202
+ const LIGHTBOX_MARKER = /(?:^|\s)!$/;
1203
+ /**
1204
+ * A short, stable lightbox id from an image src (FNV-1a, base36): the same image always gets the same id, so repeated references share one dialog.
1205
+ */
1206
+ const lightboxId = (src) => {
1207
+ let hash = 2166136261;
1208
+ for (let i = 0; i < src.length; i += 1) {
1209
+ hash ^= src.charCodeAt(i);
1210
+ hash = Math.imul(hash, 16777619);
1211
+ }
1212
+ return `markdown-lightbox-${(hash >>> 0).toString(36)}`;
1213
+ };
1214
+ /** The button that replaces an opted-in image and opens its dialog. */
1215
+ const lightboxTrigger = (id, image) => ({
1216
+ type: `element`,
1217
+ tagName: `button`,
1218
+ properties: {
1219
+ type: `button`,
1220
+ className: [`markdown-lightbox-trigger`],
1221
+ command: `show-modal`,
1222
+ commandfor: id
1223
+ },
1224
+ children: [image]
1225
+ });
1226
+ /** The modal dialog, appended at the document end, showing the image full size, and its caption under it when one is given. */
1227
+ const lightboxDialog = (id, image, caption) => ({
1228
+ type: `element`,
1229
+ tagName: `dialog`,
1230
+ properties: {
1231
+ id,
1232
+ className: [`markdown-lightbox`],
1233
+ closedby: `any`
1234
+ },
1235
+ children: [{
1236
+ type: `element`,
1237
+ tagName: `button`,
1238
+ properties: {
1239
+ type: `button`,
1240
+ className: [`markdown-lightbox-close`],
1241
+ command: `close`,
1242
+ commandfor: id,
1243
+ ariaLabel: `Close image`
1244
+ },
1245
+ children: [image]
1246
+ }, ...caption ? [{
1247
+ type: `element`,
1248
+ tagName: `p`,
1249
+ properties: { className: [`markdown-lightbox-caption`] },
1250
+ children: caption
1251
+ }] : []]
1252
+ });
1253
+ /** Whether a node is a caption's figure (`<figure class="markdown-caption">`, as the caption plugins make): an image that is its content shows the figure's caption in its lightbox dialog, with the `caption` option. */
1254
+ const isCaptionFigure = (node) => {
1255
+ const className = node?.properties?.className;
1256
+ return node?.tagName === `figure` && Array.isArray(className) && className.includes("markdown-caption");
1257
+ };
1258
+ // src/abbr.ts
1259
+ const DEFINITION = /^\*\[([^[\]]+)\]:\s*(.+)$/;
1260
+ const REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/g;
1261
+ /**
1262
+ * The `[term, title]` pairs a paragraph's text defines, when every line of it is a `*[term]: title` definition; otherwise `undefined` (the paragraph is ordinary text).
1263
+ */
1264
+ const parseAbbreviations = (text) => {
1265
+ const definitions = [];
1266
+ for (const line of text.split(`\n`)) {
1267
+ const match = DEFINITION.exec(line);
1268
+ if (!match) return void 0;
1269
+ definitions.push([match[1] ?? ``, (match[2] ?? ``).trim()]);
1270
+ }
1271
+ return definitions;
1272
+ };
1273
+ /**
1274
+ * Build the scan for a document's abbreviations. Terms match on word boundaries, longest first; the scan returns the pieces of a text value, or `undefined` when it holds no term.
1275
+ */
1276
+ const createAbbreviationScan = (abbreviations) => {
1277
+ const terms = [...abbreviations.keys()].sort((a, b) => b.length - a.length).map((term) => term.replace(REGEX_SPECIAL, `\\$&`));
1278
+ const pattern = new RegExp(`\\b(${terms.join(`|`)})\\b`, `g`);
1279
+ return (value) => {
1280
+ const parts = [];
1281
+ let last = 0;
1282
+ for (const match of value.matchAll(pattern)) {
1283
+ const term = match[1] ?? ``;
1284
+ if (match.index > last) parts.push({ text: value.slice(last, match.index) });
1285
+ parts.push({
1286
+ term,
1287
+ title: abbreviations.get(term) ?? ``
1288
+ });
1289
+ last = match.index + term.length;
1290
+ }
1291
+ if (parts.length === 0) return void 0;
1292
+ if (last < value.length) parts.push({ text: value.slice(last) });
1293
+ return parts;
1294
+ };
1295
+ };
1296
+ // src/steps.ts
1297
+ const STEP_HEADER_RE = /^(@{1,6})(\d+)\. ?(.*)/s;
1298
+ const parseHeaderPrefix = (text) => {
1299
+ const m = STEP_HEADER_RE.exec(text);
1300
+ if (!m) return null;
1301
+ const [, hashes = "", digits = "", rest = ""] = m;
1302
+ return {
1303
+ depth: hashes.length,
1304
+ number: parseInt(digits, 10),
1305
+ rest
1306
+ };
1307
+ };
1308
+ const extractHeaderPara$1 = (para, counters, requireDepthOne = true, prevDepth = 0) => {
1309
+ const lines = paraToLines(para);
1310
+ const steps = [];
1311
+ let currentDepth = prevDepth;
1312
+ const stop = (index) => steps.length > 0 ? {
1313
+ steps,
1314
+ rest: lines.slice(index),
1315
+ restLine: (para.position?.start.line ?? 1) + index
1316
+ } : null;
1317
+ for (const [index, lineSegs] of lines.entries()) {
1318
+ const first = lineSegs[0];
1319
+ if (first?.type !== "text") return stop(index);
1320
+ const header = parseHeaderPrefix(first.value);
1321
+ if (!header) return stop(index);
1322
+ const { depth } = header;
1323
+ if (steps.length === 0 && requireDepthOne && depth !== 1) return null;
1324
+ if (depth > currentDepth + 1) return stop(index);
1325
+ for (const k of counters.keys()) if (k > depth) counters.delete(k);
1326
+ const number = (counters.get(depth) ?? 0) + 1;
1327
+ counters.set(depth, number);
1328
+ currentDepth = depth;
1329
+ const titleNodes = header.rest ? [{
1330
+ type: "text",
1331
+ value: header.rest
1332
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1333
+ steps.push({
1334
+ depth,
1335
+ number,
1336
+ titleNodes,
1337
+ body: [],
1338
+ ...index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1339
+ });
1340
+ }
1341
+ return stop(lines.length);
1342
+ };
1343
+ const splitAtGroupStarts$1 = (lines, firstLine) => {
1344
+ const paragraphs = [];
1345
+ let start = 0;
1346
+ const flush = (end) => {
1347
+ const para = linesToParagraph(lines.slice(start, end));
1348
+ if (!para) return;
1349
+ para.position = {
1350
+ start: {
1351
+ line: firstLine + start,
1352
+ column: 1
1353
+ },
1354
+ end: {
1355
+ line: firstLine + end - 1,
1356
+ column: 1
1357
+ }
1358
+ };
1359
+ paragraphs.push(para);
1360
+ };
1361
+ lines.forEach((line, index) => {
1362
+ const first = line[0];
1363
+ if (first?.type === "text" && parseHeaderPrefix(first.value)?.depth === 1 && index > start) {
1364
+ flush(index);
1365
+ start = index;
1366
+ }
1367
+ });
1368
+ flush(lines.length);
1369
+ return paragraphs;
1370
+ };
1371
+ const takeLazyLines = (node, sourceLines, base, isHeader) => {
1372
+ let parent = node;
1373
+ let last = parent.children?.at(-1);
1374
+ while (last?.type !== "paragraph") {
1375
+ if (!last?.children?.length) return null;
1376
+ parent = last;
1377
+ last = parent.children?.at(-1);
1378
+ }
1379
+ const para = last;
1380
+ const start = para.position?.start.line;
1381
+ if (start === void 0 || !parent.children) return null;
1382
+ const lines = paraToLines(para, sourceLines).map((segs, index) => ({
1383
+ segs,
1384
+ markers: markerCount(sourceLines[start - 1 + index] ?? "")
1385
+ }));
1386
+ const at = lines.findIndex(({ segs, markers }, index) => index > 0 && markers < base && isHeader(segs));
1387
+ if (at === -1) return null;
1388
+ const kept = linesToParagraph(lines.slice(0, at).map(({ segs }) => segs));
1389
+ parent.children.splice(-1, 1, ...kept ? [kept] : []);
1390
+ return lines.slice(at);
1391
+ };
1392
+ const splitBlockquote$1 = (bq, counters, currentDepth, sourceLines) => {
1393
+ const segments = [];
1394
+ let currentSegment = {
1395
+ header: null,
1396
+ body: []
1397
+ };
1398
+ const base = sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1399
+ const stepHeader = (lineSegs) => {
1400
+ const first = lineSegs[0];
1401
+ if (first?.type !== "text") return null;
1402
+ const header = parseHeaderPrefix(first.value);
1403
+ return header && header.depth <= currentDepth + 1 ? header : null;
1404
+ };
1405
+ const startStep = (header, lineSegs, hProperties) => {
1406
+ segments.push(currentSegment);
1407
+ for (const k of counters.keys()) if (k > header.depth) counters.delete(k);
1408
+ const number = (counters.get(header.depth) ?? 0) + 1;
1409
+ counters.set(header.depth, number);
1410
+ currentDepth = header.depth;
1411
+ const titleNodes = header.rest ? [{
1412
+ type: "text",
1413
+ value: header.rest
1414
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1415
+ currentSegment = {
1416
+ header: {
1417
+ depth: header.depth,
1418
+ number,
1419
+ titleNodes,
1420
+ hProperties
1421
+ },
1422
+ body: []
1423
+ };
1424
+ };
1425
+ for (const child of bq.children) {
1426
+ if (child.type !== "paragraph") {
1427
+ currentSegment.body.push(child);
1428
+ if (!sourceLines || base === void 0) continue;
1429
+ const lazy = takeLazyLines(child, sourceLines, base, (segs) => stepHeader(segs) !== null);
1430
+ if (!lazy) continue;
1431
+ let pending = [];
1432
+ const flush = () => {
1433
+ const relative = pending.map((line) => ({
1434
+ ...line,
1435
+ markers: line.markers - base
1436
+ }));
1437
+ currentSegment.body.push(...rebuildLazyLines(relative, 0));
1438
+ pending = [];
1439
+ };
1440
+ for (const line of lazy) {
1441
+ const header = line.markers < base ? stepHeader(line.segs) : null;
1442
+ if (header) {
1443
+ flush();
1444
+ startStep(header, line.segs);
1445
+ } else pending.push(line);
1446
+ }
1447
+ flush();
1448
+ continue;
1449
+ }
1450
+ let bodyLines = [];
1451
+ const lines = paraToLines(child, sourceLines);
1452
+ const first = child.position?.start.line;
1453
+ const outsideBody = (index) => !sourceLines || base === void 0 || first === void 0 || markerCount(sourceLines[first - 1 + index] ?? "") < base;
1454
+ for (const [index, lineSegs] of lines.entries()) {
1455
+ const header = outsideBody(index) ? stepHeader(lineSegs) : null;
1456
+ if (header) {
1457
+ const bodyPara = linesToParagraph(bodyLines);
1458
+ if (bodyPara) currentSegment.body.push(bodyPara);
1459
+ bodyLines = [];
1460
+ startStep(header, lineSegs, index === lines.length - 1 ? paragraphProperties(child) : void 0);
1461
+ continue;
1462
+ }
1463
+ bodyLines.push(lineSegs);
1464
+ }
1465
+ const bodyPara = linesToParagraph(bodyLines);
1466
+ if (bodyPara && child.data) bodyPara.data = child.data;
1467
+ if (bodyPara) currentSegment.body.push(bodyPara);
1468
+ }
1469
+ segments.push(currentSegment);
1470
+ return segments;
1471
+ };
1472
+ const buildTree$1 = (steps, targetDepth, start, end) => {
1473
+ const items = [];
1474
+ let i = start;
1475
+ while (i < end) {
1476
+ const s = steps[i];
1477
+ if (s?.depth !== targetDepth) {
1478
+ i++;
1479
+ continue;
1480
+ }
1481
+ let childEnd = i + 1;
1482
+ while (childEnd < end && (steps[childEnd]?.depth ?? 0) > targetDepth) childEnd++;
1483
+ const bodyChildren = [{
1484
+ type: "stepsTitle",
1485
+ children: s.titleNodes
1486
+ }, ...s.body];
1487
+ if (steps.slice(i + 1, childEnd).some((ss) => ss.depth === targetDepth + 1)) {
1488
+ const nested = buildTree$1(steps, targetDepth + 1, i + 1, childEnd);
1489
+ bodyChildren.push(nested);
1490
+ }
1491
+ items.push({
1492
+ type: "stepsItem",
1493
+ depth: targetDepth,
1494
+ number: s.number,
1495
+ children: bodyChildren,
1496
+ data: {
1497
+ attrsRole: "containerItem",
1498
+ attrsTitle: s.titleNodes,
1499
+ ...s.hProperties ? { hProperties: { ...s.hProperties } } : {}
1500
+ }
1501
+ });
1502
+ i = childEnd;
1503
+ }
1504
+ return {
1505
+ type: "stepsList",
1506
+ depth: targetDepth,
1507
+ children: items,
1508
+ data: { attrsRole: "container" }
1509
+ };
1510
+ };
1511
+ const processStepsInChildren = (children, containerClass, sourceLines, quotes = 0, parentType = "root") => {
1512
+ const newChildren = [];
1513
+ let i = 0;
1514
+ while (i < children.length) {
1515
+ const node = children[i];
1516
+ if (!node) break;
1517
+ if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
1518
+ const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseHeaderPrefix(text)?.depth === 1);
1519
+ if (split) {
1520
+ children.splice(i, 1, ...split);
1521
+ continue;
1522
+ }
1523
+ }
1524
+ if (node.type === "paragraph") {
1525
+ const counters = /* @__PURE__ */ new Map();
1526
+ const headerPara = extractHeaderPara$1(node, counters);
1527
+ if (headerPara) {
1528
+ const rawSteps = [...headerPara.steps];
1529
+ let rest = headerPara.rest;
1530
+ let restLine = headerPara.restLine;
1531
+ let j = i + 1;
1532
+ const lastStep = () => rawSteps[rawSteps.length - 1];
1533
+ let currentStepIdx = rawSteps.length - 1;
1534
+ let currentDepth = lastStep().depth;
1535
+ while (rest.length === 0 && j < children.length) {
1536
+ const sibling = children[j];
1537
+ if (!sibling) break;
1538
+ if (sibling.type === "blockquote") {
1539
+ const line = sibling.position?.start.line;
1540
+ if (sourceLines && line !== void 0 && isAlertMarkerLine(sourceLines[line - 1] ?? "")) break;
1541
+ const segments = splitBlockquote$1(sibling, counters, currentDepth, sourceLines);
1542
+ const firstSeg = segments[0];
1543
+ if (firstSeg?.header === null) rawSteps[currentStepIdx]?.body.push(...firstSeg.body);
1544
+ for (let s = 1; s < segments.length; s++) {
1545
+ const seg = segments[s];
1546
+ if (seg?.header) {
1547
+ rawSteps.push({
1548
+ ...seg.header,
1549
+ body: seg.body
1550
+ });
1551
+ currentStepIdx = rawSteps.length - 1;
1552
+ currentDepth = seg.header.depth;
1553
+ }
1554
+ }
1555
+ j++;
1556
+ } else if (sibling.type === "paragraph") {
1557
+ const prevDepth = lastStep().depth;
1558
+ const contHeaders = extractHeaderPara$1(sibling, counters, false, prevDepth);
1559
+ if (contHeaders) {
1560
+ rawSteps.push(...contHeaders.steps);
1561
+ rest = contHeaders.rest;
1562
+ restLine = contHeaders.restLine;
1563
+ currentStepIdx = rawSteps.length - 1;
1564
+ currentDepth = lastStep().depth;
1565
+ j++;
1566
+ continue;
1567
+ }
1568
+ break;
1569
+ } else break;
1570
+ }
1571
+ const list = buildTree$1(rawSteps, 1, 0, rawSteps.length);
1572
+ list.containerClass = containerClass;
1573
+ if (parentType === "stepsItem") list.inBody = true;
1574
+ newChildren.push(list);
1575
+ children.splice(j, 0, ...splitAtGroupStarts$1(rest, restLine));
1576
+ i = j;
1577
+ continue;
1578
+ }
1579
+ }
1580
+ newChildren.push(node);
1581
+ i++;
1582
+ }
1583
+ children.splice(0, children.length, ...newChildren);
1584
+ };
1585
+ /**
1586
+ * Turn every step sequence in `root` into `stepsList` nodes, at the top level and inside any container (blockquotes, list items, tab panels…), walking into the lists it builds so nested sequences are found too. With the document's `source`, a step header that CommonMark lazily continued into a nested paragraph is recognized too, as markdown-it reads it.
1587
+ */
1588
+ const processSteps = (root, containerClass, source) => {
1589
+ const sourceLines = source?.split(/\r?\n/);
1590
+ const walk = (node, quotes) => {
1591
+ if (!Array.isArray(node.children) || node.children.length === 0) return;
1592
+ const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
1593
+ processStepsInChildren(node.children, containerClass, sourceLines, inner, node.type);
1594
+ for (const child of node.children) walk(child, inner);
1595
+ };
1596
+ processStepsInChildren(root.children, containerClass, sourceLines);
1597
+ for (const child of root.children) walk(child, 0);
1598
+ if (sourceLines) endGroupsAtMarkers(root, sourceLines, `stepsList`);
1599
+ };
1600
+ // src/tabs.ts
1601
+ const TAB_HEADER_RE = /^(%{1,6})(\+)? (.*)/;
1602
+ const parseTabHeader = (line) => {
1603
+ const m = TAB_HEADER_RE.exec(line);
1604
+ if (!m) return null;
1605
+ const [, percents = "", plus, label = ""] = m;
1606
+ return {
1607
+ depth: percents.length,
1608
+ open: plus === "+",
1609
+ label
1610
+ };
1611
+ };
1612
+ const djb2 = (str) => {
1613
+ let h = 5381;
1614
+ for (let i = 0; i < str.length; i++) h = (h << 5) + h + str.charCodeAt(i) >>> 0;
1615
+ return h;
1616
+ };
1617
+ /** Strip a trailing attrs block like `{.class #id}` from plain label text, so attrs-consumed markers don't leak into `aria-label`. */
1618
+ const stripAttrs = (text) => text.replace(/\s*\{[^}]*\}\s*$/, "").trimEnd();
1619
+ const slugify = (text) => text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
1620
+ const groupId = (labels) => {
1621
+ const key = [...labels].map(slugify).sort().join("-");
1622
+ return djb2(key).toString(16);
1623
+ };
1624
+ /**
1625
+ * A group's block id, unique on the page even when label sets match: its group id with the group's index among the document's tab groups, counted in the order they open (each nested group after the group around it), so every engine counts the same groups the same way.
1626
+ */
1627
+ const tabsBlockId = (index, groupId) => "tabs-" + djb2(`${index}:${groupId}`).toString(16);
1628
+ /** The plain text of a tab label (text and inline code; other markup contributes its text, images nothing), for its group id and `aria-label`. */
1629
+ const tabLabelText = (nodes) => nodes.map((n) => {
1630
+ if (n.type === "text" || n.type === "inlineCode") return "value" in n ? n.value : "";
1631
+ if ("children" in n) return tabLabelText(n.children);
1632
+ return "";
1633
+ }).join("");
1634
+ const extractHeaderPara = (para, requireDepthOne = true, prevDepth = 0) => {
1635
+ const lines = paraToLines(para);
1636
+ const tabs = [];
1637
+ let currentDepth = prevDepth;
1638
+ for (const [index, lineSegs] of lines.entries()) {
1639
+ const first = lineSegs[0];
1640
+ if (first?.type !== "text") return null;
1641
+ const header = parseTabHeader(first.value);
1642
+ if (!header) return null;
1643
+ const { depth, open } = header;
1644
+ if (tabs.length === 0 && requireDepthOne && depth !== 1) return null;
1645
+ if (depth > currentDepth + 1) return tabs.length > 0 ? {
1646
+ tabs,
1647
+ rest: lines.slice(index),
1648
+ restLine: (para.position?.start.line ?? 1) + index
1649
+ } : null;
1650
+ currentDepth = depth;
1651
+ const labelText = header.label;
1652
+ const labelNodes = labelText ? [{
1653
+ type: "text",
1654
+ value: labelText
1655
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1656
+ tabs.push({
1657
+ depth,
1658
+ open,
1659
+ labelNodes,
1660
+ labelText: tabLabelText(labelNodes),
1661
+ body: [],
1662
+ ...index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1663
+ });
1664
+ }
1665
+ return tabs.length > 0 ? {
1666
+ tabs,
1667
+ rest: [],
1668
+ restLine: 0
1669
+ } : null;
1670
+ };
1671
+ const splitAtGroupStarts = (lines, firstLine) => {
1672
+ const paragraphs = [];
1673
+ let start = 0;
1674
+ const flush = (end) => {
1675
+ const para = linesToParagraph(lines.slice(start, end));
1676
+ if (!para) return;
1677
+ para.position = {
1678
+ start: {
1679
+ line: firstLine + start,
1680
+ column: 1
1681
+ },
1682
+ end: {
1683
+ line: firstLine + end - 1,
1684
+ column: 1
1685
+ }
1686
+ };
1687
+ paragraphs.push(para);
1688
+ };
1689
+ lines.forEach((line, index) => {
1690
+ const first = line[0];
1691
+ if (first?.type === "text" && parseTabHeader(first.value)?.depth === 1 && index > start) {
1692
+ flush(index);
1693
+ start = index;
1694
+ }
1695
+ });
1696
+ flush(lines.length);
1697
+ return paragraphs;
1698
+ };
1699
+ const isExplicitBQ = (bq) => bq.children.some((child) => {
1700
+ if (child.type !== "paragraph") return false;
1701
+ const first = child.children[0];
1702
+ return first?.type === "text" && parseTabHeader(first.value.split("\n")[0] ?? "") !== null;
1703
+ });
1704
+ const extractTrailingTabHeader = (node, sourceLines) => {
1705
+ const path = [];
1706
+ let current = node;
1707
+ while ("children" in current && current.children.length > 0) {
1708
+ const last = current.children[current.children.length - 1];
1709
+ if (last.type === "paragraph") {
1710
+ path.push(current);
1711
+ const lines = paraToLines(last, sourceLines);
1712
+ const at = lines.findIndex((line) => line[0]?.type === "text" && parseTabHeader(line[0].value));
1713
+ if (at === -1) return null;
1714
+ const headerLine = lines[at] ?? [];
1715
+ const first = headerLine[0];
1716
+ const header = first?.type === "text" ? parseTabHeader(first.value) : null;
1717
+ if (!header) return null;
1718
+ const labelText = header.label;
1719
+ const labelNodes = labelText ? [{
1720
+ type: "text",
1721
+ value: labelText
1722
+ }, ...headerLine.slice(1)] : headerLine.slice(1);
1723
+ return {
1724
+ header: {
1725
+ depth: header.depth,
1726
+ open: header.open,
1727
+ labelNodes,
1728
+ labelText: tabLabelText(labelNodes),
1729
+ body: [],
1730
+ ...at === lines.length - 1 ? { hProperties: paragraphProperties(last) } : {}
1731
+ },
1732
+ truncatedPara: linesToParagraph(lines.slice(0, at)),
1733
+ rest: lines.slice(at + 1),
1734
+ path,
1735
+ start: last.position?.start.line,
1736
+ at,
1737
+ line: headerLine
1738
+ };
1739
+ }
1740
+ if (!("children" in last)) break;
1741
+ path.push(current);
1742
+ current = last;
1743
+ }
1744
+ return null;
1745
+ };
1746
+ const splitBlockquote = (bq, explicit, sourceLines) => {
1747
+ const segments = [];
1748
+ let currentSegment = {
1749
+ header: null,
1750
+ body: [],
1751
+ explicit,
1752
+ lines: []
1753
+ };
1754
+ const base = sourceLines && !explicit ? blockquoteMarkers(bq, sourceLines) : void 0;
1755
+ const splitLines = (lines, markersOf, para) => {
1756
+ let bodyLines = [];
1757
+ for (const [index, lineSegs] of lines.entries()) {
1758
+ const first = lineSegs[0];
1759
+ const outsidePanel = !markersOf || base === void 0 || markersOf(index) < base;
1760
+ if (first?.type === "text" && outsidePanel) {
1761
+ const header = parseTabHeader(first.value);
1762
+ if (header) {
1763
+ if (bodyLines.length > 0) {
1764
+ const bodyPara = linesToParagraph(bodyLines);
1765
+ if (bodyPara) currentSegment.body.push(bodyPara);
1766
+ bodyLines = [];
1767
+ }
1768
+ segments.push(currentSegment);
1769
+ const labelText = header.label;
1770
+ const labelNodes = labelText ? [{
1771
+ type: "text",
1772
+ value: labelText
1773
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1774
+ currentSegment = {
1775
+ header: {
1776
+ depth: header.depth,
1777
+ open: header.open,
1778
+ labelNodes,
1779
+ labelText: tabLabelText(labelNodes),
1780
+ body: [],
1781
+ ...para && index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1782
+ },
1783
+ body: [],
1784
+ explicit,
1785
+ lines: [],
1786
+ line: lineSegs,
1787
+ ...markersOf ? { markers: markersOf(index) } : {}
1788
+ };
1789
+ continue;
1790
+ }
1791
+ }
1792
+ bodyLines.push(lineSegs);
1793
+ if (markersOf) currentSegment.lines.push({
1794
+ segs: lineSegs,
1795
+ markers: markersOf(index)
1796
+ });
1797
+ }
1798
+ if (bodyLines.length > 0) {
1799
+ const bodyPara = linesToParagraph(bodyLines);
1800
+ if (bodyPara && para?.data) bodyPara.data = para.data;
1801
+ if (bodyPara) currentSegment.body.push(bodyPara);
1802
+ }
1803
+ };
1804
+ for (const child of bq.children) {
1805
+ if (child.type !== "paragraph") {
1806
+ const extracted = extractTrailingTabHeader(child, sourceLines);
1807
+ if (extracted) {
1808
+ const cloned = JSON.parse(JSON.stringify(child));
1809
+ let target = cloned;
1810
+ for (let pi = 1; pi < extracted.path.length; pi++) target = target.children[target.children.length - 1];
1811
+ const deepestParent = target;
1812
+ if (extracted.truncatedPara) deepestParent.children[deepestParent.children.length - 1] = extracted.truncatedPara;
1813
+ else deepestParent.children.splice(deepestParent.children.length - 1, 1);
1814
+ currentSegment.body.push(cloned);
1815
+ segments.push(currentSegment);
1816
+ const { start, at } = extracted;
1817
+ const markersOf = sourceLines && start !== void 0 ? (index) => markerCount(sourceLines[start - 1 + index] ?? "") : void 0;
1818
+ currentSegment = {
1819
+ header: extracted.header,
1820
+ body: [],
1821
+ explicit,
1822
+ lines: [],
1823
+ line: extracted.line,
1824
+ ...markersOf ? { markers: markersOf(at) } : {}
1825
+ };
1826
+ splitLines(extracted.rest, markersOf ? (index) => markersOf(at + 1 + index) : void 0);
1827
+ } else currentSegment.body.push(child);
1828
+ continue;
1829
+ }
1830
+ const start = child.position?.start.line;
1831
+ splitLines(paraToLines(child, sourceLines), sourceLines && start !== void 0 ? (index) => markerCount(sourceLines[start - 1 + index] ?? "") : void 0, child);
1832
+ }
1833
+ segments.push(currentSegment);
1834
+ return segments;
1835
+ };
1836
+ const afterSkip = (segments, level, quoted) => {
1837
+ const lines = [];
1838
+ const blocks = [];
1839
+ for (const seg of segments) {
1840
+ if (level === void 0 || seg.markers === void 0) {
1841
+ if (seg.line) lines.push({
1842
+ segs: seg.line,
1843
+ markers: 0
1844
+ });
1845
+ blocks.push(...seg.body);
1846
+ continue;
1847
+ }
1848
+ if (seg.line) lines.push({
1849
+ segs: seg.line,
1850
+ markers: seg.markers - level
1851
+ });
1852
+ lines.push(...seg.lines.map((line) => ({
1853
+ ...line,
1854
+ markers: line.markers - level
1855
+ })));
1856
+ blocks.push(...seg.body.filter((block) => block.type !== "paragraph"));
1857
+ }
1858
+ const content = rebuildLazyLines(lines, 0);
1859
+ if (blocks.length > 0 && !quoted) content.push(...blocks);
1860
+ else if (blocks.length > 0) {
1861
+ const last = content.at(-1);
1862
+ if (last?.type === "blockquote") last.children.push(...blocks);
1863
+ else content.push({
1864
+ type: "blockquote",
1865
+ children: blocks
1866
+ });
1867
+ }
1868
+ return content;
1869
+ };
1870
+ const buildTree = (rawTabs, targetDepth, start, end, containerClass) => {
1871
+ const depthLabels = rawTabs.slice(start, end).filter((t) => t.depth === targetDepth).map((t) => stripAttrs(t.labelText));
1872
+ const gId = groupId(depthLabels);
1873
+ const explicitOpenIdx = rawTabs.slice(start, end).findIndex((t) => t.depth === targetDepth && t.open);
1874
+ const explicitOpenAbsIdx = explicitOpenIdx >= 0 ? start + explicitOpenIdx : -1;
1875
+ const items = [];
1876
+ let tabCounter = 0;
1877
+ let i = start;
1878
+ while (i < end) {
1879
+ const raw = rawTabs[i];
1880
+ if (!raw) break;
1881
+ if (raw.depth !== targetDepth) {
1882
+ i++;
1883
+ continue;
1884
+ }
1885
+ let childEnd = i + 1;
1886
+ while (childEnd < end && (rawTabs[childEnd]?.depth ?? 0) > targetDepth) childEnd++;
1887
+ const isOpen = explicitOpenAbsIdx >= 0 ? i === explicitOpenAbsIdx : tabCounter === 0;
1888
+ const bodyChildren = [{
1889
+ type: "tabsLabel",
1890
+ children: raw.labelNodes
1891
+ }, ...raw.body];
1892
+ if (rawTabs.slice(i + 1, childEnd).some((t) => t.depth === targetDepth + 1)) {
1893
+ const nested = buildTree(rawTabs, targetDepth + 1, i + 1, childEnd, containerClass);
1894
+ nested.containerClass = containerClass;
1895
+ bodyChildren.push(nested);
1896
+ }
1897
+ bodyChildren.push(...raw.after ?? []);
1898
+ items.push({
1899
+ type: "tabsItem",
1900
+ depth: targetDepth,
1901
+ labelText: raw.labelText,
1902
+ open: isOpen,
1903
+ children: bodyChildren,
1904
+ data: {
1905
+ attrsRole: "containerItem",
1906
+ attrsTitle: raw.labelNodes,
1907
+ attrsItemTitle: true,
1908
+ hProperties: { ...raw.hProperties }
1909
+ }
1910
+ });
1911
+ tabCounter++;
1912
+ i = childEnd;
1913
+ }
1914
+ return {
1915
+ type: "tabsList",
1916
+ depth: targetDepth,
1917
+ groupId: gId,
1918
+ children: items,
1919
+ data: {
1920
+ attrsRole: "container",
1921
+ hProperties: {}
1922
+ }
1923
+ };
1924
+ };
1925
+ const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines, quotes = 0, parentType = "root") => {
1926
+ const newChildren = [];
1927
+ let i = 0;
1928
+ while (i < children.length) {
1929
+ const node = children[i];
1930
+ if (!node) break;
1931
+ if (node.type === "paragraph" && sourceLines && !INDENTED_BLOCKS.has(parentType)) {
1932
+ const split = splitAtOpeningLine(node, sourceLines, quotes, (text) => parseTabHeader(text)?.depth === 1);
1933
+ if (split) {
1934
+ children.splice(i, 1, ...split);
1935
+ continue;
1936
+ }
1937
+ }
1938
+ if (node.type === "paragraph") {
1939
+ const headerPara = extractHeaderPara(node);
1940
+ if (headerPara) {
1941
+ const rawTabs = [...headerPara.tabs];
1942
+ const lastTab = () => rawTabs[rawTabs.length - 1];
1943
+ let rest = headerPara.rest;
1944
+ let restLine = headerPara.restLine;
1945
+ let j = i + 1;
1946
+ let currentTabIdx = rawTabs.length - 1;
1947
+ let prevEndLine = node.position?.end.line ?? 0;
1948
+ while (rest.length === 0 && j < children.length) {
1949
+ const sibling = children[j];
1950
+ if (!sibling) break;
1951
+ const siblingStartLine = sibling.position?.start.line ?? prevEndLine + 1;
1952
+ if (checkBlankLines && siblingStartLine > prevEndLine + 1) break;
1953
+ if (sibling.type === "blockquote") {
1954
+ const bq = sibling;
1955
+ const explicit = isExplicitBQ(bq);
1956
+ const segments = splitBlockquote(bq, explicit, sourceLines);
1957
+ const base = explicit && sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1958
+ const firstSeg = segments[0];
1959
+ if (firstSeg?.header === null) rawTabs[currentTabIdx]?.body.push(...firstSeg.body);
1960
+ const depthOffset = explicit ? lastTab().depth : 0;
1961
+ const owner = lastTab();
1962
+ let ending = null;
1963
+ for (const [index, seg] of segments.slice(1).entries()) {
1964
+ if (!seg.header) continue;
1965
+ const markers = seg.markers;
1966
+ if (explicit && markers !== void 0 && base !== void 0 && markers < base) {
1967
+ if (seg.header.depth > lastTab().depth + 1) continue;
1968
+ const relative = seg.lines.map((line) => ({
1969
+ ...line,
1970
+ markers: line.markers - (markers + 1)
1971
+ }));
1972
+ seg.header.body.push(...rebuildLazyLines(relative, 0), ...seg.body.slice(seg.lines.length > 0 ? 1 : 0));
1973
+ rawTabs.push(seg.header);
1974
+ currentTabIdx = rawTabs.length - 1;
1975
+ continue;
1976
+ }
1977
+ const adjustedDepth = depthOffset + seg.header.depth;
1978
+ if (adjustedDepth > lastTab().depth + 1) {
1979
+ if (explicit) {
1980
+ owner.after = [...owner.after ?? [], ...afterSkip([seg], base, false)];
1981
+ continue;
1982
+ }
1983
+ const quoteMarkers = sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1984
+ ending = afterSkip(segments.slice(index + 1), quoteMarkers === void 0 ? void 0 : quoteMarkers - 1, true);
1985
+ break;
1986
+ }
1987
+ seg.header.depth = adjustedDepth;
1988
+ if (explicit) {
1989
+ const deferredInnerSegs = [];
1990
+ const escapedSegs = [];
1991
+ for (const b of seg.body) {
1992
+ if (b.type !== "blockquote") {
1993
+ const escaped = escapedSegs.at(-1);
1994
+ if (escaped) escaped.body.push(b);
1995
+ else seg.header.body.push(b);
1996
+ continue;
1997
+ }
1998
+ const innerBq = b;
1999
+ if (!isExplicitBQ(innerBq)) {
2000
+ const innerSegs = splitBlockquote(innerBq, false, sourceLines);
2001
+ const innerFirst = innerSegs[0];
2002
+ if (innerFirst?.header === null) seg.header.body.push(...innerFirst.body);
2003
+ for (const innerSeg of innerSegs.slice(1)) {
2004
+ if (!innerSeg.header) continue;
2005
+ const markers = innerSeg.markers;
2006
+ if (markers !== void 0 && base !== void 0) {
2007
+ if (markers < base) {
2008
+ const relative = innerSeg.lines.map((line) => ({
2009
+ ...line,
2010
+ markers: line.markers - (markers + 1)
2011
+ }));
2012
+ innerSeg.header.body.push(...rebuildLazyLines(relative, 0));
2013
+ escapedSegs.push(innerSeg.header);
2014
+ deferredInnerSegs.push(innerSeg);
2015
+ continue;
2016
+ }
2017
+ } else if (innerSeg.body.length === 0) {
2018
+ innerSeg.header.depth = depthOffset;
2019
+ escapedSegs.push(innerSeg.header);
2020
+ deferredInnerSegs.push(innerSeg);
2021
+ continue;
2022
+ }
2023
+ const tentativeDepth = depthOffset + innerSeg.header.depth;
2024
+ if (tentativeDepth > lastTab().depth + 1) continue;
2025
+ innerSeg.header.depth = tentativeDepth;
2026
+ innerSeg.header.body.push(...innerSeg.body);
2027
+ deferredInnerSegs.push(innerSeg);
2028
+ }
2029
+ } else seg.header.body.push(...innerBq.children);
2030
+ }
2031
+ rawTabs.push(seg.header);
2032
+ currentTabIdx = rawTabs.length - 1;
2033
+ for (const innerSeg of deferredInnerSegs) {
2034
+ if (!innerSeg.header) continue;
2035
+ rawTabs.push(innerSeg.header);
2036
+ currentTabIdx = rawTabs.length - 1;
2037
+ }
2038
+ } else {
2039
+ seg.header.body.push(...seg.body);
2040
+ rawTabs.push(seg.header);
2041
+ currentTabIdx = rawTabs.length - 1;
2042
+ }
2043
+ }
2044
+ prevEndLine = sibling.position?.end.line ?? prevEndLine;
2045
+ j++;
2046
+ if (ending) {
2047
+ children.splice(j, 0, ...ending);
2048
+ break;
2049
+ }
2050
+ } else if (sibling.type === "paragraph") {
2051
+ const prevDepth = lastTab().depth;
2052
+ const contHeaders = extractHeaderPara(sibling, false, prevDepth);
2053
+ if (contHeaders) {
2054
+ rawTabs.push(...contHeaders.tabs);
2055
+ rest = contHeaders.rest;
2056
+ restLine = contHeaders.restLine;
2057
+ currentTabIdx = rawTabs.length - 1;
2058
+ prevEndLine = sibling.position?.end.line ?? prevEndLine;
2059
+ j++;
2060
+ continue;
2061
+ }
2062
+ break;
2063
+ } else break;
2064
+ }
2065
+ const list = buildTree(rawTabs, 1, 0, rawTabs.length, containerClass);
2066
+ list.containerClass = containerClass;
2067
+ newChildren.push(list);
2068
+ children.splice(j, 0, ...splitAtGroupStarts(rest, restLine));
2069
+ i = j;
2070
+ continue;
2071
+ }
2072
+ }
2073
+ newChildren.push(node);
2074
+ i++;
2075
+ }
2076
+ children.splice(0, children.length, ...newChildren);
2077
+ };
2078
+ /**
2079
+ * Recognize every tab group in the tree: at the root, then inside every container (step bodies, list items, other tabs' panels). Blank-line checks only apply at the root: blank lines inside a blockquote leave position gaps that would wrongly end a group once its content has been lifted out of the blockquote. With the document's `source`, a tab header that CommonMark lazily continued into a nested panel is placed by its `>` markers, as markdown-it reads it.
2080
+ */
2081
+ const processTabs = (root, containerClass, source) => {
2082
+ const sourceLines = source?.split(/\r?\n/);
2083
+ const walk = (node, quotes) => {
2084
+ if (!Array.isArray(node.children) || node.children.length === 0) return;
2085
+ const inner = quotes + (MARKED_BLOCKS.has(node.type ?? "") ? 1 : 0);
2086
+ processTabsInChildren(node.children, containerClass, false, sourceLines, inner, node.type);
2087
+ for (const child of node.children) walk(child, inner);
2088
+ };
2089
+ processTabsInChildren(root.children, containerClass, true, sourceLines);
2090
+ for (const child of root.children) walk(child, 0);
2091
+ if (sourceLines) endGroupsAtMarkers(root, sourceLines, `tabsList`);
2092
+ };
2093
+ // src/editable.ts
2094
+ const editableTree = (root, build, setChildren, setData) => {
2095
+ const originals = /* @__PURE__ */ new Map();
2096
+ const copy = (node) => {
2097
+ const children = node.children;
2098
+ if (!children) {
2099
+ const leaf = { ...node };
2100
+ originals.set(leaf, {
2101
+ node,
2102
+ children: []
2103
+ });
2104
+ return leaf;
2105
+ }
2106
+ const branch = {
2107
+ ...node,
2108
+ children: children.map(copy)
2109
+ };
2110
+ originals.set(branch, {
2111
+ node,
2112
+ children
2113
+ });
2114
+ return branch;
2115
+ };
2116
+ const writeBack = (node) => {
2117
+ const original = originals.get(node);
2118
+ if (!original) return build(node, writeBack);
2119
+ if (setData && node.data !== original.node.data) setData(original.node, node.data);
2120
+ if (!node.children) return original.node;
2121
+ const children = node.children.map(writeBack);
2122
+ if (children.length !== original.children.length || children.some((child, index) => child !== original.children[index])) setChildren(original.node, children);
2123
+ return original.node;
2124
+ };
2125
+ return {
2126
+ tree: copy(root),
2127
+ writeBack
2128
+ };
2129
+ };
2130
+ // src/inline-svg.ts
2131
+ const hash$1 = (str) => {
2132
+ let h = 2166136261;
2133
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
2134
+ return h.toString(36);
2135
+ };
2136
+ /**
2137
+ * The id of a deduplicated SVG's sprite: a hash of its path relative to the document, with `/` separators, so the same document renders the same ids on any machine or OS (an absolute path would leak the build machine into the HTML).
2138
+ */
2139
+ const svgSpriteId = (relativePath) => `svg-sprite-${hash$1(relativePath)}`;
2140
+ const extractSvgParts = (content) => {
2141
+ const openTag = /<svg([^>]*?)>/i.exec(content)?.[1] ?? ``;
2142
+ return {
2143
+ viewBox: /viewBox="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
2144
+ width: /\bwidth="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
2145
+ height: /\bheight="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
2146
+ inner: /<svg[^>]*>([\s\S]*?)<\/svg\s*>/i.exec(content)?.[1]?.trim() ?? ``
2147
+ };
2148
+ };
2149
+ const escAttr = (s) => s.replace(/&/g, `&amp;`).replace(/"/g, `&quot;`);
2150
+ const buildFullInline = (content, alt) => {
2151
+ if (!alt) return content;
2152
+ return content.replace(/<svg([^>]*)>/i, `<svg$1 aria-label="${escAttr(alt)}">`);
2153
+ };
2154
+ const buildSpriteHtml = (id, viewBox, inner) => `<svg aria-hidden="true" style="display:none" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="${id}" viewBox="${escAttr(viewBox)}">${inner}</symbol></defs></svg>`;
2155
+ const buildUseHtml = (id, viewBox, width, height, alt) => {
2156
+ return `<svg ${[
2157
+ viewBox && `viewBox="${escAttr(viewBox)}"`,
2158
+ width && `width="${escAttr(width)}"`,
2159
+ height && `height="${escAttr(height)}"`,
2160
+ alt && `aria-label="${escAttr(alt)}"`
2161
+ ].filter(Boolean).join(` `)}><use href="#${id}"></use></svg>`;
2162
+ };
2163
+ /**
2164
+ * The markup for every occurrence of one SVG file, given each occurrence's alt text in document order: the SVG itself, or, with deduplication and more than one occurrence, a hidden sprite on the first and a `<use>` of it on each. `null` when the limits leave the images as they are. `relativePath` is the file's path relative to the document, with `/` separators, for the sprite id.
2165
+ */
2166
+ const inlineSvgMarkup = (content, alts, relativePath, limits) => {
2167
+ if (content.length > limits.maxImageSize) return null;
2168
+ if (alts.length > limits.maxOccurrences) return null;
2169
+ if (alts.length * content.length > limits.maxTotalSize) return null;
2170
+ if (!limits.deduplication || alts.length < 2) return alts.map((alt) => buildFullInline(content, alt));
2171
+ const { viewBox, width, height, inner } = extractSvgParts(content);
2172
+ const id = svgSpriteId(relativePath);
2173
+ const sprite = buildSpriteHtml(id, viewBox, inner);
2174
+ return alts.map((alt, index) => {
2175
+ const use = buildUseHtml(id, viewBox, width, height, alt);
2176
+ return index === 0 ? sprite + use : use;
2177
+ });
2178
+ };
2179
+ // src/mermaid-pele.ts
2180
+ /** The class a fenced `mermaid` block's `<code>` takes. */
2181
+ const MERMAID_LANGUAGE_CLASS = `language-mermaid`;
2182
+ /** The class a `<pre>` holding a diagram written as HTML takes, as Mermaid reads it in the browser. */
2183
+ const MERMAID_PRE_CLASS = `mermaid`;
2184
+ const hash = (str) => {
2185
+ let h = 2166136261;
2186
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
2187
+ return h.toString(36);
2188
+ };
2189
+ /** A diagram's source as Pele reads it: line endings normalized, as markdown-it already has them. */
2190
+ const mermaidSource = (text) => text.replace(/\r\n?/g, `\n`);
2191
+ /**
2192
+ * A document's id prefixes, one per diagram in document order: `prefix`, then a hash of the diagram's source, so a diagram's ids don't change when others are edited, with `-2`, `-3`… for repeats of one diagram. Pele namespaces every id it writes (an accessible title, a gradient) under it. Create one per document.
2193
+ */
2194
+ const mermaidIdPrefixes = (prefix) => {
2195
+ const seen = /* @__PURE__ */ new Map();
2196
+ return (source) => {
2197
+ const id = `${prefix}-${hash(source)}`;
2198
+ const count = (seen.get(id) ?? 0) + 1;
2199
+ seen.set(id, count);
2200
+ return count === 1 ? id : `${id}-${count}`;
2201
+ };
2202
+ };
2203
+ const escapeAttribute = (value) => value.replace(/&/g, `&amp;`).replace(/"/g, `&quot;`);
2204
+ const classes = (value) => value.split(/\s+/).filter((name) => name !== `` && name !== MERMAID_LANGUAGE_CLASS && name !== MERMAID_PRE_CLASS);
2205
+ /**
2206
+ * Pele's `<svg>` with the attributes the diagram's element had (from an attribute block such as `{#id .class}`): classes are added after Pele's, a style after Pele's (whose theme variables the diagram needs), and anything else is set, replacing Pele's value. The class that marked the element as a diagram is dropped.
2207
+ */
2208
+ const mermaidSvg = (svg, attributes) => {
2209
+ const open = /^<svg\b[^>]*>/.exec(svg)?.[0];
2210
+ if (!open || attributes.length === 0) return svg;
2211
+ const own = [...open.slice(4, -1).matchAll(/\s([^\s=]+)="([^"]*)"/g)].map(([, name = ``, value = ``]) => [name, value]);
2212
+ const merged = new Map(own);
2213
+ for (const [name, value] of attributes) {
2214
+ const escaped = escapeAttribute(value);
2215
+ const current = merged.get(name);
2216
+ if (name === `class`) {
2217
+ const added = classes(value).map(escapeAttribute).join(` `);
2218
+ if (added) merged.set(name, current ? `${current} ${added}` : added);
2219
+ } else if (name === `style` && current) merged.set(name, `${current.replace(/;$/, ``)};${escaped}`);
2220
+ else merged.set(name, escaped);
2221
+ }
2222
+ return `<svg${[...merged].map(([name, value]) => ` ${name}="${value}"`).join(``)}>${svg.slice(open.length)}`;
2223
+ };
2224
+ /** A hast element's properties as attributes; `className` becomes `class`, and other names are kept as the attribute plugins write them. */
2225
+ const hastAttributes = (properties) => {
2226
+ const text = (value) => typeof value === `string` ? value : typeof value === `number` ? String(value) : value === true ? `` : Array.isArray(value) ? value.map((item) => text(item) ?? ``).join(` `) : void 0;
2227
+ const attributes = [];
2228
+ for (const [key, value] of Object.entries(properties ?? {})) {
2229
+ const written = text(value);
2230
+ if (written !== void 0) attributes.push([key === `className` ? `class` : key, written]);
2231
+ }
2232
+ return attributes;
2233
+ };
2234
+ const hasClass = (node, name) => {
2235
+ const value = node.properties?.className;
2236
+ return Array.isArray(value) ? value.includes(name) : typeof value === `string` && value.split(/\s+/).includes(name);
2237
+ };
2238
+ const textOf = (node) => node.type === `text` ? node.value ?? `` : (node.children ?? []).map(textOf).join(``);
2239
+ /**
2240
+ * The diagram a `<pre>` holds, as rehype-mermaid finds them: a `<pre>` whose only content (besides whitespace) is a `<code class="language-mermaid">`, which is how a fenced `mermaid` block renders, or a `<pre class="mermaid">`. `element` is the one whose attributes the `<svg>` takes. `undefined` for anything else.
2241
+ */
2242
+ const mermaidDiagram = (pre) => {
2243
+ if (pre.tagName !== `pre`) return void 0;
2244
+ if (hasClass(pre, MERMAID_PRE_CLASS)) return {
2245
+ source: mermaidSource(textOf(pre)),
2246
+ element: pre
2247
+ };
2248
+ let code;
2249
+ for (const child of pre.children ?? []) {
2250
+ if (child.type === `text` && /^\s*$/.test(child.value ?? ``)) continue;
2251
+ if (code || child.tagName !== `code`) return void 0;
2252
+ code = child;
2253
+ }
2254
+ return code && hasClass(code, MERMAID_LANGUAGE_CLASS) ? {
2255
+ source: mermaidSource(textOf(code)),
2256
+ element: code
2257
+ } : void 0;
2258
+ };
2259
+ /**
2260
+ * The error a diagram that failed to render throws: Pele's message, at the document line Pele located the problem on (counting from `line`, the line the diagram's element starts on), or at `line` itself when Pele has no location. The original error is the `cause`.
2261
+ */
2262
+ const mermaidFailure = (error, line) => {
2263
+ const reason = error instanceof Error ? error.message : String(error);
2264
+ const within = error instanceof Error && `line` in error && typeof error.line === `number` && error.line > 0 ? error.line : 0;
2265
+ const at = line === void 0 ? `` : ` on line ${String(line + within)}`;
2266
+ return new Error(`Could not render the mermaid diagram${at}: ${reason}`, { cause: error });
2267
+ };
2268
+ export { ALERT_ALIASES, CAPTION_CLASS, DEFAULT_TITLE, ICONS, LIGHTBOX_MARKER, containersKept, createAbbreviationScan, createAlertResolver, createDelimitedTransform, createKbdTransform, editableTree, escapedIndices, figureProperties, hastAttributes, headingRank, inlineSvgMarkup, isAlertMarkerLine, isCaptionFigure, isCaptionLine, isEmptyParagraph, isImageParagraph, itemColumns, lightboxDialog, lightboxId, lightboxTrigger, maskEscaped, maskTreeEscapes, mermaidDiagram, mermaidFailure, mermaidIdPrefixes, mermaidSource, mermaidSvg, parseAbbreviations, placeCutAttributes, plainText, processCaptions, processSteps, processStepsInChildren, processTabs, processTabsInChildren, scriptSyntax, sectionize, splitAlertParagraph, startsCaption, stripAttrs, stripCaptionMarker, svgSpriteId, tabLabelText, tabsBlockId, unmaskEscaped };
2269
+
2270
+ //# sourceMappingURL=index.mjs.map