@mirrordown/core 0.0.0 → 0.1.0

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,1818 @@
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
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
137
+ /** Resolve alert keywords, titles and icons the same way in every engine. */
138
+ const createAlertResolver = (options = {}) => {
139
+ const { types: extraTypes = {}, titles = {}, matchCaseInsensitive = true, icons = true, containerClass = `markdown-alert` } = options;
140
+ const aliasMap = {
141
+ ...ALERT_ALIASES,
142
+ ...extraTypes
143
+ };
144
+ const customTitles = titles;
145
+ const isAlertType = (type) => type in DEFAULT_TITLE;
146
+ return {
147
+ resolve: (keyword) => aliasMap[matchCaseInsensitive ? keyword.toLowerCase() : keyword],
148
+ titleFor: (type) => customTitles[type] ?? (isAlertType(type) ? DEFAULT_TITLE[type] : void 0) ?? capitalize(type),
149
+ iconFor: (type) => icons && isAlertType(type) ? ICONS[type] : ``,
150
+ containerClass
151
+ };
152
+ };
153
+ /** Plain text of inline content: text, inline code and image alt count; raw HTML does not. */
154
+ 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(``);
155
+ const withValue = (node, value) => ({
156
+ ...node,
157
+ value
158
+ });
159
+ /**
160
+ * 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.
161
+ */
162
+ const splitAlertParagraph = (children) => {
163
+ const [first, ...rest] = children;
164
+ if (first?.type !== `text` || first.value === void 0) return null;
165
+ const opening = first.value.trimStart();
166
+ const match = MARKER_RE.exec(opening);
167
+ if (!match) return null;
168
+ const [marker, keyword = ``, fold] = match;
169
+ const line = [];
170
+ const body = [];
171
+ for (const child of [withValue(first, opening.slice(marker.length)), ...rest]) if (body.length > 0 || line.at(-1)?.type === `break`) body.push(child);
172
+ else if (child.type === `text` && child.value?.includes(`\n`)) {
173
+ const at = child.value.indexOf(`\n`);
174
+ line.push(withValue(child, child.value.slice(0, at)));
175
+ body.push(withValue(child, child.value.slice(at + 1)));
176
+ } else line.push(child);
177
+ if (line.at(-1)?.type === `break`) line.pop();
178
+ 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 !== ``);
179
+ const lastTitle = titleNodes.at(-1);
180
+ if (lastTitle?.type === `text`) titleNodes[titleNodes.length - 1] = withValue(lastTitle, (lastTitle.value ?? ``).trimEnd());
181
+ return {
182
+ keyword,
183
+ fold: fold === `+` || fold === `-` ? fold : void 0,
184
+ titleNodes,
185
+ titleText: plainText(titleNodes).trim(),
186
+ bodyNodes: body.filter((n) => n.type !== `text` || n.value !== ``)
187
+ };
188
+ };
189
+ // src/spans.ts
190
+ /** Wrap a scan with built-node tracking and source-based escape masking. */
191
+ const createSpanTransform = (nodes, scan) => {
192
+ const built = /* @__PURE__ */ new WeakSet();
193
+ const contents = /* @__PURE__ */ new WeakMap();
194
+ const tracked = {
195
+ text: (value) => {
196
+ const node = nodes.text(value);
197
+ built.add(node);
198
+ return node;
199
+ },
200
+ wrap: (children) => {
201
+ const node = nodes.wrap(children);
202
+ built.add(node);
203
+ contents.set(node, children);
204
+ return node;
205
+ }
206
+ };
207
+ const transform = (children, parentType = ``) => scan(children, parentType, tracked);
208
+ const restore = (list) => {
209
+ for (const node of list) {
210
+ if (!built.has(node)) continue;
211
+ if (node.type === `text` && node.value !== void 0) node.value = unmaskEscaped(node.value);
212
+ else restore(contents.get(node) ?? []);
213
+ }
214
+ };
215
+ const withSource = (children, parentType, source) => {
216
+ const masked = children.map((node) => {
217
+ const start = node.position?.start.offset;
218
+ const end = node.position?.end.offset;
219
+ if (node.type !== `text` || node.value === void 0 || start === void 0 || end === void 0) return node;
220
+ const escaped = escapedIndices(node.value, source, start, end);
221
+ return escaped.size === 0 ? node : tracked.text(maskEscaped(node.value, escaped));
222
+ });
223
+ const next = transform(masked, parentType);
224
+ if (next === masked) return void 0;
225
+ restore(next);
226
+ return [...next];
227
+ };
228
+ return Object.assign(transform, {
229
+ built: (node) => built.has(node),
230
+ withSource
231
+ });
232
+ };
233
+ // src/delimited.ts
234
+ const INLINE_PARENTS = /* @__PURE__ */ new Set([
235
+ `delete`,
236
+ `emphasis`,
237
+ `insert`,
238
+ `link`,
239
+ `linkReference`,
240
+ `mark`,
241
+ `spoiler`,
242
+ `spoilerContent`,
243
+ `strong`,
244
+ `subscript`,
245
+ `superscript`
246
+ ]);
247
+ const WHITESPACE$2 = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/u;
248
+ const PUNCTUATION$1 = /^[\p{P}\p{S}]$/u;
249
+ const classify$1 = (char) => WHITESPACE$2.test(char) ? `space` : PUNCTUATION$1.test(char) ? `punct` : `other`;
250
+ /**
251
+ * 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.
252
+ */
253
+ const balancePairs = (delimiters) => {
254
+ const jumps = [];
255
+ const openersBottom = [
256
+ -1,
257
+ -1,
258
+ -1,
259
+ -1,
260
+ -1,
261
+ -1
262
+ ];
263
+ let headerIdx = 0;
264
+ let lastItem = -2;
265
+ delimiters.forEach((closer, closerIdx) => {
266
+ jumps.push(0);
267
+ if (lastItem !== closer.item - 1) headerIdx = closerIdx;
268
+ lastItem = closer.item;
269
+ if (!closer.close) return;
270
+ const bottom = closer.open ? 3 : 0;
271
+ const minOpenerIdx = openersBottom[bottom] ?? -1;
272
+ let openerIdx = headerIdx - (jumps[headerIdx] ?? 0) - 1;
273
+ let newMinOpenerIdx = openerIdx;
274
+ for (; openerIdx > minOpenerIdx; openerIdx -= (jumps[openerIdx] ?? 0) + 1) {
275
+ const opener = delimiters[openerIdx];
276
+ if (!opener?.open || opener.end >= 0) continue;
277
+ const lastJump = openerIdx > 0 && delimiters[openerIdx - 1]?.open === false ? (jumps[openerIdx - 1] ?? 0) + 1 : 0;
278
+ jumps[closerIdx] = closerIdx - openerIdx + lastJump;
279
+ jumps[openerIdx] = lastJump;
280
+ closer.open = false;
281
+ opener.end = closerIdx;
282
+ opener.close = false;
283
+ newMinOpenerIdx = -1;
284
+ lastItem = -2;
285
+ break;
286
+ }
287
+ if (newMinOpenerIdx !== -1) openersBottom[bottom] = newMinOpenerIdx;
288
+ });
289
+ };
290
+ /** Build the delimiter scan for one marker: `=` (mark), `+` (ins), `-` (del) or `|` (spoiler). */
291
+ const createDelimitedTransform = (marker, nodes) => createSpanTransform(nodes, (children, parentType, { text, wrap }) => {
292
+ const edge = INLINE_PARENTS.has(parentType) ? `punct` : `space`;
293
+ const beside = (node, side) => {
294
+ if (!node) return edge;
295
+ if (node.type === `text` && node.value) return classify$1(side === `before` ? node.value.slice(-1) : node.value.charAt(0));
296
+ return node.type === `break` ? `space` : `punct`;
297
+ };
298
+ const items = [];
299
+ const delimiters = [];
300
+ children.forEach((child, index) => {
301
+ const value = child.type === `text` ? child.value : void 0;
302
+ if (value === void 0) {
303
+ items.push({
304
+ kind: `node`,
305
+ node: child
306
+ });
307
+ return;
308
+ }
309
+ let pending = ``;
310
+ const flush = () => {
311
+ if (pending) items.push({
312
+ kind: `text`,
313
+ value: pending
314
+ });
315
+ pending = ``;
316
+ };
317
+ let i = 0;
318
+ while (i < value.length) {
319
+ let end = i;
320
+ while (value.charAt(end) === marker) end++;
321
+ if (end - i < 2) {
322
+ pending += value.slice(i, Math.max(end, i + 1));
323
+ i = Math.max(end, i + 1);
324
+ continue;
325
+ }
326
+ const last = i > 0 ? classify$1(value.charAt(i - 1)) : beside(children[index - 1], `before`);
327
+ const next = end < value.length ? classify$1(value.charAt(end)) : beside(children[index + 1], `after`);
328
+ const open = next !== `space` && (next !== `punct` || last !== `other`);
329
+ const close = last !== `space` && (last !== `punct` || next !== `other`);
330
+ flush();
331
+ let length = end - i;
332
+ if (length % 2) {
333
+ items.push({
334
+ kind: `text`,
335
+ value: marker
336
+ });
337
+ length--;
338
+ }
339
+ for (let pair = 0; pair < length; pair += 2) if (open || close) {
340
+ delimiters.push({
341
+ open,
342
+ close,
343
+ end: -1,
344
+ item: items.length
345
+ });
346
+ items.push({ kind: `delimiter` });
347
+ } else items.push({
348
+ kind: `text`,
349
+ value: marker + marker
350
+ });
351
+ i = end;
352
+ }
353
+ flush();
354
+ });
355
+ balancePairs(delimiters);
356
+ if (!delimiters.some(({ end }) => end >= 0)) return children;
357
+ const lone = [];
358
+ for (const delimiter of delimiters) {
359
+ const closer = delimiters[delimiter.end];
360
+ if (!closer) continue;
361
+ items[delimiter.item] = { kind: `opening` };
362
+ items[closer.item] = { kind: `closing` };
363
+ const previous = items[closer.item - 1];
364
+ if (previous?.kind === `text` && previous.value === marker) lone.push(closer.item - 1);
365
+ }
366
+ for (const i of lone.reverse()) {
367
+ let j = i + 1;
368
+ while (items[j]?.kind === `closing`) j++;
369
+ j--;
370
+ const moved = items[i];
371
+ const target = items[j];
372
+ if (i !== j && moved && target) {
373
+ items[i] = target;
374
+ items[j] = moved;
375
+ }
376
+ }
377
+ const root = [];
378
+ const stack = [root];
379
+ let buffer = ``;
380
+ const append = (node) => {
381
+ (stack.at(-1) ?? root).push(node);
382
+ };
383
+ const flushText = () => {
384
+ if (!buffer) return;
385
+ append(text(buffer));
386
+ buffer = ``;
387
+ };
388
+ for (const item of items) if (item.kind === `text`) buffer += item.value;
389
+ else if (item.kind === `delimiter`) buffer += marker + marker;
390
+ else if (item.kind === `node`) {
391
+ flushText();
392
+ append(item.node);
393
+ } else if (item.kind === `opening`) {
394
+ flushText();
395
+ stack.push([]);
396
+ } else {
397
+ flushText();
398
+ append(wrap(stack.pop() ?? []));
399
+ }
400
+ flushText();
401
+ return root;
402
+ });
403
+ // src/kbd.ts
404
+ const isChar = (unit, char) => unit !== void 0 && `char` in unit && unit.char === char;
405
+ /**
406
+ * 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.
407
+ */
408
+ const findClose = (units, start) => {
409
+ let depth = 1;
410
+ let skipNext = false;
411
+ for (let i = start + 1; i < units.length; i++) {
412
+ if (skipNext) {
413
+ skipNext = false;
414
+ continue;
415
+ }
416
+ const unit = units[i];
417
+ const next = units[i + 1];
418
+ if (isChar(unit, `]`) && isChar(next, `]`)) {
419
+ depth--;
420
+ if (depth === 0) return i;
421
+ skipNext = true;
422
+ } else if (isChar(unit, `[`) && isChar(next, `[`)) {
423
+ depth++;
424
+ skipNext = true;
425
+ } else if (isChar(unit, `\n`) || unit && `node` in unit && unit.node.type === `break`) return -1;
426
+ }
427
+ return -1;
428
+ };
429
+ /** Build the `[[key]]` scan. */
430
+ const createKbdTransform = (nodes) => createSpanTransform(nodes, (children, _parentType, { text, wrap }) => {
431
+ const units = children.flatMap((child) => {
432
+ const value = child.type === `text` ? child.value : void 0;
433
+ return value === void 0 ? [{ node: child }] : Array.from(value, (char) => ({ char }));
434
+ });
435
+ const scan = (list) => {
436
+ const out = [];
437
+ let keyed = false;
438
+ let buffer = ``;
439
+ const flushText = () => {
440
+ if (buffer) out.push(text(buffer));
441
+ buffer = ``;
442
+ };
443
+ for (let i = 0; i < list.length;) {
444
+ const unit = list[i];
445
+ if (isChar(unit, `[`) && isChar(list[i + 1], `[`)) {
446
+ const end = findClose(list, i);
447
+ if (end >= 0) {
448
+ keyed = true;
449
+ flushText();
450
+ out.push(wrap(scan(list.slice(i + 2, end)).nodes));
451
+ i = end + 2;
452
+ continue;
453
+ }
454
+ }
455
+ if (unit && `char` in unit) buffer += unit.char;
456
+ else if (unit) {
457
+ flushText();
458
+ out.push(unit.node);
459
+ }
460
+ i++;
461
+ }
462
+ flushText();
463
+ return {
464
+ nodes: out,
465
+ keyed
466
+ };
467
+ };
468
+ const { nodes: result, keyed } = scan(units);
469
+ return keyed ? result : children;
470
+ });
471
+ // src/scripts.ts
472
+ /** The marker characters, and the micromark token types each produces. */
473
+ const SCRIPTS = {
474
+ "~": {
475
+ code: 126,
476
+ type: `subscript`
477
+ },
478
+ "^": {
479
+ code: 94,
480
+ type: `superscript`
481
+ }
482
+ };
483
+ const tokenType = (type) => type;
484
+ const temporary = (type) => tokenType(`${type}SequenceTemporary`);
485
+ const scriptOf = (tokenType) => Object.values(SCRIPTS).find(({ type }) => temporary(type) === tokenType);
486
+ const PUNCTUATION = /\p{P}|\p{S}/u;
487
+ const WHITESPACE$1 = /\s/;
488
+ const classify = (code) => code === null || code < 0 || code === 32 || WHITESPACE$1.test(String.fromCharCode(code)) ? 1 : PUNCTUATION.test(String.fromCharCode(code)) ? 2 : void 0;
489
+ const splice = (list, start, remove, items) => {
490
+ list.splice(start, remove);
491
+ for (let i = 0; i < items.length; i += 1e4) list.splice(start + i, 0, ...items.slice(i, i + 1e4));
492
+ };
493
+ const resolveAll = (constructs, events, context) => {
494
+ const called = [];
495
+ let result = events;
496
+ for (const { resolveAll: resolve } of constructs) if (resolve && !called.includes(resolve)) {
497
+ result = resolve(result, context);
498
+ called.push(resolve);
499
+ }
500
+ return result;
501
+ };
502
+ const resolveScripts = (events, context) => {
503
+ for (let index = 0; index < events.length; index++) {
504
+ const [kind, closer] = events.at(index) ?? [];
505
+ const script = closer && scriptOf(closer.type);
506
+ if (kind !== `enter` || !closer || !script || !closer._close) continue;
507
+ for (let open = index - 1; open >= 0; open--) {
508
+ const [openKind, opener] = events.at(open) ?? [];
509
+ if (openKind !== `exit` || !opener || opener.type !== closer.type || !opener._open) continue;
510
+ opener.type = tokenType(`${script.type}Sequence`);
511
+ closer.type = tokenType(`${script.type}Sequence`);
512
+ const span = {
513
+ type: tokenType(script.type),
514
+ start: { ...opener.start },
515
+ end: { ...closer.end }
516
+ };
517
+ const text = {
518
+ type: tokenType(`${script.type}Text`),
519
+ start: { ...opener.end },
520
+ end: { ...closer.start }
521
+ };
522
+ const next = [
523
+ [
524
+ `enter`,
525
+ span,
526
+ context
527
+ ],
528
+ [
529
+ `enter`,
530
+ opener,
531
+ context
532
+ ],
533
+ [
534
+ `exit`,
535
+ opener,
536
+ context
537
+ ],
538
+ [
539
+ `enter`,
540
+ text,
541
+ context
542
+ ],
543
+ ...resolveAll(context.parser.constructs.insideSpan.null ?? [], events.slice(open + 1, index), context),
544
+ [
545
+ `exit`,
546
+ text,
547
+ context
548
+ ],
549
+ [
550
+ `enter`,
551
+ closer,
552
+ context
553
+ ],
554
+ [
555
+ `exit`,
556
+ closer,
557
+ context
558
+ ],
559
+ [
560
+ `exit`,
561
+ span,
562
+ context
563
+ ]
564
+ ];
565
+ splice(events, open - 1, index - open + 3, next);
566
+ index = open + next.length - 2;
567
+ break;
568
+ }
569
+ }
570
+ for (const [, token] of events) if (scriptOf(token.type)) token.type = `data`;
571
+ return events;
572
+ };
573
+ function tokenize(effects, ok, nok) {
574
+ const { previous, events } = this;
575
+ return (start) => {
576
+ const script = Object.values(SCRIPTS).find((s) => s.code === start);
577
+ if (!script) return nok(start);
578
+ if (previous === script.code && events.at(-1)?.[1].type !== `characterEscape`) return nok(start);
579
+ effects.enter(temporary(script.type));
580
+ effects.consume(start);
581
+ return (after) => {
582
+ if (after === script.code) return nok(after);
583
+ const token = effects.exit(temporary(script.type));
584
+ const before = classify(previous);
585
+ const next = classify(after);
586
+ token._open = !next || next === 2 && Boolean(before);
587
+ token._close = !before || before === 2 && Boolean(next);
588
+ return ok(after);
589
+ };
590
+ };
591
+ }
592
+ /**
593
+ * The micromark syntax extension for one of the two markers: `~` (subscript, `subscript` tokens) or `^` (superscript, `superscript` tokens). The constructs of both markers resolve together.
594
+ */
595
+ const scriptSyntax = (marker) => {
596
+ const construct = {
597
+ name: SCRIPTS[marker].type,
598
+ tokenize,
599
+ resolveAll: resolveScripts
600
+ };
601
+ return {
602
+ text: { [SCRIPTS[marker].code]: construct },
603
+ insideSpan: { null: [construct] }
604
+ };
605
+ };
606
+ // src/hast.ts
607
+ const WHITESPACE = /^[ \t\n\f\r]*$/;
608
+ const isWhitespaceText = (node) => node.type === `text` && WHITESPACE.test(node.value ?? ``);
609
+ /** A `<p>` with no children, or only whitespace text: squeeze-paragraphs removes it. */
610
+ const isEmptyParagraph = (node) => node.tagName === `p` && (node.children ?? []).every(isWhitespaceText);
611
+ /**
612
+ * 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.
613
+ */
614
+ const isImageParagraph = (node) => {
615
+ const scan = (parent, inLink) => {
616
+ let result = `empty`;
617
+ for (const child of parent.children ?? []) {
618
+ if (isWhitespaceText(child)) continue;
619
+ if (child.type !== `element`) return `other`;
620
+ if (child.tagName === `img`) {
621
+ result = `image`;
622
+ continue;
623
+ }
624
+ if (!inLink && (child.tagName === `a` || child.tagName === `button`)) {
625
+ const inner = scan(child, true);
626
+ if (inner === `other`) return `other`;
627
+ if (inner === `image`) result = `image`;
628
+ continue;
629
+ }
630
+ return `other`;
631
+ }
632
+ return result;
633
+ };
634
+ return node.tagName === `p` && scan(node, false) === `image`;
635
+ };
636
+ /** The rank of an `h1`–`h6` element, or `undefined`. */
637
+ const headingRank = (node) => {
638
+ if (node.type !== `element`) return void 0;
639
+ const match = /^h([1-6])$/i.exec(node.tagName ?? ``);
640
+ return match ? Number(match[1]) : void 0;
641
+ };
642
+ /**
643
+ * Wrap heading-delimited runs of `nodes` into `<section data-depth>` elements, nesting deeper headings inside shallower ones. Content before the first heading stays unwrapped.
644
+ */
645
+ const sectionize = (nodes, section, floor = 0) => {
646
+ let minRank = Infinity;
647
+ for (const node of nodes) {
648
+ const rank = headingRank(node);
649
+ if (rank !== void 0 && rank > floor && rank < minRank) minRank = rank;
650
+ }
651
+ if (minRank === Infinity) return [...nodes];
652
+ const result = [];
653
+ let i = 0;
654
+ for (; i < nodes.length; i++) {
655
+ const node = nodes[i];
656
+ if (!node || headingRank(node) === minRank) break;
657
+ result.push(node);
658
+ }
659
+ while (i < nodes.length) {
660
+ const start = i;
661
+ for (i++; i < nodes.length; i++) {
662
+ const node = nodes[i];
663
+ if (!node || headingRank(node) === minRank) break;
664
+ }
665
+ result.push(section(minRank, sectionize(nodes.slice(start, i), section, minRank)));
666
+ }
667
+ return result;
668
+ };
669
+ /**
670
+ * 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.
671
+ */
672
+ const LIGHTBOX_MARKER = /(?:^|\s)!$/;
673
+ /**
674
+ * 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.
675
+ */
676
+ const lightboxId = (src) => {
677
+ let hash = 2166136261;
678
+ for (let i = 0; i < src.length; i += 1) {
679
+ hash ^= src.charCodeAt(i);
680
+ hash = Math.imul(hash, 16777619);
681
+ }
682
+ return `markdown-lightbox-${(hash >>> 0).toString(36)}`;
683
+ };
684
+ /** The button that replaces an opted-in image and opens its dialog. */
685
+ const lightboxTrigger = (id, image) => ({
686
+ type: `element`,
687
+ tagName: `button`,
688
+ properties: {
689
+ type: `button`,
690
+ className: [`markdown-lightbox-trigger`],
691
+ command: `show-modal`,
692
+ commandfor: id
693
+ },
694
+ children: [image]
695
+ });
696
+ /** The modal dialog, appended at the document end, showing the image full size. */
697
+ const lightboxDialog = (id, image) => ({
698
+ type: `element`,
699
+ tagName: `dialog`,
700
+ properties: {
701
+ id,
702
+ className: [`markdown-lightbox`],
703
+ closedby: `any`
704
+ },
705
+ children: [{
706
+ type: `element`,
707
+ tagName: `button`,
708
+ properties: {
709
+ type: `button`,
710
+ className: [`markdown-lightbox-close`],
711
+ command: `close`,
712
+ commandfor: id,
713
+ ariaLabel: `Close image`
714
+ },
715
+ children: [image]
716
+ }]
717
+ });
718
+ // src/abbr.ts
719
+ const DEFINITION = /^\*\[([^[\]]+)\]:\s*(.+)$/;
720
+ const REGEX_SPECIAL = /[.*+?^${}()|[\]\\]/g;
721
+ /**
722
+ * 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).
723
+ */
724
+ const parseAbbreviations = (text) => {
725
+ const definitions = [];
726
+ for (const line of text.split(`\n`)) {
727
+ const match = DEFINITION.exec(line);
728
+ if (!match) return void 0;
729
+ definitions.push([match[1] ?? ``, (match[2] ?? ``).trim()]);
730
+ }
731
+ return definitions;
732
+ };
733
+ /**
734
+ * 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.
735
+ */
736
+ const createAbbreviationScan = (abbreviations) => {
737
+ const terms = [...abbreviations.keys()].sort((a, b) => b.length - a.length).map((term) => term.replace(REGEX_SPECIAL, `\\$&`));
738
+ const pattern = new RegExp(`\\b(${terms.join(`|`)})\\b`, `g`);
739
+ return (value) => {
740
+ const parts = [];
741
+ let last = 0;
742
+ for (const match of value.matchAll(pattern)) {
743
+ const term = match[1] ?? ``;
744
+ if (match.index > last) parts.push({ text: value.slice(last, match.index) });
745
+ parts.push({
746
+ term,
747
+ title: abbreviations.get(term) ?? ``
748
+ });
749
+ last = match.index + term.length;
750
+ }
751
+ if (parts.length === 0) return void 0;
752
+ if (last < value.length) parts.push({ text: value.slice(last) });
753
+ return parts;
754
+ };
755
+ };
756
+ // src/cut-attributes.ts
757
+ /** Paragraphs recognition rebuilt from lines, as it does where a tab or step header lazily continued them. */
758
+ const rebuiltParagraphs = /* @__PURE__ */ new WeakSet();
759
+ /** Joins `added` into `properties`: classes accumulate, other attributes replace. */
760
+ const mergeProperties = (properties, added) => {
761
+ const merged = { ...properties };
762
+ for (const [key, value] of Object.entries(added)) {
763
+ const existing = merged[key];
764
+ merged[key] = key === "className" && Array.isArray(existing) && Array.isArray(value) ? [...existing, ...value] : value;
765
+ }
766
+ return merged;
767
+ };
768
+ const listLoose = (list) => list?.type !== "list" || Boolean(list.spread) || list.children.some((item) => item.spread ?? item.children.length > 1);
769
+ /**
770
+ * 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.
771
+ */
772
+ const placeCutAttributes = (root, sourceLines, read) => {
773
+ const parents = /* @__PURE__ */ new Map();
774
+ const cut = [];
775
+ const items = [];
776
+ const walk = (node) => {
777
+ if (node.type === "paragraph" && rebuiltParagraphs.has(node)) cut.push(node);
778
+ const data = node.data;
779
+ if (typeof data === "object" && data !== null && "attrsRole" in data && data.attrsRole === "containerItem" && "attrsTitle" in data && Array.isArray(data.attrsTitle)) items.push(node);
780
+ if (!("children" in node)) return;
781
+ for (const child of node.children) {
782
+ parents.set(child, node);
783
+ walk(child);
784
+ }
785
+ };
786
+ walk(root);
787
+ for (const para of cut) {
788
+ const children = para.children;
789
+ const last = children.at(-1);
790
+ if (last?.type !== "text") continue;
791
+ const value = last.value.trimEnd();
792
+ const lineStart = value.lastIndexOf("\n");
793
+ const line = value.slice(lineStart + 1);
794
+ const open = line.lastIndexOf("{");
795
+ if (open === -1) continue;
796
+ const properties = read(line.slice(open));
797
+ if (!properties) continue;
798
+ const before = line.slice(0, open);
799
+ const ownLine = before.trim() === "" && (lineStart !== -1 || children.at(-2)?.type === "break");
800
+ if (before === "" && !ownLine) continue;
801
+ const ancestors = [];
802
+ for (let p = parents.get(para); p; p = parents.get(p)) ancestors.unshift(p);
803
+ const parent = ancestors.at(-1);
804
+ let target = para;
805
+ if (ownLine) {
806
+ const end = last.position?.end;
807
+ const written = end ? sourceLines[end.line - 1] ?? "" : "";
808
+ const prefix = /^[ \t>]*/.exec(written)?.[0] ?? "";
809
+ const markers = prefix.split(">").length - 1;
810
+ const outside = end ? ancestors.findIndex((ancestor) => {
811
+ const start = ancestor.position?.start;
812
+ if (ancestor.type === "blockquote" && start) {
813
+ const own = (sourceLines[start.line - 1] ?? "").slice(0, start.column - 1);
814
+ return markers < own.split(">").length;
815
+ }
816
+ const first = ancestor.type === "listItem" ? ancestor.children[0]?.position?.start : void 0;
817
+ return first !== void 0 && prefix.length < first.column - 1;
818
+ }) : -1;
819
+ const container = ancestors[outside];
820
+ if (container) target = container.type === "listItem" ? ancestors[outside - 1] ?? container : container;
821
+ else if (parent?.type === "listItem" && !listLoose(ancestors.at(-2))) target = parent;
822
+ } else if (parent?.type === "listItem" && parent.children[0] === para) target = parent;
823
+ const data = target.data;
824
+ target.data = {
825
+ ...data,
826
+ hProperties: mergeProperties(data?.hProperties, properties)
827
+ };
828
+ const kept = ownLine ? value.slice(0, Math.max(lineStart, 0)) : value.slice(0, lineStart + 1) + before;
829
+ if (kept.trimEnd() === "") children.pop();
830
+ else last.value = kept.trimEnd();
831
+ }
832
+ for (const item of items) {
833
+ const title = item.data.attrsTitle;
834
+ const last = title.at(-1);
835
+ if (last?.type !== "text") continue;
836
+ const value = last.value.trimEnd();
837
+ const open = value.lastIndexOf("{");
838
+ if (open === -1) continue;
839
+ const properties = read(value.slice(open));
840
+ if (!properties) continue;
841
+ item.data.hProperties = mergeProperties(item.data.hProperties, properties);
842
+ last.value = value.slice(0, open).trimEnd();
843
+ if (last.value === "" && title.length > 1) title.pop();
844
+ }
845
+ };
846
+ // src/lazy.ts
847
+ /** The `>` markers a source line starts with. */
848
+ const markerCount = (line) => (/^[ \t>]*/.exec(line)?.[0] ?? "").split(">").length - 1;
849
+ /** 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). */
850
+ const blockquoteMarkers = (bq, sourceLines) => {
851
+ const start = bq.position?.start;
852
+ if (!start) return void 0;
853
+ return markerCount((sourceLines[start.line - 1] ?? "").slice(0, start.column - 1)) + 1;
854
+ };
855
+ /**
856
+ * 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.
857
+ */
858
+ const paraToLines = (para, sourceLines) => {
859
+ const lines = [[]];
860
+ for (const child of para.children) if (child.type === "break") {
861
+ lines.at(-1)?.push(child);
862
+ lines.push([]);
863
+ } else if (child.type === "text") {
864
+ const parts = child.value.split("\n");
865
+ for (const [p, part] of parts.entries()) {
866
+ if (p > 0) lines.push([]);
867
+ if (part === "") continue;
868
+ const text = {
869
+ type: "text",
870
+ value: part
871
+ };
872
+ const position = child.position;
873
+ if (position) {
874
+ const line = position.start.line + p;
875
+ const source = sourceLines?.[line - 1];
876
+ const start = p === 0 ? position.start : source === void 0 ? void 0 : {
877
+ line,
878
+ column: (/^[ \t>]*/.exec(source)?.[0].length ?? 0) + 1
879
+ };
880
+ const end = p === parts.length - 1 ? position.end : source === void 0 ? void 0 : {
881
+ line,
882
+ column: source.trimEnd().length + 1
883
+ };
884
+ if (start && end) text.position = {
885
+ start,
886
+ end
887
+ };
888
+ }
889
+ lines.at(-1)?.push(text);
890
+ }
891
+ } else lines.at(-1)?.push(child);
892
+ return lines;
893
+ };
894
+ /** 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. */
895
+ const paragraphProperties = (para) => {
896
+ return para.data?.hProperties;
897
+ };
898
+ /** Appends `node` to `children`, merging text into the text before it, as remark keeps a run of text in one node. */
899
+ const pushPhrasing = (children, node) => {
900
+ const last = children.at(-1);
901
+ if (node.type !== "text" || last?.type !== "text") {
902
+ children.push(node);
903
+ return;
904
+ }
905
+ const start = last.position?.start ?? (last.value.trim() === "" ? node.position?.start : void 0);
906
+ const end = node.position?.end ?? last.position?.end;
907
+ const merged = {
908
+ type: "text",
909
+ value: last.value + node.value
910
+ };
911
+ if (start && end) merged.position = {
912
+ start,
913
+ end
914
+ };
915
+ children[children.length - 1] = merged;
916
+ };
917
+ /** 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. */
918
+ const linesToParagraph = (lines) => {
919
+ const children = [];
920
+ for (const [i, line] of lines.entries()) {
921
+ if (i > 0 && lines[i - 1]?.at(-1)?.type !== "break") pushPhrasing(children, {
922
+ type: "text",
923
+ value: "\n"
924
+ });
925
+ for (const node of line) pushPhrasing(children, node);
926
+ }
927
+ if (children.length === 0) return null;
928
+ const para = {
929
+ type: "paragraph",
930
+ children
931
+ };
932
+ const start = children[0]?.position?.start;
933
+ const end = children.at(-1)?.position?.end;
934
+ if (start && end) para.position = {
935
+ start,
936
+ end
937
+ };
938
+ rebuiltParagraphs.add(para);
939
+ return para;
940
+ };
941
+ /**
942
+ * 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.
943
+ */
944
+ const rebuildLazyLines = (lines, level) => {
945
+ const deeper = lines.findIndex(({ markers }) => markers > level);
946
+ const own = deeper === -1 ? lines : lines.slice(0, deeper);
947
+ const para = linesToParagraph(own.map(({ segs }) => segs));
948
+ const blocks = para ? [para] : [];
949
+ if (deeper !== -1) {
950
+ const quote = {
951
+ type: "blockquote",
952
+ children: rebuildLazyLines(lines.slice(deeper), level + 1)
953
+ };
954
+ blocks.push(quote);
955
+ }
956
+ return blocks;
957
+ };
958
+ // src/steps.ts
959
+ const STEP_HEADER_RE = /^(@{1,6})(\d+)\. ?(.*)/s;
960
+ const parseHeaderPrefix = (text) => {
961
+ const m = STEP_HEADER_RE.exec(text);
962
+ if (!m) return null;
963
+ const [, hashes = "", digits = "", rest = ""] = m;
964
+ return {
965
+ depth: hashes.length,
966
+ number: parseInt(digits, 10),
967
+ rest
968
+ };
969
+ };
970
+ const extractHeaderPara$1 = (para, counters, requireDepthOne = true, prevDepth = 0) => {
971
+ const lines = paraToLines(para);
972
+ const steps = [];
973
+ let currentDepth = prevDepth;
974
+ const stop = (index) => steps.length > 0 ? {
975
+ steps,
976
+ rest: lines.slice(index),
977
+ restLine: (para.position?.start.line ?? 1) + index
978
+ } : null;
979
+ for (const [index, lineSegs] of lines.entries()) {
980
+ const first = lineSegs[0];
981
+ if (first?.type !== "text") return stop(index);
982
+ const header = parseHeaderPrefix(first.value);
983
+ if (!header) return stop(index);
984
+ const { depth } = header;
985
+ if (steps.length === 0 && requireDepthOne && depth !== 1) return null;
986
+ if (depth > currentDepth + 1) return stop(index);
987
+ for (const k of counters.keys()) if (k > depth) counters.delete(k);
988
+ const number = (counters.get(depth) ?? 0) + 1;
989
+ counters.set(depth, number);
990
+ currentDepth = depth;
991
+ const titleNodes = header.rest ? [{
992
+ type: "text",
993
+ value: header.rest
994
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
995
+ steps.push({
996
+ depth,
997
+ number,
998
+ titleNodes,
999
+ body: [],
1000
+ ...index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1001
+ });
1002
+ }
1003
+ return stop(lines.length);
1004
+ };
1005
+ const splitAtGroupStarts$1 = (lines, firstLine) => {
1006
+ const paragraphs = [];
1007
+ let start = 0;
1008
+ const flush = (end) => {
1009
+ const para = linesToParagraph(lines.slice(start, end));
1010
+ if (!para) return;
1011
+ para.position = {
1012
+ start: {
1013
+ line: firstLine + start,
1014
+ column: 1
1015
+ },
1016
+ end: {
1017
+ line: firstLine + end - 1,
1018
+ column: 1
1019
+ }
1020
+ };
1021
+ paragraphs.push(para);
1022
+ };
1023
+ lines.forEach((line, index) => {
1024
+ const first = line[0];
1025
+ if (first?.type === "text" && parseHeaderPrefix(first.value)?.depth === 1 && index > start) {
1026
+ flush(index);
1027
+ start = index;
1028
+ }
1029
+ });
1030
+ flush(lines.length);
1031
+ return paragraphs;
1032
+ };
1033
+ const takeLazyLines = (node, sourceLines, base, isHeader) => {
1034
+ let parent = node;
1035
+ let last = parent.children?.at(-1);
1036
+ while (last?.type !== "paragraph") {
1037
+ if (!last?.children?.length) return null;
1038
+ parent = last;
1039
+ last = parent.children?.at(-1);
1040
+ }
1041
+ const para = last;
1042
+ const start = para.position?.start.line;
1043
+ if (start === void 0 || !parent.children) return null;
1044
+ const lines = paraToLines(para, sourceLines).map((segs, index) => ({
1045
+ segs,
1046
+ markers: markerCount(sourceLines[start - 1 + index] ?? "")
1047
+ }));
1048
+ const at = lines.findIndex(({ segs, markers }, index) => index > 0 && markers < base && isHeader(segs));
1049
+ if (at === -1) return null;
1050
+ const kept = linesToParagraph(lines.slice(0, at).map(({ segs }) => segs));
1051
+ parent.children.splice(-1, 1, ...kept ? [kept] : []);
1052
+ return lines.slice(at);
1053
+ };
1054
+ const splitBlockquote$1 = (bq, counters, currentDepth, sourceLines) => {
1055
+ const segments = [];
1056
+ let currentSegment = {
1057
+ header: null,
1058
+ body: []
1059
+ };
1060
+ const base = sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1061
+ const stepHeader = (lineSegs) => {
1062
+ const first = lineSegs[0];
1063
+ if (first?.type !== "text") return null;
1064
+ const header = parseHeaderPrefix(first.value);
1065
+ return header && header.depth <= currentDepth + 1 ? header : null;
1066
+ };
1067
+ const startStep = (header, lineSegs, hProperties) => {
1068
+ segments.push(currentSegment);
1069
+ for (const k of counters.keys()) if (k > header.depth) counters.delete(k);
1070
+ const number = (counters.get(header.depth) ?? 0) + 1;
1071
+ counters.set(header.depth, number);
1072
+ currentDepth = header.depth;
1073
+ const titleNodes = header.rest ? [{
1074
+ type: "text",
1075
+ value: header.rest
1076
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1077
+ currentSegment = {
1078
+ header: {
1079
+ depth: header.depth,
1080
+ number,
1081
+ titleNodes,
1082
+ hProperties
1083
+ },
1084
+ body: []
1085
+ };
1086
+ };
1087
+ for (const child of bq.children) {
1088
+ if (child.type !== "paragraph") {
1089
+ currentSegment.body.push(child);
1090
+ if (!sourceLines || base === void 0) continue;
1091
+ const lazy = takeLazyLines(child, sourceLines, base, (segs) => stepHeader(segs) !== null);
1092
+ if (!lazy) continue;
1093
+ let pending = [];
1094
+ const flush = () => {
1095
+ const relative = pending.map((line) => ({
1096
+ ...line,
1097
+ markers: line.markers - base
1098
+ }));
1099
+ currentSegment.body.push(...rebuildLazyLines(relative, 0));
1100
+ pending = [];
1101
+ };
1102
+ for (const line of lazy) {
1103
+ const header = line.markers < base ? stepHeader(line.segs) : null;
1104
+ if (header) {
1105
+ flush();
1106
+ startStep(header, line.segs);
1107
+ } else pending.push(line);
1108
+ }
1109
+ flush();
1110
+ continue;
1111
+ }
1112
+ let bodyLines = [];
1113
+ const lines = paraToLines(child, sourceLines);
1114
+ for (const [index, lineSegs] of lines.entries()) {
1115
+ const header = stepHeader(lineSegs);
1116
+ if (header) {
1117
+ const bodyPara = linesToParagraph(bodyLines);
1118
+ if (bodyPara) currentSegment.body.push(bodyPara);
1119
+ bodyLines = [];
1120
+ startStep(header, lineSegs, index === lines.length - 1 ? paragraphProperties(child) : void 0);
1121
+ continue;
1122
+ }
1123
+ bodyLines.push(lineSegs);
1124
+ }
1125
+ const bodyPara = linesToParagraph(bodyLines);
1126
+ if (bodyPara && child.data) bodyPara.data = child.data;
1127
+ if (bodyPara) currentSegment.body.push(bodyPara);
1128
+ }
1129
+ segments.push(currentSegment);
1130
+ return segments;
1131
+ };
1132
+ const buildTree$1 = (steps, targetDepth, start, end) => {
1133
+ const items = [];
1134
+ let i = start;
1135
+ while (i < end) {
1136
+ const s = steps[i];
1137
+ if (s?.depth !== targetDepth) {
1138
+ i++;
1139
+ continue;
1140
+ }
1141
+ let childEnd = i + 1;
1142
+ while (childEnd < end && (steps[childEnd]?.depth ?? 0) > targetDepth) childEnd++;
1143
+ const bodyChildren = [{
1144
+ type: "stepsTitle",
1145
+ children: s.titleNodes
1146
+ }, ...s.body];
1147
+ if (steps.slice(i + 1, childEnd).some((ss) => ss.depth === targetDepth + 1)) {
1148
+ const nested = buildTree$1(steps, targetDepth + 1, i + 1, childEnd);
1149
+ bodyChildren.push(nested);
1150
+ }
1151
+ items.push({
1152
+ type: "stepsItem",
1153
+ depth: targetDepth,
1154
+ number: s.number,
1155
+ children: bodyChildren,
1156
+ data: {
1157
+ attrsRole: "containerItem",
1158
+ attrsTitle: s.titleNodes,
1159
+ ...s.hProperties ? { hProperties: { ...s.hProperties } } : {}
1160
+ }
1161
+ });
1162
+ i = childEnd;
1163
+ }
1164
+ return {
1165
+ type: "stepsList",
1166
+ depth: targetDepth,
1167
+ children: items,
1168
+ data: { attrsRole: "container" }
1169
+ };
1170
+ };
1171
+ const processStepsInChildren = (children, containerClass, sourceLines) => {
1172
+ const newChildren = [];
1173
+ let i = 0;
1174
+ while (i < children.length) {
1175
+ const node = children[i];
1176
+ if (!node) break;
1177
+ if (node.type === "paragraph") {
1178
+ const counters = /* @__PURE__ */ new Map();
1179
+ const headerPara = extractHeaderPara$1(node, counters);
1180
+ if (headerPara) {
1181
+ const rawSteps = [...headerPara.steps];
1182
+ let rest = headerPara.rest;
1183
+ let restLine = headerPara.restLine;
1184
+ let j = i + 1;
1185
+ const lastStep = () => rawSteps[rawSteps.length - 1];
1186
+ let currentStepIdx = rawSteps.length - 1;
1187
+ let currentDepth = lastStep().depth;
1188
+ while (rest.length === 0 && j < children.length) {
1189
+ const sibling = children[j];
1190
+ if (!sibling) break;
1191
+ if (sibling.type === "blockquote") {
1192
+ const segments = splitBlockquote$1(sibling, counters, currentDepth, sourceLines);
1193
+ const firstSeg = segments[0];
1194
+ if (firstSeg?.header === null) rawSteps[currentStepIdx]?.body.push(...firstSeg.body);
1195
+ for (let s = 1; s < segments.length; s++) {
1196
+ const seg = segments[s];
1197
+ if (seg?.header) {
1198
+ rawSteps.push({
1199
+ ...seg.header,
1200
+ body: seg.body
1201
+ });
1202
+ currentStepIdx = rawSteps.length - 1;
1203
+ currentDepth = seg.header.depth;
1204
+ }
1205
+ }
1206
+ j++;
1207
+ } else if (sibling.type === "paragraph") {
1208
+ const prevDepth = lastStep().depth;
1209
+ const contHeaders = extractHeaderPara$1(sibling, counters, false, prevDepth);
1210
+ if (contHeaders) {
1211
+ rawSteps.push(...contHeaders.steps);
1212
+ rest = contHeaders.rest;
1213
+ restLine = contHeaders.restLine;
1214
+ currentStepIdx = rawSteps.length - 1;
1215
+ currentDepth = lastStep().depth;
1216
+ j++;
1217
+ continue;
1218
+ }
1219
+ break;
1220
+ } else break;
1221
+ }
1222
+ const list = buildTree$1(rawSteps, 1, 0, rawSteps.length);
1223
+ list.containerClass = containerClass;
1224
+ newChildren.push(list);
1225
+ children.splice(j, 0, ...splitAtGroupStarts$1(rest, restLine));
1226
+ i = j;
1227
+ continue;
1228
+ }
1229
+ }
1230
+ newChildren.push(node);
1231
+ i++;
1232
+ }
1233
+ children.splice(0, children.length, ...newChildren);
1234
+ };
1235
+ /**
1236
+ * 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.
1237
+ */
1238
+ const processSteps = (root, containerClass, source) => {
1239
+ const sourceLines = source?.split(/\r?\n/);
1240
+ const walk = (node) => {
1241
+ if (!Array.isArray(node.children) || node.children.length === 0) return;
1242
+ processStepsInChildren(node.children, containerClass, sourceLines);
1243
+ for (const child of node.children) walk(child);
1244
+ };
1245
+ processStepsInChildren(root.children, containerClass, sourceLines);
1246
+ for (const child of root.children) walk(child);
1247
+ };
1248
+ // src/tabs.ts
1249
+ const TAB_HEADER_RE = /^(%{1,6})(\+)? (.*)/;
1250
+ const parseTabHeader = (line) => {
1251
+ const m = TAB_HEADER_RE.exec(line);
1252
+ if (!m) return null;
1253
+ const [, percents = "", plus, label = ""] = m;
1254
+ return {
1255
+ depth: percents.length,
1256
+ open: plus === "+",
1257
+ label
1258
+ };
1259
+ };
1260
+ const djb2 = (str) => {
1261
+ let h = 5381;
1262
+ for (let i = 0; i < str.length; i++) h = (h << 5) + h + str.charCodeAt(i) >>> 0;
1263
+ return h;
1264
+ };
1265
+ /** Strip a trailing attrs block like `{.class #id}` from plain label text, so attrs-consumed markers don't leak into `aria-label`. */
1266
+ const stripAttrs = (text) => text.replace(/\s*\{[^}]*\}\s*$/, "").trimEnd();
1267
+ const slugify = (text) => text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
1268
+ const groupId = (labels) => {
1269
+ const key = [...labels].map(slugify).sort().join("-");
1270
+ return djb2(key).toString(16);
1271
+ };
1272
+ /**
1273
+ * 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.
1274
+ */
1275
+ const tabsBlockId = (index, groupId) => "tabs-" + djb2(`${index}:${groupId}`).toString(16);
1276
+ /** 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`. */
1277
+ const tabLabelText = (nodes) => nodes.map((n) => {
1278
+ if (n.type === "text" || n.type === "inlineCode") return "value" in n ? n.value : "";
1279
+ if ("children" in n) return tabLabelText(n.children);
1280
+ return "";
1281
+ }).join("");
1282
+ const extractHeaderPara = (para, requireDepthOne = true, prevDepth = 0) => {
1283
+ const lines = paraToLines(para);
1284
+ const tabs = [];
1285
+ let currentDepth = prevDepth;
1286
+ for (const [index, lineSegs] of lines.entries()) {
1287
+ const first = lineSegs[0];
1288
+ if (first?.type !== "text") return null;
1289
+ const header = parseTabHeader(first.value);
1290
+ if (!header) return null;
1291
+ const { depth, open } = header;
1292
+ if (tabs.length === 0 && requireDepthOne && depth !== 1) return null;
1293
+ if (depth > currentDepth + 1) return tabs.length > 0 ? {
1294
+ tabs,
1295
+ rest: lines.slice(index),
1296
+ restLine: (para.position?.start.line ?? 1) + index
1297
+ } : null;
1298
+ currentDepth = depth;
1299
+ const labelText = header.label;
1300
+ const labelNodes = labelText ? [{
1301
+ type: "text",
1302
+ value: labelText
1303
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1304
+ tabs.push({
1305
+ depth,
1306
+ open,
1307
+ labelNodes,
1308
+ labelText: tabLabelText(labelNodes),
1309
+ body: [],
1310
+ ...index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1311
+ });
1312
+ }
1313
+ return tabs.length > 0 ? {
1314
+ tabs,
1315
+ rest: [],
1316
+ restLine: 0
1317
+ } : null;
1318
+ };
1319
+ const splitAtGroupStarts = (lines, firstLine) => {
1320
+ const paragraphs = [];
1321
+ let start = 0;
1322
+ const flush = (end) => {
1323
+ const para = linesToParagraph(lines.slice(start, end));
1324
+ if (!para) return;
1325
+ para.position = {
1326
+ start: {
1327
+ line: firstLine + start,
1328
+ column: 1
1329
+ },
1330
+ end: {
1331
+ line: firstLine + end - 1,
1332
+ column: 1
1333
+ }
1334
+ };
1335
+ paragraphs.push(para);
1336
+ };
1337
+ lines.forEach((line, index) => {
1338
+ const first = line[0];
1339
+ if (first?.type === "text" && parseTabHeader(first.value)?.depth === 1 && index > start) {
1340
+ flush(index);
1341
+ start = index;
1342
+ }
1343
+ });
1344
+ flush(lines.length);
1345
+ return paragraphs;
1346
+ };
1347
+ const isExplicitBQ = (bq) => bq.children.some((child) => {
1348
+ if (child.type !== "paragraph") return false;
1349
+ const first = child.children[0];
1350
+ return first?.type === "text" && parseTabHeader(first.value.split("\n")[0] ?? "") !== null;
1351
+ });
1352
+ const extractTrailingTabHeader = (node, sourceLines) => {
1353
+ const path = [];
1354
+ let current = node;
1355
+ while ("children" in current && current.children.length > 0) {
1356
+ const last = current.children[current.children.length - 1];
1357
+ if (last.type === "paragraph") {
1358
+ path.push(current);
1359
+ const lines = paraToLines(last, sourceLines);
1360
+ const at = lines.findIndex((line) => line[0]?.type === "text" && parseTabHeader(line[0].value));
1361
+ if (at === -1) return null;
1362
+ const headerLine = lines[at] ?? [];
1363
+ const first = headerLine[0];
1364
+ const header = first?.type === "text" ? parseTabHeader(first.value) : null;
1365
+ if (!header) return null;
1366
+ const labelText = header.label;
1367
+ const labelNodes = labelText ? [{
1368
+ type: "text",
1369
+ value: labelText
1370
+ }, ...headerLine.slice(1)] : headerLine.slice(1);
1371
+ return {
1372
+ header: {
1373
+ depth: header.depth,
1374
+ open: header.open,
1375
+ labelNodes,
1376
+ labelText: tabLabelText(labelNodes),
1377
+ body: [],
1378
+ ...at === lines.length - 1 ? { hProperties: paragraphProperties(last) } : {}
1379
+ },
1380
+ truncatedPara: linesToParagraph(lines.slice(0, at)),
1381
+ rest: lines.slice(at + 1),
1382
+ path,
1383
+ start: last.position?.start.line,
1384
+ at,
1385
+ line: headerLine
1386
+ };
1387
+ }
1388
+ if (!("children" in last)) break;
1389
+ path.push(current);
1390
+ current = last;
1391
+ }
1392
+ return null;
1393
+ };
1394
+ const splitBlockquote = (bq, explicit, sourceLines) => {
1395
+ const segments = [];
1396
+ let currentSegment = {
1397
+ header: null,
1398
+ body: [],
1399
+ explicit,
1400
+ lines: []
1401
+ };
1402
+ const splitLines = (lines, markersOf, para) => {
1403
+ let bodyLines = [];
1404
+ for (const [index, lineSegs] of lines.entries()) {
1405
+ const first = lineSegs[0];
1406
+ if (first?.type === "text") {
1407
+ const header = parseTabHeader(first.value);
1408
+ if (header) {
1409
+ if (bodyLines.length > 0) {
1410
+ const bodyPara = linesToParagraph(bodyLines);
1411
+ if (bodyPara) currentSegment.body.push(bodyPara);
1412
+ bodyLines = [];
1413
+ }
1414
+ segments.push(currentSegment);
1415
+ const labelText = header.label;
1416
+ const labelNodes = labelText ? [{
1417
+ type: "text",
1418
+ value: labelText
1419
+ }, ...lineSegs.slice(1)] : lineSegs.slice(1);
1420
+ currentSegment = {
1421
+ header: {
1422
+ depth: header.depth,
1423
+ open: header.open,
1424
+ labelNodes,
1425
+ labelText: tabLabelText(labelNodes),
1426
+ body: [],
1427
+ ...para && index === lines.length - 1 ? { hProperties: paragraphProperties(para) } : {}
1428
+ },
1429
+ body: [],
1430
+ explicit,
1431
+ lines: [],
1432
+ line: lineSegs,
1433
+ ...markersOf ? { markers: markersOf(index) } : {}
1434
+ };
1435
+ continue;
1436
+ }
1437
+ }
1438
+ bodyLines.push(lineSegs);
1439
+ if (markersOf) currentSegment.lines.push({
1440
+ segs: lineSegs,
1441
+ markers: markersOf(index)
1442
+ });
1443
+ }
1444
+ if (bodyLines.length > 0) {
1445
+ const bodyPara = linesToParagraph(bodyLines);
1446
+ if (bodyPara && para?.data) bodyPara.data = para.data;
1447
+ if (bodyPara) currentSegment.body.push(bodyPara);
1448
+ }
1449
+ };
1450
+ for (const child of bq.children) {
1451
+ if (child.type !== "paragraph") {
1452
+ const extracted = extractTrailingTabHeader(child, sourceLines);
1453
+ if (extracted) {
1454
+ const cloned = JSON.parse(JSON.stringify(child));
1455
+ let target = cloned;
1456
+ for (let pi = 1; pi < extracted.path.length; pi++) target = target.children[target.children.length - 1];
1457
+ const deepestParent = target;
1458
+ if (extracted.truncatedPara) deepestParent.children[deepestParent.children.length - 1] = extracted.truncatedPara;
1459
+ else deepestParent.children.splice(deepestParent.children.length - 1, 1);
1460
+ currentSegment.body.push(cloned);
1461
+ segments.push(currentSegment);
1462
+ const { start, at } = extracted;
1463
+ const markersOf = sourceLines && start !== void 0 ? (index) => markerCount(sourceLines[start - 1 + index] ?? "") : void 0;
1464
+ currentSegment = {
1465
+ header: extracted.header,
1466
+ body: [],
1467
+ explicit,
1468
+ lines: [],
1469
+ line: extracted.line,
1470
+ ...markersOf ? { markers: markersOf(at) } : {}
1471
+ };
1472
+ splitLines(extracted.rest, markersOf ? (index) => markersOf(at + 1 + index) : void 0);
1473
+ } else currentSegment.body.push(child);
1474
+ continue;
1475
+ }
1476
+ const start = child.position?.start.line;
1477
+ splitLines(paraToLines(child, sourceLines), sourceLines && start !== void 0 ? (index) => markerCount(sourceLines[start - 1 + index] ?? "") : void 0, child);
1478
+ }
1479
+ segments.push(currentSegment);
1480
+ return segments;
1481
+ };
1482
+ const afterSkip = (segments, level, quoted) => {
1483
+ const lines = [];
1484
+ const blocks = [];
1485
+ for (const seg of segments) {
1486
+ if (level === void 0 || seg.markers === void 0) {
1487
+ if (seg.line) lines.push({
1488
+ segs: seg.line,
1489
+ markers: 0
1490
+ });
1491
+ blocks.push(...seg.body);
1492
+ continue;
1493
+ }
1494
+ if (seg.line) lines.push({
1495
+ segs: seg.line,
1496
+ markers: seg.markers - level
1497
+ });
1498
+ lines.push(...seg.lines.map((line) => ({
1499
+ ...line,
1500
+ markers: line.markers - level
1501
+ })));
1502
+ blocks.push(...seg.body.filter((block) => block.type !== "paragraph"));
1503
+ }
1504
+ const content = rebuildLazyLines(lines, 0);
1505
+ if (blocks.length > 0 && !quoted) content.push(...blocks);
1506
+ else if (blocks.length > 0) {
1507
+ const last = content.at(-1);
1508
+ if (last?.type === "blockquote") last.children.push(...blocks);
1509
+ else content.push({
1510
+ type: "blockquote",
1511
+ children: blocks
1512
+ });
1513
+ }
1514
+ return content;
1515
+ };
1516
+ const buildTree = (rawTabs, targetDepth, start, end, containerClass) => {
1517
+ const depthLabels = rawTabs.slice(start, end).filter((t) => t.depth === targetDepth).map((t) => stripAttrs(t.labelText));
1518
+ const gId = groupId(depthLabels);
1519
+ const explicitOpenIdx = rawTabs.slice(start, end).findIndex((t) => t.depth === targetDepth && t.open);
1520
+ const explicitOpenAbsIdx = explicitOpenIdx >= 0 ? start + explicitOpenIdx : -1;
1521
+ const items = [];
1522
+ let tabCounter = 0;
1523
+ let i = start;
1524
+ while (i < end) {
1525
+ const raw = rawTabs[i];
1526
+ if (!raw) break;
1527
+ if (raw.depth !== targetDepth) {
1528
+ i++;
1529
+ continue;
1530
+ }
1531
+ let childEnd = i + 1;
1532
+ while (childEnd < end && (rawTabs[childEnd]?.depth ?? 0) > targetDepth) childEnd++;
1533
+ const isOpen = explicitOpenAbsIdx >= 0 ? i === explicitOpenAbsIdx : tabCounter === 0;
1534
+ const bodyChildren = [{
1535
+ type: "tabsLabel",
1536
+ children: raw.labelNodes
1537
+ }, ...raw.body];
1538
+ if (rawTabs.slice(i + 1, childEnd).some((t) => t.depth === targetDepth + 1)) {
1539
+ const nested = buildTree(rawTabs, targetDepth + 1, i + 1, childEnd, containerClass);
1540
+ nested.containerClass = containerClass;
1541
+ bodyChildren.push(nested);
1542
+ }
1543
+ bodyChildren.push(...raw.after ?? []);
1544
+ items.push({
1545
+ type: "tabsItem",
1546
+ depth: targetDepth,
1547
+ labelText: raw.labelText,
1548
+ open: isOpen,
1549
+ children: bodyChildren,
1550
+ data: {
1551
+ attrsRole: "containerItem",
1552
+ attrsTitle: raw.labelNodes,
1553
+ attrsItemTitle: true,
1554
+ hProperties: { ...raw.hProperties }
1555
+ }
1556
+ });
1557
+ tabCounter++;
1558
+ i = childEnd;
1559
+ }
1560
+ return {
1561
+ type: "tabsList",
1562
+ depth: targetDepth,
1563
+ groupId: gId,
1564
+ children: items,
1565
+ data: {
1566
+ attrsRole: "container",
1567
+ hProperties: {}
1568
+ }
1569
+ };
1570
+ };
1571
+ const processTabsInChildren = (children, containerClass, checkBlankLines = true, sourceLines) => {
1572
+ const newChildren = [];
1573
+ let i = 0;
1574
+ while (i < children.length) {
1575
+ const node = children[i];
1576
+ if (!node) break;
1577
+ if (node.type === "paragraph") {
1578
+ const headerPara = extractHeaderPara(node);
1579
+ if (headerPara) {
1580
+ const rawTabs = [...headerPara.tabs];
1581
+ const lastTab = () => rawTabs[rawTabs.length - 1];
1582
+ let rest = headerPara.rest;
1583
+ let restLine = headerPara.restLine;
1584
+ let j = i + 1;
1585
+ let currentTabIdx = rawTabs.length - 1;
1586
+ let prevEndLine = node.position?.end.line ?? 0;
1587
+ while (rest.length === 0 && j < children.length) {
1588
+ const sibling = children[j];
1589
+ if (!sibling) break;
1590
+ const siblingStartLine = sibling.position?.start.line ?? prevEndLine + 1;
1591
+ if (checkBlankLines && siblingStartLine > prevEndLine + 1) break;
1592
+ if (sibling.type === "blockquote") {
1593
+ const bq = sibling;
1594
+ const explicit = isExplicitBQ(bq);
1595
+ const segments = splitBlockquote(bq, explicit, sourceLines);
1596
+ const base = explicit && sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1597
+ const firstSeg = segments[0];
1598
+ if (firstSeg?.header === null) rawTabs[currentTabIdx]?.body.push(...firstSeg.body);
1599
+ const depthOffset = explicit ? lastTab().depth : 0;
1600
+ const owner = lastTab();
1601
+ let ending = null;
1602
+ for (const [index, seg] of segments.slice(1).entries()) {
1603
+ if (!seg.header) continue;
1604
+ const markers = seg.markers;
1605
+ if (explicit && markers !== void 0 && base !== void 0 && markers < base) {
1606
+ if (seg.header.depth > lastTab().depth + 1) continue;
1607
+ const relative = seg.lines.map((line) => ({
1608
+ ...line,
1609
+ markers: line.markers - (markers + 1)
1610
+ }));
1611
+ seg.header.body.push(...rebuildLazyLines(relative, 0), ...seg.body.slice(seg.lines.length > 0 ? 1 : 0));
1612
+ rawTabs.push(seg.header);
1613
+ currentTabIdx = rawTabs.length - 1;
1614
+ continue;
1615
+ }
1616
+ const adjustedDepth = depthOffset + seg.header.depth;
1617
+ if (adjustedDepth > lastTab().depth + 1) {
1618
+ if (explicit) {
1619
+ owner.after = [...owner.after ?? [], ...afterSkip([seg], base, false)];
1620
+ continue;
1621
+ }
1622
+ const quoteMarkers = sourceLines ? blockquoteMarkers(bq, sourceLines) : void 0;
1623
+ ending = afterSkip(segments.slice(index + 1), quoteMarkers === void 0 ? void 0 : quoteMarkers - 1, true);
1624
+ break;
1625
+ }
1626
+ seg.header.depth = adjustedDepth;
1627
+ if (explicit) {
1628
+ const deferredInnerSegs = [];
1629
+ const escapedSegs = [];
1630
+ for (const b of seg.body) {
1631
+ if (b.type !== "blockquote") {
1632
+ const escaped = escapedSegs.at(-1);
1633
+ if (escaped) escaped.body.push(b);
1634
+ else seg.header.body.push(b);
1635
+ continue;
1636
+ }
1637
+ const innerBq = b;
1638
+ if (!isExplicitBQ(innerBq)) {
1639
+ const innerSegs = splitBlockquote(innerBq, false, sourceLines);
1640
+ const innerFirst = innerSegs[0];
1641
+ if (innerFirst?.header === null) seg.header.body.push(...innerFirst.body);
1642
+ for (const innerSeg of innerSegs.slice(1)) {
1643
+ if (!innerSeg.header) continue;
1644
+ const markers = innerSeg.markers;
1645
+ if (markers !== void 0 && base !== void 0) {
1646
+ if (markers < base) {
1647
+ const relative = innerSeg.lines.map((line) => ({
1648
+ ...line,
1649
+ markers: line.markers - (markers + 1)
1650
+ }));
1651
+ innerSeg.header.body.push(...rebuildLazyLines(relative, 0));
1652
+ escapedSegs.push(innerSeg.header);
1653
+ deferredInnerSegs.push(innerSeg);
1654
+ continue;
1655
+ }
1656
+ } else if (innerSeg.body.length === 0) {
1657
+ innerSeg.header.depth = depthOffset;
1658
+ escapedSegs.push(innerSeg.header);
1659
+ deferredInnerSegs.push(innerSeg);
1660
+ continue;
1661
+ }
1662
+ const tentativeDepth = depthOffset + innerSeg.header.depth;
1663
+ if (tentativeDepth > lastTab().depth + 1) continue;
1664
+ innerSeg.header.depth = tentativeDepth;
1665
+ innerSeg.header.body.push(...innerSeg.body);
1666
+ deferredInnerSegs.push(innerSeg);
1667
+ }
1668
+ } else seg.header.body.push(...innerBq.children);
1669
+ }
1670
+ rawTabs.push(seg.header);
1671
+ currentTabIdx = rawTabs.length - 1;
1672
+ for (const innerSeg of deferredInnerSegs) {
1673
+ if (!innerSeg.header) continue;
1674
+ rawTabs.push(innerSeg.header);
1675
+ currentTabIdx = rawTabs.length - 1;
1676
+ }
1677
+ } else {
1678
+ seg.header.body.push(...seg.body);
1679
+ rawTabs.push(seg.header);
1680
+ currentTabIdx = rawTabs.length - 1;
1681
+ }
1682
+ }
1683
+ prevEndLine = sibling.position?.end.line ?? prevEndLine;
1684
+ j++;
1685
+ if (ending) {
1686
+ children.splice(j, 0, ...ending);
1687
+ break;
1688
+ }
1689
+ } else if (sibling.type === "paragraph") {
1690
+ const prevDepth = lastTab().depth;
1691
+ const contHeaders = extractHeaderPara(sibling, false, prevDepth);
1692
+ if (contHeaders) {
1693
+ rawTabs.push(...contHeaders.tabs);
1694
+ rest = contHeaders.rest;
1695
+ restLine = contHeaders.restLine;
1696
+ currentTabIdx = rawTabs.length - 1;
1697
+ prevEndLine = sibling.position?.end.line ?? prevEndLine;
1698
+ j++;
1699
+ continue;
1700
+ }
1701
+ break;
1702
+ } else break;
1703
+ }
1704
+ const list = buildTree(rawTabs, 1, 0, rawTabs.length, containerClass);
1705
+ list.containerClass = containerClass;
1706
+ newChildren.push(list);
1707
+ children.splice(j, 0, ...splitAtGroupStarts(rest, restLine));
1708
+ i = j;
1709
+ continue;
1710
+ }
1711
+ }
1712
+ newChildren.push(node);
1713
+ i++;
1714
+ }
1715
+ children.splice(0, children.length, ...newChildren);
1716
+ };
1717
+ /**
1718
+ * 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.
1719
+ */
1720
+ const processTabs = (root, containerClass, source) => {
1721
+ const sourceLines = source?.split(/\r?\n/);
1722
+ const walk = (node) => {
1723
+ if (!Array.isArray(node.children) || node.children.length === 0) return;
1724
+ processTabsInChildren(node.children, containerClass, false, sourceLines);
1725
+ for (const child of node.children) walk(child);
1726
+ };
1727
+ processTabsInChildren(root.children, containerClass, true, sourceLines);
1728
+ for (const child of root.children) walk(child);
1729
+ };
1730
+ // src/editable.ts
1731
+ const editableTree = (root, build, setChildren, setData) => {
1732
+ const originals = /* @__PURE__ */ new Map();
1733
+ const copy = (node) => {
1734
+ const children = node.children;
1735
+ if (!children) {
1736
+ const leaf = { ...node };
1737
+ originals.set(leaf, {
1738
+ node,
1739
+ children: []
1740
+ });
1741
+ return leaf;
1742
+ }
1743
+ const branch = {
1744
+ ...node,
1745
+ children: children.map(copy)
1746
+ };
1747
+ originals.set(branch, {
1748
+ node,
1749
+ children
1750
+ });
1751
+ return branch;
1752
+ };
1753
+ const writeBack = (node) => {
1754
+ const original = originals.get(node);
1755
+ if (!original) return build(node, writeBack);
1756
+ if (setData && node.data !== original.node.data) setData(original.node, node.data);
1757
+ if (!node.children) return original.node;
1758
+ const children = node.children.map(writeBack);
1759
+ if (children.length !== original.children.length || children.some((child, index) => child !== original.children[index])) setChildren(original.node, children);
1760
+ return original.node;
1761
+ };
1762
+ return {
1763
+ tree: copy(root),
1764
+ writeBack
1765
+ };
1766
+ };
1767
+ // src/inline-svg.ts
1768
+ const hash = (str) => {
1769
+ let h = 2166136261;
1770
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
1771
+ return h.toString(36);
1772
+ };
1773
+ /**
1774
+ * 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).
1775
+ */
1776
+ const svgSpriteId = (relativePath) => `svg-sprite-${hash(relativePath)}`;
1777
+ const extractSvgParts = (content) => {
1778
+ const openTag = /<svg([^>]*?)>/i.exec(content)?.[1] ?? ``;
1779
+ return {
1780
+ viewBox: /viewBox="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
1781
+ width: /\bwidth="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
1782
+ height: /\bheight="([^"]*)"/i.exec(openTag)?.[1] ?? ``,
1783
+ inner: /<svg[^>]*>([\s\S]*?)<\/svg\s*>/i.exec(content)?.[1]?.trim() ?? ``
1784
+ };
1785
+ };
1786
+ const escAttr = (s) => s.replace(/&/g, `&amp;`).replace(/"/g, `&quot;`);
1787
+ const buildFullInline = (content, alt) => {
1788
+ if (!alt) return content;
1789
+ return content.replace(/<svg([^>]*)>/i, `<svg$1 aria-label="${escAttr(alt)}">`);
1790
+ };
1791
+ 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>`;
1792
+ const buildUseHtml = (id, viewBox, width, height, alt) => {
1793
+ return `<svg ${[
1794
+ viewBox && `viewBox="${escAttr(viewBox)}"`,
1795
+ width && `width="${escAttr(width)}"`,
1796
+ height && `height="${escAttr(height)}"`,
1797
+ alt && `aria-label="${escAttr(alt)}"`
1798
+ ].filter(Boolean).join(` `)}><use href="#${id}"></use></svg>`;
1799
+ };
1800
+ /**
1801
+ * 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.
1802
+ */
1803
+ const inlineSvgMarkup = (content, alts, relativePath, limits) => {
1804
+ if (content.length > limits.maxImageSize) return null;
1805
+ if (alts.length > limits.maxOccurrences) return null;
1806
+ if (alts.length * content.length > limits.maxTotalSize) return null;
1807
+ if (!limits.deduplication || alts.length < 2) return alts.map((alt) => buildFullInline(content, alt));
1808
+ const { viewBox, width, height, inner } = extractSvgParts(content);
1809
+ const id = svgSpriteId(relativePath);
1810
+ const sprite = buildSpriteHtml(id, viewBox, inner);
1811
+ return alts.map((alt, index) => {
1812
+ const use = buildUseHtml(id, viewBox, width, height, alt);
1813
+ return index === 0 ? sprite + use : use;
1814
+ });
1815
+ };
1816
+ export { ALERT_ALIASES, DEFAULT_TITLE, ICONS, LIGHTBOX_MARKER, createAbbreviationScan, createAlertResolver, createDelimitedTransform, createKbdTransform, editableTree, escapedIndices, headingRank, inlineSvgMarkup, isEmptyParagraph, isImageParagraph, lightboxDialog, lightboxId, lightboxTrigger, maskEscaped, maskTreeEscapes, parseAbbreviations, placeCutAttributes, plainText, processSteps, processStepsInChildren, processTabs, processTabsInChildren, scriptSyntax, sectionize, splitAlertParagraph, stripAttrs, svgSpriteId, tabLabelText, tabsBlockId, unmaskEscaped };
1817
+
1818
+ //# sourceMappingURL=index.mjs.map