@oharness/server 0.5.0 → 0.6.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/markdown.js CHANGED
@@ -18,109 +18,560 @@ const el = (tag, cls, text) => {
18
18
  };
19
19
 
20
20
  // --- markdown -------------------------------------------------------------
21
- // A small subset: fences, headings, lists, quotes, and inline code/bold/italic
22
- // /links. Anything unrecognised stays literal text, which is the safe default
23
- // for a renderer fed by a language model.
21
+ // GitHub-flavoured blocks (fences, headings, rules, tables, quotes, nested and
22
+ // task lists) and inlines (code, emphasis, strikethrough, links, autolinks,
23
+ // escapes). Raw HTML is not rendered: it stays literal text, which is the safe
24
+ // default for a renderer fed by a language model. The one exception is <br>,
25
+ // which models write inside table cells and which becomes a node, not markup.
26
+
27
+ const HR_RE = /^ {0,3}([-*_])( *\1){2,} *$/;
28
+ const HEADING_RE = /^ {0,3}(#{1,6})(?: +(.*?))?(?: +#+)? *$/;
29
+ const QUOTE_RE = /^ {0,3}> ?/;
30
+ const LIST_RE = /^( *)([-*+]|\d{1,9}[.)])( +|$)/;
31
+ const TASK_RE = /^\[([ xX])\] +/;
32
+ const TABLE_DELIM_RE = /^ *\|? *:?-+:? *(\| *:?-+:? *)*\|? *$/;
33
+ const FENCE_CLOSE_RE = /^ {0,3}(@@FENCE@@+|~~~+) *$/;
24
34
 
25
35
  function renderMarkdown(source) {
26
36
  const root = el("div", "md");
27
- const lines = source.split("\n");
37
+ const lines = String(source).replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
38
+ renderBlocks(root, lines);
39
+ return root;
40
+ }
41
+
42
+ function matchFence(line) {
43
+ const match = /^( {0,3})(@@FENCE@@+|~~~+)(.*)$/.exec(line);
44
+ if (!match) return null;
45
+ // A backtick fence cannot carry a backtick in its info string; without this
46
+ // a one-line code span written with triple backticks swallows the reply.
47
+ if (match[2][0] !== "~" && match[3].includes("@@BT@@")) return null;
48
+ return { indent: match[1].length, marker: match[2], lang: match[3].trim().split(/\s+/)[0] || "" };
49
+ }
50
+
51
+ function isTableStart(lines, i) {
52
+ if (i + 1 >= lines.length) return false;
53
+ const head = lines[i];
54
+ const delim = lines[i + 1];
55
+ if (!head.includes("|") || !delim.includes("|") || !TABLE_DELIM_RE.test(delim)) return false;
56
+ return splitRow(head).length === splitRow(delim).length;
57
+ }
58
+
59
+ function isBlockStart(lines, i) {
60
+ const line = lines[i];
61
+ return (
62
+ matchFence(line) !== null ||
63
+ HEADING_RE.test(line) ||
64
+ HR_RE.test(line) ||
65
+ QUOTE_RE.test(line) ||
66
+ LIST_RE.test(line) ||
67
+ isTableStart(lines, i)
68
+ );
69
+ }
70
+
71
+ const indentOf = (line) => line.length - line.trimStart().length;
72
+ const stripIndent = (line, n) => line.slice(Math.min(n, indentOf(line)));
73
+
74
+ function renderBlocks(parent, lines) {
28
75
  let i = 0;
29
76
 
30
77
  while (i < lines.length) {
31
78
  const line = lines[i];
32
79
 
33
- if (/^@@FENCE@@/.test(line)) {
34
- const lang = line.slice(3).trim();
80
+ if (line.trim() === "") {
81
+ i++;
82
+ continue;
83
+ }
84
+
85
+ const fence = matchFence(line);
86
+ if (fence) {
35
87
  const body = [];
36
88
  i++;
37
- while (i < lines.length && !/^@@FENCE@@/.test(lines[i])) body.push(lines[i++]);
89
+ while (i < lines.length) {
90
+ const close = FENCE_CLOSE_RE.exec(lines[i]);
91
+ if (close && close[1][0] === fence.marker[0] && close[1].length >= fence.marker.length) break;
92
+ body.push(stripIndent(lines[i], fence.indent));
93
+ i++;
94
+ }
95
+ // Past the closing fence, or past the end while a reply is still
96
+ // streaming: an unclosed fence shows as code until the rest arrives.
38
97
  i++;
39
- root.appendChild(codeBlock(body.join("\n"), lang));
98
+ parent.appendChild(codeBlock(body.join("\n"), fence.lang));
40
99
  continue;
41
100
  }
42
101
 
43
- const heading = /^(#{1,4})\s+(.*)$/.exec(line);
102
+ const heading = HEADING_RE.exec(line);
44
103
  if (heading) {
45
- root.appendChild(inline(el("h" + (heading[1].length + 2), "md-h"), heading[2]));
104
+ const depth = heading[1].length;
105
+ // Shifted down two levels: the page's own chrome owns h1 and h2.
106
+ const node = el("h" + Math.min(depth + 2, 6), "md-h md-h" + Math.min(depth, 4));
107
+ parent.appendChild(inline(node, heading[2] || ""));
46
108
  i++;
47
109
  continue;
48
110
  }
49
111
 
50
- if (/^\s*>/.test(line)) {
51
- const body = [];
52
- while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ""));
53
- const quote = el("blockquote", "md-quote");
54
- quote.appendChild(renderMarkdown(body.join("\n")));
55
- root.appendChild(quote);
112
+ // Before lists, which "* * *" would otherwise also match.
113
+ if (HR_RE.test(line)) {
114
+ parent.appendChild(el("hr", "md-hr"));
115
+ i++;
56
116
  continue;
57
117
  }
58
118
 
59
- if (/^\s*([-*+]|\d+\.)\s+/.test(line)) {
60
- const ordered = /^\s*\d+\./.test(line);
61
- const list = el(ordered ? "ol" : "ul", "md-list");
62
- while (i < lines.length && /^\s*([-*+]|\d+\.)\s+/.test(lines[i])) {
63
- const item = lines[i++].replace(/^\s*([-*+]|\d+\.)\s+/, "");
64
- list.appendChild(inline(el("li"), item));
119
+ if (isTableStart(lines, i)) {
120
+ i = renderTable(parent, lines, i);
121
+ continue;
122
+ }
123
+
124
+ if (QUOTE_RE.test(line)) {
125
+ const body = [];
126
+ while (i < lines.length) {
127
+ if (QUOTE_RE.test(lines[i])) body.push(lines[i++].replace(QUOTE_RE, ""));
128
+ // A lazy continuation line belongs to the quote's paragraph.
129
+ else if (lines[i].trim() !== "" && body.length && body[body.length - 1].trim() !== "" && !isBlockStart(lines, i)) body.push(lines[i++]);
130
+ else break;
65
131
  }
66
- root.appendChild(list);
132
+ const quote = el("blockquote", "md-quote");
133
+ renderBlocks(quote, body);
134
+ parent.appendChild(quote);
67
135
  continue;
68
136
  }
69
137
 
70
- if (line.trim() === "") {
71
- i++;
138
+ if (LIST_RE.test(line)) {
139
+ i = renderList(parent, lines, i);
72
140
  continue;
73
141
  }
74
142
 
75
143
  const paragraph = [];
76
- while (
77
- i < lines.length &&
78
- lines[i].trim() !== "" &&
79
- !/^@@FENCE@@|^\s*>|^#{1,4}\s|^\s*([-*+]|\d+\.)\s+/.test(lines[i])
80
- ) {
81
- paragraph.push(lines[i++]);
82
- }
83
- root.appendChild(inline(el("p", "md-p"), paragraph.join("\n")));
144
+ while (i < lines.length && lines[i].trim() !== "" && (paragraph.length === 0 || !isBlockStart(lines, i))) {
145
+ // Trailing double spaces and a trailing backslash are hard breaks; the
146
+ // paragraph keeps its newlines anyway, so the markers only need removing.
147
+ paragraph.push(lines[i++].trim().replace(/\\$/, ""));
148
+ }
149
+ parent.appendChild(inline(el("p", "md-p"), paragraph.join("\n")));
84
150
  }
151
+ }
85
152
 
86
- return root;
153
+ function splitRow(line) {
154
+ let text = line.trim();
155
+ if (text.startsWith("|")) text = text.slice(1);
156
+ if (text.endsWith("|") && !text.endsWith("\\|")) text = text.slice(0, -1);
157
+
158
+ const cells = [];
159
+ let cell = "";
160
+ let code = false;
161
+ for (let k = 0; k < text.length; k++) {
162
+ const ch = text[k];
163
+ if (ch === "\\" && text[k + 1] === "|") {
164
+ cell += "|";
165
+ k++;
166
+ continue;
167
+ }
168
+ if (ch === "@@BT@@") code = !code;
169
+ if (ch === "|" && !code) {
170
+ cells.push(cell.trim());
171
+ cell = "";
172
+ continue;
173
+ }
174
+ cell += ch;
175
+ }
176
+ cells.push(cell.trim());
177
+ return cells;
178
+ }
179
+
180
+ function renderTable(parent, lines, start) {
181
+ const head = splitRow(lines[start]);
182
+ const aligns = splitRow(lines[start + 1]).map((cell) =>
183
+ cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : cell.startsWith(":") ? "left" : "",
184
+ );
185
+
186
+ const cellNode = (tag, text, k) => {
187
+ const node = el(tag);
188
+ if (aligns[k]) node.style.textAlign = aligns[k];
189
+ return inline(node, text || "");
190
+ };
191
+
192
+ const table = el("table", "md-table");
193
+ const thead = el("thead");
194
+ const headRow = el("tr");
195
+ head.forEach((text, k) => headRow.appendChild(cellNode("th", text, k)));
196
+ thead.appendChild(headRow);
197
+ table.appendChild(thead);
198
+
199
+ const tbody = el("tbody");
200
+ let i = start + 2;
201
+ while (i < lines.length && lines[i].trim() !== "" && lines[i].includes("|") && !isBlockStart(lines, i)) {
202
+ const cells = splitRow(lines[i++]);
203
+ const row = el("tr");
204
+ // Every row as wide as the header: extra cells are dropped, missing ones
205
+ // left empty, so a ragged row cannot shift the columns under it.
206
+ head.forEach((_, k) => row.appendChild(cellNode("td", cells[k], k)));
207
+ tbody.appendChild(row);
208
+ }
209
+ if (tbody.childNodes.length) table.appendChild(tbody);
210
+
211
+ // Scrolls on its own, so a wide table never widens the transcript.
212
+ const wrap = el("div", "md-table-wrap");
213
+ wrap.appendChild(table);
214
+ parent.appendChild(wrap);
215
+ return i;
216
+ }
217
+
218
+ function renderList(parent, lines, start) {
219
+ const first = LIST_RE.exec(lines[start]);
220
+ const indent = first[1].length;
221
+ const ordered = /\d/.test(first[2]);
222
+ const list = el(ordered ? "ol" : "ul", "md-list");
223
+ if (ordered && parseInt(first[2], 10) !== 1) list.start = parseInt(first[2], 10);
224
+
225
+ const items = [];
226
+ let loose = false;
227
+ let i = start;
228
+
229
+ while (i < lines.length) {
230
+ const marker = LIST_RE.exec(lines[i]);
231
+ // A sibling sits at this list's own indent (give or take a space) and uses
232
+ // the same kind of marker; anything else ends the list.
233
+ if (!marker || HR_RE.test(lines[i]) || marker[1].length >= indent + 2 || /\d/.test(marker[2]) !== ordered) break;
234
+
235
+ const empty = marker[3] === "";
236
+ const wide = marker[3].length > 4;
237
+ const content = empty || wide ? marker[1].length + marker[2].length + 1 : marker[0].length;
238
+ const nest = Math.min(content, indent + 2);
239
+ const body = [wide ? lines[i].slice(content) : lines[i].slice(marker[0].length)];
240
+ i++;
241
+
242
+ while (i < lines.length) {
243
+ const line = lines[i];
244
+ if (line.trim() === "") {
245
+ let next = i + 1;
246
+ while (next < lines.length && lines[next].trim() === "") next++;
247
+ if (next < lines.length && indentOf(lines[next]) >= nest) {
248
+ body.push("");
249
+ i++;
250
+ continue;
251
+ }
252
+ break;
253
+ }
254
+ if (indentOf(line) >= nest) {
255
+ body.push(stripIndent(line, content));
256
+ i++;
257
+ continue;
258
+ }
259
+ if (isBlockStart(lines, i) || body[body.length - 1].trim() === "") break;
260
+ // Lazy continuation of the item's paragraph.
261
+ body.push(line.trim());
262
+ i++;
263
+ }
264
+
265
+ while (body.length > 1 && body[body.length - 1].trim() === "") body.pop();
266
+ if (body.some((text, k) => k > 0 && text === "" && k < body.length - 1)) loose = true;
267
+ items.push(body);
268
+
269
+ // Past blank lines that separate this item from the next sibling.
270
+ while (i < lines.length && lines[i].trim() === "") {
271
+ let next = i;
272
+ while (next < lines.length && lines[next].trim() === "") next++;
273
+ const sibling = next < lines.length ? LIST_RE.exec(lines[next]) : null;
274
+ if (!sibling || sibling[1].length >= indent + 2 || /\d/.test(sibling[2]) !== ordered) break;
275
+ loose = true;
276
+ i = next;
277
+ }
278
+ }
279
+
280
+ for (const body of items) {
281
+ const li = el("li");
282
+ const task = TASK_RE.exec(body[0]);
283
+ if (task) body[0] = body[0].slice(task[0].length);
284
+ renderBlocks(li, body);
285
+ if (!loose) unwrapParagraphs(li);
286
+
287
+ if (task) {
288
+ const box = el("input", "md-check");
289
+ box.type = "checkbox";
290
+ box.checked = task[1] !== " ";
291
+ box.disabled = true;
292
+ li.className = "md-task";
293
+ const target = li.firstChild && li.firstChild.className === "md-p" ? li.firstChild : li;
294
+ target.insertBefore(box, target.firstChild);
295
+ }
296
+ list.appendChild(li);
297
+ }
298
+
299
+ if (loose) list.className += " md-loose";
300
+ parent.appendChild(list);
301
+ return i;
302
+ }
303
+
304
+ /** A tight list item reads as a line of text, not as paragraphs with margins. */
305
+ function unwrapParagraphs(li) {
306
+ for (const child of Array.from(li.childNodes)) {
307
+ if (child.className !== "md-p") continue;
308
+ while (child.firstChild) li.insertBefore(child.firstChild, child);
309
+ li.removeChild(child);
310
+ }
311
+ }
312
+
313
+ // --- inlines --------------------------------------------------------------
314
+
315
+ const ESCAPABLE = "!\"#$%&'()*+,-./:;<=>?@[\\]^_@@BT@@{|}~";
316
+ const BR_RE = /<br *\/?>/iy;
317
+ const ANGLE_LINK_RE = /<((?:https?:\/\/|mailto:)[^\s<>]+)>/iy;
318
+ const BARE_URL_RE = /https?:\/\/[^\s<>]+/iy;
319
+
320
+ /** Only http(s) and mailto: a javascript: or data: URL from model output would run script in a page that can drive the agent. */
321
+ function safeHref(href) {
322
+ const url = href.trim();
323
+ return /^(https?:\/\/|mailto:)/i.test(url) ? url : null;
324
+ }
325
+
326
+ function linkNode(href, label, parseLabel) {
327
+ const link = el("a", "md-link");
328
+ link.href = href;
329
+ link.target = "_blank";
330
+ link.rel = "noopener noreferrer";
331
+ if (parseLabel) inline(link, label);
332
+ else link.textContent = label;
333
+ return link;
334
+ }
335
+
336
+ /** The index of a run of exactly run backticks at or after from, or -1. */
337
+ function findCodeClose(text, from, run) {
338
+ let k = from;
339
+ while ((k = text.indexOf("@@BT@@", k)) !== -1) {
340
+ let len = 1;
341
+ while (text[k + len] === "@@BT@@") len++;
342
+ if (len === run) return k;
343
+ k += len;
344
+ }
345
+ return -1;
346
+ }
347
+
348
+ /** Past a code span starting at k, or k itself when the backticks never close. */
349
+ function skipCode(text, k) {
350
+ let run = 1;
351
+ while (text[k + run] === "@@BT@@") run++;
352
+ const close = findCodeClose(text, k + run, run);
353
+ return close === -1 ? k + run : close + run;
354
+ }
355
+
356
+ /** [label](destination "title"), starting at the bracket; null when it is not one. */
357
+ function parseLink(text, start) {
358
+ let depth = 0;
359
+ let k = start;
360
+ for (; k < text.length; k++) {
361
+ const ch = text[k];
362
+ if (ch === "\\") k++;
363
+ else if (ch === "@@BT@@") k = skipCode(text, k) - 1;
364
+ else if (ch === "[") depth++;
365
+ else if (ch === "]" && --depth === 0) break;
366
+ }
367
+ if (k >= text.length || text[k + 1] !== "(") return null;
368
+
369
+ const label = text.slice(start + 1, k);
370
+ let p = k + 2;
371
+ while (text[p] === " ") p++;
372
+
373
+ let href;
374
+ if (text[p] === "<") {
375
+ const end = text.indexOf(">", p);
376
+ if (end === -1) return null;
377
+ href = text.slice(p + 1, end);
378
+ p = end + 1;
379
+ } else {
380
+ const begin = p;
381
+ let parens = 0;
382
+ for (; p < text.length; p++) {
383
+ const ch = text[p];
384
+ if (ch === "\\") p++;
385
+ else if (ch === "(") parens++;
386
+ else if (ch === ")" && parens-- === 0) break;
387
+ else if (ch === " " || ch === "\n") break;
388
+ }
389
+ href = text.slice(begin, p);
390
+ }
391
+
392
+ while (text[p] === " ") p++;
393
+ if (text[p] === "\"" || text[p] === "'") {
394
+ const end = text.indexOf(text[p], p + 1);
395
+ if (end === -1) return null;
396
+ p = end + 1;
397
+ while (text[p] === " ") p++;
398
+ }
399
+ if (text[p] !== ")") return null;
400
+ return { label, href, end: p + 1 };
401
+ }
402
+
403
+ /**
404
+ * Where the emphasis opened at start with size delimiters closes, as
405
+ * [contentEnd, resumeAt], or null.
406
+ *
407
+ * The opener must touch a non-space and the closer follow one, so "2 * 3 * 4"
408
+ * stays arithmetic; underscores must also sit outside a word, so snake_case
409
+ * names stay names.
410
+ */
411
+ function findEmphasisClose(text, start, ch, size) {
412
+ const word = /[\p{L}\p{N}]/u;
413
+ const after = text[start + size];
414
+ if (after === undefined || /\s/.test(after)) return null;
415
+ if (ch === "_" && start > 0 && word.test(text[start - 1])) return null;
416
+
417
+ let k = start + size + 1;
418
+ while (k < text.length) {
419
+ const c = text[k];
420
+ if (c === "\\") {
421
+ k += 2;
422
+ continue;
423
+ }
424
+ if (c === "@@BT@@") {
425
+ k = skipCode(text, k);
426
+ continue;
427
+ }
428
+ if (c !== ch) {
429
+ k++;
430
+ continue;
431
+ }
432
+ let run = 1;
433
+ while (text[k + run] === ch) run++;
434
+ const closes =
435
+ !/\s/.test(text[k - 1]) &&
436
+ (ch !== "_" || !word.test(text[k + run] || "")) &&
437
+ (size === 2 ? run >= 2 : run === 1 || run >= 3);
438
+ // A longer closing run gives up its last delimiters, so ***a*** closes
439
+ // the strong outside the emphasis inside it.
440
+ if (closes) return [k + run - size, k + run];
441
+ k += run;
442
+ }
443
+ return null;
87
444
  }
88
445
 
89
446
  /** Inline spans, appended as nodes so no markup can come from the text. */
90
447
  function inline(parent, text) {
91
- const pattern = /(@@BT@@[^@@BT@@]+@@BT@@)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)]+\))/g;
92
- let last = 0;
93
- let match;
448
+ let buffer = "";
449
+ const flush = () => {
450
+ if (buffer) parent.appendChild(document.createTextNode(buffer));
451
+ buffer = "";
452
+ };
453
+ const push = (node) => {
454
+ flush();
455
+ parent.appendChild(node);
456
+ };
94
457
 
95
- while ((match = pattern.exec(text)) !== null) {
96
- if (match.index > last) parent.appendChild(document.createTextNode(text.slice(last, match.index)));
97
- const token = match[0];
458
+ let i = 0;
459
+ while (i < text.length) {
460
+ const ch = text[i];
98
461
 
99
- if (token.startsWith("@@BT@@")) {
100
- parent.appendChild(el("code", "md-code", token.slice(1, -1)));
101
- } else if (token.startsWith("**")) {
102
- parent.appendChild(el("strong", null, token.slice(2, -2)));
103
- } else if (token.startsWith("*")) {
104
- parent.appendChild(el("em", null, token.slice(1, -1)));
105
- } else {
106
- const label = token.slice(1, token.indexOf("]"));
107
- const href = token.slice(token.indexOf("(") + 1, -1);
108
- // Only http(s): a javascript: or data: URL from model output would run
109
- // script in a page that can drive the agent.
110
- if (/^https?:\/\//i.test(href)) {
111
- const link = el("a", "md-link", label);
112
- link.href = href;
113
- link.target = "_blank";
114
- link.rel = "noopener noreferrer";
115
- parent.appendChild(link);
116
- } else {
117
- parent.appendChild(document.createTextNode(token));
462
+ if (ch === "\\" && i + 1 < text.length && (ESCAPABLE.includes(text[i + 1]) || text[i + 1] === "\n")) {
463
+ buffer += text[i + 1];
464
+ i += 2;
465
+ continue;
466
+ }
467
+
468
+ if (ch === "@@BT@@") {
469
+ let run = 1;
470
+ while (text[i + run] === "@@BT@@") run++;
471
+ const close = findCodeClose(text, i + run, run);
472
+ if (close === -1) {
473
+ buffer += text.slice(i, i + run);
474
+ i += run;
475
+ continue;
118
476
  }
477
+ let code = text.slice(i + run, close).replace(/\n/g, " ");
478
+ if (code.length > 2 && code[0] === " " && code[code.length - 1] === " " && code.trim()) code = code.slice(1, -1);
479
+ push(el("code", "md-code", code));
480
+ i = close + run;
481
+ continue;
119
482
  }
120
- last = match.index + token.length;
483
+
484
+ if (ch === "<") {
485
+ BR_RE.lastIndex = i;
486
+ const br = BR_RE.exec(text);
487
+ if (br) {
488
+ push(el("br"));
489
+ i += br[0].length;
490
+ continue;
491
+ }
492
+ ANGLE_LINK_RE.lastIndex = i;
493
+ const auto = ANGLE_LINK_RE.exec(text);
494
+ if (auto) {
495
+ push(linkNode(auto[1], auto[1].replace(/^mailto:/i, ""), false));
496
+ i += auto[0].length;
497
+ continue;
498
+ }
499
+ }
500
+
501
+ if (ch === "!" && text[i + 1] === "[") {
502
+ const image = parseLink(text, i + 1);
503
+ if (image) {
504
+ // A link, not an <img>: a remote image loads without a click, so an
505
+ // address composed by a model that read a hostile page would carry
506
+ // whatever it put in the query string straight off this machine.
507
+ const href = safeHref(image.href);
508
+ const label = "🖼 " + (image.label || image.href);
509
+ push(href ? linkNode(href, label, false) : el("span", "md-dead", label));
510
+ i = image.end;
511
+ continue;
512
+ }
513
+ }
514
+
515
+ if (ch === "[") {
516
+ const link = parseLink(text, i);
517
+ if (link) {
518
+ const href = safeHref(link.href);
519
+ // A label that is itself a URL would autolink into an anchor inside
520
+ // an anchor.
521
+ if (href) push(linkNode(href, link.label, !/^https?:\/\/\S*$/i.test(link.label)));
522
+ else {
523
+ // A relative path or an anchor has nowhere to go on this page; the
524
+ // label stays readable and the target is on hover.
525
+ const dead = inline(el("span", "md-dead"), link.label);
526
+ dead.title = link.href;
527
+ push(dead);
528
+ }
529
+ i = link.end;
530
+ continue;
531
+ }
532
+ }
533
+
534
+ if ((ch === "h" || ch === "H") && (i === 0 || !/[\w/]/.test(text[i - 1]))) {
535
+ BARE_URL_RE.lastIndex = i;
536
+ const bare = BARE_URL_RE.exec(text);
537
+ if (bare) {
538
+ let url = bare[0].replace(/[?!.,:;*_~'"]+$/, "");
539
+ // A closing paren belongs to the URL only when it has an opener in it,
540
+ // so "(see https://x.dev)" leaves the paren outside the link.
541
+ while (url.endsWith(")") && url.split("(").length < url.split(")").length) url = url.slice(0, -1);
542
+ push(linkNode(url, url, false));
543
+ i += url.length;
544
+ continue;
545
+ }
546
+ }
547
+
548
+ if (ch === "*" || ch === "_" || ch === "~") {
549
+ let run = 1;
550
+ while (text[i + run] === ch) run++;
551
+ const sizes = ch === "~" ? (run === 2 ? [2] : []) : run >= 2 ? [2, 1] : [1];
552
+ let done = false;
553
+ for (const size of sizes) {
554
+ const close = findEmphasisClose(text, i, ch, size);
555
+ if (!close) continue;
556
+ const tag = ch === "~" ? "del" : size === 2 ? "strong" : "em";
557
+ // The rest of a longer opening run stays inside, so ***a*** nests the
558
+ // emphasis within the strong.
559
+ push(inline(el(tag), text.slice(i + size, close[0])));
560
+ i = close[1];
561
+ done = true;
562
+ break;
563
+ }
564
+ if (done) continue;
565
+ buffer += text.slice(i, i + run);
566
+ i += run;
567
+ continue;
568
+ }
569
+
570
+ buffer += ch;
571
+ i++;
121
572
  }
122
573
 
123
- if (last < text.length) parent.appendChild(document.createTextNode(text.slice(last)));
574
+ flush();
124
575
  return parent;
125
576
  }
126
577
 
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsOrB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC"}
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyqBrB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC"}