webforai 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/bin.js +277 -334
  2. package/dist/cjs/cli/bin.js +3 -4
  3. package/dist/cjs/cli/commands/{handlers → webforai}/index.js +33 -31
  4. package/dist/cjs/cli/commands/webforai/loadHtml.js +93 -0
  5. package/dist/cjs/cli/utils.js +18 -51
  6. package/dist/cjs/extract-hast/index.js +4 -3
  7. package/dist/cjs/extract-hast/readability.js +28 -61
  8. package/dist/cjs/extract-hast/strongly.js +2 -1
  9. package/dist/cjs/extract-hast/utils.js +0 -15
  10. package/dist/cjs/html-to-mdast.js +6 -6
  11. package/dist/cjs/link-replacer.js +10 -5
  12. package/dist/cjs/loaders/playwright.js +36 -7
  13. package/dist/cjs/mdast-handlers/custom-br-handler.js +31 -0
  14. package/dist/cjs/mdast-to-markdown.js +1 -1
  15. package/dist/cjs/utils/hast-utils.js +71 -0
  16. package/dist/cli/bin.js +3 -4
  17. package/dist/cli/commands/webforai/index.js +53 -0
  18. package/dist/cli/commands/webforai/loadHtml.js +60 -0
  19. package/dist/cli/utils.js +18 -51
  20. package/dist/extract-hast/index.js +4 -3
  21. package/dist/extract-hast/readability.js +29 -62
  22. package/dist/extract-hast/strongly.js +2 -1
  23. package/dist/extract-hast/utils.js +0 -14
  24. package/dist/html-to-mdast.js +6 -6
  25. package/dist/link-replacer.js +10 -5
  26. package/dist/loaders/playwright.js +36 -7
  27. package/dist/mdast-handlers/custom-br-handler.js +8 -0
  28. package/dist/mdast-to-markdown.js +1 -1
  29. package/dist/types/cli/bin.js.map +1 -1
  30. package/dist/types/cli/commands/webforai/index.d.ts +6 -0
  31. package/dist/types/cli/commands/webforai/index.js.map +1 -0
  32. package/dist/types/cli/commands/webforai/loadHtml.d.ts +3 -0
  33. package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -0
  34. package/dist/types/cli/utils.d.ts +27 -0
  35. package/dist/types/cli/utils.js.map +1 -1
  36. package/dist/types/extract-hast/index.d.ts +9 -4
  37. package/dist/types/extract-hast/index.js.map +1 -1
  38. package/dist/types/extract-hast/readability.d.ts +2 -1
  39. package/dist/types/extract-hast/readability.js.map +1 -1
  40. package/dist/types/extract-hast/strongly.d.ts +2 -1
  41. package/dist/types/extract-hast/strongly.js.map +1 -1
  42. package/dist/types/extract-hast/utils.d.ts +0 -1
  43. package/dist/types/extract-hast/utils.js.map +1 -1
  44. package/dist/types/html-to-mdast.d.ts +4 -0
  45. package/dist/types/html-to-mdast.js.map +1 -1
  46. package/dist/types/link-replacer.d.ts +1 -1
  47. package/dist/types/link-replacer.js.map +1 -1
  48. package/dist/types/loaders/playwright.d.ts +8 -2
  49. package/dist/types/loaders/playwright.js.map +1 -1
  50. package/dist/types/mdast-handlers/custom-br-handler.d.ts +2 -0
  51. package/dist/types/mdast-handlers/custom-br-handler.js.map +1 -0
  52. package/dist/types/mdast-to-markdown.js.map +1 -1
  53. package/dist/types/utils/hast-utils.d.ts +4 -0
  54. package/dist/types/utils/hast-utils.js.map +1 -0
  55. package/dist/utils/hast-utils.js +46 -0
  56. package/package.json +21 -10
  57. package/dist/cjs/cli/commands/handlers/loadHtml.js +0 -127
  58. package/dist/cjs/html-to-markdown.test.js +0 -118
  59. package/dist/cjs/link-replacer.test.js +0 -47
  60. package/dist/cjs/loaders/fetch.test.js +0 -9
  61. package/dist/cjs/loaders/playwright.test.js +0 -17
  62. package/dist/cjs/loaders/puppeteer.test.js +0 -17
  63. package/dist/cli/commands/handlers/index.js +0 -51
  64. package/dist/cli/commands/handlers/loadHtml.js +0 -94
  65. package/dist/html-to-markdown.test.js +0 -118
  66. package/dist/link-replacer.test.js +0 -47
  67. package/dist/loaders/fetch.test.js +0 -9
  68. package/dist/loaders/playwright.test.js +0 -17
  69. package/dist/loaders/puppeteer.test.js +0 -17
  70. package/dist/types/cli/commands/handlers/index.d.ts +0 -13
  71. package/dist/types/cli/commands/handlers/index.js.map +0 -1
  72. package/dist/types/cli/commands/handlers/loadHtml.d.ts +0 -2
  73. package/dist/types/cli/commands/handlers/loadHtml.js.map +0 -1
package/dist/bin.js CHANGED
@@ -1,64 +1,14 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
-
11
- // src/loaders/fetch.ts
12
- var fetch_exports = {};
13
- __export(fetch_exports, {
14
- USER_AGENT: () => USER_AGENT,
15
- loadHtml: () => loadHtml
16
- });
17
- var USER_AGENT, loadHtml;
18
- var init_fetch = __esm({
19
- "src/loaders/fetch.ts"() {
20
- "use strict";
21
- USER_AGENT = "mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/125.0.0.0 safari/537.36";
22
- loadHtml = async (url, userAgent = USER_AGENT) => {
23
- const response = await fetch(url, { headers: { "User-Agent": userAgent } });
24
- return response.text();
25
- };
26
- }
27
- });
28
-
29
- // src/loaders/playwright.ts
30
- var playwright_exports = {};
31
- __export(playwright_exports, {
32
- loadHtml: () => loadHtml2
33
- });
34
- import { chromium } from "playwright";
35
- var loadHtml2;
36
- var init_playwright = __esm({
37
- "src/loaders/playwright.ts"() {
38
- "use strict";
39
- loadHtml2 = async (url, context) => {
40
- const _context = context ?? await chromium.launch({ headless: true });
41
- const page = await _context.newPage();
42
- await page.goto(url, { waitUntil: "load" });
43
- const html = await page.content();
44
- await page.close();
45
- if (!context) {
46
- await _context.close();
47
- }
48
- return html;
49
- };
50
- }
51
- });
1
+ #!/usr/bin/env node
52
2
 
53
3
  // src/cli/bin.ts
54
- import { intro } from "@clack/prompts";
4
+ import { intro as intro2 } from "@clack/prompts";
55
5
  import { program } from "commander";
56
- import pc2 from "picocolors";
6
+ import pc3 from "picocolors";
57
7
 
58
8
  // package.json
59
9
  var package_default = {
60
10
  name: "webforai",
61
- version: "1.5.1",
11
+ version: "1.6.0",
62
12
  description: "A library that provides a web interface for AI",
63
13
  author: "inaridiy",
64
14
  license: "MIT",
@@ -146,20 +96,30 @@ var package_default = {
146
96
  },
147
97
  peerDependencies: {
148
98
  "@cloudflare/puppeteer": ">=0.0.6",
149
- playwright: ">=1.4",
99
+ "playwright-core": ">=1.4",
150
100
  puppeteer: ">=22"
151
101
  },
102
+ peerDependenciesMeta: {
103
+ "@cloudflare/puppeteer": {
104
+ optional: true
105
+ },
106
+ "playwright-core": {
107
+ optional: false
108
+ },
109
+ puppeteer: {
110
+ optional: true
111
+ }
112
+ },
152
113
  dependencies: {
153
114
  "@clack/prompts": "^0.7.0",
154
115
  commander: "^12.1.0",
155
- "fastest-levenshtein": "^1.0.16",
156
- "hast-util-from-html": "^2.0.1",
157
- "hast-util-select": "^6.0.1",
158
- "hast-util-to-html": "^9.0.0",
116
+ "hast-util-from-html": "^2.0.3",
117
+ "hast-util-select": "^6.0.2",
118
+ "hast-util-to-html": "^9.0.3",
159
119
  "hast-util-to-mdast": "^10.1.0",
160
120
  "hast-util-to-string": "^3.0.0",
161
121
  "hast-util-to-text": "^4.0.0",
162
- "mathml-to-latex": "^1.3.0",
122
+ "mathml-to-latex": "^1.4.1",
163
123
  "mdast-util-gfm": "^3.0.0",
164
124
  "mdast-util-math": "^3.0.0",
165
125
  "mdast-util-to-markdown": "^2.1.0",
@@ -171,26 +131,28 @@ var package_default = {
171
131
  devDependencies: {
172
132
  "@cloudflare/puppeteer": "^0.0.6",
173
133
  "@tsconfig/recommended": "^1.0.3",
134
+ "fastest-levenshtein": "^1.0.16",
174
135
  "@types/hast": "^3.0.2",
175
136
  "@types/mdast": "^4.0.2",
176
- "@types/node": "^20.11.0",
137
+ "@types/node": "^20.14.10",
177
138
  arg: "^5.0.2",
178
139
  esbuild: "^0.19.11",
179
140
  glob: "^10.3.10",
180
141
  ncp: "^2.0.0",
181
142
  np: "^9.2.0",
182
- playwright: "^1.40.1",
143
+ "playwright-core": "^1.40.1",
183
144
  puppeteer: "^23.2.2",
184
145
  rimraf: "^5.0.5",
185
- tsx: "^4.7.0",
146
+ tsx: "^4.19.1",
186
147
  typescript: "^5.4.5"
187
148
  }
188
149
  };
189
150
 
190
- // src/cli/commands/handlers/index.ts
191
- import fs5 from "node:fs";
151
+ // src/cli/commands/webforai/index.ts
152
+ import fs5 from "node:fs/promises";
192
153
  import path2 from "node:path";
193
- import { outro } from "@clack/prompts";
154
+ import { intro, outro, spinner } from "@clack/prompts";
155
+ import pc2 from "picocolors";
194
156
 
195
157
  // src/html-to-mdast.ts
196
158
  import { fromHtml } from "hast-util-from-html";
@@ -215,19 +177,6 @@ var isStrInclude = (value, match) => {
215
177
  }
216
178
  return false;
217
179
  };
218
- var hasAncestors = (element, tagNames, depth = 3) => {
219
- let parent = element.parent;
220
- for (let i = 0; i < depth; i++) {
221
- if (!parent || parent.type !== "element") {
222
- return false;
223
- }
224
- if (tagNames.includes(parent.tagName)) {
225
- return true;
226
- }
227
- parent = parent.parent;
228
- }
229
- return false;
230
- };
231
180
 
232
181
  // src/extract-hast/readability.ts
233
182
  var UNLIKELY_ROLES = ["menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog"];
@@ -235,35 +184,24 @@ var REGEXPS = {
235
184
  hidden: /hidden|invisible|fallback-image/i,
236
185
  byline: /byline|author|dateline|writtenby|p-author/i,
237
186
  specialUnlikelyCandidates: /frb-|uls-menu|language-link/i,
238
- unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore/i,
187
+ unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|tooltip|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore|avatar/i,
239
188
  okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code/i
240
189
  };
241
- var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry", "main"];
242
- var PARAGRAPH_TAGS = ["p", "div", "section", "article", "main", "ul", "ol", "li"];
190
+ var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry"];
191
+ var PARAGRAPH_TAGS = ["a", "p", "div", "section", "article", "main", "ul", "ol", "li"];
243
192
  var BASE_MINIMAL_LENGTH = {
244
193
  ja: 200,
245
194
  en: 500
246
195
  };
247
- var tryGetLang = (node) => {
248
- if (node.type !== "element") {
249
- return;
250
- }
251
- const element = node;
252
- if (element.tagName !== "html") {
253
- return;
254
- }
255
- const langAttr = element.properties.lang || element.properties["xml:lang"];
256
- if (langAttr) {
257
- return langAttr;
258
- }
196
+ var metadataFilter = (node) => {
197
+ return !(["comment", "doctype"].includes(node.type) || node.type === "element" && ["script", "style", "link", "meta", "noscript", "svg", "title"].includes(node.tagName));
259
198
  };
260
- var metadataFilter = (node) => !["comment", "doctype"].includes(node.type);
261
199
  var universalElementFilter = (node) => {
262
200
  if (node.type !== "element") {
263
201
  return true;
264
202
  }
265
203
  const element = node;
266
- if (["script", "aside"].includes(element.tagName)) {
204
+ if (["aside", "nav"].includes(element.tagName)) {
267
205
  return false;
268
206
  }
269
207
  if (["hidden", "aria-hidden"].some((key) => element.properties[key])) {
@@ -294,10 +232,7 @@ var unlikelyElementFilter = (node) => {
294
232
  return true;
295
233
  }
296
234
  const element = node;
297
- if (["body", "article", "main", "section", "a"].includes(element.tagName)) {
298
- return true;
299
- }
300
- if (hasAncestors(element, ["table", "code"], 3)) {
235
+ if (["body", "article", "main", "section", "h1", "h2", "h3", "h4", "h5", "h6", "p"].includes(element.tagName)) {
301
236
  return true;
302
237
  }
303
238
  const match = matchString(element);
@@ -309,55 +244,36 @@ var unlikelyElementFilter = (node) => {
309
244
  }
310
245
  return true;
311
246
  };
312
- var isImageLink = (element) => {
313
- const a = select("a", element);
314
- const img = select("img", a);
315
- if (!(a && img)) {
316
- return false;
317
- }
318
- const imgFilename = img.properties.src?.toString().split("/").pop();
319
- const hrefFilename = a.properties.href?.toString().split("/").pop();
320
- return imgFilename === hrefFilename && imgFilename;
321
- };
322
- var removeEmptyFilter = (node) => {
247
+ var removeEmptyFilter = (node, _lang) => {
323
248
  if (node.type !== "element") {
324
249
  return true;
325
250
  }
326
251
  const element = node;
327
- if (!PARAGRAPH_TAGS.includes(element.tagName)) {
252
+ if (PARAGRAPH_TAGS.includes(element.tagName)) {
328
253
  return true;
329
254
  }
330
- if (isImageLink(element)) {
331
- return true;
332
- }
333
- const text3 = hastToString(element);
334
- if (text3.length < 10) {
255
+ if (element.tagName === "img" && !element.properties.src) {
335
256
  return false;
336
257
  }
337
258
  return true;
338
259
  };
339
- var readabilityExtractHast = (hast) => {
340
- const lang = String(select("html", hast)?.properties.lang || tryGetLang(hast) || "en");
260
+ var readabilityExtractHast = (params) => {
261
+ const { hast, lang = "en" } = params;
341
262
  const body = select("body", hast) ?? hast;
342
- const bodyText = hastToString(body);
343
- const baseFilterd = filter(body, (node) => {
344
- if (!metadataFilter(node)) {
345
- return false;
346
- }
347
- if (!universalElementFilter(node)) {
348
- return false;
349
- }
350
- return true;
351
- });
352
- if (!baseFilterd) {
353
- return { type: "root", children: [] };
354
- }
355
- const baseFilterdText = hastToString(baseFilterd);
356
- let [baseTree, baseText] = baseFilterdText.length > bodyText.length / 3 || baseFilterdText.length > 5e3 ? [baseFilterd, baseFilterdText] : [body, bodyText];
263
+ const metadataFilteredHast = filter(body, (node) => metadataFilter(node));
264
+ const metadataFilteredHastText = metadataFilteredHast && hastToString(metadataFilteredHast);
265
+ if (!(metadataFilteredHast && metadataFilteredHastText)) {
266
+ return body;
267
+ }
268
+ const baseFilterd = filter(metadataFilteredHast, (node) => universalElementFilter(node));
269
+ const baseFilterdText = baseFilterd ? hastToString(baseFilterd) : "";
270
+ const [baseTree, baseText] = baseFilterdText.length > metadataFilteredHastText.length / 3 || baseFilterdText.length > 5e3 ? [baseFilterd, baseFilterdText] : [metadataFilteredHast, metadataFilteredHastText];
357
271
  let minimalLength = lang in BASE_MINIMAL_LENGTH ? BASE_MINIMAL_LENGTH[lang] : 500;
358
272
  if (baseText.length < minimalLength) {
359
273
  minimalLength = Math.max(0, baseText.length - 200);
360
274
  }
275
+ let extractedTree = baseTree;
276
+ let extractedText = baseText;
361
277
  for (const selector of BODY_SELECTORS) {
362
278
  const content = { type: "root", children: selectAll(selector, baseFilterd) };
363
279
  const contentText = hastToString(content);
@@ -366,18 +282,18 @@ var readabilityExtractHast = (hast) => {
366
282
  }
367
283
  const links = selectAll("a", content);
368
284
  const linkText = links.map((link) => hastToString(link)).join("");
369
- const linkDensity = linkText.length / bodyText.length;
285
+ const linkDensity = linkText.length / contentText.length;
370
286
  if (linkDensity > 0.4) {
371
287
  continue;
372
288
  }
373
289
  if (contentText.length > minimalLength) {
374
- baseTree = content;
375
- baseText = contentText;
290
+ extractedTree = content;
291
+ extractedText = contentText;
376
292
  break;
377
293
  }
378
294
  }
379
- const finalFilteredTree = filter(baseTree, (node) => {
380
- if (!removeEmptyFilter(node)) {
295
+ const finalFilteredTree = filter(extractedTree, (node) => {
296
+ if (!removeEmptyFilter(node, lang)) {
381
297
  return false;
382
298
  }
383
299
  if (!unlikelyElementFilter(node)) {
@@ -385,7 +301,7 @@ var readabilityExtractHast = (hast) => {
385
301
  }
386
302
  return true;
387
303
  });
388
- const finalTree = hastToString(finalFilteredTree).length > baseText.length / 3 ? finalFilteredTree : baseTree;
304
+ const finalTree = hastToString(finalFilteredTree).length > extractedText.length / 3 ? finalFilteredTree : extractedTree;
389
305
  return finalTree;
390
306
  };
391
307
 
@@ -393,7 +309,8 @@ var readabilityExtractHast = (hast) => {
393
309
  import { select as select2 } from "hast-util-select";
394
310
  import { filter as filter2 } from "unist-util-filter";
395
311
  var REMOVE_REGEX = /fallback|icon|header|nav|speechify-ignore|assist|-ad-|banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|assist|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i;
396
- var stronglyExtractHast = (hast) => {
312
+ var stronglyExtractHast = (params) => {
313
+ const { hast } = params;
397
314
  const selectors = ["article", "#article", "#content", "main", "div.container"];
398
315
  let selectedElement = hast;
399
316
  for (const selector of selectors) {
@@ -426,17 +343,18 @@ var PRESET_EXTRACT_HAST = {
426
343
  readability: readabilityExtractHast
427
344
  };
428
345
  var DEFAULT_EXTRACT_HAST = ["readability"];
429
- var extractHast = (hast, extractors = DEFAULT_EXTRACT_HAST) => {
346
+ var extractHast = (params, extractors = DEFAULT_EXTRACT_HAST) => {
347
+ const { hast, lang } = params;
430
348
  const _extractors = Array.isArray(extractors) ? extractors : [extractors];
431
349
  const extracted = _extractors.reduce((acc, extractor) => {
432
350
  if (extractor === false) {
433
351
  return acc;
434
352
  }
435
353
  if (typeof extractor === "string" && extractor in PRESET_EXTRACT_HAST) {
436
- return PRESET_EXTRACT_HAST[extractor](acc);
354
+ return PRESET_EXTRACT_HAST[extractor]({ hast: acc, lang });
437
355
  }
438
356
  if (typeof extractor === "function") {
439
- return extractor(acc);
357
+ return extractor({ hast: acc, lang });
440
358
  }
441
359
  throw new Error(`Invalid extractor: ${extractor}`);
442
360
  }, hast) || hast;
@@ -625,10 +543,6 @@ var customTableHandler = (options) => (state, node) => {
625
543
  return defaultHandlers4.table(state, node);
626
544
  };
627
545
 
628
- // src/mdast-handlers/empty-handler.ts
629
- var emptyHandler = () => {
630
- };
631
-
632
546
  // src/mdast-handlers/math-handler.ts
633
547
  import { toHtml } from "hast-util-to-html";
634
548
  import { MathMLToLaTeX } from "mathml-to-latex";
@@ -640,11 +554,54 @@ var mathHandler = (state, node) => {
640
554
  return result;
641
555
  };
642
556
 
557
+ // src/utils/hast-utils.ts
558
+ import { select as select4, selectAll as selectAll2 } from "hast-util-select";
559
+ var getLangFromHast = (node) => {
560
+ const html = select4("html", node);
561
+ if (html && typeof html.properties.lang === "string") {
562
+ return html.properties.lang;
563
+ }
564
+ if (node.type !== "element") {
565
+ return;
566
+ }
567
+ const element = node;
568
+ if (element.tagName !== "html") {
569
+ return;
570
+ }
571
+ const langAttr = element.properties.lang || element.properties["xml:lang"];
572
+ if (langAttr) {
573
+ return langAttr;
574
+ }
575
+ return void 0;
576
+ };
577
+ var getLangFromStr = (str) => {
578
+ const match = str.match(/lang=["']([^"']+)["']/);
579
+ if (match) {
580
+ return match[1];
581
+ }
582
+ return void 0;
583
+ };
584
+ var getUrlFromHast = (node) => {
585
+ if (node.type !== "element") {
586
+ return void 0;
587
+ }
588
+ const metaTagAttributes = ["og:url", "twitter:url"];
589
+ const metaTags = selectAll2("meta", node);
590
+ for (const meta of metaTags) {
591
+ const property = meta.properties.property || meta.properties.name;
592
+ if (typeof property === "string" && metaTagAttributes.includes(property)) {
593
+ return typeof meta.properties.content === "string" ? meta.properties.content : void 0;
594
+ }
595
+ }
596
+ return void 0;
597
+ };
598
+
643
599
  // src/html-to-mdast.ts
644
600
  var htmlToMdast = (htmlOrHast, options) => {
645
- const { extractors } = options || {};
646
- const hast = typeof htmlOrHast === "string" ? fromHtml(htmlOrHast, { fragment: true }) : htmlOrHast;
647
- const extractedHast = extractHast(hast, extractors);
601
+ const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
602
+ const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
603
+ const url = defaultUrl || getUrlFromHast(hast);
604
+ const extractedHast = extractHast({ hast, lang, url }, extractors);
648
605
  const mdast = toMdast(extractedHast, {
649
606
  handlers: {
650
607
  math: mathHandler,
@@ -652,8 +609,7 @@ var htmlToMdast = (htmlOrHast, options) => {
652
609
  pre: customCodeHandler,
653
610
  a: customAHandler({ asText: options?.linkAsText }),
654
611
  img: customImgHandler({ hideImage: options?.hideImage }),
655
- table: customTableHandler({ asText: options?.tableAsText }),
656
- br: emptyHandler
612
+ table: customTableHandler({ asText: options?.tableAsText })
657
613
  }
658
614
  });
659
615
  const extractedMdast = extractMdast(mdast);
@@ -666,13 +622,18 @@ import { mathToMarkdown } from "mdast-util-math";
666
622
  import { toMarkdown } from "mdast-util-to-markdown";
667
623
 
668
624
  // src/link-replacer.ts
669
- var linkReplacer = (markdown, link) => {
670
- const regex = /(!?\[.*?\]\()(.+?)(\))/g;
671
- return markdown.replace(regex, (match, pre, thisUrl, post) => {
672
- if (thisUrl.startsWith("http") || thisUrl.startsWith("#")) {
625
+ var linkReplacer = (markdown, base) => {
626
+ const regex = /(!?\[.*?\]\()([^)\s]+)(\))/g;
627
+ return markdown.replace(regex, (match, pre, url, post) => {
628
+ if (/^(https?:|#)/.test(url)) {
629
+ return match;
630
+ }
631
+ try {
632
+ const absoluteUrl = new URL(url, base).href;
633
+ return `${pre}${absoluteUrl}${post}`;
634
+ } catch {
673
635
  return match;
674
636
  }
675
- return `${pre}${new URL(thisUrl, link).href}${post}`;
676
637
  });
677
638
  };
678
639
 
@@ -691,7 +652,7 @@ var DEFAULT_MDAST_TO_MARKDOWN_OPTIONS = {
691
652
  };
692
653
  var mdastToMarkdown = (mdast, options) => {
693
654
  const { baseUrl, ...toMarkdownOptions } = { ...DEFAULT_MDAST_TO_MARKDOWN_OPTIONS, ...options };
694
- let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions);
655
+ let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions).replace(/\*\*\*\*/g, "");
695
656
  if (baseUrl) {
696
657
  markdown = linkReplacer(markdown, baseUrl);
697
658
  }
@@ -722,71 +683,38 @@ var isUrl = (maybeUrl) => {
722
683
  }
723
684
  };
724
685
  function changeFileExtension(filePath, newExtension) {
725
- const parsedPath = filePath.split("/");
726
- const fileName = parsedPath[parsedPath.length - 1];
727
- const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
728
- if (fileName.startsWith(".")) {
729
- const parts = fileName.split(".");
730
- if (parts.length === 2) {
731
- return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
732
- }
733
- parts[parts.length - 1] = newExtension.replace(/^\./, "");
734
- return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
735
- }
736
- const lastDotIndex = fileName.lastIndexOf(".");
737
- const baseName = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName;
738
- const newFileName = `${baseName}${formattedNewExtension}`;
739
- parsedPath[parsedPath.length - 1] = newFileName;
740
- return parsedPath.join("/");
686
+ const parsed = path.parse(filePath);
687
+ const ext = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
688
+ return path.format({ ...parsed, ext });
741
689
  }
742
690
  function urlToFilename(url) {
743
691
  try {
744
692
  const urlObj = new URL(url);
745
- const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
746
- if (index === 0) {
747
- return acc;
748
- }
749
- if (acc.length >= 2) {
750
- return acc;
751
- }
752
- if (part === "www") {
753
- return acc;
754
- }
755
- return [part, ...acc];
756
- }, []);
757
- const domainString = domainParts.reverse().join("-");
693
+ const hostname = urlObj.hostname.startsWith("www.") ? urlObj.hostname.slice(4) : urlObj.hostname;
694
+ const domainParts = hostname.split(".").slice(-2).join("-");
758
695
  const pathParts = urlObj.pathname.split("/").filter(Boolean);
759
- const relevantPathParts = pathParts.slice(-2);
760
- const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
761
- let filename = [domainString, pathString].filter(Boolean).join("-");
696
+ const relevantPathParts = pathParts.slice(-2).map(decodeURIComponent).join("-");
697
+ let filename = [domainParts, relevantPathParts].filter(Boolean).join("-");
762
698
  filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
763
699
  return filename || "output";
764
700
  } catch {
765
701
  return "output";
766
702
  }
767
703
  }
768
- var sourcePathToOutputPath = (sourcePath) => {
769
- return isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
770
- };
704
+ var sourcePathToOutputPath = (sourcePath) => isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
771
705
  function getNextAvailableFilePath(filePath) {
772
- const parsedPath = path.parse(filePath);
773
- const directory = parsedPath.dir;
774
- const fullName = parsedPath.base;
775
- const [firstPart, ...restParts] = fullName.split(".");
776
- const restName = restParts.length > 0 ? `.${restParts.join(".")}` : "";
777
- const baseName = firstPart.replace(/_\d+$/, "");
778
- let counter = 1;
779
- let nextFilePath = filePath;
780
- while (fs.existsSync(nextFilePath)) {
781
- const match = firstPart.match(/_(\d+)$/);
782
- if (match) {
783
- counter = Number.parseInt(match[1], 10) + 1;
784
- }
785
- const newName = `${baseName}_${counter}${restName}`;
786
- nextFilePath = path.join(directory, newName);
706
+ const parsed = path.parse(filePath);
707
+ const dir = parsed.dir;
708
+ const ext = parsed.ext;
709
+ const baseName = parsed.name.replace(/_\d+$/, "");
710
+ const match = parsed.name.match(/_(\d+)$/);
711
+ let counter = match ? Number(match[1]) + 1 : 1;
712
+ let newPath = path.join(dir, `${baseName}_${counter}${ext}`);
713
+ while (fs.existsSync(newPath)) {
787
714
  counter++;
715
+ newPath = path.join(dir, `${baseName}_${counter}${ext}`);
788
716
  }
789
- return nextFilePath;
717
+ return newPath;
790
718
  }
791
719
 
792
720
  // src/cli/helpers/assertContinue.ts
@@ -882,9 +810,9 @@ var inputSourcePath = async () => {
882
810
  };
883
811
 
884
812
  // src/cli/helpers/selectExtractMode.ts
885
- import { select as select4 } from "@clack/prompts";
813
+ import { select as select5 } from "@clack/prompts";
886
814
  var selectExtractMode = async () => {
887
- const result = await select4({
815
+ const result = await select5({
888
816
  message: "Select processing mode:",
889
817
  options: MODES.map((mode) => ({ value: mode, label: mode })),
890
818
  initialValue: MODES[0]
@@ -894,14 +822,14 @@ var selectExtractMode = async () => {
894
822
  };
895
823
 
896
824
  // src/cli/helpers/selectLoader.ts
897
- import { select as select5 } from "@clack/prompts";
825
+ import { select as select6 } from "@clack/prompts";
898
826
  var loadersHint = {
899
827
  fetch: "Fetch HTML content from the given URL",
900
828
  playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
901
829
  puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
902
830
  };
903
831
  var selectLoader = async () => {
904
- const result = await select5({
832
+ const result = await select6({
905
833
  message: "Select loader:",
906
834
  initialValue: "fetch",
907
835
  options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
@@ -910,140 +838,155 @@ var selectLoader = async () => {
910
838
  return result;
911
839
  };
912
840
 
913
- // src/cli/commands/handlers/loadHtml.ts
841
+ // src/cli/commands/webforai/loadHtml.ts
914
842
  import fs4 from "node:fs/promises";
915
- import { cancel as cancel2, spinner as clackSpinner, confirm as confirm2, spinner } from "@clack/prompts";
916
843
  import pc from "picocolors";
917
- import { $ } from "zx";
918
- var loadHtml3 = async (sourcePath, loader) => {
919
- const loadSpinner = clackSpinner();
920
- try {
921
- switch (loader.type) {
922
- case "local":
923
- return await loadLocalHtml(sourcePath, loadSpinner);
924
- case "web":
925
- switch (loader.loader) {
926
- case "fetch":
927
- return await loadFetchHtml(sourcePath, loadSpinner);
928
- case "playwright":
929
- return await loadPlaywrightHtml(sourcePath, loadSpinner);
930
- default:
931
- throw new Error("Invalid loader");
932
- }
933
- default:
934
- throw new Error("Invalid loader type");
935
- }
936
- } catch (error) {
937
- loadSpinner.stop(pc.red("Content loading failed!"));
938
- console.error(error);
939
- process.exit(1);
940
- }
941
- };
942
- var loadLocalHtml = async (sourcePath, loadSpinner) => {
943
- loadSpinner.start("Retrieving content.");
944
- try {
945
- await fs4.access(sourcePath);
946
- } catch {
947
- throw new Error("The file does not exist");
948
- }
949
- const stats = await fs4.stat(sourcePath);
950
- if (!stats.isFile()) {
951
- throw new Error("The source must be a file");
952
- }
953
- const content = await fs4.readFile(sourcePath, "utf-8");
954
- loadSpinner.stop("Content retrieval is complete!");
955
- return content;
956
- };
957
- var loadFetchHtml = async (sourcePath, loadSpinner) => {
958
- loadSpinner.start("Downloading content.");
959
- const { loadHtml: loadHtml4 } = await Promise.resolve().then(() => (init_fetch(), fetch_exports));
960
- const content = await loadHtml4(sourcePath);
961
- loadSpinner.stop("Content download is complete!");
962
- return content;
963
- };
964
- var loadPlaywrightHtml = async (sourcePath, loadSpinner) => {
965
- const launchSpinner = clackSpinner();
966
- launchSpinner.start("Launching playwright.");
967
- const playwright = await import("playwright");
968
- let browser;
969
- try {
970
- browser = await playwright.chromium.launch();
971
- launchSpinner.stop("Playwright launched!");
972
- } catch (error) {
973
- await handlePlaywrightLaunchError(error);
974
- browser = await playwright.chromium.launch();
975
- }
976
- loadSpinner.start("Loading content.");
977
- const { loadHtml: loadHtml4 } = await Promise.resolve().then(() => (init_playwright(), playwright_exports));
978
- const content = await loadHtml4(sourcePath, browser);
979
- loadSpinner.stop("Content download is complete!");
980
- await browser.close();
981
- return content;
982
- };
983
- var handlePlaywrightLaunchError = async (error) => {
984
- if (!error.message.includes("npx playwright install")) {
985
- throw error;
986
- }
987
- const isInstall = await confirm2({
988
- message: "To continue, you will need to download a browser. Do you wish to continue?"
989
- });
990
- if (!isInstall) {
991
- cancel2("End of process.");
992
- process.exit(1);
993
- }
994
- const installDepsSpinner = spinner();
995
- installDepsSpinner.start("Installing browser dependencies.");
996
- try {
997
- await $`npx playwright install`;
998
- installDepsSpinner.stop("Browser dependencies installed!");
999
- } catch (error2) {
1000
- installDepsSpinner.stop(pc.red("Browser dependencies failed to install!"));
1001
- throw error2;
1002
- }
844
+ import { chromium as chromium2 } from "playwright-core";
845
+ import playwrightPackageJson from "playwright-core/package.json";
846
+
847
+ // src/loaders/fetch.ts
848
+ var USER_AGENT = "mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/125.0.0.0 safari/537.36";
849
+ var loadHtml = async (url, userAgent = USER_AGENT) => {
850
+ const response = await fetch(url, { headers: { "User-Agent": userAgent } });
851
+ return response.text();
1003
852
  };
1004
853
 
1005
- // src/cli/commands/handlers/index.ts
854
+ // src/loaders/playwright.ts
855
+ import { chromium, devices } from "playwright-core";
856
+ var SUPER_BYPASS_DEVICE = {
857
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
858
+ viewport: { width: 1920, height: 1080 },
859
+ deviceScaleFactor: 1,
860
+ hasTouch: false,
861
+ isMobile: false,
862
+ javaScriptEnabled: true,
863
+ locale: "en-US",
864
+ timezoneId: "America/New_York"
865
+ };
866
+ var loadHtml2 = async (url, options) => {
867
+ const { browser, waitUntil, timeout, superBypassMode } = options ?? {};
868
+ const _browser = browser ?? await chromium.launch({ headless: true });
869
+ const context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
870
+ if (superBypassMode) {
871
+ await context.addInitScript(() => {
872
+ Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
873
+ Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
874
+ Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
875
+ });
876
+ }
877
+ const page = await context.newPage();
878
+ if (superBypassMode) {
879
+ await page.route("**/*.js", (route) => {
880
+ if (route.request().url().includes("captcha-delivery")) {
881
+ return route.abort();
882
+ }
883
+ return route.continue();
884
+ });
885
+ }
886
+ await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
887
+ const html = await page.content();
888
+ await page.close();
889
+ if (browser) {
890
+ await context.close();
891
+ } else {
892
+ await _browser.close();
893
+ }
894
+ return html;
895
+ };
896
+
897
+ // src/cli/commands/webforai/loadHtml.ts
898
+ var checkPlaywrightAvailable = async () => {
899
+ const path3 = chromium2.executablePath();
900
+ const isAvailable = await fs4.access(path3).catch(() => false);
901
+ return !!isAvailable;
902
+ };
903
+ var loadHtml3 = async (sourcePath, loader, options) => {
904
+ if (loader === "local") {
905
+ options.debug && console.debug(`Loading HTML from local file: ${sourcePath}`);
906
+ const content = await fs4.readFile(sourcePath, "utf-8");
907
+ options.debug && console.debug(`HTML loaded: ${content.slice(0, 100)}`);
908
+ return content;
909
+ }
910
+ if (loader === "fetch") {
911
+ options.debug && console.debug(`Loading HTML from URL: ${sourcePath}`);
912
+ const content = await loadHtml(sourcePath);
913
+ options.debug && console.debug(`HTML loaded: ${content.slice(0, 100)}`);
914
+ return content;
915
+ }
916
+ if (loader === "playwright") {
917
+ options.debug && console.debug(`Loading HTML from playwright: ${sourcePath}`);
918
+ const isPlaywrightAvailable = await checkPlaywrightAvailable();
919
+ options.debug && console.debug(`Playwright available: ${isPlaywrightAvailable}`);
920
+ if (!isPlaywrightAvailable) {
921
+ const message = [
922
+ "To use playwright loader, you need to install playwright.",
923
+ "You can install it by running:",
924
+ "",
925
+ ` ${pc.bold(`npx playwright@${playwrightPackageJson.version} install chromium`)} `,
926
+ "",
927
+ "Hint1: If you get warning like this:",
928
+ "WARNING: It looks like you are running 'npx playwright install' without first installing your project's dependencies. ",
929
+ `${pc.bold("Ignore this warning.")}`,
930
+ "",
931
+ "Hint2: If you get message like this:",
932
+ "Host system is missing dependencies to run browsers.",
933
+ "You should install dependencies by running:",
934
+ "",
935
+ ` ${pc.bold(`sudo npx playwright@${playwrightPackageJson.version} install-deps`)} `,
936
+ "",
937
+ pc.gray("Note: Good luck with that.")
938
+ ];
939
+ console.info(message.join("\n"));
940
+ throw new Error("Playwright is not available");
941
+ }
942
+ const content = await loadHtml2(sourcePath);
943
+ options.debug && console.debug(`HTML loaded: ${content.slice(0, 100)}`);
944
+ return content;
945
+ }
946
+ throw new Error(`Unsupported loader: ${loader}`);
947
+ };
948
+
949
+ // src/cli/commands/webforai/index.ts
1006
950
  var aiModeOptions = { linkAsText: true, tableAsText: true, hideImage: true };
1007
951
  var readabilityModeOptions = { linkAsText: false, tableAsText: false, hideImage: false };
1008
- var webforaiCommand = async (initialPath, initialOutputPath, options) => {
952
+ var webforaiCommand = async (initialPath, options) => {
953
+ intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
1009
954
  const sourcePath = initialPath ?? await inputSourcePath();
1010
- if (options.debug) {
1011
- console.debug(`sourcePath: ${sourcePath}`);
1012
- }
1013
- const loader = isUrl(sourcePath) ? { type: "web", loader: options.loader ?? await selectLoader() } : { type: "local", loader: null };
1014
- if (options.debug) {
1015
- console.debug(`loader: ${loader}`);
1016
- }
1017
- const outputPath = initialOutputPath ?? await inputOutputPath(sourcePath);
1018
- if (options.debug) {
1019
- console.debug(`outputPath: ${outputPath}`);
1020
- }
955
+ options.debug && console.debug(`sourcePath: ${sourcePath}`);
956
+ const loader = isUrl(sourcePath) ? options.loader ?? await selectLoader() : "local";
957
+ options.debug && console.debug(`loader: ${loader}`);
958
+ const outputPath = options.output ?? await inputOutputPath(sourcePath);
959
+ options.debug && console.debug(`outputPath: ${outputPath}`);
1021
960
  const mode = options.mode ?? await selectExtractMode();
1022
- if (options.debug) {
1023
- console.debug(`mode: ${mode}`);
1024
- }
1025
- const content = await loadHtml3(sourcePath, loader);
1026
- if (options.debug) {
1027
- console.debug(`content: ${content}`);
961
+ options.debug && console.debug(`mode: ${mode}`);
962
+ let html;
963
+ const s = spinner();
964
+ try {
965
+ s.start("Loading content...");
966
+ html = await loadHtml3(sourcePath, loader, { debug: options.debug });
967
+ s.stop(pc2.green("Content loaded!"));
968
+ } catch (error) {
969
+ s.stop(pc2.red("Content loading failed!"));
970
+ console.error(error);
971
+ process.exit(1);
1028
972
  }
1029
- const markdown = htmlToMarkdown(content, {
1030
- baseUrl: isUrl(sourcePath) ? sourcePath : options.baseUrl,
973
+ options.debug && console.debug(`html: ${html}`);
974
+ const markdown = htmlToMarkdown(html, {
975
+ baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
1031
976
  ...mode === "ai" ? aiModeOptions : readabilityModeOptions
1032
977
  });
1033
- if (options.debug) {
1034
- console.debug(`markdown: ${markdown}`);
1035
- }
978
+ options.debug && console.debug(`markdown: ${markdown}`);
1036
979
  const directory = path2.dirname(outputPath);
1037
- if (!fs5.existsSync(directory)) {
1038
- fs5.mkdirSync(directory, { recursive: true });
980
+ const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
981
+ if (!isDirectoryExists) {
982
+ await fs5.mkdir(directory, { recursive: true });
1039
983
  }
1040
- fs5.writeFileSync(outputPath, markdown);
1041
- outro("Markdown conversion is complete!");
984
+ await fs5.writeFile(outputPath, markdown);
985
+ outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
1042
986
  };
1043
987
 
1044
988
  // src/cli/bin.ts
1045
- program.name("webforai").description("CLI tool for ultra-precise HTML to Markdown conversion").version(package_default.version, "-v, --version", "output the current version").argument("[source]", "URL or path to process").argument("[outputPath]", "Path to output file or directory").option("--mode <mode>", `Processing mode (${MODES.join(", ")})`).option("--loader <loader>", `Loader to use (${LOADERS.join(", ")})`).option("--baseUrl <baseUrl>", "Base URL for relative links").option("-o, --stdout", "output to stdout").option("-d, --debug", "output extra debugging information").on("option:debug", () => {
1046
- process.env.DEBUG = "true";
1047
- }).action(webforaiCommand);
1048
- intro(pc2.inverse(`webforai CLI version ${package_default.version}`));
989
+ program.name("webforai").description("CLI tool for ultra-precise HTML to Markdown conversion").version(package_default.version, "-v, --version", "output the current version");
990
+ program.argument("[source]", "URL or path to process").option("-o, --output <output>", "Path to output file or directory").option("-m, --mode <mode>", `Processing mode (${MODES.join(", ")})`).option("-l, --loader <loader>", `Loader to use (${LOADERS.join(", ")})`).option("-d, --debug", "output extra debugging information").action(webforaiCommand);
991
+ intro2(pc3.inverse(`webforai CLI version ${package_default.version}`));
1049
992
  program.parse();