pi-tinychat 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +139 -139
- package/bin/pi-tinychat.js +111 -111
- package/extension/plan.ts +281 -240
- package/package.json +68 -68
- package/public/app.js +3749 -3614
- package/public/favicon.svg +15 -15
- package/public/i18n.js +197 -169
- package/public/index.html +226 -226
- package/public/markdown.js +244 -244
- package/public/style.css +2814 -2762
- package/public/theme-init.js +18 -18
- package/src/compaction.js +139 -139
- package/src/errors.js +13 -13
- package/src/live.js +133 -133
- package/src/pi-path.js +70 -70
- package/src/pi-types.d.ts +85 -85
- package/src/pi.js +196 -196
- package/src/plan.js +91 -90
- package/src/server.js +910 -910
- package/src/sessions.js +256 -256
package/public/markdown.js
CHANGED
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
// A deliberately small, escape-first Markdown renderer.
|
|
2
|
-
// Every character is HTML-escaped before formatting, so raw HTML in model
|
|
3
|
-
// output can never become markup. Only http(s)/mailto/#/relative links survive.
|
|
4
|
-
|
|
5
|
-
const ESCAPES = {
|
|
6
|
-
"&": "&",
|
|
7
|
-
"<": "<",
|
|
8
|
-
">": ">",
|
|
9
|
-
'"': """,
|
|
10
|
-
"'": "'",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function escapeHtml(value) {
|
|
14
|
-
return String(value ?? "").replace(/[&<>"']/g, (character) => ESCAPES[character]);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function safeHref(url) {
|
|
18
|
-
const raw = String(url).trim().replace(/^<|>$/g, "");
|
|
19
|
-
if (/^(https?:|mailto:)/i.test(raw)) return raw;
|
|
20
|
-
if (/^[#/]/.test(raw)) return raw;
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const COPY_ICON =
|
|
25
|
-
'<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>';
|
|
26
|
-
|
|
27
|
-
function emphasize(text) {
|
|
28
|
-
return text
|
|
29
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
30
|
-
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
31
|
-
.replace(/(^|[\s(])_([^_\n]+)_/g, "$1<em>$2</em>")
|
|
32
|
-
.replace(/~~([^~\n]+)~~/g, "<del>$1</del>");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function renderInline(text) {
|
|
36
|
-
const stash = [];
|
|
37
|
-
const keep = (html) => {
|
|
38
|
-
stash.push(html);
|
|
39
|
-
return `\uE000${stash.length - 1}\uE001`;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
let html = escapeHtml(text.trim());
|
|
43
|
-
html = html.replace(/`([^`\n]+)`/g, (_, code) => keep(`<code>${code}</code>`));
|
|
44
|
-
html = html.replace(/\[([^\]\n]*)\]\(([^)\s]+)(?:\s+"[^&]*")?\)/g, (_match, label, url) => {
|
|
45
|
-
const href = safeHref(url);
|
|
46
|
-
if (!href) return `${label} (${url})`;
|
|
47
|
-
return keep(
|
|
48
|
-
`<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${emphasize(label)}</a>`,
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
html = html.replace(/(^|[\s(])(https?:\/\/[^\s<>&\uE000]+)/g, (match, lead, url) => {
|
|
52
|
-
const href = safeHref(url);
|
|
53
|
-
if (!href) return match;
|
|
54
|
-
return `${lead}${keep(`<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`)}`;
|
|
55
|
-
});
|
|
56
|
-
html = emphasize(html);
|
|
57
|
-
html = html.replace(/\n/g, "<br>");
|
|
58
|
-
return html.replace(/\uE000(\d+)\uE001/g, (_, index) => stash[Number(index)]);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function codeBlock(code, language) {
|
|
62
|
-
const lang = (language ?? "").replace(/[^\w+#.-]/g, "").slice(0, 20);
|
|
63
|
-
return `<div class="code-block"><div class="code-head">${
|
|
64
|
-
lang ? `<span class="code-lang">${escapeHtml(lang)}</span>` : "<span></span>"
|
|
65
|
-
}<button type="button" class="ghost-icon code-copy" aria-label="Copy code">${COPY_ICON}</button></div><pre><code>${escapeHtml(
|
|
66
|
-
code.replace(/\n$/, ""),
|
|
67
|
-
)}</code></pre></div>`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function splitRow(row) {
|
|
71
|
-
return row
|
|
72
|
-
.trim()
|
|
73
|
-
.replace(/^\|/, "")
|
|
74
|
-
.replace(/\|$/, "")
|
|
75
|
-
.split("|")
|
|
76
|
-
.map((cell) => cell.trim());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function unwrapParagraph(html) {
|
|
80
|
-
const match = /^<p>([\s\S]*)<\/p>$/.exec(html.trim());
|
|
81
|
-
return match && !match[1].includes("<p>") ? match[1] : html;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isBlockStart(line) {
|
|
85
|
-
return (
|
|
86
|
-
/^ {0,3}(```|~~~)/.test(line) ||
|
|
87
|
-
/^ {0,3}#{1,6}\s/.test(line) ||
|
|
88
|
-
/^ {0,3}>/.test(line) ||
|
|
89
|
-
/^ {0,3}([-*+]|\d+[.)])\s+/.test(line) ||
|
|
90
|
-
/^ {0,3}([-*_])(\s*\1){2,}\s*$/.test(line)
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function listBlock(lines, start) {
|
|
95
|
-
const first = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[start]);
|
|
96
|
-
const baseIndent = first[1].length;
|
|
97
|
-
const ordered = /\d/.test(first[2]);
|
|
98
|
-
const items = [];
|
|
99
|
-
let index = start;
|
|
100
|
-
|
|
101
|
-
while (index < lines.length) {
|
|
102
|
-
const match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[index]);
|
|
103
|
-
if (!match || match[1].length !== baseIndent || /\d/.test(match[2]) !== ordered) break;
|
|
104
|
-
const content = [match[3]];
|
|
105
|
-
index += 1;
|
|
106
|
-
while (index < lines.length) {
|
|
107
|
-
const line = lines[index];
|
|
108
|
-
if (!line.trim()) {
|
|
109
|
-
const next = lines[index + 1];
|
|
110
|
-
if (next && /^\s+\S/.test(next)) {
|
|
111
|
-
content.push("");
|
|
112
|
-
index += 1;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
const indent = /^\s*/.exec(line)[0].length;
|
|
118
|
-
const nestedItem = /^\s*([-*+]|\d+[.)])\s+/.test(line);
|
|
119
|
-
if (indent <= baseIndent) break;
|
|
120
|
-
if (nestedItem && indent < baseIndent + 2) break;
|
|
121
|
-
content.push(line.replace(new RegExp(`^\\s{0,${baseIndent + 2}}`), ""));
|
|
122
|
-
index += 1;
|
|
123
|
-
}
|
|
124
|
-
items.push(unwrapParagraph(renderMarkdown(content.join("\n"))));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const tag = ordered ? "ol" : "ul";
|
|
128
|
-
const startAttr = ordered && Number.parseInt(first[2], 10) > 1 ? ` start="${Number.parseInt(first[2], 10)}"` : "";
|
|
129
|
-
return {
|
|
130
|
-
html: `<${tag}${startAttr}>${items.map((item) => `<li>${item}</li>`).join("")}</${tag}>`,
|
|
131
|
-
next: index,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function tableBlock(lines, start) {
|
|
136
|
-
const header = splitRow(lines[start]);
|
|
137
|
-
const align = splitRow(lines[start + 1]).map((cell) => {
|
|
138
|
-
if (/^:-+:$/.test(cell)) return "center";
|
|
139
|
-
if (/-+:$/.test(cell)) return "right";
|
|
140
|
-
return "";
|
|
141
|
-
});
|
|
142
|
-
const rows = [];
|
|
143
|
-
let index = start + 2;
|
|
144
|
-
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
145
|
-
rows.push(splitRow(lines[index]));
|
|
146
|
-
index += 1;
|
|
147
|
-
}
|
|
148
|
-
const cell = (tag, value, column) =>
|
|
149
|
-
`<${tag}${align[column] ? ` class="ta-${align[column]}"` : ""}>${renderInline(value ?? "")}</${tag}>`;
|
|
150
|
-
const head = header.map((value, column) => cell("th", value, column)).join("");
|
|
151
|
-
const body = rows
|
|
152
|
-
.map((row) => `<tr>${header.map((_, column) => cell("td", row[column], column)).join("")}</tr>`)
|
|
153
|
-
.join("");
|
|
154
|
-
return {
|
|
155
|
-
html: `<div class="table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`,
|
|
156
|
-
next: index,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function renderMarkdown(markdown) {
|
|
161
|
-
const lines = String(markdown ?? "")
|
|
162
|
-
.replace(/\r\n?/g, "\n")
|
|
163
|
-
.replace(/\t/g, " ")
|
|
164
|
-
.split("\n");
|
|
165
|
-
const output = [];
|
|
166
|
-
let index = 0;
|
|
167
|
-
|
|
168
|
-
while (index < lines.length) {
|
|
169
|
-
const line = lines[index];
|
|
170
|
-
if (!line.trim()) {
|
|
171
|
-
index += 1;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const fence = /^ {0,3}(`{3,}|~{3,})\s*([\w+#.-]*)/.exec(line);
|
|
176
|
-
if (fence) {
|
|
177
|
-
const marker = fence[1][0];
|
|
178
|
-
const body = [];
|
|
179
|
-
index += 1;
|
|
180
|
-
while (index < lines.length) {
|
|
181
|
-
const current = lines[index];
|
|
182
|
-
if (new RegExp(`^ {0,3}${marker}{3,}\\s*$`).test(current)) {
|
|
183
|
-
index += 1;
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
body.push(current);
|
|
187
|
-
index += 1;
|
|
188
|
-
}
|
|
189
|
-
output.push(codeBlock(body.join("\n"), fence[2]));
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const heading = /^ {0,3}(#{1,6})\s+(.*)$/.exec(line);
|
|
194
|
-
if (heading) {
|
|
195
|
-
const level = heading[1].length;
|
|
196
|
-
output.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
197
|
-
index += 1;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (/^ {0,3}([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
202
|
-
output.push("<hr>");
|
|
203
|
-
index += 1;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (/^ {0,3}>/.test(line)) {
|
|
208
|
-
const quote = [];
|
|
209
|
-
while (index < lines.length && /^ {0,3}>/.test(lines[index])) {
|
|
210
|
-
quote.push(lines[index].replace(/^ {0,3}> ?/, ""));
|
|
211
|
-
index += 1;
|
|
212
|
-
}
|
|
213
|
-
output.push(`<blockquote>${renderMarkdown(quote.join("\n"))}</blockquote>`);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (/^ {0,3}([-*+]|\d+[.)])\s+/.test(line)) {
|
|
218
|
-
const list = listBlock(lines, index);
|
|
219
|
-
output.push(list.html);
|
|
220
|
-
index = list.next;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
line.includes("|") &&
|
|
226
|
-
/^ {0,3}\|?[\s:|-]*-{2,}[\s:|-]*\|?\s*$/.test(lines[index + 1] ?? "")
|
|
227
|
-
) {
|
|
228
|
-
const table = tableBlock(lines, index);
|
|
229
|
-
output.push(table.html);
|
|
230
|
-
index = table.next;
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const paragraph = [line];
|
|
235
|
-
index += 1;
|
|
236
|
-
while (index < lines.length && lines[index].trim() && !isBlockStart(lines[index])) {
|
|
237
|
-
paragraph.push(lines[index]);
|
|
238
|
-
index += 1;
|
|
239
|
-
}
|
|
240
|
-
output.push(`<p>${renderInline(paragraph.join("\n"))}</p>`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return output.join("");
|
|
244
|
-
}
|
|
1
|
+
// A deliberately small, escape-first Markdown renderer.
|
|
2
|
+
// Every character is HTML-escaped before formatting, so raw HTML in model
|
|
3
|
+
// output can never become markup. Only http(s)/mailto/#/relative links survive.
|
|
4
|
+
|
|
5
|
+
const ESCAPES = {
|
|
6
|
+
"&": "&",
|
|
7
|
+
"<": "<",
|
|
8
|
+
">": ">",
|
|
9
|
+
'"': """,
|
|
10
|
+
"'": "'",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function escapeHtml(value) {
|
|
14
|
+
return String(value ?? "").replace(/[&<>"']/g, (character) => ESCAPES[character]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeHref(url) {
|
|
18
|
+
const raw = String(url).trim().replace(/^<|>$/g, "");
|
|
19
|
+
if (/^(https?:|mailto:)/i.test(raw)) return raw;
|
|
20
|
+
if (/^[#/]/.test(raw)) return raw;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const COPY_ICON =
|
|
25
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>';
|
|
26
|
+
|
|
27
|
+
function emphasize(text) {
|
|
28
|
+
return text
|
|
29
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
30
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
31
|
+
.replace(/(^|[\s(])_([^_\n]+)_/g, "$1<em>$2</em>")
|
|
32
|
+
.replace(/~~([^~\n]+)~~/g, "<del>$1</del>");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderInline(text) {
|
|
36
|
+
const stash = [];
|
|
37
|
+
const keep = (html) => {
|
|
38
|
+
stash.push(html);
|
|
39
|
+
return `\uE000${stash.length - 1}\uE001`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let html = escapeHtml(text.trim());
|
|
43
|
+
html = html.replace(/`([^`\n]+)`/g, (_, code) => keep(`<code>${code}</code>`));
|
|
44
|
+
html = html.replace(/\[([^\]\n]*)\]\(([^)\s]+)(?:\s+"[^&]*")?\)/g, (_match, label, url) => {
|
|
45
|
+
const href = safeHref(url);
|
|
46
|
+
if (!href) return `${label} (${url})`;
|
|
47
|
+
return keep(
|
|
48
|
+
`<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${emphasize(label)}</a>`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
html = html.replace(/(^|[\s(])(https?:\/\/[^\s<>&\uE000]+)/g, (match, lead, url) => {
|
|
52
|
+
const href = safeHref(url);
|
|
53
|
+
if (!href) return match;
|
|
54
|
+
return `${lead}${keep(`<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`)}`;
|
|
55
|
+
});
|
|
56
|
+
html = emphasize(html);
|
|
57
|
+
html = html.replace(/\n/g, "<br>");
|
|
58
|
+
return html.replace(/\uE000(\d+)\uE001/g, (_, index) => stash[Number(index)]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function codeBlock(code, language) {
|
|
62
|
+
const lang = (language ?? "").replace(/[^\w+#.-]/g, "").slice(0, 20);
|
|
63
|
+
return `<div class="code-block"><div class="code-head">${
|
|
64
|
+
lang ? `<span class="code-lang">${escapeHtml(lang)}</span>` : "<span></span>"
|
|
65
|
+
}<button type="button" class="ghost-icon code-copy" aria-label="Copy code">${COPY_ICON}</button></div><pre><code>${escapeHtml(
|
|
66
|
+
code.replace(/\n$/, ""),
|
|
67
|
+
)}</code></pre></div>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function splitRow(row) {
|
|
71
|
+
return row
|
|
72
|
+
.trim()
|
|
73
|
+
.replace(/^\|/, "")
|
|
74
|
+
.replace(/\|$/, "")
|
|
75
|
+
.split("|")
|
|
76
|
+
.map((cell) => cell.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unwrapParagraph(html) {
|
|
80
|
+
const match = /^<p>([\s\S]*)<\/p>$/.exec(html.trim());
|
|
81
|
+
return match && !match[1].includes("<p>") ? match[1] : html;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isBlockStart(line) {
|
|
85
|
+
return (
|
|
86
|
+
/^ {0,3}(```|~~~)/.test(line) ||
|
|
87
|
+
/^ {0,3}#{1,6}\s/.test(line) ||
|
|
88
|
+
/^ {0,3}>/.test(line) ||
|
|
89
|
+
/^ {0,3}([-*+]|\d+[.)])\s+/.test(line) ||
|
|
90
|
+
/^ {0,3}([-*_])(\s*\1){2,}\s*$/.test(line)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function listBlock(lines, start) {
|
|
95
|
+
const first = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[start]);
|
|
96
|
+
const baseIndent = first[1].length;
|
|
97
|
+
const ordered = /\d/.test(first[2]);
|
|
98
|
+
const items = [];
|
|
99
|
+
let index = start;
|
|
100
|
+
|
|
101
|
+
while (index < lines.length) {
|
|
102
|
+
const match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[index]);
|
|
103
|
+
if (!match || match[1].length !== baseIndent || /\d/.test(match[2]) !== ordered) break;
|
|
104
|
+
const content = [match[3]];
|
|
105
|
+
index += 1;
|
|
106
|
+
while (index < lines.length) {
|
|
107
|
+
const line = lines[index];
|
|
108
|
+
if (!line.trim()) {
|
|
109
|
+
const next = lines[index + 1];
|
|
110
|
+
if (next && /^\s+\S/.test(next)) {
|
|
111
|
+
content.push("");
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
const indent = /^\s*/.exec(line)[0].length;
|
|
118
|
+
const nestedItem = /^\s*([-*+]|\d+[.)])\s+/.test(line);
|
|
119
|
+
if (indent <= baseIndent) break;
|
|
120
|
+
if (nestedItem && indent < baseIndent + 2) break;
|
|
121
|
+
content.push(line.replace(new RegExp(`^\\s{0,${baseIndent + 2}}`), ""));
|
|
122
|
+
index += 1;
|
|
123
|
+
}
|
|
124
|
+
items.push(unwrapParagraph(renderMarkdown(content.join("\n"))));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const tag = ordered ? "ol" : "ul";
|
|
128
|
+
const startAttr = ordered && Number.parseInt(first[2], 10) > 1 ? ` start="${Number.parseInt(first[2], 10)}"` : "";
|
|
129
|
+
return {
|
|
130
|
+
html: `<${tag}${startAttr}>${items.map((item) => `<li>${item}</li>`).join("")}</${tag}>`,
|
|
131
|
+
next: index,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tableBlock(lines, start) {
|
|
136
|
+
const header = splitRow(lines[start]);
|
|
137
|
+
const align = splitRow(lines[start + 1]).map((cell) => {
|
|
138
|
+
if (/^:-+:$/.test(cell)) return "center";
|
|
139
|
+
if (/-+:$/.test(cell)) return "right";
|
|
140
|
+
return "";
|
|
141
|
+
});
|
|
142
|
+
const rows = [];
|
|
143
|
+
let index = start + 2;
|
|
144
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
145
|
+
rows.push(splitRow(lines[index]));
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
const cell = (tag, value, column) =>
|
|
149
|
+
`<${tag}${align[column] ? ` class="ta-${align[column]}"` : ""}>${renderInline(value ?? "")}</${tag}>`;
|
|
150
|
+
const head = header.map((value, column) => cell("th", value, column)).join("");
|
|
151
|
+
const body = rows
|
|
152
|
+
.map((row) => `<tr>${header.map((_, column) => cell("td", row[column], column)).join("")}</tr>`)
|
|
153
|
+
.join("");
|
|
154
|
+
return {
|
|
155
|
+
html: `<div class="table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`,
|
|
156
|
+
next: index,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function renderMarkdown(markdown) {
|
|
161
|
+
const lines = String(markdown ?? "")
|
|
162
|
+
.replace(/\r\n?/g, "\n")
|
|
163
|
+
.replace(/\t/g, " ")
|
|
164
|
+
.split("\n");
|
|
165
|
+
const output = [];
|
|
166
|
+
let index = 0;
|
|
167
|
+
|
|
168
|
+
while (index < lines.length) {
|
|
169
|
+
const line = lines[index];
|
|
170
|
+
if (!line.trim()) {
|
|
171
|
+
index += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const fence = /^ {0,3}(`{3,}|~{3,})\s*([\w+#.-]*)/.exec(line);
|
|
176
|
+
if (fence) {
|
|
177
|
+
const marker = fence[1][0];
|
|
178
|
+
const body = [];
|
|
179
|
+
index += 1;
|
|
180
|
+
while (index < lines.length) {
|
|
181
|
+
const current = lines[index];
|
|
182
|
+
if (new RegExp(`^ {0,3}${marker}{3,}\\s*$`).test(current)) {
|
|
183
|
+
index += 1;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
body.push(current);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
output.push(codeBlock(body.join("\n"), fence[2]));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const heading = /^ {0,3}(#{1,6})\s+(.*)$/.exec(line);
|
|
194
|
+
if (heading) {
|
|
195
|
+
const level = heading[1].length;
|
|
196
|
+
output.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
197
|
+
index += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (/^ {0,3}([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
202
|
+
output.push("<hr>");
|
|
203
|
+
index += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (/^ {0,3}>/.test(line)) {
|
|
208
|
+
const quote = [];
|
|
209
|
+
while (index < lines.length && /^ {0,3}>/.test(lines[index])) {
|
|
210
|
+
quote.push(lines[index].replace(/^ {0,3}> ?/, ""));
|
|
211
|
+
index += 1;
|
|
212
|
+
}
|
|
213
|
+
output.push(`<blockquote>${renderMarkdown(quote.join("\n"))}</blockquote>`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (/^ {0,3}([-*+]|\d+[.)])\s+/.test(line)) {
|
|
218
|
+
const list = listBlock(lines, index);
|
|
219
|
+
output.push(list.html);
|
|
220
|
+
index = list.next;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (
|
|
225
|
+
line.includes("|") &&
|
|
226
|
+
/^ {0,3}\|?[\s:|-]*-{2,}[\s:|-]*\|?\s*$/.test(lines[index + 1] ?? "")
|
|
227
|
+
) {
|
|
228
|
+
const table = tableBlock(lines, index);
|
|
229
|
+
output.push(table.html);
|
|
230
|
+
index = table.next;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const paragraph = [line];
|
|
235
|
+
index += 1;
|
|
236
|
+
while (index < lines.length && lines[index].trim() && !isBlockStart(lines[index])) {
|
|
237
|
+
paragraph.push(lines[index]);
|
|
238
|
+
index += 1;
|
|
239
|
+
}
|
|
240
|
+
output.push(`<p>${renderInline(paragraph.join("\n"))}</p>`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return output.join("");
|
|
244
|
+
}
|