@oharness/server 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n.js +473 -10
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/markdown.js +512 -61
- package/dist/markdown.js.map +1 -1
- package/dist/page.js +2599 -373
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +7 -0
- package/dist/prefs.js +3 -0
- package/dist/prefs.js.map +1 -1
- package/dist/server.d.ts +14 -1
- package/dist/server.js +264 -42
- package/dist/server.js.map +1 -1
- package/dist/theme.d.ts +18 -0
- package/dist/theme.js +96 -0
- package/dist/theme.js.map +1 -0
- package/package.json +2 -2
package/dist/markdown.js
CHANGED
|
@@ -18,109 +18,560 @@ const el = (tag, cls, text) => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// --- markdown -------------------------------------------------------------
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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 (
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
98
|
+
parent.appendChild(codeBlock(body.join("\n"), fence.lang));
|
|
40
99
|
continue;
|
|
41
100
|
}
|
|
42
101
|
|
|
43
|
-
const heading =
|
|
102
|
+
const heading = HEADING_RE.exec(line);
|
|
44
103
|
if (heading) {
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
132
|
+
const quote = el("blockquote", "md-quote");
|
|
133
|
+
renderBlocks(quote, body);
|
|
134
|
+
parent.appendChild(quote);
|
|
67
135
|
continue;
|
|
68
136
|
}
|
|
69
137
|
|
|
70
|
-
if (
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
458
|
+
let i = 0;
|
|
459
|
+
while (i < text.length) {
|
|
460
|
+
const ch = text[i];
|
|
98
461
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
574
|
+
flush();
|
|
124
575
|
return parent;
|
|
125
576
|
}
|
|
126
577
|
|
package/dist/markdown.js.map
CHANGED
|
@@ -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
|
|
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"}
|