page-markdown 0.2.0 → 0.3.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/ADDITIONAL_TERMS.md +122 -122
- package/CHANGELOG.md +80 -0
- package/LICENSE +694 -694
- package/README.md +55 -10
- package/THIRD_PARTY_NOTICES.md +264 -264
- package/dist/types/concealed.d.ts +59 -0
- package/dist/types/extract.d.ts +1 -0
- package/dist/types/index.d.ts +8 -3
- package/dist/types/sanitize.d.ts +18 -1
- package/dist/types/standardize.d.ts +14 -1
- package/dist/types/types.d.ts +37 -1
- package/package.json +69 -68
- package/robots.txt +79 -79
- package/src/clean.js +1 -1
- package/src/concealed.js +418 -0
- package/src/convert.js +34 -15
- package/src/extract.js +37 -2
- package/src/index.js +23 -4
- package/src/sanitize.js +93 -65
- package/src/standardize.js +62 -5
- package/src/types.js +18 -1
package/src/sanitize.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Stage 1: clone the page into an inert document, inline open shadow roots
|
|
2
2
|
// within limits, and strip non-content. The live page is never modified.
|
|
3
|
+
import { CONCEALED_MARK, createConcealmentWalk, isText, isElement, DOM_LOCAL_NAME, DOM_GET_ROOT_NODE, DOM_CHILD_NODES, DOM_NODE_VALUE, DOM_SHADOW_ROOT, DOM_CONTENT, DOM_APPEND_CHILD, DOM_NODE_TYPE} from "./concealed.js";
|
|
3
4
|
|
|
4
5
|
/** @type {Readonly<Required<import("./types.js").SanitizeOptions>>} */
|
|
5
6
|
export const DEFAULT_SANITIZE_OPTIONS = Object.freeze({
|
|
@@ -9,6 +10,7 @@ export const DEFAULT_SANITIZE_OPTIONS = Object.freeze({
|
|
|
9
10
|
stripMeta: true,
|
|
10
11
|
stripLinkTags: true,
|
|
11
12
|
stripAds: true,
|
|
13
|
+
detectConcealedText: false,
|
|
12
14
|
// App-specific overlays or widgets to drop, e.g. "[data-app-overlay]".
|
|
13
15
|
extraStripSelectors: [],
|
|
14
16
|
shadowNodeLimit: 5000,
|
|
@@ -36,10 +38,10 @@ const AD_ELEMENT_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-(?:placement|advert|adve
|
|
|
36
38
|
|
|
37
39
|
/** @param {any} node */
|
|
38
40
|
export function isAdvertisementElement(node) {
|
|
39
|
-
if (node?.
|
|
41
|
+
if (node?.[DOM_NODE_TYPE] !== 1) return false;
|
|
40
42
|
// Sites put page-wide ad settings on <body> (heise.de's data-ad-unit).
|
|
41
|
-
if (node
|
|
42
|
-
if (AD_ELEMENT_NAME.test(node
|
|
43
|
+
if (node[DOM_LOCAL_NAME] === "html" || node[DOM_LOCAL_NAME] === "body") return false;
|
|
44
|
+
if (AD_ELEMENT_NAME.test(node[DOM_LOCAL_NAME] || "")) return true;
|
|
43
45
|
try {
|
|
44
46
|
return Boolean(node.matches?.(AD_SELECTOR));
|
|
45
47
|
} catch {
|
|
@@ -59,7 +61,7 @@ function stripSelectors(options) {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function isStripped(node, selectors, options) {
|
|
62
|
-
if (node
|
|
64
|
+
if (!isElement(node)) return false;
|
|
63
65
|
if (selectors.some((selector) => node.matches?.(selector))) return true;
|
|
64
66
|
return Boolean(options.stripAds && isAdvertisementElement(node));
|
|
65
67
|
}
|
|
@@ -96,27 +98,31 @@ function positiveLimit(value, name) {
|
|
|
96
98
|
*
|
|
97
99
|
* @param {Document|Element} root
|
|
98
100
|
* @param {import("./types.js").SanitizeOptions} [options]
|
|
99
|
-
* @returns {{ document: Document, omissions: { shadowHosts: number } }}
|
|
100
101
|
*/
|
|
101
102
|
export function sanitizeToInertDocument(root, options = {}) {
|
|
102
103
|
const opts = { ...DEFAULT_SANITIZE_OPTIONS, ...options };
|
|
103
|
-
const limits =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const limits = [
|
|
105
|
+
positiveLimit(opts.shadowNodeLimit, "shadowNodeLimit"),
|
|
106
|
+
positiveLimit(opts.shadowCharLimit, "shadowCharLimit"),
|
|
107
|
+
positiveLimit(opts.shadowDepthLimit, "shadowDepthLimit"),
|
|
108
|
+
];
|
|
108
109
|
const selectors = stripSelectors(opts);
|
|
109
|
-
const sourceDoc = /** @type {Document} */ (root
|
|
110
|
-
const source = root
|
|
110
|
+
const sourceDoc = /** @type {Document} */ (root[DOM_NODE_TYPE] === 9 ? root : root.ownerDocument);
|
|
111
|
+
const source = root[DOM_NODE_TYPE] === 9 ? /** @type {Document} */ (root).documentElement : root;
|
|
111
112
|
const inert = createInertDocument(sourceDoc);
|
|
113
|
+
const concealment = createConcealmentWalk(sourceDoc, opts.detectConcealedText);
|
|
112
114
|
|
|
113
115
|
function shallow(node) {
|
|
114
|
-
if (node
|
|
115
|
-
|
|
116
|
+
if (!isElement(node)) {
|
|
117
|
+
const copy = inert.importNode(node, false);
|
|
118
|
+
return copy;
|
|
119
|
+
}
|
|
120
|
+
const name = node.prefix ? `${node.prefix}:${node[DOM_LOCAL_NAME]}` : node[DOM_LOCAL_NAME];
|
|
116
121
|
const copy = node.namespaceURI
|
|
117
122
|
? inert.createElementNS(node.namespaceURI, name)
|
|
118
123
|
: inert.createElement(name);
|
|
119
124
|
for (const attr of Array.from(node.attributes || [])) {
|
|
125
|
+
if (attr.name.toLowerCase() === CONCEALED_MARK) continue;
|
|
120
126
|
// HTML allows colon-bearing names without a namespace (framework
|
|
121
127
|
// directives); setAttributeNS rejects those.
|
|
122
128
|
if (attr.namespaceURI) copy.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
|
|
@@ -129,89 +135,111 @@ export function sanitizeToInertDocument(root, options = {}) {
|
|
|
129
135
|
let chars = 0;
|
|
130
136
|
let overLimit = false;
|
|
131
137
|
let shadowHosts = 0;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
&& typeof node.assignedNodes === "function" && Boolean(node.getRootNode?.().host);
|
|
138
|
+
function carry(work, pair) {
|
|
139
|
+
for (let i = 2; i <= 8; i++) pair[i] = work[i];
|
|
140
|
+
return pair;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
chars = 0;
|
|
142
|
-
for (const child of Array.from(host.shadowRoot.childNodes)) {
|
|
143
|
-
const childCopy = shadowNode(child, depth);
|
|
144
|
-
if (childCopy) target.appendChild(childCopy);
|
|
145
|
-
if (overLimit) break;
|
|
146
|
-
}
|
|
147
|
-
if (overLimit) shadowHosts += 1;
|
|
148
|
-
overLimit = false;
|
|
149
|
-
[nodes, chars] = outer;
|
|
143
|
+
function isAssigningSlot(node) {
|
|
144
|
+
return isElement(node) && node[DOM_LOCAL_NAME] === "slot"
|
|
145
|
+
&& typeof node.assignedNodes === "function" && !!node[DOM_GET_ROOT_NODE]?.().host;
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
function shadowNode(
|
|
153
|
-
const
|
|
154
|
-
|
|
148
|
+
function shadowNode(work, depth, lightHost) {
|
|
149
|
+
const [node, allocated] = work;
|
|
150
|
+
const inPage = lightHost || node[DOM_GET_ROOT_NODE]?.()[DOM_NODE_TYPE] === 9;
|
|
151
|
+
if (depth > limits[2] || (!inPage && ++nodes > limits[0])) {
|
|
155
152
|
overLimit = true;
|
|
156
153
|
return null;
|
|
157
154
|
}
|
|
158
|
-
if (isStripped(node, selectors, opts)) return inert.createDocumentFragment();
|
|
159
|
-
const copy = shallow(node);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
if (!lightHost && isStripped(node, selectors, opts)) return inert.createDocumentFragment();
|
|
156
|
+
const copy = allocated || shallow(node);
|
|
157
|
+
work = concealment.inherit(work, node, copy);
|
|
158
|
+
const host = inPage && node[DOM_SHADOW_ROOT];
|
|
159
|
+
const outer = host && [nodes, chars];
|
|
160
|
+
let elementChars = 0;
|
|
161
|
+
if (host) nodes = chars = 0;
|
|
162
|
+
else {
|
|
163
|
+
if (!inPage) chars += isElement(node) ? (elementChars = (copy.outerHTML || "").length) : (node[DOM_NODE_VALUE] || "").length * 6;
|
|
164
|
+
if (chars > limits[1]) { overLimit = true; return null; }
|
|
163
165
|
}
|
|
164
|
-
|
|
165
|
-
if (chars > limits.chars) {
|
|
166
|
-
overLimit = true;
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
let children = node.childNodes;
|
|
166
|
+
let children = node[DOM_CHILD_NODES];
|
|
170
167
|
let target = copy;
|
|
171
|
-
if (
|
|
168
|
+
if (host) children = host[DOM_CHILD_NODES];
|
|
169
|
+
else if (isAssigningSlot(node)) {
|
|
172
170
|
const assigned = node.assignedNodes({ flatten: true });
|
|
173
|
-
children = assigned.length ? assigned : node
|
|
171
|
+
children = assigned.length ? assigned : node[DOM_CHILD_NODES];
|
|
174
172
|
target = inert.createDocumentFragment();
|
|
175
|
-
} else if (node
|
|
176
|
-
children = node
|
|
177
|
-
} else if (node
|
|
178
|
-
children = node
|
|
179
|
-
target = copy
|
|
173
|
+
} else if (node[DOM_SHADOW_ROOT]) {
|
|
174
|
+
children = node[DOM_SHADOW_ROOT][DOM_CHILD_NODES];
|
|
175
|
+
} else if (isElement(node) && node[DOM_LOCAL_NAME] === "template" && node[DOM_CONTENT]) {
|
|
176
|
+
children = node[DOM_CONTENT][DOM_CHILD_NODES];
|
|
177
|
+
target = copy[DOM_CONTENT] || copy;
|
|
180
178
|
}
|
|
179
|
+
const pairs = [];
|
|
180
|
+
let textAllocations = 0;
|
|
181
181
|
for (const child of Array.from(children || [])) {
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// Stripped children still pass through the recursive node-budget gate.
|
|
183
|
+
// Only direct text must exist while this element's declaration is
|
|
184
|
+
// local. Element allocation stays behind the recursive budget gate.
|
|
185
|
+
const pageText = child[DOM_GET_ROOT_NODE]?.()[DOM_NODE_TYPE] === 9;
|
|
186
|
+
const allocateText = isText(child) && (pageText || ++textAllocations <= limits[0] - nodes);
|
|
187
|
+
pairs.push([child, allocateText ? shallow(child) : null]);
|
|
188
|
+
}
|
|
189
|
+
concealment.process(node, copy, work, host ? null : pairs);
|
|
190
|
+
if (!inPage && isElement(node)) {
|
|
191
|
+
// Detection may add the compatibility hidden attribute. Charge it before
|
|
192
|
+
// accepting children, just as when detection ran during shallow cloning.
|
|
193
|
+
chars += (copy.outerHTML || "").length - elementChars;
|
|
194
|
+
if (chars > limits[1]) { overLimit = true; return null; }
|
|
195
|
+
}
|
|
196
|
+
for (const pair of pairs) {
|
|
197
|
+
const childCopy = shadowNode(carry(work, pair), depth + 1);
|
|
198
|
+
if (childCopy) target[DOM_APPEND_CHILD](childCopy);
|
|
184
199
|
if (overLimit) break;
|
|
185
200
|
}
|
|
201
|
+
if (host) {
|
|
202
|
+
if (overLimit) shadowHosts++;
|
|
203
|
+
overLimit = false;
|
|
204
|
+
[nodes, chars] = outer;
|
|
205
|
+
return copy;
|
|
206
|
+
}
|
|
186
207
|
return isAssigningSlot(node) ? target : copy;
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
const clone = shallow(source);
|
|
211
|
+
const initial = concealment.initial(source);
|
|
190
212
|
// Iterative light-DOM walk: ordinary pages get no depth limit.
|
|
191
|
-
const pending = [[source, clone]];
|
|
213
|
+
const pending = [carry(initial, [source, clone])];
|
|
192
214
|
while (pending.length) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
const work = /** @type {any[]} */ (pending.pop());
|
|
216
|
+
const [live, copy] = work;
|
|
217
|
+
if (live[DOM_SHADOW_ROOT]) {
|
|
218
|
+
shadowNode(work, 0, true);
|
|
196
219
|
continue;
|
|
197
220
|
}
|
|
198
|
-
const children = live
|
|
199
|
-
const target = copy
|
|
221
|
+
const children = live[DOM_CONTENT]?.[DOM_CHILD_NODES] || live[DOM_CHILD_NODES];
|
|
222
|
+
const target = copy[DOM_CONTENT] || copy;
|
|
200
223
|
const pairs = [];
|
|
201
224
|
for (const child of Array.from(children || [])) {
|
|
202
225
|
if (isStripped(child, selectors, opts)) continue;
|
|
203
226
|
const childCopy = shallow(child);
|
|
204
|
-
target
|
|
227
|
+
target[DOM_APPEND_CHILD](childCopy);
|
|
205
228
|
pairs.push([child, childCopy]);
|
|
206
229
|
}
|
|
207
|
-
|
|
230
|
+
concealment.process(live, copy, work, pairs);
|
|
231
|
+
for (let i = pairs.length - 1; i >= 0; i -= 1) {
|
|
232
|
+
const pair = pairs[i];
|
|
233
|
+
if (!isElement(pair[0])) continue;
|
|
234
|
+
pending.push(carry(concealment.inherit(work, pair[0]), pair));
|
|
235
|
+
}
|
|
208
236
|
}
|
|
209
237
|
|
|
210
|
-
if (clone
|
|
238
|
+
if (clone[DOM_LOCAL_NAME] === "html") {
|
|
211
239
|
inert.removeChild(inert.documentElement);
|
|
212
|
-
inert
|
|
240
|
+
inert[DOM_APPEND_CHILD](clone);
|
|
213
241
|
} else {
|
|
214
|
-
inert.body
|
|
242
|
+
inert.body[DOM_APPEND_CHILD](clone);
|
|
215
243
|
}
|
|
216
|
-
return { document: inert, omissions: { shadowHosts } };
|
|
244
|
+
return { document: inert, omissions: { shadowHosts }, concealment };
|
|
217
245
|
}
|
package/src/standardize.js
CHANGED
|
@@ -68,6 +68,21 @@ function hasMeaningfulContent(el) {
|
|
|
68
68
|
* data: placeholder. Then drop images that are still inline data.
|
|
69
69
|
*/
|
|
70
70
|
function standardizeImages(doc, base, stats) {
|
|
71
|
+
const squash = (text) => (text || "").replace(/\s+/g, " ").trim();
|
|
72
|
+
for (const element of doc.querySelectorAll("figcaption, .caption")) {
|
|
73
|
+
const caption = squash(element.textContent);
|
|
74
|
+
if (!caption || element.closest(".pswp-caption-content") || isHiddenElement(element)) continue;
|
|
75
|
+
const figure = element.closest("figure");
|
|
76
|
+
// PhotoSwipe's copy is hidden by CSS; galleries may have only that copy.
|
|
77
|
+
for (const copy of (figure || element.parentElement?.parentElement)?.querySelectorAll(".pswp-caption-content") || []) {
|
|
78
|
+
if (squash(copy.textContent) === caption) copy.remove();
|
|
79
|
+
}
|
|
80
|
+
for (const img of figure?.querySelectorAll("img") || []) {
|
|
81
|
+
for (const attr of ["alt", "title"]) {
|
|
82
|
+
if (squash(img.getAttribute(attr)) === caption) img.removeAttribute(attr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
71
86
|
for (const img of Array.from(doc.querySelectorAll("img"))) {
|
|
72
87
|
const src = img.getAttribute("src") || "";
|
|
73
88
|
if (!src || src.startsWith("data:")) {
|
|
@@ -100,6 +115,19 @@ function standardizeLinks(doc, base, stats) {
|
|
|
100
115
|
stats.emptyLinksRemoved += 1;
|
|
101
116
|
continue;
|
|
102
117
|
}
|
|
118
|
+
if (base && href) {
|
|
119
|
+
try {
|
|
120
|
+
const target = new URL(href, base);
|
|
121
|
+
// Some browser URL parsers accept percent-encoded spaces in a host.
|
|
122
|
+
// Special-scheme hosts cannot contain ASCII whitespace or controls.
|
|
123
|
+
if (/^(?:https?|ftp|wss?):$/.test(target.protocol)
|
|
124
|
+
&& /[\x00-\x20\x7f]/.test(decodeURIComponent(target.hostname))) throw new TypeError("Invalid link host");
|
|
125
|
+
} catch {
|
|
126
|
+
a.replaceWith(...Array.from(a.childNodes));
|
|
127
|
+
stats.invalidLinksUnwrapped += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
103
131
|
if (href) a.setAttribute("href", resolveUrl(href, base));
|
|
104
132
|
}
|
|
105
133
|
}
|
|
@@ -170,6 +198,10 @@ function removeOwnPermalinks(heading, stats) {
|
|
|
170
198
|
* remaining element is the heading with the heading itself.
|
|
171
199
|
*/
|
|
172
200
|
function standardizeHeadings(doc, stats) {
|
|
201
|
+
// An upper-deck kicker belongs with the title, not ordinary body prose.
|
|
202
|
+
for (const label of doc.querySelectorAll(".upper-deck__text")) {
|
|
203
|
+
label.innerHTML = `<strong>${label.innerHTML}</strong>`;
|
|
204
|
+
}
|
|
173
205
|
for (const widget of Array.from(doc.querySelectorAll(EDIT_WIDGETS))) {
|
|
174
206
|
if (!(widget.textContent || "").trim() || widget.matches?.(ALWAYS_REMOVED_WIDGETS)) {
|
|
175
207
|
widget.remove();
|
|
@@ -220,7 +252,7 @@ function tabOf(panel, tabs) {
|
|
|
220
252
|
* name, in place of a tab strip that points nowhere. A long inactive panel is
|
|
221
253
|
* left out, and named in stats.omittedTabs.
|
|
222
254
|
*/
|
|
223
|
-
function
|
|
255
|
+
export function prepareConcealment(doc, concealment) {
|
|
224
256
|
// Docker's docs write aria-role, and call a panel a tab: only the strip's is right.
|
|
225
257
|
for (const list of Array.from(doc.querySelectorAll("[aria-role='tablist']"))) {
|
|
226
258
|
list.setAttribute("role", "tablist");
|
|
@@ -228,13 +260,29 @@ function standardizeTabPanels(doc, stats) {
|
|
|
228
260
|
for (const panel of Array.from(list.nextElementSibling?.children || [])) panel.setAttribute("role", "tabpanel");
|
|
229
261
|
}
|
|
230
262
|
const panels = Array.from(doc.querySelectorAll(TAB_PANELS));
|
|
263
|
+
const hasProvenance = concealment && ["computed", "incomplete"].includes(concealment.check);
|
|
264
|
+
const inactive = new Map(panels.map((panel) => [panel, isInactivePanel(panel)]));
|
|
265
|
+
const panelLengths = new Map(panels.map((panel) => [panel, (panel.textContent || "").replace(/\s+/g, " ").trim().length]));
|
|
266
|
+
// Resolve short-panel exceptions before lifting, but copy labels only after
|
|
267
|
+
// lifting: concealed label text must never become a new visible heading.
|
|
268
|
+
for (const panel of panels) {
|
|
269
|
+
if (hasProvenance && inactive.get(panel) && panelLengths.get(panel) <= MAX_INACTIVE_PANEL_CHARS) {
|
|
270
|
+
concealment?.exempt(panel);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
concealment?.lift(doc);
|
|
274
|
+
return { panels, hasProvenance, inactive, panelLengths };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function standardizeTabPanels(doc, stats, concealment) {
|
|
278
|
+
const { panels, hasProvenance, inactive, panelLengths } = prepareConcealment(doc, concealment);
|
|
231
279
|
if (!panels.length) return;
|
|
232
280
|
const tabs = Array.from(doc.querySelectorAll(TABS));
|
|
233
281
|
for (const panel of panels) {
|
|
234
282
|
const label = (tabOf(panel, tabs)?.textContent || "").replace(/\s+/g, " ").trim().slice(0, MAX_TAB_LABEL_CHARS);
|
|
235
283
|
const text = (panel.textContent || "").replace(/\s+/g, " ").trim();
|
|
236
|
-
if (isInactivePanel(panel)) {
|
|
237
|
-
if (text.length > MAX_INACTIVE_PANEL_CHARS) {
|
|
284
|
+
if (hasProvenance ? inactive.get(panel) : isInactivePanel(panel)) {
|
|
285
|
+
if ((hasProvenance ? panelLengths.get(panel) : text.length) > MAX_INACTIVE_PANEL_CHARS) {
|
|
238
286
|
stats.omittedTabs.push(label);
|
|
239
287
|
panel.remove();
|
|
240
288
|
continue;
|
|
@@ -259,11 +307,19 @@ function standardizeAsides(doc) {
|
|
|
259
307
|
quote.append(...Array.from(aside.childNodes));
|
|
260
308
|
aside.replaceWith(quote);
|
|
261
309
|
}
|
|
310
|
+
// Readability treats "header" as clutter, even when it labels a warning.
|
|
311
|
+
for (const header of Array.from(doc.querySelectorAll("blockquote .admonition-header"))) {
|
|
312
|
+
const p = doc.createElement("p");
|
|
313
|
+
const strong = doc.createElement("strong");
|
|
314
|
+
strong.append(...Array.from(header.childNodes));
|
|
315
|
+
p.append(strong);
|
|
316
|
+
header.replaceWith(p);
|
|
317
|
+
}
|
|
262
318
|
}
|
|
263
319
|
|
|
264
320
|
/**
|
|
265
321
|
* @param {Document} doc the inert clone (mutated in place)
|
|
266
|
-
* @param {{ url?: string }} [options]
|
|
322
|
+
* @param {{ url?: string, concealment?: ReturnType<typeof import('./concealed.js').createConcealmentWalk> }} [options]
|
|
267
323
|
*/
|
|
268
324
|
export function standardizeDocument(doc, options = {}) {
|
|
269
325
|
const base = options.url || "";
|
|
@@ -271,13 +327,14 @@ export function standardizeDocument(doc, options = {}) {
|
|
|
271
327
|
lazyImagesResolved: 0,
|
|
272
328
|
inlineImagesDropped: 0,
|
|
273
329
|
scriptLinksUnwrapped: 0,
|
|
330
|
+
invalidLinksUnwrapped: 0,
|
|
274
331
|
emptyLinksRemoved: 0,
|
|
275
332
|
preWrapped: 0,
|
|
276
333
|
headingWidgetsRemoved: 0,
|
|
277
334
|
headingsUnwrapped: 0,
|
|
278
335
|
omittedTabs: /** @type {string[]} */ ([]),
|
|
279
336
|
};
|
|
280
|
-
standardizeTabPanels(doc, stats);
|
|
337
|
+
standardizeTabPanels(doc, stats, options.concealment);
|
|
281
338
|
standardizeAsides(doc);
|
|
282
339
|
standardizeHeadings(doc, stats);
|
|
283
340
|
standardizeCode(doc, stats);
|
package/src/types.js
CHANGED
|
@@ -14,6 +14,7 @@ export {};
|
|
|
14
14
|
* @property {boolean} [stripMeta]
|
|
15
15
|
* @property {boolean} [stripLinkTags]
|
|
16
16
|
* @property {boolean} [stripAds]
|
|
17
|
+
* @property {boolean} [detectConcealedText] opt-in computed-style check, default false
|
|
17
18
|
* @property {readonly string[]} [extraStripSelectors] app-specific overlays or widgets to drop, e.g. "[data-app-overlay]"
|
|
18
19
|
* @property {number} [shadowNodeLimit]
|
|
19
20
|
* @property {number} [shadowCharLimit]
|
|
@@ -69,11 +70,13 @@ export {};
|
|
|
69
70
|
*/
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
|
-
* Check `kind` before reading `chars`:
|
|
73
|
+
* Check `kind` before reading `chars`: hidden Unicode omissions use code points;
|
|
74
|
+
* concealed_text uses UTF-16 units. Other kinds do not have it.
|
|
73
75
|
* @typedef {{ kind: "shadow_content", count: number, note: string }
|
|
74
76
|
* | { kind: "json_line", count: number, note: string }
|
|
75
77
|
* | { kind: "inactive_tabs", count: number, note: string }
|
|
76
78
|
* | { kind: "comments", count: number, note: string }
|
|
79
|
+
* | { kind: "concealed_text", count: number, chars: number, note: string }
|
|
77
80
|
* | HiddenTextOmission} PageMarkdownOmission
|
|
78
81
|
*/
|
|
79
82
|
|
|
@@ -93,6 +96,9 @@ export {};
|
|
|
93
96
|
/**
|
|
94
97
|
* @typedef {object} PageMarkdownResult
|
|
95
98
|
* @property {string} version the package version that made this result
|
|
99
|
+
* @property {ConcealedText[]} concealedText capped captured blocks, raw text in UTF-16 units
|
|
100
|
+
* @property {ConcealmentCheck} concealmentCheck source computed-style capability
|
|
101
|
+
* @property {boolean} concealedTextTruncated true if any block, text or entry cap omitted report text
|
|
96
102
|
* @property {string} markdown
|
|
97
103
|
* @property {PageMarkdownMetadata} metadata
|
|
98
104
|
* @property {PageMarkdownExtraction} extraction
|
|
@@ -104,6 +110,17 @@ export {};
|
|
|
104
110
|
|
|
105
111
|
/** @typedef {{ label: string, value: string }} PageFact */
|
|
106
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The one-time probe establishes that the document's view can return every
|
|
115
|
+
* required field and exposed optional field. Afterwards, incomplete means a
|
|
116
|
+
* CONSULTED value was empty or threw; unread irrelevant values are not checked.
|
|
117
|
+
* Computed means no such failure; static-only means no computed-style API;
|
|
118
|
+
* disabled means detection was turned off.
|
|
119
|
+
* @typedef {"computed"|"static-only"|"disabled"|"incomplete"} ConcealmentCheck
|
|
120
|
+
*/
|
|
121
|
+
/** @typedef {"display_none"|"visibility_hidden"|"opacity_zero"|"font_size_zero"|"clipped"|"transparent_text"|"same_color"} ConcealmentReason */
|
|
122
|
+
/** @typedef {{ reason: ConcealmentReason, chars: number, text: string }} ConcealedText */
|
|
123
|
+
|
|
107
124
|
/**
|
|
108
125
|
* @typedef {string | string[] | ((node: HTMLElement, options: Record<string, unknown>) => boolean)} TurndownFilter
|
|
109
126
|
*/
|