@danielsimonjr/sciengjs 0.5.1 → 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/README.md +3 -3
- package/dist/browser/render.d.ts +3 -1
- package/dist/browser/render.js +7 -2
- package/dist/browser/render.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +10 -1
- package/dist/config.js.map +1 -1
- package/dist/core.js +4 -0
- package/dist/core.js.map +1 -1
- package/dist/html/backend-md.d.ts +3 -0
- package/dist/html/backend-md.js +52 -0
- package/dist/html/backend-md.js.map +1 -0
- package/dist/html/convert.d.ts +35 -0
- package/dist/html/convert.js +434 -0
- package/dist/html/convert.js.map +1 -0
- package/dist/html/fetch-image.d.ts +28 -0
- package/dist/html/fetch-image.js +178 -0
- package/dist/html/fetch-image.js.map +1 -0
- package/dist/html/images.d.ts +11 -0
- package/dist/html/images.js +53 -0
- package/dist/html/images.js.map +1 -0
- package/dist/html/index.d.ts +2 -0
- package/dist/html/index.js +3 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/md.d.ts +23 -0
- package/dist/html/md.js +72 -0
- package/dist/html/md.js.map +1 -0
- package/dist/registry.js +1 -0
- package/dist/registry.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/entry.js +1 -1
- package/dist/worker/entry.js.map +1 -1
- package/dist/worker/protocol.d.ts +11 -1
- package/dist/worker/run.js +1 -1
- package/dist/worker/run.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/** The HTML to Markdown converter: parse5 parses the document, and the walk emits GitHub Flavored Markdown. */
|
|
2
|
+
import { parse, serializeOuter } from "parse5";
|
|
3
|
+
/** The block type of a scieng page block when the page does not name it. */
|
|
4
|
+
const RAW_HTML = "raw-html";
|
|
5
|
+
/** Elements that the walk drops with their content. */
|
|
6
|
+
const SKIP = new Set(["script", "style", "head", "template", "title", "noscript", "meta", "link"]);
|
|
7
|
+
/** Elements that flow inside a paragraph. Any other element starts a block. */
|
|
8
|
+
const INLINE = new Set([
|
|
9
|
+
"a", "abbr", "b", "br", "cite", "code", "del", "em", "i", "img", "input", "ins", "kbd",
|
|
10
|
+
"mark", "q", "s", "small", "span", "strong", "sub", "sup", "time", "u", "math", "svg",
|
|
11
|
+
]);
|
|
12
|
+
/** Markdown characters that a text node escapes. */
|
|
13
|
+
const SPECIAL = /[\\*_`[\]<>|~$]/g;
|
|
14
|
+
/** An ampersand that starts an entity form: a named, decimal or hexadecimal character reference. */
|
|
15
|
+
const ENTITY_START = /&(?=#[0-9]+;|#[xX][0-9a-fA-F]+;|[A-Za-z][A-Za-z0-9]*;)/g;
|
|
16
|
+
/** A line start that Markdown reads as a block marker: a list item, a heading, a quote or a setext underline. */
|
|
17
|
+
const BLOCK_START = /^( *)([0-9]{1,9}(?=[.)])|[-+=#>])/;
|
|
18
|
+
/** The fence info text of a data-scieng-type. Any other value falls back to the inferred type. */
|
|
19
|
+
const BLOCK_TYPE = /^[A-Za-z0-9_+-]+$/;
|
|
20
|
+
function isElement(node) {
|
|
21
|
+
return "tagName" in node;
|
|
22
|
+
}
|
|
23
|
+
function isText(node) {
|
|
24
|
+
return node.nodeName === "#text";
|
|
25
|
+
}
|
|
26
|
+
function attr(el, name) {
|
|
27
|
+
return el.attrs.find((a) => a.name === name)?.value;
|
|
28
|
+
}
|
|
29
|
+
function hasClass(el, name) {
|
|
30
|
+
return (attr(el, "class") ?? "").split(/\s+/).includes(name);
|
|
31
|
+
}
|
|
32
|
+
/** The element descendants of a node, in document order. */
|
|
33
|
+
function descendants(node) {
|
|
34
|
+
const out = [];
|
|
35
|
+
if ("childNodes" in node) {
|
|
36
|
+
for (const child of node.childNodes) {
|
|
37
|
+
if (isElement(child)) {
|
|
38
|
+
out.push(child, ...descendants(child));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** A fenced block. The fence is longer than any backtick run in the content. */
|
|
45
|
+
function fenced(content, language) {
|
|
46
|
+
const longest = Math.max(0, ...(content.match(/`+/g) ?? []).map((run) => run.length));
|
|
47
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
48
|
+
return `${fence}${language}\n${content}\n${fence}`;
|
|
49
|
+
}
|
|
50
|
+
/** The block type of a scieng block, from its markup, when the page does not name the type. */
|
|
51
|
+
function inferBlockType(el) {
|
|
52
|
+
for (const inner of descendants(el)) {
|
|
53
|
+
if (inner.tagName === "pre" && hasClass(inner, "mermaid")) {
|
|
54
|
+
return "mermaid";
|
|
55
|
+
}
|
|
56
|
+
const id = attr(inner, "id") ?? "";
|
|
57
|
+
if (inner.tagName === "div" && /^dot-\d+$/.test(id)) {
|
|
58
|
+
return "dot";
|
|
59
|
+
}
|
|
60
|
+
if (hasClass(inner, "latex-block")) {
|
|
61
|
+
return "latex";
|
|
62
|
+
}
|
|
63
|
+
if (inner.tagName === "div" && (/^plotly-\d+$/.test(id) || hasClass(inner, "plotly-static"))) {
|
|
64
|
+
return "plotly";
|
|
65
|
+
}
|
|
66
|
+
if (hasClass(inner, "wavedrom-block")) {
|
|
67
|
+
return "wavedrom";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return RAW_HTML;
|
|
71
|
+
}
|
|
72
|
+
/** True for an element that the walk drops with its content, or a scieng block whose markup the walk does not read. */
|
|
73
|
+
export function isDropped(el) {
|
|
74
|
+
return SKIP.has(el.tagName) || isSciengBlock(el);
|
|
75
|
+
}
|
|
76
|
+
/** True for a scieng page block: a div with the source attribute. */
|
|
77
|
+
function isSciengBlock(el) {
|
|
78
|
+
return el.tagName === "div" && attr(el, "data-scieng-source") !== undefined;
|
|
79
|
+
}
|
|
80
|
+
/** The fenced block of a scieng block. Its markup is not read. The fence language is the block type. */
|
|
81
|
+
function sciengFence(el) {
|
|
82
|
+
const named = attr(el, "data-scieng-type");
|
|
83
|
+
const type = named !== undefined && BLOCK_TYPE.test(named) ? named : inferBlockType(el);
|
|
84
|
+
const language = type === RAW_HTML ? "html" : type;
|
|
85
|
+
return fenced(attr(el, "data-scieng-source") ?? "", language);
|
|
86
|
+
}
|
|
87
|
+
/** The TeX of a math element, from its annotation of type application/x-tex. */
|
|
88
|
+
function texOf(el) {
|
|
89
|
+
const annotation = descendants(el).find((d) => d.tagName === "annotation" && attr(d, "encoding") === "application/x-tex");
|
|
90
|
+
return annotation === undefined ? undefined : rawText(annotation).trim();
|
|
91
|
+
}
|
|
92
|
+
function isDisplayMath(node) {
|
|
93
|
+
return isElement(node) && node.tagName === "math" && attr(node, "display") === "block";
|
|
94
|
+
}
|
|
95
|
+
/** Adds the xmlns attribute to a serialized svg when it has none. */
|
|
96
|
+
function svgAsset(el, ctx) {
|
|
97
|
+
let markup = serializeOuter(el);
|
|
98
|
+
if (attr(el, "xmlns") === undefined) {
|
|
99
|
+
markup = markup.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
100
|
+
}
|
|
101
|
+
ctx.counter.count += 1;
|
|
102
|
+
const name = `image-${ctx.counter.count}.svg`;
|
|
103
|
+
ctx.assets.push({ name, data: new TextEncoder().encode(markup), mediaType: "image/svg+xml" });
|
|
104
|
+
return ``;
|
|
105
|
+
}
|
|
106
|
+
function escapeText(value) {
|
|
107
|
+
return value.replace(SPECIAL, "\\$&").replace(ENTITY_START, "\\&");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Escapes the block marker at the start of each line of a run of inline Markdown.
|
|
111
|
+
* A line starts at the start of the run and after each hard break. The spaces at a line start are removed.
|
|
112
|
+
* A line that starts with a code span starts with a backtick, so the escape never changes code.
|
|
113
|
+
*/
|
|
114
|
+
function escapeLineStarts(text) {
|
|
115
|
+
return text
|
|
116
|
+
.split("\\\n")
|
|
117
|
+
.map((line, i) => (i === 0 ? line : line.replace(/^ +/, "")).replace(BLOCK_START, (_m, sp, mark) => /^[0-9]/.test(mark) ? `${sp}${mark}\\` : `${sp}\\${mark}`))
|
|
118
|
+
.join("\\\n");
|
|
119
|
+
}
|
|
120
|
+
/** Inline Markdown of a run that forms one block: a paragraph, a list item or a footnote. */
|
|
121
|
+
function blockText(nodes, ctx) {
|
|
122
|
+
return escapeLineStarts(inline(nodes, ctx).trim());
|
|
123
|
+
}
|
|
124
|
+
/** A link destination. A destination with a space, a paren or an angle bracket goes in angle brackets. */
|
|
125
|
+
function destination(url) {
|
|
126
|
+
const flat = url.replace(/[\r\n]/g, (c) => encodeURIComponent(c));
|
|
127
|
+
return /[\s()<>]/.test(flat) || flat === "" ? `<${flat.replace(/[<>]/g, "\\$&")}>` : flat;
|
|
128
|
+
}
|
|
129
|
+
/** Emphasis around a text. The spaces at the edges of the text move outside the markers. */
|
|
130
|
+
function emphasis(text, marker) {
|
|
131
|
+
const inner = text.trim();
|
|
132
|
+
if (inner === "") {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
const lead = /^\s/.test(text) ? " " : "";
|
|
136
|
+
const trail = /\s$/.test(text) ? " " : "";
|
|
137
|
+
return `${lead}${marker}${inner}${marker}${trail}`;
|
|
138
|
+
}
|
|
139
|
+
/** A code span. The fence is longer than any backtick run. Padding keeps an edge backtick or edge spaces. */
|
|
140
|
+
function codeSpan(raw, inCell) {
|
|
141
|
+
let text = raw.replace(/\r?\n/g, " ");
|
|
142
|
+
if (text === "") {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
if (inCell) {
|
|
146
|
+
text = text.replace(/\|/g, "\\|");
|
|
147
|
+
}
|
|
148
|
+
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
149
|
+
const fence = "`".repeat(longest + 1);
|
|
150
|
+
const allSpace = /^ +$/.test(text);
|
|
151
|
+
const pad = !allSpace && (text.startsWith("`") || text.endsWith("`") || (text.startsWith(" ") && text.endsWith(" ")));
|
|
152
|
+
return pad ? `${fence} ${text} ${fence}` : `${fence}${text}${fence}`;
|
|
153
|
+
}
|
|
154
|
+
/** The text of a node with no escape, for code. */
|
|
155
|
+
function rawText(node) {
|
|
156
|
+
if (isText(node)) {
|
|
157
|
+
return node.value;
|
|
158
|
+
}
|
|
159
|
+
if ("childNodes" in node) {
|
|
160
|
+
return node.childNodes.map(rawText).join("");
|
|
161
|
+
}
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
/** Inline Markdown for a run of nodes. */
|
|
165
|
+
function inline(nodes, ctx) {
|
|
166
|
+
return nodes.map((node) => {
|
|
167
|
+
if (isText(node)) {
|
|
168
|
+
return escapeText(node.value.replace(/\s+/g, " "));
|
|
169
|
+
}
|
|
170
|
+
return isElement(node) ? inlineElement(node, ctx) : "";
|
|
171
|
+
}).join("");
|
|
172
|
+
}
|
|
173
|
+
function inlineElement(el, ctx) {
|
|
174
|
+
if (SKIP.has(el.tagName)) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
const children = () => inline(el.childNodes, ctx);
|
|
178
|
+
switch (el.tagName) {
|
|
179
|
+
case "strong":
|
|
180
|
+
case "b":
|
|
181
|
+
return emphasis(children(), "**");
|
|
182
|
+
case "em":
|
|
183
|
+
case "i":
|
|
184
|
+
return emphasis(children(), "*");
|
|
185
|
+
case "code":
|
|
186
|
+
return codeSpan(rawText(el), ctx.inCell);
|
|
187
|
+
case "a": {
|
|
188
|
+
const href = attr(el, "href") ?? "";
|
|
189
|
+
if (href.startsWith("#fnref")) {
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
if (href.startsWith("#fn")) {
|
|
193
|
+
return `[^${href.slice(3).replace(/^-/, "")}]`;
|
|
194
|
+
}
|
|
195
|
+
return href === "" ? children() : `[${children()}](${destination(href)})`;
|
|
196
|
+
}
|
|
197
|
+
case "img":
|
|
198
|
+
return ` ?? "")})`;
|
|
199
|
+
case "math": {
|
|
200
|
+
const tex = texOf(el);
|
|
201
|
+
return tex === undefined ? escapeText(rawText(el).trim().replace(/\s+/g, " ")) : `$${tex.replace(/\s+/g, " ")}$`;
|
|
202
|
+
}
|
|
203
|
+
case "svg":
|
|
204
|
+
return svgAsset(el, ctx);
|
|
205
|
+
case "br":
|
|
206
|
+
return ctx.inCell ? "<br>" : "\\\n";
|
|
207
|
+
case "input":
|
|
208
|
+
return "";
|
|
209
|
+
default:
|
|
210
|
+
return children();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Block Markdown for a run of nodes. Each entry is one block. */
|
|
214
|
+
function blocks(nodes, ctx) {
|
|
215
|
+
const out = [];
|
|
216
|
+
let run = [];
|
|
217
|
+
const flush = () => {
|
|
218
|
+
const text = blockText(run, ctx);
|
|
219
|
+
if (text !== "") {
|
|
220
|
+
out.push(text);
|
|
221
|
+
}
|
|
222
|
+
run = [];
|
|
223
|
+
};
|
|
224
|
+
for (const node of nodes) {
|
|
225
|
+
if (isDisplayMath(node)) {
|
|
226
|
+
flush();
|
|
227
|
+
const tex = texOf(node);
|
|
228
|
+
out.push(tex === undefined ? rawText(node).trim() : `$$${tex}$$`);
|
|
229
|
+
}
|
|
230
|
+
else if (isText(node) || (isElement(node) && INLINE.has(node.tagName))) {
|
|
231
|
+
run.push(node);
|
|
232
|
+
}
|
|
233
|
+
else if (isElement(node)) {
|
|
234
|
+
flush();
|
|
235
|
+
out.push(...blockElement(node, ctx));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
flush();
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function blockElement(el, ctx) {
|
|
242
|
+
if (SKIP.has(el.tagName)) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
if (isSciengBlock(el)) {
|
|
246
|
+
return [sciengFence(el)];
|
|
247
|
+
}
|
|
248
|
+
if (/^h[1-6]$/.test(el.tagName)) {
|
|
249
|
+
const text = inline(el.childNodes, ctx).trim().replace(/\\\n/g, " ");
|
|
250
|
+
return text === "" ? [] : [`${"#".repeat(Number(el.tagName[1]))} ${text}`];
|
|
251
|
+
}
|
|
252
|
+
switch (el.tagName) {
|
|
253
|
+
case "p": {
|
|
254
|
+
const text = blockText(el.childNodes, ctx);
|
|
255
|
+
return text === "" ? [] : [text];
|
|
256
|
+
}
|
|
257
|
+
case "ul":
|
|
258
|
+
case "ol":
|
|
259
|
+
return [list(el, ctx)];
|
|
260
|
+
case "table":
|
|
261
|
+
return [table(el, ctx)];
|
|
262
|
+
case "pre":
|
|
263
|
+
return [codeBlock(el)];
|
|
264
|
+
case "blockquote":
|
|
265
|
+
return [quote(el, ctx)];
|
|
266
|
+
case "hr":
|
|
267
|
+
return ["---"];
|
|
268
|
+
default:
|
|
269
|
+
if (hasClass(el, "footnotes")) {
|
|
270
|
+
collectFootnotes(el, ctx);
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
return blocks(el.childNodes, ctx);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** True for a list item child that makes the item loose: a block that is not a nested list. */
|
|
277
|
+
function isLooseBlock(node) {
|
|
278
|
+
return isElement(node) && !INLINE.has(node.tagName) && !SKIP.has(node.tagName) &&
|
|
279
|
+
node.tagName !== "ul" && node.tagName !== "ol";
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* A list. Each block of an item is indented by the width of the item marker. An item with a block other than a
|
|
283
|
+
* nested list is loose: its blocks and the items of the list have a blank line between them.
|
|
284
|
+
*/
|
|
285
|
+
function list(el, ctx) {
|
|
286
|
+
const ordered = el.tagName === "ol";
|
|
287
|
+
const startValue = Number(attr(el, "start") ?? "1");
|
|
288
|
+
let number = Number.isNaN(startValue) ? 1 : startValue;
|
|
289
|
+
const items = [];
|
|
290
|
+
let loose = false;
|
|
291
|
+
for (const item of el.childNodes) {
|
|
292
|
+
if (!isElement(item) || item.tagName !== "li") {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const marker = ordered ? `${number}. ` : "- ";
|
|
296
|
+
number += 1;
|
|
297
|
+
let task = "";
|
|
298
|
+
const content = [];
|
|
299
|
+
for (const child of item.childNodes) {
|
|
300
|
+
if (isElement(child) && child.tagName === "input" && attr(child, "type") === "checkbox") {
|
|
301
|
+
task = child.attrs.some((a) => a.name === "checked") ? "[x] " : "[ ] ";
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
content.push(child);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const itemLoose = content.some(isLooseBlock);
|
|
308
|
+
loose ||= itemLoose;
|
|
309
|
+
const indent = " ".repeat(marker.length);
|
|
310
|
+
const body = blocks(content, ctx)
|
|
311
|
+
.join(itemLoose ? "\n\n" : "\n")
|
|
312
|
+
.split("\n")
|
|
313
|
+
.map((line, i) => (i === 0 || line === "" ? line : `${indent}${line}`))
|
|
314
|
+
.join("\n");
|
|
315
|
+
items.push(`${marker}${task}${body}`.trimEnd());
|
|
316
|
+
}
|
|
317
|
+
return items.join(loose ? "\n\n" : "\n");
|
|
318
|
+
}
|
|
319
|
+
function rowsOf(el) {
|
|
320
|
+
const rows = [];
|
|
321
|
+
for (const child of el.childNodes) {
|
|
322
|
+
if (!isElement(child)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (child.tagName === "tr") {
|
|
326
|
+
rows.push(child);
|
|
327
|
+
}
|
|
328
|
+
else if (["thead", "tbody", "tfoot"].includes(child.tagName)) {
|
|
329
|
+
rows.push(...rowsOf(child));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return rows;
|
|
333
|
+
}
|
|
334
|
+
function alignOf(cell) {
|
|
335
|
+
const style = attr(cell, "style") ?? "";
|
|
336
|
+
const match = /text-align:\s*(left|right|center)/.exec(style);
|
|
337
|
+
const value = attr(cell, "align") ?? match?.[1] ?? "";
|
|
338
|
+
if (value === "left") {
|
|
339
|
+
return ":---";
|
|
340
|
+
}
|
|
341
|
+
if (value === "right") {
|
|
342
|
+
return "---:";
|
|
343
|
+
}
|
|
344
|
+
if (value === "center") {
|
|
345
|
+
return ":---:";
|
|
346
|
+
}
|
|
347
|
+
return "---";
|
|
348
|
+
}
|
|
349
|
+
/** A GFM table. The first row is the header. */
|
|
350
|
+
function table(el, ctx) {
|
|
351
|
+
const rows = rowsOf(el);
|
|
352
|
+
const cellsOf = (row) => row.childNodes.filter((c) => isElement(c) && (c.tagName === "th" || c.tagName === "td"));
|
|
353
|
+
const header = rows[0] ? cellsOf(rows[0]) : [];
|
|
354
|
+
if (header.length === 0) {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
const width = header.length;
|
|
358
|
+
const cellText = (cell) => {
|
|
359
|
+
ctx.inCell = true;
|
|
360
|
+
try {
|
|
361
|
+
return inline(cell.childNodes, ctx).trim().replace(/\s*\n\s*/g, " ");
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
ctx.inCell = false;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const line = (cells) => {
|
|
368
|
+
const padded = Array.from({ length: width }, (_, i) => cells[i] ?? "");
|
|
369
|
+
return `| ${padded.join(" | ")} |`;
|
|
370
|
+
};
|
|
371
|
+
const out = [line(header.map(cellText)), line(header.map(alignOf))];
|
|
372
|
+
for (const row of rows.slice(1)) {
|
|
373
|
+
out.push(line(cellsOf(row).map(cellText)));
|
|
374
|
+
}
|
|
375
|
+
return out.join("\n");
|
|
376
|
+
}
|
|
377
|
+
/** A fenced code block. The fence language comes from a `language-X` class of the code element. */
|
|
378
|
+
function codeBlock(pre) {
|
|
379
|
+
const code = pre.childNodes.find((c) => isElement(c) && c.tagName === "code");
|
|
380
|
+
const language = code ? /language-(\S+)/.exec(attr(code, "class") ?? "")?.[1] ?? "" : "";
|
|
381
|
+
let content = rawText(code ?? pre);
|
|
382
|
+
if (content.endsWith("\n")) {
|
|
383
|
+
content = content.slice(0, -1);
|
|
384
|
+
}
|
|
385
|
+
return fenced(content, language);
|
|
386
|
+
}
|
|
387
|
+
function quote(el, ctx) {
|
|
388
|
+
return blocks(el.childNodes, ctx)
|
|
389
|
+
.join("\n\n")
|
|
390
|
+
.split("\n")
|
|
391
|
+
.map((line) => (line === "" ? ">" : `> ${line}`))
|
|
392
|
+
.join("\n");
|
|
393
|
+
}
|
|
394
|
+
/** Reads the footnote definitions of a footnote container. The container itself renders nothing. */
|
|
395
|
+
function collectFootnotes(el, ctx) {
|
|
396
|
+
const visit = (node) => {
|
|
397
|
+
for (const child of node.childNodes) {
|
|
398
|
+
if (!isElement(child)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const id = attr(child, "id") ?? "";
|
|
402
|
+
if (child.tagName === "li" && id.startsWith("fn")) {
|
|
403
|
+
const text = blockText(child.childNodes, ctx);
|
|
404
|
+
ctx.footnotes.push(`[^${id.slice(2).replace(/^-/, "")}]: ${text}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
visit(child);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
visit(el);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Converts an HTML document to GitHub Flavored Markdown and its assets.
|
|
415
|
+
* The Markdown ends with one newline, or is empty.
|
|
416
|
+
*/
|
|
417
|
+
export function convertHtml(html, options = {}) {
|
|
418
|
+
const document = parse(html);
|
|
419
|
+
const ctx = {
|
|
420
|
+
footnotes: [],
|
|
421
|
+
assets: [],
|
|
422
|
+
assetPrefix: options.assetPrefix ?? "",
|
|
423
|
+
counter: options.counter ?? { count: 0 },
|
|
424
|
+
inCell: false,
|
|
425
|
+
};
|
|
426
|
+
const out = [...blocks(document.childNodes, ctx), ...ctx.footnotes];
|
|
427
|
+
const markdown = out.length === 0 ? "" : `${out.join("\n\n")}\n`;
|
|
428
|
+
return { markdown, assets: ctx.assets };
|
|
429
|
+
}
|
|
430
|
+
/** Converts an HTML document to GitHub Flavored Markdown. The assets are dropped. */
|
|
431
|
+
export function htmlToMarkdown(html, options = {}) {
|
|
432
|
+
return convertHtml(html, options).markdown;
|
|
433
|
+
}
|
|
434
|
+
//# sourceMappingURL=convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../../src/html/convert.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,OAAO,EAAE,KAAK,EAAE,cAAc,EAA8B,MAAM,QAAQ,CAAC;AA0C3E,4EAA4E;AAC5E,MAAM,QAAQ,GAAG,UAAU,CAAC;AAE5B,uDAAuD;AACvD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEnG,+EAA+E;AAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC;IACrB,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK;IACtF,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK;CACtF,CAAC,CAAC;AAEH,oDAAoD;AACpD,MAAM,OAAO,GAAG,kBAAkB,CAAC;AAEnC,oGAAoG;AACpG,MAAM,YAAY,GAAG,yDAAyD,CAAC;AAE/E,iHAAiH;AACjH,MAAM,WAAW,GAAG,mCAAmC,CAAC;AAExD,kGAAkG;AAClG,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAEvC,SAAS,SAAS,CAAC,IAAU;IAC3B,OAAO,SAAS,IAAI,IAAI,CAAC;AAC3B,CAAC;AAED,SAAS,MAAM,CAAC,IAAU;IACxB,OAAO,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC;AACnC,CAAC;AAED,SAAS,IAAI,CAAC,EAAW,EAAE,IAAY;IACrC,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,KAAK,CAAC;AACtD,CAAC;AAED,SAAS,QAAQ,CAAC,EAAW,EAAE,IAAY;IACzC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,4DAA4D;AAC5D,SAAS,WAAW,CAAC,IAAU;IAC7B,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrB,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,gFAAgF;AAChF,SAAS,MAAM,CAAC,OAAe,EAAE,QAAgB;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtF,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,GAAG,KAAK,GAAG,QAAQ,KAAK,OAAO,KAAK,KAAK,EAAE,CAAC;AACrD,CAAC;AAED,+FAA+F;AAC/F,SAAS,cAAc,CAAC,EAAW;IACjC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,IAAI,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;YAC1D,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACpD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,CAAC;YACnC,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;YAC7F,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC;YACtC,OAAO,UAAU,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,SAAS,CAAC,EAAW;IACnC,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,qEAAqE;AACrE,SAAS,aAAa,CAAC,EAAW;IAChC,OAAO,EAAE,CAAC,OAAO,KAAK,KAAK,IAAI,IAAI,CAAC,EAAE,EAAE,oBAAoB,CAAC,KAAK,SAAS,CAAC;AAC9E,CAAC;AAED,wGAAwG;AACxG,SAAS,WAAW,CAAC,EAAW;IAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IACxF,MAAM,QAAQ,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,oBAAoB,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;AAChE,CAAC;AAED,gFAAgF;AAChF,SAAS,KAAK,CAAC,EAAW;IACxB,MAAM,UAAU,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,YAAY,IAAI,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,mBAAmB,CACjF,CAAC;IACF,OAAO,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3E,CAAC;AAED,SAAS,aAAa,CAAC,IAAU;IAC/B,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,OAAO,CAAC;AACzF,CAAC;AAED,qEAAqE;AACrE,SAAS,QAAQ,CAAC,EAAW,EAAE,GAAY;IACzC,IAAI,MAAM,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,yCAAyC,CAAC,CAAC;IAC9E,CAAC;IACD,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,SAAS,GAAG,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC;IAC9C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAC;IAC9F,OAAO,OAAO,GAAG,CAAC,WAAW,GAAG,IAAI,GAAG,CAAC;AAC1C,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,IAAI;SACR,KAAK,CAAC,MAAM,CAAC;SACb,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CACf,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,EAAU,EAAE,IAAY,EAAE,EAAE,CAC/F,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;SAC9D,IAAI,CAAC,MAAM,CAAC,CAAC;AAClB,CAAC;AAED,6FAA6F;AAC7F,SAAS,SAAS,CAAC,KAAsB,EAAE,GAAY;IACrD,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,0GAA0G;AAC1G,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5F,CAAC;AAED,4FAA4F;AAC5F,SAAS,QAAQ,CAAC,IAAY,EAAE,MAAc;IAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1C,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,EAAE,CAAC;AACrD,CAAC;AAED,6GAA6G;AAC7G,SAAS,QAAQ,CAAC,GAAW,EAAE,MAAe;IAC5C,IAAI,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACtC,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACnF,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtH,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;AACvE,CAAC;AAED,mDAAmD;AACnD,SAAS,OAAO,CAAC,IAAU;IACzB,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IACD,IAAI,YAAY,IAAI,IAAI,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,0CAA0C;AAC1C,SAAS,MAAM,CAAC,KAAsB,EAAE,GAAY;IAClD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACjB,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,EAAW,EAAE,GAAY;IAC9C,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IAClD,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,QAAQ,CAAC;QACd,KAAK,GAAG;YACN,OAAO,QAAQ,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,CAAC;QACpC,KAAK,IAAI,CAAC;QACV,KAAK,GAAG;YACN,OAAO,QAAQ,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC;QACnC,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,KAAK,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC9B,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,OAAO,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC;YACjD,CAAC;YACD,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,KAAK,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;QAC5E,CAAC;QACD,KAAK,KAAK;YACR,OAAO,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC;QACjH,KAAK,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC;YACtB,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC;QACnH,CAAC;QACD,KAAK,KAAK;YACR,OAAO,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QAC3B,KAAK,IAAI;YACP,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACtC,KAAK,OAAO;YACV,OAAO,EAAE,CAAC;QACZ;YACE,OAAO,QAAQ,EAAE,CAAC;IACtB,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,SAAS,MAAM,CAAC,KAAsB,EAAE,GAAY;IAClD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,GAAG,GAAW,EAAE,CAAC;IACrB,MAAM,KAAK,GAAG,GAAS,EAAE;QACvB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACjC,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YAChB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;QACD,GAAG,GAAG,EAAE,CAAC;IACX,CAAC,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,KAAK,EAAE,CAAC;YACR,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;QACpE,CAAC;aAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACzE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,CAAC;aAAM,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,KAAK,EAAE,CAAC;YACR,GAAG,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,KAAK,EAAE,CAAC;IACR,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,EAAW,EAAE,GAAY;IAC7C,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACrE,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;YAC3C,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,KAAK,IAAI,CAAC;QACV,KAAK,IAAI;YACP,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACzB,KAAK,OAAO;YACV,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1B,KAAK,KAAK;YACR,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC;QACzB,KAAK,YAAY;YACf,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QAC1B,KAAK,IAAI;YACP,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB;YACE,IAAI,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC,EAAE,CAAC;gBAC9B,gBAAgB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;gBAC1B,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,OAAO,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,+FAA+F;AAC/F,SAAS,YAAY,CAAC,IAAU;IAC9B,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;QAC5E,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,SAAS,IAAI,CAAC,EAAW,EAAE,GAAY;IACrC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC;IACpC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;IACpD,IAAI,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;IACvD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,UAAU,EAAE,CAAC;QACjC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC9C,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9C,MAAM,IAAI,CAAC,CAAC;QACZ,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAW,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpC,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC;gBACxF,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;YACzE,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7C,KAAK,KAAK,SAAS,CAAC;QACpB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC;aAC9B,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;aAC/B,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC;aACtE,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAC3C,CAAC;AAED,SAAS,MAAM,CAAC,EAAW;IACzB,MAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;aAAM,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,OAAO,CAAC,IAAa;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACtD,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QACtB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gDAAgD;AAChD,SAAS,KAAK,CAAC,EAAW,EAAE,GAAY;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IACxB,MAAM,OAAO,GAAG,CAAC,GAAY,EAAa,EAAE,CAC1C,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC;IACzG,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IAC5B,MAAM,QAAQ,GAAG,CAAC,IAAa,EAAU,EAAE;QACzC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC;YACH,OAAO,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;QACvE,CAAC;gBAAS,CAAC;YACT,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,KAAe,EAAU,EAAE;QACvC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvE,OAAO,KAAK,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACrC,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,mGAAmG;AACnG,SAAS,SAAS,CAAC,GAAY;IAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC;IAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACzF,IAAI,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED,SAAS,KAAK,CAAC,EAAW,EAAE,GAAY;IACtC,OAAO,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,CAAC;SAC9B,IAAI,CAAC,MAAM,CAAC;SACZ,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;SAChD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,oGAAoG;AACpG,SAAS,gBAAgB,CAAC,EAAW,EAAE,GAAY;IACjD,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtB,SAAS;YACX,CAAC;YACD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBAClD,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;gBAC9C,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,KAAK,CAAC,CAAC;YACf,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,KAAK,CAAC,EAAE,CAAC,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAAO,GAAmB,EAAE;IACpE,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAY;QACnB,SAAS,EAAE,EAAE;QACb,MAAM,EAAE,EAAE;QACV,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;QACtC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE;QACxC,MAAM,EAAE,KAAK;KACd,CAAC;IACF,MAAM,GAAG,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;IACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;AAC1C,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAO,GAAmB,EAAE;IACvE,OAAO,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FetchImage } from "./images.ts";
|
|
2
|
+
/** Resolves a host name to all of its addresses, as `dns.promises.lookup(host, { all: true })` does. */
|
|
3
|
+
export type LookupAll = (host: string) => Promise<readonly {
|
|
4
|
+
address: string;
|
|
5
|
+
family: number;
|
|
6
|
+
}[]>;
|
|
7
|
+
/** The inputs of one image fetcher. The engine passes the DNS resolver; a test passes a double. */
|
|
8
|
+
export interface ImageFetcherOptions {
|
|
9
|
+
readonly lookup: LookupAll;
|
|
10
|
+
/** The time limit of one download over all its redirects, in milliseconds. */
|
|
11
|
+
readonly timeoutMs: number;
|
|
12
|
+
/** The size limit of the body, in bytes. */
|
|
13
|
+
readonly maxBytes: number;
|
|
14
|
+
/** Allows 127/8 and ::1. Only a test against a local server sets it. The default refuses loopback. */
|
|
15
|
+
readonly allowLoopback?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when the address is loopback, private, link-local or unspecified:
|
|
19
|
+
* 0/8, 10/8, 127/8, 169.254/16, 172.16/12, 192.168/16, ::, ::1, fc00::/7, fe80::/10, and an IPv4-mapped address of these.
|
|
20
|
+
* A text that is not an IP address also gives true.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isPrivateAddress(address: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Makes the fetcher of remote images. Each hop of a redirect passes the scheme check, and its connection passes the
|
|
25
|
+
* address check: the connection uses only an address that the check accepted. The fetcher follows at most three
|
|
26
|
+
* redirects. The reply must be an image/* type inside the size limit and the time limit.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createImageFetcher(opts: ImageFetcherOptions): FetchImage;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** The remote image download of html to md: manual redirects, an address check on each hop, a time limit and a size limit. */
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { request as httpsRequest } from "node:https";
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
/** The most redirects that one download follows. */
|
|
6
|
+
const MAX_REDIRECTS = 3;
|
|
7
|
+
const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
|
|
8
|
+
/**
|
|
9
|
+
* True when the address is loopback, private, link-local or unspecified:
|
|
10
|
+
* 0/8, 10/8, 127/8, 169.254/16, 172.16/12, 192.168/16, ::, ::1, fc00::/7, fe80::/10, and an IPv4-mapped address of these.
|
|
11
|
+
* A text that is not an IP address also gives true.
|
|
12
|
+
*/
|
|
13
|
+
export function isPrivateAddress(address) {
|
|
14
|
+
let addr = address.trim();
|
|
15
|
+
if (addr.length >= 2 && addr.startsWith("[") && addr.endsWith("]"))
|
|
16
|
+
addr = addr.slice(1, -1);
|
|
17
|
+
const kind = isIP(addr);
|
|
18
|
+
if (kind === 0)
|
|
19
|
+
return true;
|
|
20
|
+
if (kind === 4) {
|
|
21
|
+
const [a, b] = addr.split(".").map(Number);
|
|
22
|
+
return a === 0 || a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) ||
|
|
23
|
+
(a === 192 && b === 168) || (a === 169 && b === 254);
|
|
24
|
+
}
|
|
25
|
+
const h = ipv6Hextets(addr);
|
|
26
|
+
if (h === undefined)
|
|
27
|
+
return true;
|
|
28
|
+
if (h.slice(0, 7).every((x) => x === 0) && (h[7] === 0 || h[7] === 1))
|
|
29
|
+
return true;
|
|
30
|
+
if (h.slice(0, 5).every((x) => x === 0) && h[5] === 0xffff) {
|
|
31
|
+
return isPrivateAddress(`${h[6] >> 8}.${h[6] & 0xff}.${h[7] >> 8}.${h[7] & 0xff}`);
|
|
32
|
+
}
|
|
33
|
+
return (h[0] & 0xfe00) === 0xfc00 || (h[0] & 0xffc0) === 0xfe80;
|
|
34
|
+
}
|
|
35
|
+
/** The eight hextets of an IPv6 address that isIP accepts, or undefined for a form that this parser does not read. */
|
|
36
|
+
function ipv6Hextets(addr) {
|
|
37
|
+
let s = addr.split("%")[0];
|
|
38
|
+
if (s.includes(".")) {
|
|
39
|
+
const colon = s.lastIndexOf(":");
|
|
40
|
+
const o = s.slice(colon + 1).split(".").map(Number);
|
|
41
|
+
if (o.length !== 4 || o.some((x) => !(x >= 0 && x <= 255)))
|
|
42
|
+
return undefined;
|
|
43
|
+
s = `${s.slice(0, colon + 1)}${((o[0] << 8) | o[1]).toString(16)}:${((o[2] << 8) | o[3]).toString(16)}`;
|
|
44
|
+
}
|
|
45
|
+
const dc = s.indexOf("::");
|
|
46
|
+
let groups;
|
|
47
|
+
if (dc === -1) {
|
|
48
|
+
groups = s.split(":");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const left = dc === 0 ? [] : s.slice(0, dc).split(":");
|
|
52
|
+
const right = s.slice(dc + 2) === "" ? [] : s.slice(dc + 2).split(":");
|
|
53
|
+
groups = [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
54
|
+
}
|
|
55
|
+
if (groups.length !== 8 || groups.some((g) => !/^[0-9a-fA-F]{1,4}$/.test(g)))
|
|
56
|
+
return undefined;
|
|
57
|
+
return groups.map((g) => parseInt(g, 16));
|
|
58
|
+
}
|
|
59
|
+
/** True when the address may be connected to. */
|
|
60
|
+
function permitted(address, allowLoopback) {
|
|
61
|
+
if (allowLoopback && (/^127\./.test(address) || address === "::1"))
|
|
62
|
+
return true;
|
|
63
|
+
return !isPrivateAddress(address);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks the scheme and the host before the request. A host name is resolved here, and again by the connection,
|
|
67
|
+
* which checks its own answer. An answer that changes between the two lookups is refused at connect time.
|
|
68
|
+
*/
|
|
69
|
+
async function checkTarget(url, resolve, allowLoopback) {
|
|
70
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
71
|
+
throw new Error(`only http and https images are fetched, not ${url.protocol}`);
|
|
72
|
+
}
|
|
73
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
74
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
75
|
+
throw new Error(`the host ${host} is a private address`);
|
|
76
|
+
}
|
|
77
|
+
const all = isIP(host) !== 0 ? [{ address: host }] : await resolve(host);
|
|
78
|
+
const bad = all.find((a) => !permitted(a.address, allowLoopback));
|
|
79
|
+
if (bad !== undefined)
|
|
80
|
+
throw new Error(`the host ${host} resolves to the private address ${bad.address}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The lookup of the connection. It resolves all addresses of the host and refuses the connection when one of them is
|
|
84
|
+
* private. The connection then uses an address that passed the check.
|
|
85
|
+
*/
|
|
86
|
+
function checkedLookup(resolve, allowLoopback) {
|
|
87
|
+
return (hostname, options, callback) => {
|
|
88
|
+
resolve(hostname).then((all) => {
|
|
89
|
+
if (all.length === 0)
|
|
90
|
+
throw new Error(`the host ${hostname} has no address`);
|
|
91
|
+
const bad = all.find((a) => !permitted(a.address, allowLoopback));
|
|
92
|
+
if (bad !== undefined)
|
|
93
|
+
throw new Error(`the host ${hostname} resolves to the private address ${bad.address}`);
|
|
94
|
+
const wanted = all.filter((a) => !options.family || a.family === options.family);
|
|
95
|
+
const usable = wanted.length > 0 ? wanted : all;
|
|
96
|
+
if (options.all) {
|
|
97
|
+
callback(null, usable.map((a) => ({ address: a.address, family: a.family })));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
callback(null, usable[0].address, usable[0].family);
|
|
101
|
+
}
|
|
102
|
+
}).catch((err) => callback(err, "", 0));
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Sends one GET request on a new connection, so each connection passes the lookup check, and gives the response head. */
|
|
106
|
+
function get(url, lookup, signal) {
|
|
107
|
+
const send = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const req = send(url, { method: "GET", lookup, signal, agent: false, headers: { accept: "image/*" } }, resolve);
|
|
110
|
+
req.on("error", reject);
|
|
111
|
+
req.end();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Reads the body as a stream and stops when it passes the size limit. */
|
|
115
|
+
async function readCapped(reply, maxBytes) {
|
|
116
|
+
const declared = Number(reply.headers["content-length"] ?? "");
|
|
117
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
118
|
+
reply.destroy();
|
|
119
|
+
throw new Error(`the image is ${declared} bytes, over the limit of ${maxBytes}`);
|
|
120
|
+
}
|
|
121
|
+
const parts = [];
|
|
122
|
+
let total = 0;
|
|
123
|
+
for await (const chunk of reply) {
|
|
124
|
+
const part = chunk;
|
|
125
|
+
total += part.byteLength;
|
|
126
|
+
if (total > maxBytes) {
|
|
127
|
+
reply.destroy();
|
|
128
|
+
throw new Error(`the image is over the limit of ${maxBytes} bytes`);
|
|
129
|
+
}
|
|
130
|
+
parts.push(part);
|
|
131
|
+
}
|
|
132
|
+
const out = new Uint8Array(total);
|
|
133
|
+
let at = 0;
|
|
134
|
+
for (const p of parts) {
|
|
135
|
+
out.set(p, at);
|
|
136
|
+
at += p.byteLength;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Makes the fetcher of remote images. Each hop of a redirect passes the scheme check, and its connection passes the
|
|
142
|
+
* address check: the connection uses only an address that the check accepted. The fetcher follows at most three
|
|
143
|
+
* redirects. The reply must be an image/* type inside the size limit and the time limit.
|
|
144
|
+
*/
|
|
145
|
+
export function createImageFetcher(opts) {
|
|
146
|
+
const allowLoopback = opts.allowLoopback === true;
|
|
147
|
+
const lookup = checkedLookup(opts.lookup, allowLoopback);
|
|
148
|
+
return async (raw) => {
|
|
149
|
+
const signal = AbortSignal.timeout(opts.timeoutMs);
|
|
150
|
+
let url = new URL(raw);
|
|
151
|
+
for (let hop = 0;; hop += 1) {
|
|
152
|
+
await checkTarget(url, opts.lookup, allowLoopback);
|
|
153
|
+
const reply = await get(url, lookup, signal);
|
|
154
|
+
const status = reply.statusCode ?? 0;
|
|
155
|
+
if (REDIRECT_STATUS.has(status)) {
|
|
156
|
+
reply.destroy();
|
|
157
|
+
const location = reply.headers.location;
|
|
158
|
+
if (location === undefined)
|
|
159
|
+
throw new Error(`HTTP ${status} with no location`);
|
|
160
|
+
if (hop >= MAX_REDIRECTS)
|
|
161
|
+
throw new Error(`more than ${MAX_REDIRECTS} redirects`);
|
|
162
|
+
url = new URL(location, url);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (status < 200 || status > 299) {
|
|
166
|
+
reply.destroy();
|
|
167
|
+
throw new Error(`HTTP ${status}`);
|
|
168
|
+
}
|
|
169
|
+
const mime = (reply.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
|
|
170
|
+
if (!mime.startsWith("image/")) {
|
|
171
|
+
reply.destroy();
|
|
172
|
+
throw new Error(`the reply is ${mime === "" ? "of no type" : mime}, not an image`);
|
|
173
|
+
}
|
|
174
|
+
return { bytes: await readCapped(reply, opts.maxBytes), mime };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=fetch-image.js.map
|