@pierre/words 0.0.0-stage → 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.
@@ -0,0 +1,1094 @@
1
+ import { createHtmlSession } from "@pierre/words";
2
+ //#region html/render.ts
3
+ const lineGroup = "<span data-markdown=\"code-lines\">";
4
+ /**
5
+ * Wrap code in groups of 64 lines, so the browser lays out only the last group
6
+ * when a streamed line changes. Each LF stays inside its group, so `textContent`
7
+ * does not change. An LF inside an element never splits a group.
8
+ */
9
+ function groupLines(body) {
10
+ let result = "";
11
+ let start = 0;
12
+ let lines = 0;
13
+ let depth = 0;
14
+ let tag = body.indexOf("<");
15
+ for (let at = body.indexOf("\n"); at !== -1; at = body.indexOf("\n", at + 1)) {
16
+ for (; tag !== -1 && tag < at; tag = body.indexOf("<", tag + 1)) if (body.charCodeAt(tag + 1) === 47) depth--;
17
+ else if (body.charCodeAt(body.indexOf(">", tag) - 1) !== 47) depth++;
18
+ if (++lines >= 64 && depth === 0) {
19
+ result += lineGroup + body.slice(start, at + 1) + "</span>";
20
+ start = at + 1;
21
+ lines = 0;
22
+ }
23
+ }
24
+ return result === "" || start < body.length ? result + lineGroup + body.slice(start) + "</span>" : result;
25
+ }
26
+ /** Gutter line numbers, in the same 64-line groups as the code. */
27
+ function lineNumbers(count) {
28
+ let html = "";
29
+ for (let line = 1; line <= count; line += 64) {
30
+ html += lineGroup + line;
31
+ for (let next = line + 1; next < line + 64 && next <= count; next++) html += "\n" + next;
32
+ html += (line + 64 <= count ? "\n" : "") + "</span>";
33
+ }
34
+ return html;
35
+ }
36
+ /** The code buttons as bits: 1 for copy, 2 for download. Equal inline option objects give equal bits. */
37
+ const controlFlags = (controls) => {
38
+ const code = typeof controls === "object" ? controls.code : controls;
39
+ return code === false ? 0 : typeof code === "object" ? (code.copy === false ? 0 : 1) | (code.download === false ? 0 : 2) : 3;
40
+ };
41
+ /** The HTML before and after the code of a fenced block. */
42
+ function codeWrapper(attributes, language, incomplete, numbers, options) {
43
+ const flags = controlFlags(options.controls);
44
+ const button = (action) => `<button type="button" data-code-action="${action.toLowerCase()}" aria-label="${action} code"${incomplete ? " disabled=\"\"" : ""}>${action}</button>`;
45
+ return [`<div data-markdown="code-block" data-language="${escapeHtml(language)}"${incomplete ? " data-incomplete=\"true\"" : ""}><div data-markdown="code-header"><span>${escapeHtml(language)}</span><span>${(flags & 1) !== 0 ? button("Copy") : ""}${(flags & 2) !== 0 ? button("Download") : ""}</span></div><pre dir="ltr">${numbers === void 0 ? "" : `<span data-markdown="line-numbers" aria-hidden="true">${lineNumbers(numbers)}</span>`}<code${attributes}>`, "</code></pre></div>"];
46
+ }
47
+ /** Escape text for use in HTML. */
48
+ function escapeHtml(value) {
49
+ let result = "";
50
+ let from = 0;
51
+ for (let at = 0; at < value.length; at++) {
52
+ let entity;
53
+ switch (value.charCodeAt(at)) {
54
+ case 34:
55
+ entity = "&quot;";
56
+ break;
57
+ case 38:
58
+ entity = "&amp;";
59
+ break;
60
+ case 39:
61
+ entity = "&#39;";
62
+ break;
63
+ case 60:
64
+ entity = "&lt;";
65
+ break;
66
+ case 62:
67
+ entity = "&gt;";
68
+ break;
69
+ default: continue;
70
+ }
71
+ result += value.slice(from, at) + entity;
72
+ from = at + 1;
73
+ }
74
+ return from === 0 ? value : result + value.slice(from);
75
+ }
76
+ /** Decode only the entities that Wasm writes. The result is text, not HTML. */
77
+ function decodeHtml(value) {
78
+ let result = "";
79
+ let from = 0;
80
+ for (let at = value.indexOf("&"); at !== -1; at = value.indexOf("&", at + 1)) {
81
+ let character;
82
+ let end = at + 3;
83
+ while (end < at + 6 && value.charCodeAt(end) !== 59) end++;
84
+ switch (value.slice(at + 1, end)) {
85
+ case "amp":
86
+ character = "&";
87
+ break;
88
+ case "lt":
89
+ character = "<";
90
+ break;
91
+ case "gt":
92
+ character = ">";
93
+ break;
94
+ case "quot":
95
+ character = "\"";
96
+ break;
97
+ case "#39":
98
+ case "#x27":
99
+ character = "'";
100
+ break;
101
+ default: continue;
102
+ }
103
+ result += value.slice(from, at) + character;
104
+ from = end + 1;
105
+ }
106
+ return from === 0 ? value : result + value.slice(from);
107
+ }
108
+ /** Hooks can come from untyped code, and a non-string would print as text. */
109
+ function hookHtml(html) {
110
+ if (typeof html !== "string") throw new TypeError("HTML hooks must return strings");
111
+ return html;
112
+ }
113
+ /**
114
+ * Return `url` if it is relative or uses http, https, mailto, tel, or a scheme
115
+ * the parser autolinks (ftp, xmpp). Reject network paths.
116
+ */
117
+ function defaultUrlTransform(url) {
118
+ let normalized = "";
119
+ let from = 0;
120
+ for (let at = 0; at < url.length; at++) {
121
+ const code = url.charCodeAt(at);
122
+ if (code <= 32 || code >= 127 && code <= 159) {
123
+ normalized += url.slice(from, at);
124
+ from = at + 1;
125
+ }
126
+ }
127
+ normalized = from === 0 ? url : normalized + url.slice(from);
128
+ const second = normalized.charCodeAt(1);
129
+ if (normalized.charCodeAt(0) === 92 || normalized.charCodeAt(0) === 47 && (second === 47 || second === 92)) return void 0;
130
+ let colon = -1;
131
+ scheme: for (let at = 0; at < normalized.length; at++) switch (normalized.charCodeAt(at)) {
132
+ case 35:
133
+ case 47:
134
+ case 63: break scheme;
135
+ case 58: colon = at;
136
+ }
137
+ if (colon >= 0) switch (normalized.slice(0, colon).toLowerCase()) {
138
+ case "http":
139
+ case "https":
140
+ case "ftp":
141
+ case "mailto":
142
+ case "tel":
143
+ case "xmpp": break;
144
+ default: return;
145
+ }
146
+ return url;
147
+ }
148
+ const voidTags = /* @__PURE__ */ new Set([
149
+ "br",
150
+ "hr",
151
+ "img",
152
+ "input"
153
+ ]);
154
+ /** Whether the tag at [at, end) of Wasm HTML is void. All but `<input>` end with ` />`. */
155
+ const isVoidAt = (html, at, end) => html.charCodeAt(end - 2) === 47 || html.startsWith("<input", at);
156
+ /** Whether a character code continues a tag name, as `\w` does. */
157
+ const isNameCode = (code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
158
+ /** The language of a fence from the attributes of its `<code>`. */
159
+ function codeLanguage(attributes) {
160
+ const at = attributes.indexOf("class=\"language-");
161
+ return at < 0 ? "text" : decodeHtml(attributes.slice(at + 16, attributes.indexOf("\"", at + 16)));
162
+ }
163
+ /**
164
+ * Run `transform` on each `tag` element, without a syntax tree. Inner elements
165
+ * run first, and each element runs once.
166
+ */
167
+ function mapElements(html, tag, transform) {
168
+ const expression = new RegExp(`<(/?)${tag}\\b[^>]*>`, "g");
169
+ const isVoid = voidTags.has(tag);
170
+ const open = [];
171
+ let result = "";
172
+ let cursor = 0;
173
+ for (let match = expression.exec(html); match !== null; match = expression.exec(html)) {
174
+ let piece = html.slice(cursor, match.index);
175
+ cursor = expression.lastIndex;
176
+ if (match[1] === "" && !isVoid) {
177
+ if (open.length === 0) result += piece;
178
+ else open[open.length - 1] += piece;
179
+ open.push(match[0]);
180
+ continue;
181
+ }
182
+ if (match[1] !== "" && (isVoid || open.length === 0)) piece += match[0];
183
+ else {
184
+ const replacement = hookHtml(transform(match[1] === "" ? match[0] : open.pop() + piece + match[0]));
185
+ piece = match[1] === "" ? piece + replacement : replacement;
186
+ }
187
+ if (open.length === 0) result += piece;
188
+ else open[open.length - 1] += piece;
189
+ }
190
+ return result + open.join("") + html.slice(cursor);
191
+ }
192
+ /** Apply element filters and URL checks to HTML from Wasm, and add `data-markdown`. Runs before hooks. */
193
+ function decorate(html, options) {
194
+ const { allowedElements, disallowedElements } = options;
195
+ if (allowedElements !== void 0 && disallowedElements !== void 0) throw new Error("Use either allowedElements or disallowedElements");
196
+ const filtered = allowedElements !== void 0 || disallowedElements !== void 0;
197
+ const excluded = (tag) => allowedElements !== void 0 ? !allowedElements.includes(tag) : disallowedElements.includes(tag);
198
+ let result = "";
199
+ let from = 0;
200
+ let dropped = 0;
201
+ for (let at = html.indexOf("<"), end = 0; at !== -1; at = html.indexOf("<", end)) {
202
+ const closing = html.charCodeAt(at + 1) === 47;
203
+ if (closing && !filtered) {
204
+ end = at + 2;
205
+ continue;
206
+ }
207
+ end = html.indexOf(">", at) + 1;
208
+ if (end === 0) break;
209
+ if (dropped > 0) {
210
+ if (!closing) dropped += isVoidAt(html, at, end) ? 0 : 1;
211
+ else if (--dropped === 0) from = end;
212
+ continue;
213
+ }
214
+ if (closing) {
215
+ if (options.unwrapDisallowed === true && excluded(html.slice(at + 2, end - 1))) {
216
+ result += html.slice(from, at);
217
+ from = end;
218
+ }
219
+ continue;
220
+ }
221
+ let space = at + 1;
222
+ for (let code = html.charCodeAt(space); code !== 32 && code !== 62; code = html.charCodeAt(++space));
223
+ const tag = html.slice(at + 1, space);
224
+ if (filtered && excluded(tag)) {
225
+ result += html.slice(from, at);
226
+ from = end;
227
+ if (options.unwrapDisallowed !== true && !voidTags.has(tag)) dropped = 1;
228
+ continue;
229
+ }
230
+ if (space === end - 1) {
231
+ result += html.slice(from, space) + " data-markdown=\"" + tag + "\">";
232
+ from = end;
233
+ continue;
234
+ }
235
+ result += html.slice(from, at);
236
+ from = end;
237
+ let stop = end - 1;
238
+ if (html.charCodeAt(stop - 1) === 47) for (stop--; html.charCodeAt(stop - 1) === 32; stop--);
239
+ const text = html.slice(space, stop);
240
+ const pending = tag === "a" && text === " data-markdown-incomplete";
241
+ let attributes = "";
242
+ if (!pending && text !== "") {
243
+ const note = tag === "a" ? text.indexOf("\" data-footnote-") : -1;
244
+ const footnote = note > 0 && text.charCodeAt(note - 1) !== 61;
245
+ let invalidImage = false;
246
+ let copied = 0;
247
+ for (let equals = text.indexOf("=\""); equals !== -1; equals = text.indexOf("=\"", copied)) {
248
+ const start = text.lastIndexOf(" ", equals);
249
+ const key = text.slice(start + 1, equals);
250
+ const close = text.indexOf("\"", equals + 2);
251
+ const value = text.slice(equals + 2, close);
252
+ attributes += text.slice(copied, start);
253
+ copied = close + 1;
254
+ switch (key) {
255
+ case "align":
256
+ attributes += ` style="text-align:${value}"`;
257
+ break;
258
+ case "id":
259
+ attributes += ` id="${escapeHtml(options.idPrefix ?? "")}${value}"`;
260
+ break;
261
+ case "href":
262
+ case "src": {
263
+ const decoded = decodeHtml(value);
264
+ const transformed = options.urlTransform === void 0 ? decoded : options.urlTransform(decoded, key);
265
+ let url = transformed == null ? "" : defaultUrlTransform(transformed) ?? "";
266
+ if (url === "" && transformed !== "") {
267
+ invalidImage ||= tag === "img";
268
+ break;
269
+ }
270
+ if (footnote && url.startsWith("#")) url = "#" + (options.idPrefix ?? "") + url.slice(1);
271
+ attributes += ` ${key}="${escapeHtml(url)}"`;
272
+ break;
273
+ }
274
+ default: attributes += text.slice(start, copied);
275
+ }
276
+ }
277
+ if (invalidImage) continue;
278
+ attributes += text.slice(copied);
279
+ }
280
+ if (tag === "img") attributes += " loading=\"lazy\"";
281
+ else if (tag === "input") attributes += " readonly=\"\"";
282
+ result += `<${tag} data-markdown="${pending ? "incomplete-link" : tag}"${attributes}>`;
283
+ }
284
+ return result + html.slice(from);
285
+ }
286
+ /** Format session output with HTML options. Keeps completed HTML and at most one live highlighter. */
287
+ function createHtmlRenderer(options = {}) {
288
+ let parser = createHtmlSession();
289
+ let stable = "";
290
+ let stableRaw = "";
291
+ let nextCode = 0;
292
+ let live;
293
+ let liveUses = 0;
294
+ let previousUpdate;
295
+ let result;
296
+ let revision = 0;
297
+ let pendingRaw = "";
298
+ let pendingInput;
299
+ let pendingActive = false;
300
+ let pendingCode = -1;
301
+ let pendingHtml = "";
302
+ let pendingFence;
303
+ let codeCache = /* @__PURE__ */ new Map();
304
+ let oldCodeCache = /* @__PURE__ */ new Map();
305
+ let fenceKey = 0;
306
+ let fence;
307
+ const dispose = () => {
308
+ const session = live?.session;
309
+ live = void 0;
310
+ session?.dispose();
311
+ };
312
+ /** Drop all state after an option change or a hook error, so the next render starts clean. */
313
+ const resetState = () => {
314
+ dispose();
315
+ parser = createHtmlSession();
316
+ stable = "";
317
+ stableRaw = "";
318
+ nextCode = 0;
319
+ previousUpdate = void 0;
320
+ result = void 0;
321
+ pendingInput = void 0;
322
+ pendingHtml = "";
323
+ pendingActive = false;
324
+ pendingCode = -1;
325
+ pendingFence = void 0;
326
+ codeCache = /* @__PURE__ */ new Map();
327
+ oldCodeCache = /* @__PURE__ */ new Map();
328
+ fence = void 0;
329
+ };
330
+ const format = (input, firstCode, active) => {
331
+ const decorated = decorate(input, options);
332
+ let html = "";
333
+ let index = firstCode;
334
+ let cursor = 0;
335
+ for (let at = decorated.indexOf("<pre "), scan = 0; at !== -1; at = decorated.indexOf("<pre ", scan)) {
336
+ const pre = decorated.indexOf(">", at);
337
+ scan = pre;
338
+ if (!decorated.startsWith("<code ", pre + 1)) continue;
339
+ const open = decorated.indexOf(">", pre + 1);
340
+ const end = decorated.indexOf("</code></pre>", open);
341
+ if (end < 0) break;
342
+ const codeAttributes = decorated.slice(pre + 6, open);
343
+ const content = decorated.slice(open + 1, end);
344
+ const code = decodeHtml(content);
345
+ const language = codeLanguage(codeAttributes);
346
+ const isIncomplete = active && decorated.slice(at, pre).includes("data-markdown-incomplete");
347
+ const number = index++;
348
+ html += decorated.slice(cursor, at);
349
+ cursor = scan = end + 13;
350
+ const custom = options.plugins?.renderers?.find((renderer) => typeof renderer.language === "string" ? renderer.language === language : renderer.language.includes(language));
351
+ if (custom !== void 0) {
352
+ html += hookHtml(custom.render({
353
+ code,
354
+ language,
355
+ isIncomplete
356
+ }));
357
+ continue;
358
+ }
359
+ const plugin = options.plugins?.code;
360
+ if (isIncomplete) {
361
+ if (live !== void 0 && (live.index !== number || live.language !== language)) dispose();
362
+ if (live === void 0) {
363
+ const session = plugin?.createSession?.();
364
+ if (session !== void 0) live = {
365
+ session,
366
+ index: number,
367
+ language
368
+ };
369
+ }
370
+ }
371
+ let highlighted;
372
+ if (live !== void 0 && live.index === number) {
373
+ liveUses++;
374
+ highlighted = live.session.highlight({
375
+ code,
376
+ language,
377
+ isIncomplete
378
+ });
379
+ } else if (plugin !== void 0) {
380
+ const key = language + "\0" + code;
381
+ highlighted = isIncomplete ? void 0 : codeCache.get(key) ?? oldCodeCache.get(key);
382
+ if (highlighted === void 0) {
383
+ highlighted = plugin.highlight({
384
+ code,
385
+ language,
386
+ isIncomplete
387
+ });
388
+ if (!isIncomplete && typeof highlighted === "string") codeCache.set(key, highlighted);
389
+ } else codeCache.set(key, highlighted);
390
+ }
391
+ if (highlighted != null && typeof highlighted !== "string") throw new TypeError("Code highlighters must return HTML strings");
392
+ if (!isIncomplete && live?.index === number) dispose();
393
+ let count = 1;
394
+ for (let line = code.indexOf("\n"); line !== -1 && line < code.length - 1; line = code.indexOf("\n", line + 1)) count++;
395
+ const [before, after] = codeWrapper(codeAttributes, language, isIncomplete, options.lineNumbers === false ? void 0 : count, options);
396
+ html += before + groupLines(highlighted ?? content) + after;
397
+ }
398
+ html += decorated.slice(cursor);
399
+ for (const [tag, render] of Object.entries(options.components ?? {})) {
400
+ let named = tag !== "";
401
+ for (let at = 0; named && at < tag.length; at++) {
402
+ const code = tag.charCodeAt(at);
403
+ named = code >= 97 && code <= 122 || at > 0 && code >= 48 && code <= 57;
404
+ }
405
+ if (render === void 0 || !named && tag !== "inlineCode") continue;
406
+ const inline = tag === "inlineCode";
407
+ if (!inline && (tag !== "code" || options.components?.inlineCode === void 0)) {
408
+ html = mapElements(html, tag, render);
409
+ continue;
410
+ }
411
+ let result = "";
412
+ let from = 0;
413
+ let pre = html.indexOf("<pre");
414
+ let code = inline ? html.indexOf("<code") : -1;
415
+ while (inline ? code !== -1 : pre !== -1) {
416
+ const isPre = pre !== -1 && (!inline || pre < code);
417
+ const at = isPre ? pre : code;
418
+ const name = at + (isPre ? 4 : 5);
419
+ const open = isNameCode(html.charCodeAt(name)) ? -1 : html.indexOf(">", name);
420
+ const close = open < 0 ? -1 : html.indexOf(isPre ? "</pre>" : "</code>", open + 1);
421
+ const next = close < 0 ? name : close + (isPre ? 6 : 7);
422
+ if (close >= 0 && isPre !== inline) {
423
+ const element = html.slice(at, next);
424
+ result += html.slice(from, at) + (inline ? hookHtml(render(element)) : mapElements(element, "code", render));
425
+ from = next;
426
+ }
427
+ if (pre !== -1 && pre < next) pre = html.indexOf("<pre", next);
428
+ if (inline && code < next) code = html.indexOf("<code", next);
429
+ }
430
+ html = result + html.slice(from);
431
+ }
432
+ return [html, index];
433
+ };
434
+ /**
435
+ * Format the last open fence as changed lines when no hook or filter needs
436
+ * its full element. The HTML around the fence is reused while it is unchanged,
437
+ * so this also works for a fence in a list or quote.
438
+ */
439
+ const formatFence = (input, firstCode, active) => {
440
+ if (!active || options.allowedElements !== void 0 || options.disallowedElements !== void 0 || options.components !== void 0 && Object.values(options.components).some((hook) => hook !== void 0)) return;
441
+ const start = input.indexOf("<pre data-markdown-incomplete");
442
+ if (start < 0) return;
443
+ const open = input.indexOf(">", start) + 1;
444
+ const body = input.indexOf(">", open) + 1;
445
+ const end = input.indexOf("<", body);
446
+ if (!input.startsWith("<code", open) || end < 0) return;
447
+ const attributes = input.slice(open + 5, body - 1);
448
+ const language = codeLanguage(attributes);
449
+ if (options.plugins?.renderers?.some((renderer) => typeof renderer.language === "string" ? renderer.language === language : renderer.language.includes(language)) === true) return;
450
+ const plugin = options.plugins?.code;
451
+ const prefix = input.slice(0, start);
452
+ const suffix = input.slice(end + 13);
453
+ if (fence === void 0 || prefix !== fence.prefix || suffix !== fence.suffix || attributes !== fence.attributes || firstCode !== fence.firstCode) {
454
+ const [before, index] = format(prefix, firstCode, active);
455
+ fence = {
456
+ key: ++fenceKey,
457
+ prefix,
458
+ suffix,
459
+ attributes,
460
+ firstCode,
461
+ index,
462
+ before,
463
+ after: format(suffix, index + 1, active)[0],
464
+ highlighted: false,
465
+ escaped: void 0,
466
+ start: 0,
467
+ line: 0,
468
+ code: "",
469
+ codeStart: 0
470
+ };
471
+ }
472
+ if (live !== void 0 && (live.index !== fence.index || live.language !== language)) dispose();
473
+ if (live === void 0 && plugin !== void 0) {
474
+ const session = plugin.createSession?.();
475
+ if (session !== void 0) live = {
476
+ session,
477
+ index: fence.index,
478
+ language
479
+ };
480
+ }
481
+ if (plugin !== void 0 && live?.session.highlightLines === void 0) return;
482
+ const escaped = input.slice(body, end);
483
+ const length = escaped.endsWith("\n") ? escaped.length - 1 : escaped.length;
484
+ const lastStart = length === 0 ? 0 : escaped.lastIndexOf("\n", length - 1) + 1;
485
+ const unchanged = escaped === fence.escaped;
486
+ const from = fence.escaped !== void 0 && lastStart >= fence.start && escaped.slice(0, fence.start) === fence.escaped.slice(0, fence.start) ? fence.start : 0;
487
+ const firstLine = from === 0 ? 0 : fence.line;
488
+ let lineCount = firstLine + 1;
489
+ for (let at = escaped.indexOf("\n", from); at !== -1; at = escaped.indexOf("\n", at + 1)) lineCount++;
490
+ let code = escaped;
491
+ let codeStart = lastStart;
492
+ if (plugin !== void 0) {
493
+ const head = (from === 0 ? "" : fence.code.slice(0, fence.codeStart)) + decodeHtml(escaped.slice(from, lastStart));
494
+ code = head + decodeHtml(escaped.slice(lastStart));
495
+ codeStart = head.length;
496
+ }
497
+ if (live !== void 0) liveUses++;
498
+ let lines = live?.session.highlightLines?.({
499
+ code,
500
+ language,
501
+ isIncomplete: true
502
+ }) ?? null;
503
+ if (fence.highlighted !== (lines !== null)) {
504
+ fence.highlighted = lines !== null;
505
+ fence.key = ++fenceKey;
506
+ }
507
+ lines ??= {
508
+ firstLine: unchanged ? lineCount : firstLine,
509
+ lineCount,
510
+ html: unchanged ? "" : escaped.slice(from),
511
+ lastLineOffset: unchanged ? 0 : lastStart - from,
512
+ fullHtml: escaped
513
+ };
514
+ fence.escaped = escaped;
515
+ fence.start = lastStart;
516
+ fence.line = Math.max(0, lineCount - (escaped.endsWith("\n") ? 2 : 1));
517
+ fence.code = code;
518
+ fence.codeStart = codeStart;
519
+ const update = lines;
520
+ const count = Math.max(1, update.lineCount - (code.endsWith("\n") ? 1 : 0));
521
+ const numbers = options.lineNumbers === false ? void 0 : count;
522
+ const context = fence;
523
+ const renderOptions = options;
524
+ let fullPending;
525
+ return {
526
+ key: context.key,
527
+ firstLine: update.firstLine,
528
+ lineCount: update.lineCount,
529
+ lastLine: Math.max(0, update.lineCount - (code.endsWith("\n") ? 2 : 1)),
530
+ lastLineOffset: update.lastLineOffset,
531
+ html: update.html,
532
+ numbers,
533
+ get pending() {
534
+ if (fullPending !== void 0) return fullPending;
535
+ const [before, after] = codeWrapper(decorate(`<code${attributes}>`, renderOptions).slice(5, -1), language, true, numbers, renderOptions);
536
+ return fullPending = context.before + before + groupLines(update.fullHtml) + after + context.after;
537
+ }
538
+ };
539
+ };
540
+ return {
541
+ /** Change options. A reset drops caches, so completed output is formatted again. */
542
+ configure(next, reset = true) {
543
+ if (reset) resetState();
544
+ options = next;
545
+ },
546
+ render(source, incomplete, active = false) {
547
+ try {
548
+ const update = parser.render(source, { incomplete });
549
+ if (update === previousUpdate && active === pendingActive && result !== void 0) return result;
550
+ const changed = update !== previousUpdate;
551
+ let reset = false;
552
+ let addition = "";
553
+ if (changed && previousUpdate?.stable === stableRaw && !update.reset) {
554
+ if (update.addition !== "") {
555
+ [addition, nextCode] = format(update.addition, nextCode, active);
556
+ stable += addition;
557
+ }
558
+ stableRaw = update.stable;
559
+ pendingRaw = update.pending;
560
+ } else if (changed) {
561
+ const raw = update.stable + update.pending;
562
+ if (previousUpdate === void 0 || raw.slice(0, stableRaw.length) !== stableRaw) {
563
+ reset = true;
564
+ dispose();
565
+ pendingInput = void 0;
566
+ fence = void 0;
567
+ stable = "";
568
+ stableRaw = "";
569
+ nextCode = 0;
570
+ oldCodeCache = codeCache;
571
+ codeCache = /* @__PURE__ */ new Map();
572
+ }
573
+ let cut = Math.max(update.stable.length, stableRaw.length);
574
+ if (update.reset && cut === stableRaw.length) {
575
+ const previousRaw = previousUpdate === void 0 ? "" : previousUpdate.stable + previousUpdate.pending;
576
+ let same = cut;
577
+ const open = raw.indexOf("<pre data-markdown-incomplete", cut);
578
+ const limit = Math.min(open < 0 ? raw.length : open, previousRaw.length);
579
+ while (same < limit && raw.charCodeAt(same) === previousRaw.charCodeAt(same)) same++;
580
+ let depth = 0;
581
+ for (let at = same < raw.length ? raw.indexOf("<", cut) : -1, end = 0; at !== -1; at = raw.indexOf("<", end)) {
582
+ end = raw.indexOf(">", at) + 1;
583
+ if (end === 0 || end >= same) break;
584
+ if (raw.charCodeAt(at + 1) === 47) depth--;
585
+ else if (!isVoidAt(raw, at, end)) depth++;
586
+ if (depth === 0 && raw.charCodeAt(end) === 10) cut = end + 1;
587
+ }
588
+ }
589
+ if (cut > stableRaw.length) {
590
+ [addition, nextCode] = format(raw.slice(stableRaw.length, cut), nextCode, active);
591
+ stable += addition;
592
+ }
593
+ stableRaw = cut === update.stable.length ? update.stable : raw.slice(0, cut);
594
+ pendingRaw = raw.slice(cut);
595
+ }
596
+ previousUpdate = update;
597
+ if (pendingInput !== pendingRaw || pendingActive !== active || pendingCode !== nextCode) {
598
+ const uses = liveUses;
599
+ pendingFence = formatFence(pendingRaw, nextCode, active);
600
+ if (pendingFence === void 0) [pendingHtml] = format(pendingRaw, nextCode, active);
601
+ if (liveUses === uses) dispose();
602
+ pendingInput = pendingRaw;
603
+ pendingActive = active;
604
+ pendingCode = nextCode;
605
+ }
606
+ const completedHtml = stable;
607
+ const code = pendingFence;
608
+ const pending = pendingHtml;
609
+ let fullPending;
610
+ let fullHtml;
611
+ result = {
612
+ get html() {
613
+ return fullHtml ??= completedHtml + (code?.pending ?? pending);
614
+ },
615
+ stable: completedHtml,
616
+ addition,
617
+ get pending() {
618
+ return fullPending ??= code?.pending ?? pending;
619
+ },
620
+ code,
621
+ reset,
622
+ revision: ++revision
623
+ };
624
+ return result;
625
+ } catch (error) {
626
+ resetState();
627
+ throw error;
628
+ }
629
+ },
630
+ dispose
631
+ };
632
+ }
633
+ //#endregion
634
+ //#region html/dom.ts
635
+ const isFade = (node) => node.nodeType === 1 && node.getAttribute("data-markdown") === "token";
636
+ const isText = (node) => node.nodeType === 3 || isFade(node);
637
+ const passiveText = "[data-markdown=\"token\"], [data-markdown=\"code-header\"], [data-markdown=\"line-numbers\"], [data-markdown=\"caret\"]";
638
+ /** Wrap new text in fade spans, and unwrap each span when its animation ends. */
639
+ function createFades(root) {
640
+ const pending = /* @__PURE__ */ new Map();
641
+ const document = root.ownerDocument;
642
+ const query = document.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)");
643
+ let timer;
644
+ let timerDeadline = Infinity;
645
+ let requested = false;
646
+ let duration = 180;
647
+ const enabled = () => requested && duration > 0 && query?.matches !== true;
648
+ const arm = (next) => {
649
+ if (Number.isFinite(next) && timer !== void 0 && next >= timerDeadline) return;
650
+ if (timer !== void 0) clearTimeout(timer);
651
+ timer = void 0;
652
+ timerDeadline = next;
653
+ if (Number.isFinite(next)) timer = setTimeout(() => flush(), Math.max(0, next - performance.now()));
654
+ };
655
+ const flush = (all = false) => {
656
+ timer = void 0;
657
+ timerDeadline = Infinity;
658
+ const now = performance.now();
659
+ const parents = /* @__PURE__ */ new Set();
660
+ let next = Infinity;
661
+ for (const [span, deadline] of pending) {
662
+ if (!all && deadline > now) {
663
+ next = Math.min(next, deadline);
664
+ continue;
665
+ }
666
+ const parent = span.parentNode;
667
+ pending.delete(span);
668
+ if (parent !== null && root.contains(span)) {
669
+ while (span.firstChild !== null) parent.insertBefore(span.firstChild, span);
670
+ parent.removeChild(span);
671
+ parents.add(parent);
672
+ }
673
+ }
674
+ for (const parent of parents) parent.normalize();
675
+ arm(next);
676
+ };
677
+ const stop = () => {
678
+ arm(Infinity);
679
+ if (pending.size > 0) flush(true);
680
+ };
681
+ const motionChanged = () => {
682
+ if (query?.matches === true) stop();
683
+ };
684
+ const animate = (text) => {
685
+ if (!enabled() || text.data.trim() === "" || text.parentElement === null) return;
686
+ if (text.data.endsWith("\n") && text.data.length > 1 && text.parentElement.closest("pre code") !== null) text.splitText(text.data.length - 1);
687
+ if (text.parentElement.closest(passiveText) !== null) return;
688
+ const span = document.createElement("span");
689
+ span.dataset.markdown = "token";
690
+ span.dataset.animated = "true";
691
+ span.style.animationDuration = `${duration}ms`;
692
+ text.replaceWith(span);
693
+ span.append(text);
694
+ pending.set(span, performance.now() + duration);
695
+ };
696
+ return {
697
+ connect() {
698
+ query?.addEventListener("change", motionChanged);
699
+ },
700
+ disconnect() {
701
+ stop();
702
+ query?.removeEventListener("change", motionChanged);
703
+ },
704
+ finish() {
705
+ let next = Infinity;
706
+ for (const [span, deadline] of pending) if (!root.contains(span)) pending.delete(span);
707
+ else next = Math.min(next, deadline);
708
+ arm(next);
709
+ },
710
+ update(active, options) {
711
+ requested = active && options !== false;
712
+ const value = typeof options === "object" ? options.duration ?? 180 : 180;
713
+ duration = Number.isFinite(value) ? Math.max(0, value) : 180;
714
+ if (!enabled()) stop();
715
+ },
716
+ tree(node) {
717
+ if (!enabled()) return;
718
+ if (node.nodeType === 3) animate(node);
719
+ else {
720
+ const walker = document.createTreeWalker(node, 4);
721
+ let text = walker.nextNode();
722
+ while (text !== null) {
723
+ const next = walker.nextNode();
724
+ animate(text);
725
+ text = next;
726
+ }
727
+ }
728
+ },
729
+ append(parent, value, before) {
730
+ const previous = before === null ? parent.lastChild : before.previousSibling;
731
+ if (!(enabled() && value.trim() !== "" && parent.nodeType === 1 && parent.closest(passiveText) === null) && previous?.nodeType === 3) {
732
+ previous.appendData(value);
733
+ return;
734
+ }
735
+ const text = document.createTextNode(value);
736
+ parent.insertBefore(text, before);
737
+ animate(text);
738
+ }
739
+ };
740
+ }
741
+ /** Patch `parent` to match `incoming`, and reuse nodes. A fade span counts as text. Returns the node at `committed`. */
742
+ function patchChildren(parent, incoming, first, fades, committed = -1, marker, revising = false) {
743
+ const previousText = revising ? parent.textContent ?? "" : void 0;
744
+ let current = first;
745
+ let target = incoming.firstChild;
746
+ let index = 0;
747
+ let firstPending = null;
748
+ while (target !== null) {
749
+ const nextTarget = target.nextSibling;
750
+ let actual;
751
+ if (target === marker) {
752
+ actual = target;
753
+ parent.insertBefore(target, current);
754
+ } else if (target.nodeType === 3) {
755
+ const value = target.nodeValue ?? "";
756
+ let previous = "";
757
+ let end = current;
758
+ while (end !== null && isText(end)) {
759
+ previous += end.textContent ?? "";
760
+ end = end.nextSibling;
761
+ }
762
+ const append = value.startsWith(previous);
763
+ const lineAppend = !append && previous.endsWith("\n") && value.endsWith("\n") && value.slice(0, -1).startsWith(previous.slice(0, -1));
764
+ if (current !== end && (append || lineAppend)) {
765
+ actual = current;
766
+ const suffix = append ? value.slice(previous.length) : value.slice(previous.length - 1, -1);
767
+ if (suffix !== "") {
768
+ const lead = current.previousSibling;
769
+ let before = end;
770
+ if (lineAppend) {
771
+ let last = end === null ? parent.lastChild : end.previousSibling;
772
+ while (last !== null && last.nodeType === 1) last = last.lastChild;
773
+ if (last !== null) {
774
+ const text = last;
775
+ before = text.length === 1 ? text : text.splitText(text.length - 1);
776
+ if (before.parentNode !== parent) parent.insertBefore(before, end);
777
+ }
778
+ }
779
+ fades.append(parent, suffix, before);
780
+ actual = lead === null ? parent.firstChild : lead.nextSibling;
781
+ }
782
+ } else {
783
+ while (current !== end) {
784
+ const next = current.nextSibling;
785
+ parent.removeChild(current);
786
+ current = next;
787
+ }
788
+ actual = target;
789
+ parent.insertBefore(target, end);
790
+ if (previous === "") {
791
+ if (previousText === void 0 || !previousText.includes(target.textContent ?? "")) fades.tree(target);
792
+ actual = target.parentNode === parent ? target : target.parentNode;
793
+ }
794
+ }
795
+ current = end;
796
+ } else if (current?.nodeType === 1 && target.nodeType === 1 && current.tagName === target.tagName && !isFade(current)) {
797
+ actual = current;
798
+ const element = current;
799
+ const wanted = target;
800
+ if (wanted.nextElementSibling === null || !element.isEqualNode(wanted)) {
801
+ for (let at = element.attributes.length - 1; at >= 0; at--) {
802
+ const attribute = element.attributes[at];
803
+ if (!wanted.hasAttribute(attribute.name)) element.removeAttribute(attribute.name);
804
+ }
805
+ for (const attribute of wanted.attributes) if (element.getAttribute(attribute.name) !== attribute.value) element.setAttribute(attribute.name, attribute.value);
806
+ patchChildren(element, wanted, element.firstChild, fades, -1, marker, revising);
807
+ }
808
+ current = element.nextSibling;
809
+ } else {
810
+ actual = target;
811
+ const next = current?.nextSibling ?? null;
812
+ parent.insertBefore(target, current);
813
+ if (current !== null) parent.removeChild(current);
814
+ if (previousText === void 0 || !previousText.includes(target.textContent ?? "")) fades.tree(target);
815
+ current = next;
816
+ }
817
+ if (index === committed) firstPending = actual;
818
+ index++;
819
+ target = nextTarget;
820
+ }
821
+ while (current !== null) {
822
+ const next = current.nextSibling;
823
+ parent.removeChild(current);
824
+ current = next;
825
+ }
826
+ return firstPending;
827
+ }
828
+ /** Insert a comment before the last code line, so later updates patch only that line. */
829
+ function markLastCodeLine(code, trailingNewline) {
830
+ let before = code.firstChild;
831
+ for (let node = code.lastChild; node !== null; node = node.previousSibling) {
832
+ if (node.nodeType !== 3) continue;
833
+ const text = node;
834
+ let end = text.data.length;
835
+ if (trailingNewline && text.data.endsWith("\n")) {
836
+ end--;
837
+ trailingNewline = false;
838
+ }
839
+ if (end === 0) continue;
840
+ const at = text.data.lastIndexOf("\n", end - 1);
841
+ if (at < 0) continue;
842
+ before = at + 1 === text.data.length ? text.nextSibling : text.splitText(at + 1);
843
+ break;
844
+ }
845
+ const marker = code.ownerDocument.createComment("markdown-code");
846
+ code.insertBefore(marker, before);
847
+ return marker;
848
+ }
849
+ /** Render Markdown into `root`, and patch its DOM nodes on each update. */
850
+ function createDomRenderer(root, options = {}, formatter = createHtmlRenderer(options)) {
851
+ const document = root.ownerDocument;
852
+ const checkpoint = document.createComment("markdown");
853
+ const template = document.createElement("template");
854
+ const tail = document.createElement("template");
855
+ const caret = document.createElement("span");
856
+ const fades = createFades(root);
857
+ let revision = 0;
858
+ let initialized = false;
859
+ let previousCaret;
860
+ let codeState;
861
+ root.dataset.markdown = "root";
862
+ root.insertBefore(checkpoint, root.firstChild);
863
+ caret.dataset.markdown = "caret";
864
+ caret.setAttribute("aria-hidden", "true");
865
+ const click = (event) => {
866
+ const button = event.target?.closest("button[data-code-action]");
867
+ if (button == null || button.disabled || !root.contains(button)) return;
868
+ const block = button.closest("[data-markdown=\"code-block\"]");
869
+ const code = block?.querySelector("pre code")?.textContent ?? "";
870
+ if (button.dataset.codeAction === "copy") document.defaultView?.navigator.clipboard?.writeText(code).then(() => {
871
+ button.textContent = "Copied";
872
+ setTimeout(() => {
873
+ button.textContent = "Copy";
874
+ }, 2e3);
875
+ }).catch(() => {
876
+ button.textContent = "Copy";
877
+ });
878
+ else {
879
+ const url = URL.createObjectURL(new Blob([code], { type: "text/plain;charset=utf-8" }));
880
+ const link = document.createElement("a");
881
+ link.href = url;
882
+ let language = block?.getAttribute("data-language") ?? "";
883
+ for (let at = 0; at < language.length; at++) {
884
+ const code = language.charCodeAt(at);
885
+ if (((code | 32) < 97 || (code | 32) > 122) && (code < 48 || code > 57)) language = "";
886
+ }
887
+ link.download = `code.${language === "" ? "txt" : language}`;
888
+ link.click();
889
+ setTimeout(() => URL.revokeObjectURL(url), 3e4);
890
+ }
891
+ };
892
+ return {
893
+ connect() {
894
+ fades.connect();
895
+ root.addEventListener("click", click);
896
+ },
897
+ disconnect() {
898
+ fades.disconnect();
899
+ formatter.dispose();
900
+ root.removeEventListener("click", click);
901
+ },
902
+ /**
903
+ * Reformat completed output only when an option value changes, so inline
904
+ * option objects stay cheap. New hook and `urlTransform` functions apply to
905
+ * later output. A changed set of hook names reformats everything. Plugins
906
+ * compare by their parts, because a wrapper object is often written inline.
907
+ */
908
+ configure(next) {
909
+ if (next === options) return;
910
+ const hooks = (value) => Object.keys(value ?? {}).filter((tag) => value?.[tag] !== void 0).join(" ");
911
+ const samePlugins = (a, b) => {
912
+ const left = a?.renderers ?? [];
913
+ const right = b?.renderers ?? [];
914
+ return a?.code === b?.code && left.length === right.length && left.every((renderer, at) => renderer.render === right[at].render && String(renderer.language) === String(right[at].language));
915
+ };
916
+ let reset = false;
917
+ const keys = /* @__PURE__ */ new Set([...Object.keys(options), ...Object.keys(next)]);
918
+ for (const key of keys) {
919
+ const previous = options[key];
920
+ const value = next[key];
921
+ reset ||= !(previous === value || (key === "controls" ? controlFlags(previous) === controlFlags(value) : key === "components" ? hooks(previous) === hooks(value) : key === "urlTransform" ? previous === void 0 === (value === void 0) : key === "plugins" ? samePlugins(previous, value) : Array.isArray(previous) && Array.isArray(value) && previous.length === value.length && previous.every((tag, at) => tag === value[at])));
922
+ }
923
+ formatter.configure(next, reset);
924
+ options = next;
925
+ },
926
+ update(source, incomplete, active = false, animated = false, shape) {
927
+ root.toggleAttribute("data-streaming", active);
928
+ const update = formatter.render(source, incomplete, active);
929
+ const changed = update.revision !== revision;
930
+ const reset = !initialized || changed && (update.reset || update.revision !== revision + 1);
931
+ fades.update(active, animated);
932
+ const nextCaret = active ? shape : void 0;
933
+ if (!changed && nextCaret === previousCaret) return;
934
+ caret.remove();
935
+ const codeUpdate = update.code;
936
+ if (changed && !reset && codeUpdate !== void 0 && codeState !== void 0 && update.addition === "" && codeState.key === codeUpdate.key && root.contains(codeState.marker) && (codeUpdate.firstLine === codeUpdate.lineCount || codeUpdate.firstLine === codeState.line && codeUpdate.firstLine <= codeUpdate.lastLine) && codeUpdate.numbers === void 0 === (codeState.numbers === null) && (codeUpdate.numbers ?? 0) >= codeState.numberCount) {
937
+ if (codeUpdate.firstLine < codeUpdate.lineCount) {
938
+ const { html, lastLineOffset } = codeUpdate;
939
+ const marker = document.createComment("markdown-code");
940
+ let group = codeState.marker.parentNode;
941
+ let first = codeState.marker.nextSibling;
942
+ codeState.marker.remove();
943
+ for (let start = 0, line = codeUpdate.firstLine;;) {
944
+ let end = start;
945
+ let left = 64 - line % 64;
946
+ for (; left > 0; left--) {
947
+ const at = html.indexOf("\n", end);
948
+ if (at < 0) break;
949
+ end = at + 1;
950
+ }
951
+ if (left > 0) end = html.length;
952
+ if (group === null) {
953
+ if (start === end) break;
954
+ group = document.createElement("span");
955
+ group.setAttribute("data-markdown", "code-lines");
956
+ codeState.code.append(group);
957
+ first = null;
958
+ }
959
+ const at = lastLineOffset >= start && (lastLineOffset < end || end === html.length) ? lastLineOffset : -1;
960
+ template.innerHTML = html.slice(start, at < 0 ? end : at);
961
+ const fragment = template.content;
962
+ if (at >= 0) {
963
+ fragment.append(marker);
964
+ tail.innerHTML = html.slice(at, end);
965
+ fragment.append(tail.content);
966
+ }
967
+ patchChildren(group, fragment, first, fades, -1, at < 0 ? void 0 : marker);
968
+ if (end === html.length) break;
969
+ line += 64 - line % 64;
970
+ start = end;
971
+ group = null;
972
+ }
973
+ codeState.marker = marker;
974
+ codeState.line = codeUpdate.lastLine;
975
+ }
976
+ const count = codeUpdate.numbers ?? 0;
977
+ if (codeState.numbers !== null && count > codeState.numberCount) {
978
+ let group = codeState.numbers.lastElementChild;
979
+ const append = (text) => {
980
+ const last = group.lastChild;
981
+ if (last?.nodeType === 3) last.appendData(text);
982
+ else group.append(text);
983
+ };
984
+ let text = "";
985
+ for (let line = codeState.numberCount + 1; line <= count; line++) {
986
+ if (line % 64 !== 1) {
987
+ text += "\n" + line;
988
+ continue;
989
+ }
990
+ append(text + "\n");
991
+ group = document.createElement("span");
992
+ group.setAttribute("data-markdown", "code-lines");
993
+ codeState.numbers.append(group);
994
+ text = String(line);
995
+ }
996
+ append(text);
997
+ codeState.numberCount = count;
998
+ }
999
+ } else if (changed) {
1000
+ codeState?.marker.remove();
1001
+ codeState = void 0;
1002
+ if (reset) {
1003
+ if (checkpoint.parentNode !== null) root.removeChild(checkpoint);
1004
+ root.insertBefore(checkpoint, root.firstChild);
1005
+ }
1006
+ template.innerHTML = reset ? update.stable : update.addition;
1007
+ const fragment = template.content;
1008
+ const count = fragment.childNodes.length;
1009
+ tail.innerHTML = update.pending;
1010
+ const incomingCode = codeUpdate === void 0 ? null : tail.content.querySelector("[data-incomplete] > pre > code");
1011
+ const codeMarker = incomingCode === null || codeUpdate === void 0 ? void 0 : markLastCodeLine(incomingCode.lastElementChild ?? incomingCode, codeUpdate.lastLine < codeUpdate.lineCount - 1);
1012
+ fragment.append(tail.content);
1013
+ const firstPending = patchChildren(root, fragment, checkpoint.nextSibling, fades, count, codeMarker, initialized && reset);
1014
+ if (checkpoint.nextSibling !== firstPending) root.insertBefore(checkpoint, firstPending);
1015
+ if (codeUpdate !== void 0 && codeMarker?.parentNode != null) {
1016
+ const code = codeMarker.parentNode.parentNode;
1017
+ const numbers = code.previousElementSibling;
1018
+ codeState = {
1019
+ code,
1020
+ marker: codeMarker,
1021
+ line: codeUpdate.lastLine,
1022
+ key: codeUpdate.key,
1023
+ numbers: numbers?.getAttribute("data-markdown") === "line-numbers" ? numbers : null,
1024
+ numberCount: codeUpdate.numbers ?? 0
1025
+ };
1026
+ }
1027
+ if (!initialized) {
1028
+ initialized = true;
1029
+ fades.tree(root);
1030
+ }
1031
+ }
1032
+ revision = update.revision;
1033
+ previousCaret = nextCaret;
1034
+ if (nextCaret !== void 0) {
1035
+ caret.dataset.shape = nextCaret;
1036
+ let parent = root;
1037
+ let before = null;
1038
+ for (let node = root.lastChild; node !== null;) {
1039
+ if (node.nodeType === 8 || node.nodeType === 3 && node.data.trim() === "" || node.nodeType === 1 && node.hasAttribute("data-footnotes")) {
1040
+ node = node.previousSibling;
1041
+ continue;
1042
+ }
1043
+ let block = false;
1044
+ if (node.nodeType === 1) switch (node.nodeName) {
1045
+ case "BLOCKQUOTE":
1046
+ case "DIV":
1047
+ case "H1":
1048
+ case "H2":
1049
+ case "H3":
1050
+ case "H4":
1051
+ case "H5":
1052
+ case "H6":
1053
+ case "LI":
1054
+ case "OL":
1055
+ case "P":
1056
+ case "PRE":
1057
+ case "SECTION":
1058
+ case "TABLE":
1059
+ case "TBODY":
1060
+ case "TD":
1061
+ case "TH":
1062
+ case "THEAD":
1063
+ case "TR":
1064
+ case "UL": block = true;
1065
+ }
1066
+ if (!block) {
1067
+ before = node.nextSibling;
1068
+ break;
1069
+ }
1070
+ parent = node;
1071
+ if (node.nodeName === "PRE") {
1072
+ const code = node.lastElementChild;
1073
+ parent = code?.lastElementChild ?? code ?? node;
1074
+ let last = parent.lastChild;
1075
+ while (last?.nodeType === 1 && last.lastChild !== null) last = last.lastChild;
1076
+ if (last?.nodeType === 3 && last.data.endsWith("\n")) {
1077
+ const text = last;
1078
+ before = text.length > 1 ? text.splitText(text.length - 1) : text;
1079
+ parent = before.parentNode;
1080
+ }
1081
+ break;
1082
+ }
1083
+ node = node.lastChild;
1084
+ }
1085
+ parent.insertBefore(caret, before);
1086
+ }
1087
+ fades.finish();
1088
+ }
1089
+ };
1090
+ }
1091
+ //#endregion
1092
+ export { escapeHtml as i, createHtmlRenderer as n, defaultUrlTransform as r, createDomRenderer as t };
1093
+
1094
+ //# sourceMappingURL=dom-BAclEeRf.js.map