webforai 2.1.0 → 3.0.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.
Files changed (154) hide show
  1. package/README.md +63 -1
  2. package/dist/adapters/index.js +58 -0
  3. package/dist/adapters/registry.js +149 -0
  4. package/dist/adapters/sites/declarative.js +178 -0
  5. package/dist/adapters/sites/hackernews.js +94 -0
  6. package/dist/adapters/sites/youtube.js +165 -0
  7. package/dist/adapters/types.js +0 -0
  8. package/dist/bin.js +3199 -399
  9. package/dist/cjs/adapters/index.js +86 -0
  10. package/dist/cjs/adapters/registry.js +178 -0
  11. package/dist/cjs/adapters/sites/declarative.js +202 -0
  12. package/dist/cjs/adapters/sites/hackernews.js +117 -0
  13. package/dist/cjs/adapters/sites/youtube.js +191 -0
  14. package/dist/cjs/adapters/types.js +16 -0
  15. package/dist/cjs/constants.js +2 -2
  16. package/dist/cjs/extractors/index.js +8 -0
  17. package/dist/cjs/extractors/lib/constants.js +186 -0
  18. package/dist/cjs/extractors/lib/sanitize.js +425 -0
  19. package/dist/cjs/extractors/lib/score.js +264 -0
  20. package/dist/cjs/extractors/lib/terminators.js +131 -0
  21. package/dist/cjs/extractors/lib/ui-chrome.js +119 -0
  22. package/dist/cjs/extractors/pipeExtractors.js +12 -9
  23. package/dist/cjs/extractors/presets/auto.js +44 -0
  24. package/dist/cjs/extractors/presets/minimal-filter.js +27 -48
  25. package/dist/cjs/extractors/presets/takumi.js +84 -122
  26. package/dist/cjs/html-to-markdown.js +44 -7
  27. package/dist/cjs/html-to-mdast.js +9 -1
  28. package/dist/cjs/index.js +33 -2
  29. package/dist/cjs/mdast-handlers/custom-a-handler.js +25 -9
  30. package/dist/cjs/mdast-handlers/custom-img-handler.js +23 -1
  31. package/dist/cjs/mdast-handlers/custom-table-handler.js +16 -0
  32. package/dist/cjs/mdast-handlers/definition-list-handler.js +99 -0
  33. package/dist/cjs/mdast-handlers/math-handler.js +12 -3
  34. package/dist/cjs/mdast-handlers/sup-sub-handler.js +39 -0
  35. package/dist/cjs/metadata/index.js +223 -0
  36. package/dist/cjs/normalize/index.js +168 -0
  37. package/dist/cjs/platform/client.js +147 -0
  38. package/dist/cjs/platform/error.js +36 -0
  39. package/dist/cjs/platform/fetch.js +37 -0
  40. package/dist/cjs/platform/index.js +40 -0
  41. package/dist/cjs/platform/types.js +34 -0
  42. package/dist/cjs/utils/hast-fast.js +284 -0
  43. package/dist/constants.js +2 -2
  44. package/dist/extractors/index.js +5 -0
  45. package/dist/extractors/lib/constants.js +150 -0
  46. package/dist/extractors/lib/sanitize.js +415 -0
  47. package/dist/extractors/lib/score.js +242 -0
  48. package/dist/extractors/lib/terminators.js +107 -0
  49. package/dist/extractors/lib/ui-chrome.js +96 -0
  50. package/dist/extractors/pipeExtractors.js +12 -9
  51. package/dist/extractors/presets/auto.js +20 -0
  52. package/dist/extractors/presets/minimal-filter.js +35 -47
  53. package/dist/extractors/presets/takumi.js +91 -122
  54. package/dist/html-to-markdown.js +42 -6
  55. package/dist/html-to-mdast.js +9 -1
  56. package/dist/index.js +34 -3
  57. package/dist/mdast-handlers/custom-a-handler.js +25 -9
  58. package/dist/mdast-handlers/custom-img-handler.js +23 -1
  59. package/dist/mdast-handlers/custom-table-handler.js +16 -0
  60. package/dist/mdast-handlers/definition-list-handler.js +76 -0
  61. package/dist/mdast-handlers/math-handler.js +12 -3
  62. package/dist/mdast-handlers/sup-sub-handler.js +16 -0
  63. package/dist/metadata/index.js +198 -0
  64. package/dist/normalize/index.js +149 -0
  65. package/dist/platform/client.js +125 -0
  66. package/dist/platform/error.js +13 -0
  67. package/dist/platform/fetch.js +14 -0
  68. package/dist/platform/index.js +19 -0
  69. package/dist/platform/types.js +9 -0
  70. package/dist/types/adapters/index.d.ts +24 -0
  71. package/dist/types/adapters/index.js.map +1 -0
  72. package/dist/types/adapters/registry.d.ts +35 -0
  73. package/dist/types/adapters/registry.js.map +1 -0
  74. package/dist/types/adapters/sites/declarative.d.ts +13 -0
  75. package/dist/types/adapters/sites/declarative.js.map +1 -0
  76. package/dist/types/adapters/sites/hackernews.d.ts +13 -0
  77. package/dist/types/adapters/sites/hackernews.js.map +1 -0
  78. package/dist/types/adapters/sites/youtube.d.ts +31 -0
  79. package/dist/types/adapters/sites/youtube.js.map +1 -0
  80. package/dist/types/adapters/types.d.ts +99 -0
  81. package/dist/types/adapters/types.js.map +1 -0
  82. package/dist/types/cli/bin.js.map +1 -1
  83. package/dist/types/cli/commands/skill/content.d.ts +9 -0
  84. package/dist/types/cli/commands/skill/content.js.map +1 -0
  85. package/dist/types/cli/commands/skill/index.d.ts +19 -0
  86. package/dist/types/cli/commands/skill/index.js.map +1 -0
  87. package/dist/types/cli/commands/webforai/interactive.d.ts +7 -0
  88. package/dist/types/cli/commands/webforai/interactive.js.map +1 -0
  89. package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -1
  90. package/dist/types/cli/commands/webforai/options.d.ts +55 -0
  91. package/dist/types/cli/commands/webforai/options.js.map +1 -0
  92. package/dist/types/cli/commands/webforai/run.d.ts +10 -0
  93. package/dist/types/cli/commands/webforai/run.js.map +1 -0
  94. package/dist/types/cli/constants.d.ts +14 -4
  95. package/dist/types/cli/constants.js.map +1 -1
  96. package/dist/types/cli/helpers/selectExtractMode.js.map +1 -1
  97. package/dist/types/cli/helpers/selectLoader.js.map +1 -1
  98. package/dist/types/constants.d.ts +7 -1
  99. package/dist/types/constants.js.map +1 -1
  100. package/dist/types/extractors/index.d.ts +2 -0
  101. package/dist/types/extractors/index.js.map +1 -1
  102. package/dist/types/extractors/lib/constants.d.ts +81 -0
  103. package/dist/types/extractors/lib/constants.js.map +1 -0
  104. package/dist/types/extractors/lib/sanitize.d.ts +66 -0
  105. package/dist/types/extractors/lib/sanitize.js.map +1 -0
  106. package/dist/types/extractors/lib/score.d.ts +85 -0
  107. package/dist/types/extractors/lib/score.js.map +1 -0
  108. package/dist/types/extractors/lib/terminators.d.ts +26 -0
  109. package/dist/types/extractors/lib/terminators.js.map +1 -0
  110. package/dist/types/extractors/lib/ui-chrome.d.ts +18 -0
  111. package/dist/types/extractors/lib/ui-chrome.js.map +1 -0
  112. package/dist/types/extractors/pipeExtractors.d.ts +5 -0
  113. package/dist/types/extractors/pipeExtractors.js.map +1 -1
  114. package/dist/types/extractors/presets/auto.d.ts +30 -0
  115. package/dist/types/extractors/presets/auto.js.map +1 -0
  116. package/dist/types/extractors/presets/minimal-filter.d.ts +10 -2
  117. package/dist/types/extractors/presets/minimal-filter.js.map +1 -1
  118. package/dist/types/extractors/presets/takumi.d.ts +19 -3
  119. package/dist/types/extractors/presets/takumi.js.map +1 -1
  120. package/dist/types/extractors/types.d.ts +8 -0
  121. package/dist/types/html-to-markdown.d.ts +38 -0
  122. package/dist/types/html-to-markdown.js.map +1 -1
  123. package/dist/types/html-to-mdast.d.ts +11 -0
  124. package/dist/types/html-to-mdast.js.map +1 -1
  125. package/dist/types/index.d.ts +6 -3
  126. package/dist/types/index.js.map +1 -1
  127. package/dist/types/mdast-handlers/custom-a-handler.js.map +1 -1
  128. package/dist/types/mdast-handlers/custom-img-handler.js.map +1 -1
  129. package/dist/types/mdast-handlers/custom-table-handler.js.map +1 -1
  130. package/dist/types/mdast-handlers/definition-list-handler.d.ts +13 -0
  131. package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -0
  132. package/dist/types/mdast-handlers/math-handler.js.map +1 -1
  133. package/dist/types/mdast-handlers/sup-sub-handler.d.ts +10 -0
  134. package/dist/types/mdast-handlers/sup-sub-handler.js.map +1 -0
  135. package/dist/types/metadata/index.d.ts +47 -0
  136. package/dist/types/metadata/index.js.map +1 -0
  137. package/dist/types/normalize/index.d.ts +53 -0
  138. package/dist/types/normalize/index.js.map +1 -0
  139. package/dist/types/platform/client.d.ts +54 -0
  140. package/dist/types/platform/client.js.map +1 -0
  141. package/dist/types/platform/error.d.ts +20 -0
  142. package/dist/types/platform/error.js.map +1 -0
  143. package/dist/types/platform/fetch.d.ts +34 -0
  144. package/dist/types/platform/fetch.js.map +1 -0
  145. package/dist/types/platform/index.d.ts +4 -0
  146. package/dist/types/platform/index.js.map +1 -0
  147. package/dist/types/platform/types.d.ts +135 -0
  148. package/dist/types/platform/types.js.map +1 -0
  149. package/dist/types/utils/hast-fast.d.ts +101 -0
  150. package/dist/types/utils/hast-fast.js.map +1 -0
  151. package/dist/utils/hast-fast.js +246 -0
  152. package/package.json +10 -3
  153. package/dist/types/cli/commands/webforai/index.d.ts +0 -6
  154. package/dist/types/cli/commands/webforai/index.js.map +0 -1
@@ -0,0 +1,415 @@
1
+ // src/extractors/lib/sanitize.ts
2
+ import {
3
+ MetricsCollector as MetricsCollectorCtor,
4
+ classList,
5
+ isElement,
6
+ isTruthyAttribute,
7
+ matchString,
8
+ numericProperty,
9
+ pruneInPlace,
10
+ stringProperty,
11
+ walk
12
+ } from "../../utils/hast-fast.js";
13
+ import {
14
+ CHROME_TAGS,
15
+ CONTEXTUAL_LANDMARK_TAGS,
16
+ DROP_WHEN_EMPTY_TAGS,
17
+ EMBEDDED_CONTENT_TAGS,
18
+ HIDDEN_CLASS_NAMES,
19
+ NON_CONTENT_TAGS,
20
+ REGEXPS,
21
+ RESPONSIVE_DISPLAY_OVERRIDE,
22
+ UNLIKELY_ROLES
23
+ } from "./constants.js";
24
+ import { isUiChromeText } from "./ui-chrome.js";
25
+ var INVISIBLE_STYLE = /(^|;)\s*(display\s*:\s*none|visibility\s*:\s*hidden)\s*(;|$)/i;
26
+ var hasHiddenClass = (element) => {
27
+ const classes = classList(element);
28
+ if (!classes.some((name) => HIDDEN_CLASS_NAMES.has(name))) {
29
+ return classes.some((name) => REGEXPS.hidden.test(name));
30
+ }
31
+ return !classes.some((name) => RESPONSIVE_DISPLAY_OVERRIDE.test(name));
32
+ };
33
+ var isNonContent = (node) => {
34
+ if (node.type === "comment" || node.type === "doctype") {
35
+ return true;
36
+ }
37
+ return isElement(node) && NON_CONTENT_TAGS.has(node.tagName);
38
+ };
39
+ var isHidden = (element) => {
40
+ if (containsMath(element)) {
41
+ return false;
42
+ }
43
+ if (isTruthyAttribute(element, "hidden")) {
44
+ return true;
45
+ }
46
+ const style = stringProperty(element, "style");
47
+ if (style && INVISIBLE_STYLE.test(style)) {
48
+ return true;
49
+ }
50
+ if (hasHiddenClass(element)) {
51
+ return true;
52
+ }
53
+ const width = numericProperty(element, "data-rwidth");
54
+ const height = numericProperty(element, "data-rheight");
55
+ if (width === 0 && height === 0) {
56
+ return true;
57
+ }
58
+ return false;
59
+ };
60
+ var PLACEHOLDER_IMAGE_SRC = /^data:image\/|^about:blank$|spacer\.gif$|blank\.(gif|png)$/i;
61
+ var isPlaceholderImage = (element) => {
62
+ if (element.tagName === "img") {
63
+ const src = stringProperty(element, "src");
64
+ return src !== void 0 && PLACEHOLDER_IMAGE_SRC.test(src);
65
+ }
66
+ if (element.tagName !== "picture") {
67
+ return false;
68
+ }
69
+ for (const child of element.children) {
70
+ if (isElement(child) && child.tagName === "img") {
71
+ return isPlaceholderImage(child);
72
+ }
73
+ }
74
+ return false;
75
+ };
76
+ var hoistNoscriptImages = (tree) => {
77
+ const visit = (parent) => {
78
+ for (let index = 0; index < parent.children.length; index++) {
79
+ const child = parent.children[index];
80
+ if (!isElement(child)) {
81
+ continue;
82
+ }
83
+ if (child.tagName !== "noscript") {
84
+ visit(child);
85
+ continue;
86
+ }
87
+ const hasImage = child.children.some(
88
+ (node) => isElement(node) && (node.tagName === "img" || node.tagName === "picture")
89
+ );
90
+ if (!hasImage) {
91
+ continue;
92
+ }
93
+ const rescued = child.children;
94
+ parent.children.splice(index, 1, ...rescued);
95
+ const previous = parent.children[index - 1];
96
+ if (index > 0 && isElement(previous) && isPlaceholderImage(previous)) {
97
+ parent.children.splice(index - 1, 1);
98
+ index -= 1;
99
+ }
100
+ index += rescued.length - 1;
101
+ }
102
+ };
103
+ if ("children" in tree) {
104
+ visit(tree);
105
+ }
106
+ };
107
+ var containsMath = (element) => {
108
+ if (element.tagName === "math") {
109
+ return true;
110
+ }
111
+ for (const child of element.children) {
112
+ if (isElement(child) && containsMath(child)) {
113
+ return true;
114
+ }
115
+ }
116
+ return false;
117
+ };
118
+ var stripNonContent = (tree) => {
119
+ hoistNoscriptImages(tree);
120
+ return pruneInPlace(tree, (node) => {
121
+ if (isNonContent(node)) {
122
+ return false;
123
+ }
124
+ if (isElement(node) && isHidden(node)) {
125
+ return false;
126
+ }
127
+ return true;
128
+ });
129
+ };
130
+ var findLandmarkChrome = (tree) => {
131
+ const chrome = /* @__PURE__ */ new Set();
132
+ const visit = (node, depth, insideContent) => {
133
+ if (!("children" in node)) {
134
+ return;
135
+ }
136
+ for (const child of node.children) {
137
+ if (!isElement(child)) {
138
+ continue;
139
+ }
140
+ const nowInsideContent = insideContent || child.tagName === "article" || child.tagName === "main" || stringProperty(child, "role") === "main";
141
+ if (CONTEXTUAL_LANDMARK_TAGS.has(child.tagName) && !insideContent && depth <= 3) {
142
+ chrome.add(child);
143
+ continue;
144
+ }
145
+ visit(child, depth + 1, nowInsideContent);
146
+ }
147
+ };
148
+ visit(tree, 0, false);
149
+ return chrome;
150
+ };
151
+ var isUnlikelyCandidate = (element) => {
152
+ if (element.tagName === "article" || element.tagName === "main" || element.tagName === "body") {
153
+ return false;
154
+ }
155
+ const role = stringProperty(element, "role");
156
+ if (role && UNLIKELY_ROLES.has(role)) {
157
+ return true;
158
+ }
159
+ if (CHROME_TAGS.has(element.tagName)) {
160
+ return true;
161
+ }
162
+ const match = matchString(element);
163
+ if (REGEXPS.specialUnlikelyCandidates.test(match)) {
164
+ return true;
165
+ }
166
+ for (const name of [...classList(element), stringProperty(element, "id") ?? ""]) {
167
+ if (REGEXPS.stronglyUnlikely.test(name)) {
168
+ return true;
169
+ }
170
+ }
171
+ if (REGEXPS.unlikelyCandidates.test(match) && !REGEXPS.okMaybeItsaCandidate.test(match)) {
172
+ return true;
173
+ }
174
+ return false;
175
+ };
176
+ var findUnlikelyElements = (tree) => {
177
+ const landmarks = findLandmarkChrome(tree);
178
+ const doomed = [];
179
+ const visit = (node) => {
180
+ if (!("children" in node)) {
181
+ return;
182
+ }
183
+ for (const child of node.children) {
184
+ if (!isElement(child)) {
185
+ continue;
186
+ }
187
+ if (landmarks.has(child) || isUnlikelyCandidate(child)) {
188
+ doomed.push(child);
189
+ continue;
190
+ }
191
+ visit(child);
192
+ }
193
+ };
194
+ visit(tree);
195
+ return doomed;
196
+ };
197
+ var stripUnlikely = (tree) => {
198
+ const doomed = new Set(findUnlikelyElements(tree));
199
+ return pruneInPlace(tree, (node) => !(isElement(node) && doomed.has(node)));
200
+ };
201
+ var isEmbeddedContent = (element) => {
202
+ if (EMBEDDED_CONTENT_TAGS.has(element.tagName)) {
203
+ return true;
204
+ }
205
+ return element.tagName === "br" || element.tagName === "hr" || element.tagName === "input";
206
+ };
207
+ var collectPreformattedElements = (tree) => {
208
+ const protectedElements = /* @__PURE__ */ new Set();
209
+ const markSubtree = (node) => {
210
+ if (!("children" in node)) {
211
+ return;
212
+ }
213
+ for (const child of node.children) {
214
+ if (isElement(child)) {
215
+ protectedElements.add(child);
216
+ markSubtree(child);
217
+ }
218
+ }
219
+ };
220
+ walk(tree, (node) => {
221
+ if (isElement(node) && (node.tagName === "pre" || node.tagName === "code")) {
222
+ protectedElements.add(node);
223
+ markSubtree(node);
224
+ return false;
225
+ }
226
+ });
227
+ return protectedElements;
228
+ };
229
+ var dropEmptyElements = (tree, collector) => {
230
+ const preformatted = collectPreformattedElements(tree);
231
+ const visit = (parent) => {
232
+ for (let index = 0; index < parent.children.length; index++) {
233
+ const child = parent.children[index];
234
+ if (!isElement(child)) {
235
+ continue;
236
+ }
237
+ if (preformatted.has(child) || !DROP_WHEN_EMPTY_TAGS.has(child.tagName)) {
238
+ visit(child);
239
+ continue;
240
+ }
241
+ if (collector.metrics(child).text > 0 || hasEmbeddedDescendant(child)) {
242
+ visit(child);
243
+ continue;
244
+ }
245
+ if (holdsWhitespace(child)) {
246
+ parent.children[index] = { type: "text", value: " " };
247
+ continue;
248
+ }
249
+ parent.children.splice(index, 1);
250
+ index -= 1;
251
+ }
252
+ };
253
+ if ("children" in tree) {
254
+ visit(tree);
255
+ }
256
+ return tree;
257
+ };
258
+ var holdsWhitespace = (element) => {
259
+ for (const child of element.children) {
260
+ if (child.type === "text" && /\s/.test(child.value)) {
261
+ return true;
262
+ }
263
+ if (isElement(child) && holdsWhitespace(child)) {
264
+ return true;
265
+ }
266
+ }
267
+ return false;
268
+ };
269
+ var hasEmbeddedDescendant = (element) => {
270
+ for (const child of element.children) {
271
+ if (!isElement(child)) {
272
+ continue;
273
+ }
274
+ if (isEmbeddedContent(child) || hasEmbeddedDescendant(child)) {
275
+ return true;
276
+ }
277
+ }
278
+ return false;
279
+ };
280
+ var REFERENCE_HEADING = /^(see also|references?|further reading|external links?|bibliography|sources|footnotes|citations|関連項目|参考文献|外部リンク|脚注|出典|参考リンク|参考資料)$/i;
281
+ var MAX_DANGLING_HEADING_LENGTH = 60;
282
+ var isHeadingTag = (element) => /^h[1-6]$/.test(element.tagName);
283
+ var previousElement = (siblings, index) => {
284
+ for (let cursor = index - 1; cursor >= 0; cursor--) {
285
+ const sibling = siblings[cursor];
286
+ if (isElement(sibling)) {
287
+ return sibling;
288
+ }
289
+ if (sibling.type === "text" && sibling.value.trim().length > 0) {
290
+ return void 0;
291
+ }
292
+ }
293
+ return void 0;
294
+ };
295
+ var condemnLinkBlock = (block, siblings, index, collector, doomed) => {
296
+ const heading = previousElement(siblings, index);
297
+ const isHeading = heading !== void 0 && isHeadingTag(heading);
298
+ if (isHeading && REFERENCE_HEADING.test(elementText(heading).trim())) {
299
+ return;
300
+ }
301
+ if (introducedByColon(siblings, index, collector)) {
302
+ return;
303
+ }
304
+ doomed.push(block);
305
+ if (isHeading && elementText(heading).trim().length <= MAX_DANGLING_HEADING_LENGTH) {
306
+ doomed.push(heading);
307
+ }
308
+ };
309
+ var introducedByColon = (siblings, index, collector) => {
310
+ const before = previousElement(siblings, index);
311
+ if (!before || collector.metrics(before).text === 0) {
312
+ return false;
313
+ }
314
+ return /[::]\s*$/.test(elementText(before).trim());
315
+ };
316
+ var findCleanupTargets = (tree, collector) => {
317
+ const doomed = [];
318
+ const visit = (node) => {
319
+ if (!("children" in node)) {
320
+ return;
321
+ }
322
+ const siblings = node.children;
323
+ for (let index = 0; index < siblings.length; index++) {
324
+ const child = siblings[index];
325
+ if (!isElement(child)) {
326
+ continue;
327
+ }
328
+ if (child.tagName === "article" || child.tagName === "main") {
329
+ visit(child);
330
+ continue;
331
+ }
332
+ if (isLinkOnlyBlock(child, collector)) {
333
+ condemnLinkBlock(child, siblings, index, collector, doomed);
334
+ continue;
335
+ }
336
+ if (isMidArticleWidget(child)) {
337
+ doomed.push(child);
338
+ continue;
339
+ }
340
+ visit(child);
341
+ }
342
+ };
343
+ visit(tree);
344
+ return doomed;
345
+ };
346
+ var LINK_BLOCK_TAGS = /* @__PURE__ */ new Set(["ul", "ol", "nav", "div", "p"]);
347
+ var MIN_LINK_BLOCK_ANCHORS = 3;
348
+ var isLinkOnlyBlock = (element, collector) => {
349
+ if (!LINK_BLOCK_TAGS.has(element.tagName)) {
350
+ return false;
351
+ }
352
+ const metrics = collector.metrics(element);
353
+ if (metrics.text === 0 || metrics.commas > 0) {
354
+ return false;
355
+ }
356
+ if (metrics.link / metrics.text <= 0.9) {
357
+ return false;
358
+ }
359
+ return countAnchors(element) >= MIN_LINK_BLOCK_ANCHORS;
360
+ };
361
+ var countAnchors = (element) => {
362
+ let count = 0;
363
+ const stack = [element];
364
+ while (stack.length > 0 && count < MIN_LINK_BLOCK_ANCHORS) {
365
+ const node = stack.pop();
366
+ if (isElement(node) && node.tagName === "a") {
367
+ count += 1;
368
+ }
369
+ if ("children" in node) {
370
+ for (const child of node.children) {
371
+ stack.push(child);
372
+ }
373
+ }
374
+ }
375
+ return count;
376
+ };
377
+ var isMidArticleWidget = (element) => {
378
+ if (element.tagName === "form" || element.tagName === "fieldset") {
379
+ return true;
380
+ }
381
+ if (element.tagName === "button") {
382
+ return true;
383
+ }
384
+ return isUiChromeText(elementText(element));
385
+ };
386
+ var cleanContent = (tree, collector) => {
387
+ const doomed = new Set(findCleanupTargets(tree, collector));
388
+ if (doomed.size > 0) {
389
+ pruneInPlace(tree, (node) => !(isElement(node) && doomed.has(node)));
390
+ }
391
+ return dropEmptyElements(tree, new MetricsCollectorCtor());
392
+ };
393
+ var elementText = (node) => {
394
+ if (node.type === "text") {
395
+ return node.value;
396
+ }
397
+ if (!("children" in node)) {
398
+ return "";
399
+ }
400
+ let text = "";
401
+ for (const child of node.children) {
402
+ text += elementText(child);
403
+ }
404
+ return text;
405
+ };
406
+ export {
407
+ cleanContent,
408
+ dropEmptyElements,
409
+ findCleanupTargets,
410
+ findLandmarkChrome,
411
+ findUnlikelyElements,
412
+ hoistNoscriptImages,
413
+ stripNonContent,
414
+ stripUnlikely
415
+ };
@@ -0,0 +1,242 @@
1
+ // src/extractors/lib/score.ts
2
+ import {
3
+ classList,
4
+ isElement,
5
+ matchString,
6
+ numericProperty,
7
+ stringProperty,
8
+ walk
9
+ } from "../../utils/hast-fast.js";
10
+ import { CANDIDATE_TAGS, CONTENT_ROLES, REGEXPS, SCOREABLE_TAGS, minContentLength } from "./constants.js";
11
+ var TAG_BASE_SCORE = {
12
+ div: 5,
13
+ pre: 3,
14
+ td: 3,
15
+ blockquote: 3,
16
+ article: 10,
17
+ main: 10,
18
+ section: 5,
19
+ address: -3,
20
+ ol: -3,
21
+ ul: -3,
22
+ dl: -3,
23
+ dd: -3,
24
+ dt: -3,
25
+ li: -3,
26
+ form: -3,
27
+ h1: -5,
28
+ h2: -5,
29
+ h3: -5,
30
+ h4: -5,
31
+ h5: -5,
32
+ h6: -5,
33
+ th: -5
34
+ };
35
+ var MAX_ANCESTOR_DEPTH = 5;
36
+ var MIN_SCOREABLE_TEXT = 25;
37
+ var buildScoringContext = (root, collector, lang) => {
38
+ const parents = /* @__PURE__ */ new WeakMap();
39
+ const scoreable = [];
40
+ let hasGeometry = false;
41
+ walk(root, (node, parent) => {
42
+ if (parent) {
43
+ parents.set(node, parent);
44
+ }
45
+ if (!isElement(node)) {
46
+ return;
47
+ }
48
+ if (!hasGeometry && numericProperty(node, "data-rheight") !== void 0) {
49
+ hasGeometry = true;
50
+ }
51
+ if (isParagraphLike(node)) {
52
+ scoreable.push(node);
53
+ }
54
+ });
55
+ return { collector, parents, lang, hasGeometry, scoreable };
56
+ };
57
+ var classWeight = (element) => {
58
+ let weight = 0;
59
+ const id = stringProperty(element, "id") ?? "";
60
+ const classes = classList(element).join(" ");
61
+ if (REGEXPS.negative.test(classes)) {
62
+ weight -= 25;
63
+ }
64
+ if (REGEXPS.positive.test(classes)) {
65
+ weight += 25;
66
+ }
67
+ if (REGEXPS.negative.test(id)) {
68
+ weight -= 25;
69
+ }
70
+ if (REGEXPS.positive.test(id)) {
71
+ weight += 25;
72
+ }
73
+ return weight;
74
+ };
75
+ var semanticBonus = (element) => {
76
+ let bonus = 0;
77
+ if (element.tagName === "article") {
78
+ bonus += 60;
79
+ }
80
+ if (element.tagName === "main") {
81
+ bonus += 50;
82
+ }
83
+ const role = stringProperty(element, "role");
84
+ if (role && CONTENT_ROLES.has(role)) {
85
+ bonus += 40;
86
+ }
87
+ const itemprop = stringProperty(element, "itemprop");
88
+ if (itemprop?.includes("articleBody")) {
89
+ bonus += 80;
90
+ }
91
+ const match = matchString(element);
92
+ if (/(^|[\s_-])(markdown|prose|article-?content|post-?content|entry-?content|doc-?content|md-?content)([\s_-]|$)/i.test(
93
+ match
94
+ )) {
95
+ bonus += 35;
96
+ }
97
+ return bonus;
98
+ };
99
+ var geometryMultiplier = (element, context) => {
100
+ if (!context.hasGeometry) {
101
+ return 1;
102
+ }
103
+ const height = numericProperty(element, "data-rheight");
104
+ const width = numericProperty(element, "data-rwidth");
105
+ if (height === void 0 || width === void 0) {
106
+ return 1;
107
+ }
108
+ if (height === 0 || width === 0) {
109
+ return 0;
110
+ }
111
+ if (width < 220) {
112
+ return 0.35;
113
+ }
114
+ return 1;
115
+ };
116
+ var paragraphScore = (textLength, commas, densityDivisor) => 1 + commas + Math.min(Math.floor(textLength / densityDivisor), 3);
117
+ var ancestorWeight = (level) => {
118
+ if (level === 0) {
119
+ return 1;
120
+ }
121
+ if (level === 1) {
122
+ return 2;
123
+ }
124
+ return level * 3;
125
+ };
126
+ var isParagraphLike = (element) => {
127
+ if (!SCOREABLE_TAGS.has(element.tagName)) {
128
+ return false;
129
+ }
130
+ if (element.tagName !== "div" && element.tagName !== "section") {
131
+ return true;
132
+ }
133
+ return !element.children.some((child) => isElement(child) && CANDIDATE_TAGS.has(child.tagName));
134
+ };
135
+ var scoreCandidates = (_root, context) => {
136
+ const { collector, parents } = context;
137
+ const scores = /* @__PURE__ */ new Map();
138
+ const densityDivisor = minContentLength(context.lang) / 4;
139
+ for (const node of context.scoreable) {
140
+ const metrics = collector.metrics(node);
141
+ if (metrics.text < MIN_SCOREABLE_TEXT) {
142
+ continue;
143
+ }
144
+ const contribution = paragraphScore(metrics.text, metrics.commas, densityDivisor);
145
+ let current = node;
146
+ for (let level = 0; level < MAX_ANCESTOR_DEPTH && current; level++) {
147
+ const parent = parents.get(current);
148
+ if (!(parent && isElement(parent))) {
149
+ break;
150
+ }
151
+ if (CANDIDATE_TAGS.has(parent.tagName) || parent.tagName === "article" || parent.tagName === "main") {
152
+ const base = scores.get(parent) ?? (TAG_BASE_SCORE[parent.tagName] ?? 0) + classWeight(parent) + semanticBonus(parent);
153
+ scores.set(parent, base + contribution / ancestorWeight(level));
154
+ }
155
+ current = parent;
156
+ }
157
+ }
158
+ const candidates = [];
159
+ for (const [element, rawScore] of scores) {
160
+ const density = collector.linkDensity(element);
161
+ const score = rawScore * (1 - density) * geometryMultiplier(element, context);
162
+ if (score > 0) {
163
+ candidates.push({ element, score });
164
+ }
165
+ }
166
+ candidates.sort((a, b) => b.score - a.score);
167
+ return candidates;
168
+ };
169
+ var collectArticleNodes = (top, candidates, context) => {
170
+ const { collector, parents } = context;
171
+ const parent = parents.get(top.element);
172
+ if (!(parent && isElement(parent))) {
173
+ return [top.element];
174
+ }
175
+ const scoreOf = new Map(candidates.map((candidate) => [candidate.element, candidate.score]));
176
+ const threshold = Math.max(10, top.score * 0.2);
177
+ const selected = [];
178
+ for (const child of parent.children) {
179
+ if (!isElement(child)) {
180
+ continue;
181
+ }
182
+ if (child === top.element) {
183
+ selected.push(child);
184
+ continue;
185
+ }
186
+ const siblingScore = scoreOf.get(child) ?? 0;
187
+ if (siblingScore >= threshold) {
188
+ selected.push(child);
189
+ continue;
190
+ }
191
+ if (child.tagName === "p" || child.tagName === "section") {
192
+ const metrics = collector.metrics(child);
193
+ const density = collector.linkDensity(child);
194
+ if (metrics.text > 80 && density < 0.25) {
195
+ selected.push(child);
196
+ }
197
+ }
198
+ }
199
+ return selected.length > 0 ? selected : [top.element];
200
+ };
201
+ var MAX_CLIMB_HOPS = 5;
202
+ var MIN_CLIMB_RELATIVE_GAIN = 0.08;
203
+ var MIN_CLIMB_ABSOLUTE_GAIN = 500;
204
+ var MAX_CLIMB_GAIN_LINK_DENSITY = 1 / 3;
205
+ var CLIMB_SCORE_FLOOR = 1 / 3;
206
+ var climbToArticleBoundary = (top, candidates, context) => {
207
+ const { collector, parents } = context;
208
+ const scoreOf = new Map(candidates.map((candidate) => [candidate.element, candidate.score]));
209
+ const scoreFloor = top.score * CLIMB_SCORE_FLOOR;
210
+ let current = top.element;
211
+ for (let hop = 0; hop < MAX_CLIMB_HOPS; hop++) {
212
+ const parent = parents.get(current);
213
+ if (!(parent && isElement(parent))) {
214
+ break;
215
+ }
216
+ const parentScore = scoreOf.get(parent);
217
+ if (parentScore === void 0 || parentScore < scoreFloor) {
218
+ break;
219
+ }
220
+ const currentMetrics = collector.metrics(current);
221
+ const parentMetrics = collector.metrics(parent);
222
+ const gainedText = parentMetrics.text - currentMetrics.text;
223
+ if (gainedText < Math.max(MIN_CLIMB_ABSOLUTE_GAIN, currentMetrics.text * MIN_CLIMB_RELATIVE_GAIN)) {
224
+ break;
225
+ }
226
+ const gainedLink = parentMetrics.link - currentMetrics.link;
227
+ if (gainedLink / gainedText > MAX_CLIMB_GAIN_LINK_DENSITY) {
228
+ break;
229
+ }
230
+ current = parent;
231
+ }
232
+ return current;
233
+ };
234
+ export {
235
+ buildScoringContext,
236
+ classWeight,
237
+ climbToArticleBoundary,
238
+ collectArticleNodes,
239
+ geometryMultiplier,
240
+ scoreCandidates,
241
+ semanticBonus
242
+ };