webforai 2.1.1 → 4.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 (203) hide show
  1. package/README.md +86 -4
  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 +4142 -549
  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/detect-client-shell.js +78 -0
  17. package/dist/cjs/extract-meta-refresh.js +71 -0
  18. package/dist/cjs/extractors/index.js +5 -0
  19. package/dist/cjs/extractors/lib/constants.js +186 -0
  20. package/dist/cjs/extractors/lib/sanitize.js +429 -0
  21. package/dist/cjs/extractors/lib/score.js +264 -0
  22. package/dist/cjs/extractors/lib/terminators.js +131 -0
  23. package/dist/cjs/extractors/lib/ui-chrome.js +119 -0
  24. package/dist/cjs/extractors/pipeExtractors.js +12 -9
  25. package/dist/cjs/extractors/presets/auto.js +44 -0
  26. package/dist/cjs/extractors/presets/minimal-filter.js +27 -48
  27. package/dist/cjs/extractors/presets/takumi.js +84 -122
  28. package/dist/cjs/html-to-markdown.js +44 -7
  29. package/dist/cjs/html-to-mdast.js +10 -1
  30. package/dist/cjs/index.js +43 -2
  31. package/dist/cjs/link-replacer.js +19 -13
  32. package/dist/cjs/loaders/cf-puppeteer.js +14 -7
  33. package/dist/cjs/loaders/fetch.js +14 -2
  34. package/dist/cjs/loaders/playwright.js +32 -30
  35. package/dist/cjs/loaders/puppeteer.js +14 -7
  36. package/dist/cjs/md-splitter.js +24 -11
  37. package/dist/cjs/mdast-handlers/custom-a-handler.js +25 -9
  38. package/dist/cjs/mdast-handlers/custom-code-handler.js +18 -7
  39. package/dist/cjs/mdast-handlers/custom-div-handler.js +12 -33
  40. package/dist/cjs/mdast-handlers/custom-img-handler.js +23 -1
  41. package/dist/cjs/mdast-handlers/custom-table-handler.js +16 -0
  42. package/dist/cjs/mdast-handlers/definition-list-handler.js +110 -0
  43. package/dist/cjs/mdast-handlers/math-handler.js +12 -3
  44. package/dist/cjs/mdast-handlers/sup-sub-handler.js +39 -0
  45. package/dist/cjs/mdast-to-markdown.js +2 -5
  46. package/dist/cjs/metadata/index.js +223 -0
  47. package/dist/cjs/normalize/index.js +177 -0
  48. package/dist/cjs/platform/client.js +204 -0
  49. package/dist/cjs/platform/error.js +36 -0
  50. package/dist/cjs/platform/fetch.js +37 -0
  51. package/dist/cjs/platform/index.js +44 -0
  52. package/dist/cjs/platform/types.js +40 -0
  53. package/dist/cjs/utils/hast-fast.js +284 -0
  54. package/dist/constants.js +2 -2
  55. package/dist/detect-client-shell.js +53 -0
  56. package/dist/extract-meta-refresh.js +47 -0
  57. package/dist/extractors/index.js +3 -0
  58. package/dist/extractors/lib/constants.js +150 -0
  59. package/dist/extractors/lib/sanitize.js +419 -0
  60. package/dist/extractors/lib/score.js +242 -0
  61. package/dist/extractors/lib/terminators.js +107 -0
  62. package/dist/extractors/lib/ui-chrome.js +96 -0
  63. package/dist/extractors/pipeExtractors.js +12 -9
  64. package/dist/extractors/presets/auto.js +20 -0
  65. package/dist/extractors/presets/minimal-filter.js +35 -47
  66. package/dist/extractors/presets/takumi.js +91 -122
  67. package/dist/html-to-markdown.js +42 -6
  68. package/dist/html-to-mdast.js +10 -1
  69. package/dist/index.js +45 -2
  70. package/dist/link-replacer.js +19 -13
  71. package/dist/loaders/cf-puppeteer.js +15 -8
  72. package/dist/loaders/fetch.js +14 -2
  73. package/dist/loaders/playwright.js +32 -30
  74. package/dist/loaders/puppeteer.js +15 -8
  75. package/dist/md-splitter.js +24 -11
  76. package/dist/mdast-handlers/custom-a-handler.js +25 -9
  77. package/dist/mdast-handlers/custom-code-handler.js +17 -7
  78. package/dist/mdast-handlers/custom-div-handler.js +12 -33
  79. package/dist/mdast-handlers/custom-img-handler.js +23 -1
  80. package/dist/mdast-handlers/custom-table-handler.js +16 -0
  81. package/dist/mdast-handlers/definition-list-handler.js +87 -0
  82. package/dist/mdast-handlers/math-handler.js +12 -3
  83. package/dist/mdast-handlers/sup-sub-handler.js +16 -0
  84. package/dist/mdast-to-markdown.js +2 -5
  85. package/dist/metadata/index.js +198 -0
  86. package/dist/normalize/index.js +159 -0
  87. package/dist/platform/client.js +182 -0
  88. package/dist/platform/error.js +13 -0
  89. package/dist/platform/fetch.js +14 -0
  90. package/dist/platform/index.js +23 -0
  91. package/dist/platform/types.js +13 -0
  92. package/dist/types/adapters/index.d.ts +24 -0
  93. package/dist/types/adapters/index.js.map +1 -0
  94. package/dist/types/adapters/registry.d.ts +35 -0
  95. package/dist/types/adapters/registry.js.map +1 -0
  96. package/dist/types/adapters/sites/declarative.d.ts +13 -0
  97. package/dist/types/adapters/sites/declarative.js.map +1 -0
  98. package/dist/types/adapters/sites/hackernews.d.ts +13 -0
  99. package/dist/types/adapters/sites/hackernews.js.map +1 -0
  100. package/dist/types/adapters/sites/youtube.d.ts +31 -0
  101. package/dist/types/adapters/sites/youtube.js.map +1 -0
  102. package/dist/types/adapters/types.d.ts +99 -0
  103. package/dist/types/adapters/types.js.map +1 -0
  104. package/dist/types/cli/bin.js.map +1 -1
  105. package/dist/types/cli/commands/jobs/files.d.ts +28 -0
  106. package/dist/types/cli/commands/jobs/files.js.map +1 -0
  107. package/dist/types/cli/commands/jobs/llms-txt.d.ts +33 -0
  108. package/dist/types/cli/commands/jobs/llms-txt.js.map +1 -0
  109. package/dist/types/cli/commands/jobs/options.d.ts +58 -0
  110. package/dist/types/cli/commands/jobs/options.js.map +1 -0
  111. package/dist/types/cli/commands/jobs/run.d.ts +44 -0
  112. package/dist/types/cli/commands/jobs/run.js.map +1 -0
  113. package/dist/types/cli/commands/skill/content.d.ts +9 -0
  114. package/dist/types/cli/commands/skill/content.js.map +1 -0
  115. package/dist/types/cli/commands/skill/index.d.ts +19 -0
  116. package/dist/types/cli/commands/skill/index.js.map +1 -0
  117. package/dist/types/cli/commands/webforai/interactive.d.ts +7 -0
  118. package/dist/types/cli/commands/webforai/interactive.js.map +1 -0
  119. package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -1
  120. package/dist/types/cli/commands/webforai/options.d.ts +62 -0
  121. package/dist/types/cli/commands/webforai/options.js.map +1 -0
  122. package/dist/types/cli/commands/webforai/run.d.ts +10 -0
  123. package/dist/types/cli/commands/webforai/run.js.map +1 -0
  124. package/dist/types/cli/constants.d.ts +14 -4
  125. package/dist/types/cli/constants.js.map +1 -1
  126. package/dist/types/cli/helpers/selectExtractMode.js.map +1 -1
  127. package/dist/types/cli/helpers/selectLoader.js.map +1 -1
  128. package/dist/types/cli/utils.d.ts +8 -1
  129. package/dist/types/cli/utils.js.map +1 -1
  130. package/dist/types/constants.d.ts +7 -1
  131. package/dist/types/constants.js.map +1 -1
  132. package/dist/types/detect-client-shell.d.ts +24 -0
  133. package/dist/types/detect-client-shell.js.map +1 -0
  134. package/dist/types/extract-meta-refresh.d.ts +29 -0
  135. package/dist/types/extract-meta-refresh.js.map +1 -0
  136. package/dist/types/extractors/index.d.ts +1 -0
  137. package/dist/types/extractors/index.js.map +1 -1
  138. package/dist/types/extractors/lib/constants.d.ts +81 -0
  139. package/dist/types/extractors/lib/constants.js.map +1 -0
  140. package/dist/types/extractors/lib/sanitize.d.ts +66 -0
  141. package/dist/types/extractors/lib/sanitize.js.map +1 -0
  142. package/dist/types/extractors/lib/score.d.ts +85 -0
  143. package/dist/types/extractors/lib/score.js.map +1 -0
  144. package/dist/types/extractors/lib/terminators.d.ts +26 -0
  145. package/dist/types/extractors/lib/terminators.js.map +1 -0
  146. package/dist/types/extractors/lib/ui-chrome.d.ts +18 -0
  147. package/dist/types/extractors/lib/ui-chrome.js.map +1 -0
  148. package/dist/types/extractors/pipeExtractors.d.ts +5 -0
  149. package/dist/types/extractors/pipeExtractors.js.map +1 -1
  150. package/dist/types/extractors/presets/auto.d.ts +30 -0
  151. package/dist/types/extractors/presets/auto.js.map +1 -0
  152. package/dist/types/extractors/presets/minimal-filter.d.ts +10 -2
  153. package/dist/types/extractors/presets/minimal-filter.js.map +1 -1
  154. package/dist/types/extractors/presets/takumi.d.ts +19 -3
  155. package/dist/types/extractors/presets/takumi.js.map +1 -1
  156. package/dist/types/extractors/types.d.ts +8 -0
  157. package/dist/types/html-to-markdown.d.ts +38 -0
  158. package/dist/types/html-to-markdown.js.map +1 -1
  159. package/dist/types/html-to-mdast.d.ts +11 -0
  160. package/dist/types/html-to-mdast.js.map +1 -1
  161. package/dist/types/index.d.ts +8 -3
  162. package/dist/types/index.js.map +1 -1
  163. package/dist/types/link-replacer.d.ts +3 -1
  164. package/dist/types/link-replacer.js.map +1 -1
  165. package/dist/types/loaders/cf-puppeteer.js.map +1 -1
  166. package/dist/types/loaders/fetch.d.ts +4 -0
  167. package/dist/types/loaders/fetch.js.map +1 -1
  168. package/dist/types/loaders/playwright.js.map +1 -1
  169. package/dist/types/loaders/puppeteer.js.map +1 -1
  170. package/dist/types/md-splitter.d.ts +1 -1
  171. package/dist/types/md-splitter.js.map +1 -1
  172. package/dist/types/mdast-handlers/custom-a-handler.js.map +1 -1
  173. package/dist/types/mdast-handlers/custom-code-handler.d.ts +2 -0
  174. package/dist/types/mdast-handlers/custom-code-handler.js.map +1 -1
  175. package/dist/types/mdast-handlers/custom-div-handler.js.map +1 -1
  176. package/dist/types/mdast-handlers/custom-img-handler.js.map +1 -1
  177. package/dist/types/mdast-handlers/custom-table-handler.js.map +1 -1
  178. package/dist/types/mdast-handlers/definition-list-handler.d.ts +2 -0
  179. package/dist/types/mdast-handlers/definition-list-handler.js.map +1 -0
  180. package/dist/types/mdast-handlers/math-handler.js.map +1 -1
  181. package/dist/types/mdast-handlers/sup-sub-handler.d.ts +10 -0
  182. package/dist/types/mdast-handlers/sup-sub-handler.js.map +1 -0
  183. package/dist/types/mdast-to-markdown.js.map +1 -1
  184. package/dist/types/metadata/index.d.ts +47 -0
  185. package/dist/types/metadata/index.js.map +1 -0
  186. package/dist/types/normalize/index.d.ts +53 -0
  187. package/dist/types/normalize/index.js.map +1 -0
  188. package/dist/types/platform/client.d.ts +55 -0
  189. package/dist/types/platform/client.js.map +1 -0
  190. package/dist/types/platform/error.d.ts +22 -0
  191. package/dist/types/platform/error.js.map +1 -0
  192. package/dist/types/platform/fetch.d.ts +36 -0
  193. package/dist/types/platform/fetch.js.map +1 -0
  194. package/dist/types/platform/index.d.ts +4 -0
  195. package/dist/types/platform/index.js.map +1 -0
  196. package/dist/types/platform/types.d.ts +164 -0
  197. package/dist/types/platform/types.js.map +1 -0
  198. package/dist/types/utils/hast-fast.d.ts +101 -0
  199. package/dist/types/utils/hast-fast.js.map +1 -0
  200. package/dist/utils/hast-fast.js +246 -0
  201. package/package.json +25 -7
  202. package/dist/types/cli/commands/webforai/index.d.ts +0 -6
  203. package/dist/types/cli/commands/webforai/index.js.map +0 -1
@@ -0,0 +1,53 @@
1
+ // src/detect-client-shell.ts
2
+ var SHELL_SUSPICION_THRESHOLD = 300;
3
+ var SHELL_EMPTY_THRESHOLD = 50;
4
+ var BODY_PATTERN = /<body[^>]*>([\s\S]*?)<\/body>/i;
5
+ var HEAD_PATTERN = /<head[^>]*>[\s\S]*?<\/head>/gi;
6
+ var NOSCRIPT_PATTERN = /<noscript[^>]*>([\s\S]*?)<\/noscript>/gi;
7
+ var INVISIBLE_ELEMENT_PATTERN = /<(script|style|template|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi;
8
+ var COMMENT_PATTERN = /<!--[\s\S]*?-->/g;
9
+ var TAG_PATTERN = /<[^>]+>/g;
10
+ var MOUNT_POINT_PATTERN = /<[a-z][^>]*\sid=["'](?:root|app|__next|___gatsby|__nuxt|q-app|svelte|react-root)["']|<app-root[\s>]|\sdata-reactroot[\s>=]/i;
11
+ var CHALLENGE_PATTERN = /cf-chl|challenge-platform|cf_chl_opt|just a moment|attention required|checking your browser|_incapsula_|px-captcha|datadome/i;
12
+ var ENTITIES = {
13
+ "&nbsp;": " ",
14
+ "&amp;": "&",
15
+ "&lt;": "<",
16
+ "&gt;": ">",
17
+ "&quot;": '"',
18
+ "&#39;": "'"
19
+ };
20
+ var visibleTextOf = (markup) => markup.replace(COMMENT_PATTERN, " ").replace(INVISIBLE_ELEMENT_PATTERN, " ").replace(TAG_PATTERN, " ").replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? " ").replace(/\s+/g, " ").trim();
21
+ var noscriptMentionsJavascript = (html) => {
22
+ for (const match of html.matchAll(NOSCRIPT_PATTERN)) {
23
+ if (/javascript/i.test(match[1] ?? "")) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ };
29
+ var detectClientShell = (html) => {
30
+ const body = BODY_PATTERN.exec(html)?.[1] ?? html.replace(HEAD_PATTERN, " ");
31
+ const visibleTextLength = visibleTextOf(body).length;
32
+ if (visibleTextLength >= SHELL_SUSPICION_THRESHOLD) {
33
+ return { isShell: false, visibleTextLength };
34
+ }
35
+ if (CHALLENGE_PATTERN.test(html)) {
36
+ return { isShell: true, reason: "anti-bot-challenge", visibleTextLength };
37
+ }
38
+ if (MOUNT_POINT_PATTERN.test(body)) {
39
+ return { isShell: true, reason: "spa-shell", visibleTextLength };
40
+ }
41
+ if (noscriptMentionsJavascript(html)) {
42
+ return { isShell: true, reason: "noscript-only", visibleTextLength };
43
+ }
44
+ if (visibleTextLength < SHELL_EMPTY_THRESHOLD) {
45
+ return { isShell: true, reason: "empty-body", visibleTextLength };
46
+ }
47
+ return { isShell: false, visibleTextLength };
48
+ };
49
+ export {
50
+ SHELL_EMPTY_THRESHOLD,
51
+ SHELL_SUSPICION_THRESHOLD,
52
+ detectClientShell
53
+ };
@@ -0,0 +1,47 @@
1
+ // src/extract-meta-refresh.ts
2
+ var MAX_META_REFRESH_DELAY_SECONDS = 10;
3
+ var META_TAG_PATTERN = /<meta\b[^>]*>/gi;
4
+ var HTTP_EQUIV_REFRESH_PATTERN = /\bhttp-equiv\s*=\s*("\s*refresh\s*"|'\s*refresh\s*'|refresh\b)/i;
5
+ var CONTENT_ATTR_PATTERN = /\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
6
+ var CONTENT_VALUE_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*[;,]\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+?))\s*$/i;
7
+ var resolveTarget = (raw, baseUrl) => {
8
+ try {
9
+ return baseUrl === void 0 ? new URL(raw) : new URL(raw, baseUrl);
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ };
14
+ var extractMetaRefresh = (html, baseUrl) => {
15
+ for (const [tag] of html.matchAll(META_TAG_PATTERN)) {
16
+ if (!HTTP_EQUIV_REFRESH_PATTERN.test(tag)) {
17
+ continue;
18
+ }
19
+ const contentMatch = CONTENT_ATTR_PATTERN.exec(tag);
20
+ const content = contentMatch?.[1] ?? contentMatch?.[2] ?? contentMatch?.[3];
21
+ if (!content) {
22
+ continue;
23
+ }
24
+ const value = CONTENT_VALUE_PATTERN.exec(content);
25
+ if (!value) {
26
+ continue;
27
+ }
28
+ const delaySeconds = Number.parseFloat(value[1] ?? "");
29
+ const raw = (value[2] ?? value[3] ?? value[4] ?? "").trim();
30
+ if (!raw || delaySeconds > MAX_META_REFRESH_DELAY_SECONDS) {
31
+ continue;
32
+ }
33
+ const target = resolveTarget(raw, baseUrl);
34
+ if (!target || target.protocol !== "http:" && target.protocol !== "https:") {
35
+ continue;
36
+ }
37
+ if (baseUrl !== void 0 && resolveTarget(baseUrl, void 0)?.href === target.href) {
38
+ continue;
39
+ }
40
+ return { url: target.href, delaySeconds };
41
+ }
42
+ return void 0;
43
+ };
44
+ export {
45
+ MAX_META_REFRESH_DELAY_SECONDS,
46
+ extractMetaRefresh
47
+ };
@@ -4,7 +4,10 @@ import {
4
4
  } from "./pipeExtractors.js";
5
5
  import { takumiExtractor } from "./presets/takumi.js";
6
6
  import { minimalFilter } from "./presets/minimal-filter.js";
7
+ import { autoExtractor, createAutoExtractor } from "./presets/auto.js";
7
8
  export {
9
+ autoExtractor,
10
+ createAutoExtractor,
8
11
  minimalFilter,
9
12
  pipeExtractors,
10
13
  takumiExtractor
@@ -0,0 +1,150 @@
1
+ // src/extractors/lib/constants.ts
2
+ var REGEXPS = {
3
+ /**
4
+ * Containers that almost never hold the article body.
5
+ *
6
+ * Note the words that are deliberately *absent*. `header` and `footer` match the heading
7
+ * anchors and article furniture that documentation generators emit (`<a class="header">`,
8
+ * `class="post-header"`), so matching them here deletes the page's headings. The landmark
9
+ * versions of those elements are handled structurally instead.
10
+ */
11
+ unlikelyCandidates: /-ad-|ai2html|banner|breadcrumb|combx|comment(?!ary)|community|cover-wrap|disqus|gdpr|legends|menu|related|remark|replies|rss|shoutbox|sidebar|site-index|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore|avatar|subscribe|newsletter|paywall|consent|cookie|toolbar|masthead|promo|share|sharing|widget|tooltip|dropdown|modal|overlay|backdrop|carousel|announcement|notification|toast|skip-link|screen-reader|visually-hidden|sr-only/i,
12
+ /** Rescues containers whose class merely happens to contain an unlikely substring. */
13
+ okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code|post|entry|story|markdown|prose|doc/i,
14
+ /** Very high-confidence boilerplate, not rescued by the clause above. */
15
+ stronglyUnlikely: /^(nav|navbar|breadcrumbs?|cookie-?(banner|consent|notice)|consent-?(banner|manager)|skip-?link|site-?header|site-?footer|global-?(header|footer)|social-?(share|links?)|share-?(bar|buttons?)|related-?(posts?|articles?)|newsletter-?(signup|form)|comment-?(form|list)|toc-?sidebar)$/i,
16
+ /** Class/id fragments that mark real article content. */
17
+ positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story|markdown|prose|documentation|doc-?content|readme/i,
18
+ /** Class/id fragments that mark furniture. */
19
+ negative: /-ad-|hidden|^hid$| hid$|^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|widget|toolbar|nav|menu|cookie|consent|subscribe|newsletter|popup|modal|overlay|breadcrumb|pagination|social/i,
20
+ /** Placeholder imagery that should never reach the output. */
21
+ hidden: /fallback-image/i,
22
+ byline: /byline|author|dateline|writtenby|p-author|c-author|post-meta|entry-meta/i,
23
+ /** Wiki/CMS chrome that generic scoring alone tends to keep. */
24
+ specialUnlikelyCandidates: /frb-|uls-menu|language-link|mw-editsection|noprint|navbox|metadata|catlinks|printfooter/i
25
+ };
26
+ var NON_CONTENT_TAGS = /* @__PURE__ */ new Set([
27
+ "script",
28
+ "style",
29
+ "link",
30
+ "meta",
31
+ "noscript",
32
+ "template",
33
+ "base",
34
+ "param",
35
+ "source",
36
+ "track",
37
+ "title"
38
+ ]);
39
+ var CHROME_TAGS = /* @__PURE__ */ new Set(["nav", "aside", "dialog", "menu"]);
40
+ var CONTEXTUAL_LANDMARK_TAGS = /* @__PURE__ */ new Set(["header", "footer"]);
41
+ var HIDDEN_CLASS_NAMES = /* @__PURE__ */ new Set([
42
+ "hidden",
43
+ "invisible",
44
+ "is-hidden",
45
+ "d-none",
46
+ "display-none",
47
+ "hide",
48
+ "is-invisible"
49
+ ]);
50
+ var RESPONSIVE_DISPLAY_OVERRIDE = /^(sm|md|lg|xl|2xl|print):(block|flex|grid|inline|inline-block|inline-flex|table|contents|list-item)$/;
51
+ var UNLIKELY_ROLES = /* @__PURE__ */ new Set([
52
+ "menu",
53
+ "menubar",
54
+ "menuitem",
55
+ "complementary",
56
+ "navigation",
57
+ "alert",
58
+ "alertdialog",
59
+ "dialog",
60
+ "banner",
61
+ "contentinfo",
62
+ "search",
63
+ "searchbox",
64
+ "toolbar",
65
+ "tablist",
66
+ "tooltip"
67
+ // `presentation` is deliberately absent. It marks an element as decorative *structure* — a
68
+ // layout table, a wrapper — while the content inside it is usually real. Treating it as
69
+ // chrome deletes article text.
70
+ ]);
71
+ var CONTENT_ROLES = /* @__PURE__ */ new Set(["main", "article", "document"]);
72
+ var SCOREABLE_TAGS = /* @__PURE__ */ new Set(["p", "td", "pre", "blockquote", "dd", "li", "figure", "section", "div"]);
73
+ var CANDIDATE_TAGS = /* @__PURE__ */ new Set([
74
+ "div",
75
+ "section",
76
+ "article",
77
+ "main",
78
+ "td",
79
+ "pre",
80
+ "blockquote",
81
+ "form",
82
+ "ul",
83
+ "ol",
84
+ "dl"
85
+ ]);
86
+ var EMBEDDED_CONTENT_TAGS = /* @__PURE__ */ new Set([
87
+ "img",
88
+ "picture",
89
+ "video",
90
+ "audio",
91
+ "iframe",
92
+ "embed",
93
+ "object",
94
+ "svg",
95
+ // Formulae hold no text of their own, so an empty-wrapper rule would otherwise delete them.
96
+ "math",
97
+ "mjx-container",
98
+ "canvas"
99
+ ]);
100
+ var DROP_WHEN_EMPTY_TAGS = /* @__PURE__ */ new Set([
101
+ "p",
102
+ "h1",
103
+ "h2",
104
+ "h3",
105
+ "h4",
106
+ "h5",
107
+ "h6",
108
+ "li",
109
+ "td",
110
+ "th",
111
+ "blockquote",
112
+ "figcaption",
113
+ "dd",
114
+ "dt",
115
+ "a",
116
+ "span",
117
+ "div",
118
+ "section",
119
+ "article"
120
+ ]);
121
+ var MIN_CONTENT_LENGTH = {
122
+ ja: 140,
123
+ zh: 140,
124
+ ko: 160,
125
+ th: 160,
126
+ default: 400
127
+ };
128
+ var minContentLength = (lang) => {
129
+ if (!lang) {
130
+ return MIN_CONTENT_LENGTH.default;
131
+ }
132
+ const primary = lang.toLowerCase().split("-")[0];
133
+ return MIN_CONTENT_LENGTH[primary] ?? MIN_CONTENT_LENGTH.default;
134
+ };
135
+ export {
136
+ CANDIDATE_TAGS,
137
+ CHROME_TAGS,
138
+ CONTENT_ROLES,
139
+ CONTEXTUAL_LANDMARK_TAGS,
140
+ DROP_WHEN_EMPTY_TAGS,
141
+ EMBEDDED_CONTENT_TAGS,
142
+ HIDDEN_CLASS_NAMES,
143
+ MIN_CONTENT_LENGTH,
144
+ NON_CONTENT_TAGS,
145
+ REGEXPS,
146
+ RESPONSIVE_DISPLAY_OVERRIDE,
147
+ SCOREABLE_TAGS,
148
+ UNLIKELY_ROLES,
149
+ minContentLength
150
+ };
@@ -0,0 +1,419 @@
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 LITERAL_TEXT_TAGS = /* @__PURE__ */ new Set(["code", "kbd", "samp", "var", "pre"]);
378
+ var isMidArticleWidget = (element) => {
379
+ if (LITERAL_TEXT_TAGS.has(element.tagName)) {
380
+ return false;
381
+ }
382
+ if (element.tagName === "form" || element.tagName === "fieldset") {
383
+ return true;
384
+ }
385
+ if (element.tagName === "button") {
386
+ return true;
387
+ }
388
+ return isUiChromeText(elementText(element));
389
+ };
390
+ var cleanContent = (tree, collector) => {
391
+ const doomed = new Set(findCleanupTargets(tree, collector));
392
+ if (doomed.size > 0) {
393
+ pruneInPlace(tree, (node) => !(isElement(node) && doomed.has(node)));
394
+ }
395
+ return dropEmptyElements(tree, new MetricsCollectorCtor());
396
+ };
397
+ var elementText = (node) => {
398
+ if (node.type === "text") {
399
+ return node.value;
400
+ }
401
+ if (!("children" in node)) {
402
+ return "";
403
+ }
404
+ let text = "";
405
+ for (const child of node.children) {
406
+ text += elementText(child);
407
+ }
408
+ return text;
409
+ };
410
+ export {
411
+ cleanContent,
412
+ dropEmptyElements,
413
+ findCleanupTargets,
414
+ findLandmarkChrome,
415
+ findUnlikelyElements,
416
+ hoistNoscriptImages,
417
+ stripNonContent,
418
+ stripUnlikely
419
+ };