page-markdown 0.1.4 → 0.2.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/CHANGELOG.md +149 -0
- package/README.md +139 -20
- package/dist/types/clean.d.ts +23 -0
- package/dist/types/convert.d.ts +19 -0
- package/dist/types/extract.d.ts +42 -0
- package/dist/types/facts.d.ts +15 -0
- package/dist/types/hidden.d.ts +14 -0
- package/dist/types/index.d.ts +107 -0
- package/dist/types/metadata.d.ts +18 -0
- package/dist/types/refine.d.ts +31 -0
- package/dist/types/sanitize.d.ts +28 -0
- package/dist/types/standardize.d.ts +16 -0
- package/dist/types/types.d.ts +167 -0
- package/package.json +17 -5
- package/src/clean.js +21 -9
- package/src/convert.js +142 -27
- package/src/extract.js +211 -70
- package/src/facts.js +102 -96
- package/src/hidden.js +46 -45
- package/src/index.js +200 -162
- package/src/metadata.js +79 -78
- package/src/refine.js +115 -104
- package/src/sanitize.js +217 -193
- package/src/standardize.js +150 -17
- package/src/types.js +122 -0
package/src/standardize.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Stage 2: normalise markup on the inert clone so extraction and conversion
|
|
2
2
|
// see consistent structure. Runs before extraction.
|
|
3
|
+
import { isHiddenElement } from "./extract.js";
|
|
3
4
|
|
|
4
5
|
const LAZY_SRC_ATTRS = ["data-src", "data-lazy-src", "data-original", "data-lazy", "data-url"];
|
|
5
6
|
const LANGUAGE_CLASS = [
|
|
@@ -12,9 +13,24 @@ const LANGUAGE_CLASS = [
|
|
|
12
13
|
const LANGUAGE_ALIASES = { python3: "python", py3: "python" };
|
|
13
14
|
const NON_LANGUAGES = new Set(["default", "none", "text", "plain", "plaintext", "nohighlight", "notranslate"]);
|
|
14
15
|
// Wrappers sites put around a heading next to "edit" or permalink widgets.
|
|
15
|
-
|
|
16
|
+
// Removed whatever their text: "edit", "Section titled ...", a zero-width space.
|
|
17
|
+
const ALWAYS_REMOVED_WIDGETS = ".mw-editsection, .hash-link, .sl-anchor-link";
|
|
18
|
+
const EDIT_WIDGETS = `${ALWAYS_REMOVED_WIDGETS}, .headerlink, .anchor, .heading-anchor, a.anchor, [aria-hidden='true'] a`;
|
|
16
19
|
// Content without text that a heading wrapper may still hold.
|
|
17
20
|
const MEDIA = "img, picture, video, audio";
|
|
21
|
+
// Highlighters mark up each code line with one of these.
|
|
22
|
+
const CODE_LINE_ELEMENTS = new Set(["div", "p", "li", "tr", "section"]);
|
|
23
|
+
const TAB_PANELS = "[role='tabpanel'], sp-tab-panel, sl-tab-panel";
|
|
24
|
+
const TABS = "[role='tab'], sp-tab, sl-tab";
|
|
25
|
+
// Longer than this, an inactive panel is a page of its own (an API or change log
|
|
26
|
+
// tab), not a variant of the one shown (pnpm, Yarn).
|
|
27
|
+
const MAX_INACTIVE_PANEL_CHARS = 5000;
|
|
28
|
+
const MAX_TAB_LABEL_CHARS = 40;
|
|
29
|
+
// Asides that are part of the text: notes and warnings. Readability drops every <aside>.
|
|
30
|
+
const NOTE_ASIDE = /(?:^|[\s_-])(?:aside|admonition|callout|note|tip|caution|warning|danger)(?:$|[\s_-])/i;
|
|
31
|
+
const OTHER_ASIDE = /related|sidebar|share|promo|newsletter|toc\b/i;
|
|
32
|
+
// As URL parsers read a scheme: control characters and spaces don't count.
|
|
33
|
+
const isUnsafeUrl = (value) => /^(?:javascript|vbscript|data):/i.test(String(value).replace(/[\x00-\x20]+/g, ""));
|
|
18
34
|
|
|
19
35
|
function resolveUrl(value, base) {
|
|
20
36
|
const raw = String(value || "").trim();
|
|
@@ -44,7 +60,7 @@ function languageFrom(el, maxDepth = 3) {
|
|
|
44
60
|
|
|
45
61
|
function hasMeaningfulContent(el) {
|
|
46
62
|
if ((el.textContent || "").trim()) return true;
|
|
47
|
-
return Boolean(el.querySelector?.("img
|
|
63
|
+
return Boolean(el.querySelector?.("img, svg title"));
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
/**
|
|
@@ -62,7 +78,7 @@ function standardizeImages(doc, base, stats) {
|
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
const current = img.getAttribute("src") || "";
|
|
65
|
-
if (!current || current
|
|
81
|
+
if (!current || isUnsafeUrl(current)) {
|
|
66
82
|
img.remove();
|
|
67
83
|
stats.inlineImagesDropped += 1;
|
|
68
84
|
continue;
|
|
@@ -74,7 +90,7 @@ function standardizeImages(doc, base, stats) {
|
|
|
74
90
|
function standardizeLinks(doc, base, stats) {
|
|
75
91
|
for (const a of Array.from(doc.querySelectorAll("a"))) {
|
|
76
92
|
const href = a.getAttribute("href") || "";
|
|
77
|
-
if (
|
|
93
|
+
if (isUnsafeUrl(href)) {
|
|
78
94
|
a.replaceWith(...Array.from(a.childNodes));
|
|
79
95
|
stats.scriptLinksUnwrapped += 1;
|
|
80
96
|
continue;
|
|
@@ -88,26 +104,66 @@ function standardizeLinks(doc, base, stats) {
|
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
/** Text of a code block, with <br> and block-level lines as line breaks. */
|
|
108
|
+
function codeText(root) {
|
|
109
|
+
let text = "";
|
|
110
|
+
const walk = (node) => {
|
|
111
|
+
for (const child of Array.from(node.childNodes)) {
|
|
112
|
+
if (child.nodeType === 3) text += child.nodeValue;
|
|
113
|
+
else if (child.nodeType !== 1) continue;
|
|
114
|
+
else if (child.localName === "br") text += "\n";
|
|
115
|
+
else {
|
|
116
|
+
const block = CODE_LINE_ELEMENTS.has(child.localName);
|
|
117
|
+
if (block && text && !text.endsWith("\n")) text += "\n";
|
|
118
|
+
walk(child);
|
|
119
|
+
if (block && text && !text.endsWith("\n")) text += "\n";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
walk(root);
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
91
127
|
/** <pre> without <code> (and code without a language) gets a language-tagged <code>. */
|
|
92
128
|
function standardizeCode(doc, stats) {
|
|
93
129
|
for (const pre of Array.from(doc.querySelectorAll("pre"))) {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
for (const button of Array.from(pre.querySelectorAll("button"))) button.remove();
|
|
131
|
+
const inner = pre.querySelector("code");
|
|
132
|
+
const lang = languageFrom(inner || pre, inner ? 4 : 3);
|
|
133
|
+
// One <code> and only whitespace around it: that <code> is the block.
|
|
134
|
+
const sole = pre.children.length === 1 && pre.firstElementChild === inner
|
|
135
|
+
&& !(pre.textContent || "").replace(inner.textContent || "", "").trim();
|
|
136
|
+
const code = sole ? inner : doc.createElement("code");
|
|
137
|
+
if (!sole) stats.preWrapped += 1;
|
|
138
|
+
// Turndown reads textContent, so lines kept apart by markup become text.
|
|
139
|
+
code.textContent = codeText(sole ? inner : pre);
|
|
140
|
+
while (pre.firstChild) pre.removeChild(pre.firstChild);
|
|
141
|
+
pre.appendChild(code);
|
|
105
142
|
if (lang && !/(?:^|\s)language-/.test(code.getAttribute("class") || "")) {
|
|
106
143
|
code.setAttribute("class", `${code.getAttribute("class") || ""} language-${lang}`.trim());
|
|
107
144
|
}
|
|
108
145
|
}
|
|
109
146
|
}
|
|
110
147
|
|
|
148
|
+
/**
|
|
149
|
+
* A link to the heading's own id, inside it or right beside it, is a permalink
|
|
150
|
+
* whatever its class or wording ("Link to ...", "Section titled ...", "#"). A
|
|
151
|
+
* heading whose whole text is that link keeps the text. Left in place, the link
|
|
152
|
+
* makes a short heading's wrapper look like clutter to Readability.
|
|
153
|
+
*/
|
|
154
|
+
function removeOwnPermalinks(heading, stats) {
|
|
155
|
+
const id = heading.getAttribute("id");
|
|
156
|
+
if (!id) return;
|
|
157
|
+
const title = (heading.textContent || "").trim();
|
|
158
|
+
const inside = Array.from(heading.querySelectorAll("[href]"));
|
|
159
|
+
for (const link of [...inside, heading.previousElementSibling, heading.nextElementSibling]) {
|
|
160
|
+
if (link?.getAttribute("href") !== `#${id}`) continue;
|
|
161
|
+
if (inside.includes(link) && (link.textContent || "").trim() === title) link.replaceWith(...Array.from(link.childNodes));
|
|
162
|
+
else link.remove();
|
|
163
|
+
stats.headingWidgetsRemoved += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
111
167
|
/**
|
|
112
168
|
* Headings wrapped with edit/permalink widgets (Wikipedia's div.mw-heading,
|
|
113
169
|
* Sphinx headerlinks): drop the widgets, then replace a wrapper whose only
|
|
@@ -115,22 +171,96 @@ function standardizeCode(doc, stats) {
|
|
|
115
171
|
*/
|
|
116
172
|
function standardizeHeadings(doc, stats) {
|
|
117
173
|
for (const widget of Array.from(doc.querySelectorAll(EDIT_WIDGETS))) {
|
|
118
|
-
if (!(widget.textContent || "").trim() || widget.matches?.(
|
|
174
|
+
if (!(widget.textContent || "").trim() || widget.matches?.(ALWAYS_REMOVED_WIDGETS)) {
|
|
119
175
|
widget.remove();
|
|
120
176
|
stats.headingWidgetsRemoved += 1;
|
|
121
177
|
}
|
|
122
178
|
}
|
|
123
179
|
for (const heading of Array.from(doc.querySelectorAll("h1, h2, h3, h4, h5, h6"))) {
|
|
180
|
+
removeOwnPermalinks(heading, stats);
|
|
124
181
|
const parent = heading.parentElement;
|
|
125
182
|
if (!parent || parent.localName !== "div") continue;
|
|
126
183
|
const others = Array.from(parent.children).filter((el) => el !== heading);
|
|
127
|
-
|
|
184
|
+
const hasBareText = Array.from(parent.childNodes).some((node) => node.nodeType === 3 && node.nodeValue.trim());
|
|
185
|
+
if (!hasBareText && others.every((el) => !(el.textContent || "").trim() && !el.matches?.(MEDIA) && !el.querySelector?.(MEDIA))) {
|
|
128
186
|
parent.replaceWith(heading);
|
|
129
187
|
stats.headingsUnwrapped += 1;
|
|
130
188
|
}
|
|
131
189
|
}
|
|
132
190
|
}
|
|
133
191
|
|
|
192
|
+
function isInactivePanel(panel) {
|
|
193
|
+
if (isHiddenElement(panel) || panel.getAttribute("aria-hidden") === "true" || panel.classList.contains("hidden")) return true;
|
|
194
|
+
// Custom elements that mark only the group: <sp-tabs selected="overview">.
|
|
195
|
+
const value = panel.getAttribute("value");
|
|
196
|
+
const selected = value && panel.parentElement?.closest("[selected]")?.getAttribute("selected");
|
|
197
|
+
return Boolean(selected) && selected !== value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function tabOf(panel, tabs) {
|
|
201
|
+
const id = panel.getAttribute("id");
|
|
202
|
+
const labelledBy = panel.getAttribute("aria-labelledby");
|
|
203
|
+
const value = panel.getAttribute("value") || panel.getAttribute("name");
|
|
204
|
+
const named = tabs.find((tab) => (labelledBy && tab.getAttribute("id") === labelledBy)
|
|
205
|
+
|| (id && (tab.getAttribute("aria-controls") === id || tab.getAttribute("href") === `#${id}`))
|
|
206
|
+
|| (value && (tab.getAttribute("value") === value || tab.getAttribute("panel") === value)));
|
|
207
|
+
if (named) return named;
|
|
208
|
+
// Docusaurus ties them by position alone.
|
|
209
|
+
const parent = panel.parentElement;
|
|
210
|
+
const index = Array.from(parent?.children || []).filter((el) => el.matches?.(TAB_PANELS)).indexOf(panel);
|
|
211
|
+
for (let node = parent, depth = 0; node && depth < 4; node = node.parentElement, depth += 1) {
|
|
212
|
+
const list = node.querySelector("[role='tablist']");
|
|
213
|
+
if (list) return list.querySelectorAll("[role='tab']")[index];
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Tabs read as one document: inactive panels are shown, each under its tab's
|
|
220
|
+
* name, in place of a tab strip that points nowhere. A long inactive panel is
|
|
221
|
+
* left out, and named in stats.omittedTabs.
|
|
222
|
+
*/
|
|
223
|
+
function standardizeTabPanels(doc, stats) {
|
|
224
|
+
// Docker's docs write aria-role, and call a panel a tab: only the strip's is right.
|
|
225
|
+
for (const list of Array.from(doc.querySelectorAll("[aria-role='tablist']"))) {
|
|
226
|
+
list.setAttribute("role", "tablist");
|
|
227
|
+
for (const tab of Array.from(list.children)) tab.setAttribute("role", "tab");
|
|
228
|
+
for (const panel of Array.from(list.nextElementSibling?.children || [])) panel.setAttribute("role", "tabpanel");
|
|
229
|
+
}
|
|
230
|
+
const panels = Array.from(doc.querySelectorAll(TAB_PANELS));
|
|
231
|
+
if (!panels.length) return;
|
|
232
|
+
const tabs = Array.from(doc.querySelectorAll(TABS));
|
|
233
|
+
for (const panel of panels) {
|
|
234
|
+
const label = (tabOf(panel, tabs)?.textContent || "").replace(/\s+/g, " ").trim().slice(0, MAX_TAB_LABEL_CHARS);
|
|
235
|
+
const text = (panel.textContent || "").replace(/\s+/g, " ").trim();
|
|
236
|
+
if (isInactivePanel(panel)) {
|
|
237
|
+
if (text.length > MAX_INACTIVE_PANEL_CHARS) {
|
|
238
|
+
stats.omittedTabs.push(label);
|
|
239
|
+
panel.remove();
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
for (const attr of ["hidden", "aria-hidden", "style"]) panel.removeAttribute(attr);
|
|
243
|
+
panel.classList.remove("hidden");
|
|
244
|
+
}
|
|
245
|
+
if (label && panels.length > 1 && !text.toLowerCase().startsWith(label.toLowerCase())) {
|
|
246
|
+
const heading = doc.createElement("p");
|
|
247
|
+
heading.appendChild(doc.createElement("strong")).textContent = label;
|
|
248
|
+
panel.insertBefore(heading, panel.firstChild);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const el of Array.from(doc.querySelectorAll(`[role='tablist'], ${TABS}`))) el.remove();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function standardizeAsides(doc) {
|
|
255
|
+
for (const aside of Array.from(doc.querySelectorAll("main aside, article aside, [role='main'] aside"))) {
|
|
256
|
+
const name = `${aside.getAttribute("class") || ""} ${aside.getAttribute("aria-label") || ""} ${aside.getAttribute("role") || ""}`;
|
|
257
|
+
if (!NOTE_ASIDE.test(name) || OTHER_ASIDE.test(name)) continue;
|
|
258
|
+
const quote = doc.createElement("blockquote");
|
|
259
|
+
quote.append(...Array.from(aside.childNodes));
|
|
260
|
+
aside.replaceWith(quote);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
134
264
|
/**
|
|
135
265
|
* @param {Document} doc the inert clone (mutated in place)
|
|
136
266
|
* @param {{ url?: string }} [options]
|
|
@@ -145,7 +275,10 @@ export function standardizeDocument(doc, options = {}) {
|
|
|
145
275
|
preWrapped: 0,
|
|
146
276
|
headingWidgetsRemoved: 0,
|
|
147
277
|
headingsUnwrapped: 0,
|
|
278
|
+
omittedTabs: /** @type {string[]} */ ([]),
|
|
148
279
|
};
|
|
280
|
+
standardizeTabPanels(doc, stats);
|
|
281
|
+
standardizeAsides(doc);
|
|
149
282
|
standardizeHeadings(doc, stats);
|
|
150
283
|
standardizeCode(doc, stats);
|
|
151
284
|
standardizeImages(doc, base, stats);
|
package/src/types.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/// <reference lib="dom" preserve="true" />
|
|
2
|
+
// Types only: nothing here exists at runtime. `npm run build:types` turns these
|
|
3
|
+
// and the JSDoc in the other modules into dist/types, which is what TypeScript
|
|
4
|
+
// consumers see.
|
|
5
|
+
export {};
|
|
6
|
+
|
|
7
|
+
/** @typedef {"full"|"no_links"|"text_only"} OutputMode */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} SanitizeOptions
|
|
11
|
+
* @property {boolean} [stripScripts]
|
|
12
|
+
* @property {boolean} [stripStyles]
|
|
13
|
+
* @property {boolean} [stripNoscript]
|
|
14
|
+
* @property {boolean} [stripMeta]
|
|
15
|
+
* @property {boolean} [stripLinkTags]
|
|
16
|
+
* @property {boolean} [stripAds]
|
|
17
|
+
* @property {readonly string[]} [extraStripSelectors] app-specific overlays or widgets to drop, e.g. "[data-app-overlay]"
|
|
18
|
+
* @property {number} [shadowNodeLimit]
|
|
19
|
+
* @property {number} [shadowCharLimit]
|
|
20
|
+
* @property {number} [shadowDepthLimit]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {NonNullable<ConstructorParameters<typeof import("@mozilla/readability").Readability<string>>[1]>} ReadabilityOptions */
|
|
24
|
+
|
|
25
|
+
/** @typedef {import("./extract.js").Extraction} Extraction */
|
|
26
|
+
|
|
27
|
+
/** @typedef {import("./extract.js").Extractor} Extractor */
|
|
28
|
+
|
|
29
|
+
/** @typedef {ReturnType<typeof import("./metadata.js").collectMetadata>} CollectedMetadata */
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} ExtractedMetadata
|
|
33
|
+
* @property {string} [headline] the heading chosen for the capture
|
|
34
|
+
* @property {string} [extractedTitle] the title the extractor reported
|
|
35
|
+
* @property {string} [byline] the byline the extractor reported
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** @typedef {CollectedMetadata & ExtractedMetadata} PageMarkdownMetadata */
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} PageMarkdownExtraction
|
|
42
|
+
* @property {Extraction["method"]} method
|
|
43
|
+
* @property {string} [fallbackReason] why extraction fell back to the full page or the main content
|
|
44
|
+
* @property {true} [leadRecovered] a dropped lead paragraph was restored
|
|
45
|
+
* @property {number} [pageFacts] how many facts `pageFacts` appended
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** @typedef {"tag"|"variation_selector"|"invisible_operator"|"reserved_tag_plane"} HiddenRunKind */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A run as text-sanctifier's findHiddenRuns reports it.
|
|
52
|
+
* @typedef {object} HiddenRun
|
|
53
|
+
* @property {HiddenRunKind} kind
|
|
54
|
+
* @property {number} offset UTF-16 index of the first character
|
|
55
|
+
* @property {number} end UTF-16 index just past the last character
|
|
56
|
+
* @property {number} count code points in the run
|
|
57
|
+
* @property {number} line 1-based line of the first character
|
|
58
|
+
* @property {number} column 1-based column of the first character, in UTF-16 units
|
|
59
|
+
* @property {string} [decoded] tag runs only: the ASCII the tags spell
|
|
60
|
+
* @property {boolean} [decodedTruncated] tag runs only: true when `decoded` stops short of the run
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {object} HiddenTextOmission
|
|
65
|
+
* @property {`hidden_${HiddenRunKind}`} kind
|
|
66
|
+
* @property {number} count runs removed from `markdown`
|
|
67
|
+
* @property {number} chars code points in those runs
|
|
68
|
+
* @property {string} note
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check `kind` before reading `chars`: only hidden-text omissions have it.
|
|
73
|
+
* @typedef {{ kind: "shadow_content", count: number, note: string }
|
|
74
|
+
* | { kind: "json_line", count: number, note: string }
|
|
75
|
+
* | { kind: "inactive_tabs", count: number, note: string }
|
|
76
|
+
* | { kind: "comments", count: number, note: string }
|
|
77
|
+
* | HiddenTextOmission} PageMarkdownOmission
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** @typedef {ReturnType<typeof import("./standardize.js").standardizeDocument>} StandardizationStats */
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {object} PageMarkdownStats
|
|
84
|
+
* @property {OutputMode} mode
|
|
85
|
+
* @property {number} extractedHtmlChars
|
|
86
|
+
* @property {number} convertedChars
|
|
87
|
+
* @property {number} markdownChars
|
|
88
|
+
* @property {number} pageTextChars the page's text at capture time: a capture is a
|
|
89
|
+
* snapshot of the DOM, so a caller can re-capture until this number settles
|
|
90
|
+
* @property {StandardizationStats} standardization
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} PageMarkdownResult
|
|
95
|
+
* @property {string} version the package version that made this result
|
|
96
|
+
* @property {string} markdown
|
|
97
|
+
* @property {PageMarkdownMetadata} metadata
|
|
98
|
+
* @property {PageMarkdownExtraction} extraction
|
|
99
|
+
* @property {PageMarkdownOmission[]} omissions
|
|
100
|
+
* @property {string[]} warnings
|
|
101
|
+
* @property {PageMarkdownStats} stats
|
|
102
|
+
* @property {string} [fullPageMarkdown] present only with `includeFullPage`
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/** @typedef {{ label: string, value: string }} PageFact */
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {string | string[] | ((node: HTMLElement, options: Record<string, unknown>) => boolean)} TurndownFilter
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Turndown's public methods; Turndown ships no types of its own.
|
|
113
|
+
* @typedef {object} TurndownInstance
|
|
114
|
+
* @property {(input: string | Node) => string} turndown
|
|
115
|
+
* @property {(plugin: TurndownPlugin | TurndownPlugin[]) => TurndownInstance} use
|
|
116
|
+
* @property {(key: string, rule: { filter: TurndownFilter, replacement: (content: string, node: Node, options: Record<string, unknown>) => string }) => TurndownInstance} addRule
|
|
117
|
+
* @property {(filter: TurndownFilter) => TurndownInstance} keep
|
|
118
|
+
* @property {(filter: TurndownFilter) => TurndownInstance} remove
|
|
119
|
+
* @property {(text: string) => string} escape
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/** @typedef {(service: TurndownInstance) => void} TurndownPlugin */
|