webforai 1.5.0 → 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 (97) hide show
  1. package/dist/bin.js +972 -213
  2. package/dist/cjs/cli/bin.js +33 -0
  3. package/dist/cjs/cli/commands/webforai/index.js +86 -0
  4. package/dist/cjs/cli/commands/webforai/loadHtml.js +93 -0
  5. package/dist/cjs/cli/constants.js +34 -0
  6. package/dist/cjs/cli/helpers/assertContinue.js +34 -0
  7. package/dist/cjs/cli/helpers/inputOutputPath.js +89 -0
  8. package/dist/cjs/cli/helpers/inputSourcePath.js +64 -0
  9. package/dist/cjs/cli/helpers/selectExtractMode.js +39 -0
  10. package/dist/cjs/cli/helpers/selectLoader.js +44 -0
  11. package/dist/cjs/cli/utils.js +89 -0
  12. package/dist/cjs/extract-hast/index.js +4 -3
  13. package/dist/cjs/extract-hast/readability.js +28 -61
  14. package/dist/cjs/extract-hast/strongly.js +2 -1
  15. package/dist/cjs/extract-hast/utils.js +0 -15
  16. package/dist/cjs/html-to-mdast.js +6 -6
  17. package/dist/cjs/link-replacer.js +10 -5
  18. package/dist/cjs/loaders/playwright.js +36 -7
  19. package/dist/cjs/mdast-handlers/custom-br-handler.js +31 -0
  20. package/dist/cjs/mdast-to-markdown.js +1 -1
  21. package/dist/cjs/utils/hast-utils.js +71 -0
  22. package/dist/cli/bin.js +11 -0
  23. package/dist/cli/commands/webforai/index.js +53 -0
  24. package/dist/cli/commands/webforai/loadHtml.js +60 -0
  25. package/dist/cli/constants.js +9 -0
  26. package/dist/cli/helpers/assertContinue.js +11 -0
  27. package/dist/cli/helpers/inputOutputPath.js +56 -0
  28. package/dist/cli/helpers/inputSourcePath.js +31 -0
  29. package/dist/cli/helpers/selectExtractMode.js +16 -0
  30. package/dist/cli/helpers/selectLoader.js +21 -0
  31. package/dist/cli/utils.js +52 -0
  32. package/dist/extract-hast/index.js +4 -3
  33. package/dist/extract-hast/readability.js +29 -62
  34. package/dist/extract-hast/strongly.js +2 -1
  35. package/dist/extract-hast/utils.js +0 -14
  36. package/dist/html-to-mdast.js +6 -6
  37. package/dist/link-replacer.js +10 -5
  38. package/dist/loaders/playwright.js +36 -7
  39. package/dist/mdast-handlers/custom-br-handler.js +8 -0
  40. package/dist/mdast-to-markdown.js +1 -1
  41. package/dist/types/cli/bin.js.map +1 -0
  42. package/dist/types/cli/commands/webforai/index.d.ts +6 -0
  43. package/dist/types/cli/commands/webforai/index.js.map +1 -0
  44. package/dist/types/cli/commands/webforai/loadHtml.d.ts +3 -0
  45. package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -0
  46. package/dist/types/cli/constants.d.ts +5 -0
  47. package/dist/types/cli/constants.js.map +1 -0
  48. package/dist/types/cli/helpers/assertContinue.d.ts +1 -0
  49. package/dist/types/cli/helpers/assertContinue.js.map +1 -0
  50. package/dist/types/cli/helpers/inputOutputPath.d.ts +1 -0
  51. package/dist/types/cli/helpers/inputOutputPath.js.map +1 -0
  52. package/dist/types/cli/helpers/inputSourcePath.d.ts +1 -0
  53. package/dist/types/cli/helpers/inputSourcePath.js.map +1 -0
  54. package/dist/types/cli/helpers/selectExtractMode.d.ts +1 -0
  55. package/dist/types/cli/helpers/selectExtractMode.js.map +1 -0
  56. package/dist/types/cli/helpers/selectLoader.d.ts +1 -0
  57. package/dist/types/cli/helpers/selectLoader.js.map +1 -0
  58. package/dist/types/cli/utils.d.ts +32 -0
  59. package/dist/types/cli/utils.js.map +1 -0
  60. package/dist/types/extract-hast/index.d.ts +9 -4
  61. package/dist/types/extract-hast/index.js.map +1 -1
  62. package/dist/types/extract-hast/readability.d.ts +2 -1
  63. package/dist/types/extract-hast/readability.js.map +1 -1
  64. package/dist/types/extract-hast/strongly.d.ts +2 -1
  65. package/dist/types/extract-hast/strongly.js.map +1 -1
  66. package/dist/types/extract-hast/utils.d.ts +0 -1
  67. package/dist/types/extract-hast/utils.js.map +1 -1
  68. package/dist/types/html-to-mdast.d.ts +4 -0
  69. package/dist/types/html-to-mdast.js.map +1 -1
  70. package/dist/types/link-replacer.d.ts +1 -1
  71. package/dist/types/link-replacer.js.map +1 -1
  72. package/dist/types/loaders/playwright.d.ts +8 -2
  73. package/dist/types/loaders/playwright.js.map +1 -1
  74. package/dist/types/mdast-handlers/custom-br-handler.d.ts +2 -0
  75. package/dist/types/mdast-handlers/custom-br-handler.js.map +1 -0
  76. package/dist/types/mdast-to-markdown.js.map +1 -1
  77. package/dist/types/utils/hast-utils.d.ts +4 -0
  78. package/dist/types/utils/hast-utils.js.map +1 -0
  79. package/dist/utils/hast-utils.js +46 -0
  80. package/package.json +18 -17
  81. package/dist/cjs/bin.js +0 -254
  82. package/dist/cjs/html-to-markdown.test.js +0 -146
  83. package/dist/cjs/link-replacer.test.js +0 -47
  84. package/dist/cjs/loaders/fetch.test.js +0 -9
  85. package/dist/cjs/loaders/playwright.test.js +0 -17
  86. package/dist/cjs/loaders/puppeteer.test.js +0 -17
  87. package/dist/cjs/utils/bin-utils.js +0 -124
  88. package/dist/html-to-markdown.test.js +0 -124
  89. package/dist/link-replacer.test.js +0 -47
  90. package/dist/loaders/fetch.test.js +0 -9
  91. package/dist/loaders/playwright.test.js +0 -17
  92. package/dist/loaders/puppeteer.test.js +0 -17
  93. package/dist/types/bin.js.map +0 -1
  94. package/dist/types/utils/bin-utils.d.ts +0 -5
  95. package/dist/types/utils/bin-utils.js.map +0 -1
  96. package/dist/utils/bin-utils.js +0 -87
  97. /package/dist/types/{bin.d.ts → cli/bin.d.ts} +0 -0
package/dist/bin.js CHANGED
@@ -1,233 +1,992 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/bin.ts
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
3
+ // src/cli/bin.ts
4
+ import { intro as intro2 } from "@clack/prompts";
7
5
  import { program } from "commander";
8
- import pc from "picocolors";
9
- import packageInfo from "../package.json" assert { type: "json" };
10
- import { htmlToMarkdown } from "./html-to-markdown.js";
11
- import { loadHtml as playwrightLoadHtml } from "./loaders/playwright.js";
12
- import { loadHtml as puppeteerLoadHtml } from "./loaders/puppeteer.js";
13
- import { changeFileExtension, getNextAvailableFilePath, isUrl, urlToFilename } from "./utils/bin-utils.js";
14
- var MODES = ["default", "ai"];
15
- var LOADERS = ["fetch", "playwright", "puppeteer"];
16
- var loadersHint = {
17
- fetch: "Fetch HTML content from the given URL",
18
- playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
19
- puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
6
+ import pc3 from "picocolors";
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "webforai",
11
+ version: "1.6.0",
12
+ description: "A library that provides a web interface for AI",
13
+ author: "inaridiy",
14
+ license: "MIT",
15
+ keywords: [
16
+ "web",
17
+ "ai",
18
+ "html",
19
+ "html2md",
20
+ "markdown",
21
+ "mdast",
22
+ "hast"
23
+ ],
24
+ repository: {
25
+ type: "git",
26
+ url: "https://github.com/inaridiy/webforai.git"
27
+ },
28
+ homepage: "https://github.com/inaridiy/webforai.git",
29
+ scripts: {
30
+ "copy:package.cjs.json": "pnpm ncp ./package.cjs.json ./dist/cjs/package.json && pnpm ncp ./package.cjs.json ./dist/types/package.json ",
31
+ clean: "rimraf dist",
32
+ build: "pnpm clean && tsx build.ts && pnpm copy:package.cjs.json",
33
+ test: 'echo "Error: no test specified"',
34
+ prerelease: "pnpm build",
35
+ release: "np"
36
+ },
37
+ files: [
38
+ "dist",
39
+ "!dist/types/**/*.js"
40
+ ],
41
+ main: "dist/cjs/index.js",
42
+ type: "module",
43
+ module: "dist/index.js",
44
+ types: "dist/types/index.d.ts",
45
+ bin: "dist/bin.js",
46
+ exports: {
47
+ ".": {
48
+ types: "./dist/types/index.d.ts",
49
+ import: "./dist/index.js",
50
+ require: "./dist/cjs/index.js"
51
+ },
52
+ "./types": {
53
+ types: "./dist/types/index.d.ts",
54
+ import: "./dist/index.js",
55
+ require: "./dist/cjs/index.js"
56
+ },
57
+ "./loaders/playwright": {
58
+ types: "./dist/types/loaders/playwright.d.ts",
59
+ import: "./dist/loaders/playwright.js",
60
+ require: "./dist/cjs/loaders/playwright.js"
61
+ },
62
+ "./loaders/fetch": {
63
+ types: "./dist/types/loaders/fetch.d.ts",
64
+ import: "./dist/loaders/fetch.js",
65
+ require: "./dist/cjs/loaders/fetch.js"
66
+ },
67
+ "./loaders/cf-puppeteer": {
68
+ types: "./dist/types/loaders/cf-puppeteer.d.ts",
69
+ import: "./dist/loaders/cf-puppeteer.js",
70
+ require: "./dist/cjs/loaders/cf-puppeteer.js"
71
+ },
72
+ "./loaders/puppeteer": {
73
+ types: "./dist/types/loaders/puppeteer.d.ts",
74
+ import: "./dist/loaders/puppeteer.js",
75
+ require: "./dist/cjs/loaders/puppeteer.js"
76
+ }
77
+ },
78
+ typesVersions: {
79
+ "*": {
80
+ types: [
81
+ "./dist/types/index.d.ts"
82
+ ],
83
+ "loaders/playwright": [
84
+ "./dist/types/loaders/playwright.d.ts"
85
+ ],
86
+ "loaders/cf-puppeteer": [
87
+ "./dist/types/loaders/cf-puppeteer.d.ts"
88
+ ],
89
+ "loaders/fetch": [
90
+ "./dist/types/loaders/fetch.d.ts"
91
+ ],
92
+ "loaders/puppeteer": [
93
+ "./dist/types/loaders/puppeteer.d.ts"
94
+ ]
95
+ }
96
+ },
97
+ peerDependencies: {
98
+ "@cloudflare/puppeteer": ">=0.0.6",
99
+ "playwright-core": ">=1.4",
100
+ puppeteer: ">=22"
101
+ },
102
+ peerDependenciesMeta: {
103
+ "@cloudflare/puppeteer": {
104
+ optional: true
105
+ },
106
+ "playwright-core": {
107
+ optional: false
108
+ },
109
+ puppeteer: {
110
+ optional: true
111
+ }
112
+ },
113
+ dependencies: {
114
+ "@clack/prompts": "^0.7.0",
115
+ commander: "^12.1.0",
116
+ "hast-util-from-html": "^2.0.3",
117
+ "hast-util-select": "^6.0.2",
118
+ "hast-util-to-html": "^9.0.3",
119
+ "hast-util-to-mdast": "^10.1.0",
120
+ "hast-util-to-string": "^3.0.0",
121
+ "hast-util-to-text": "^4.0.0",
122
+ "mathml-to-latex": "^1.4.1",
123
+ "mdast-util-gfm": "^3.0.0",
124
+ "mdast-util-math": "^3.0.0",
125
+ "mdast-util-to-markdown": "^2.1.0",
126
+ picocolors: "^1.0.1",
127
+ "trim-trailing-lines": "^2.1.0",
128
+ "unist-util-filter": "^5.0.1",
129
+ zx: "^8.1.5"
130
+ },
131
+ devDependencies: {
132
+ "@cloudflare/puppeteer": "^0.0.6",
133
+ "@tsconfig/recommended": "^1.0.3",
134
+ "fastest-levenshtein": "^1.0.16",
135
+ "@types/hast": "^3.0.2",
136
+ "@types/mdast": "^4.0.2",
137
+ "@types/node": "^20.14.10",
138
+ arg: "^5.0.2",
139
+ esbuild: "^0.19.11",
140
+ glob: "^10.3.10",
141
+ ncp: "^2.0.0",
142
+ np: "^9.2.0",
143
+ "playwright-core": "^1.40.1",
144
+ puppeteer: "^23.2.2",
145
+ rimraf: "^5.0.5",
146
+ tsx: "^4.19.1",
147
+ typescript: "^5.4.5"
148
+ }
20
149
  };
21
- var aiModeOptions = { linkAsText: true, tableAsText: true, hideImage: true };
22
- var readabilityModeOptions = { linkAsText: false, tableAsText: false, hideImage: false };
23
- program.name("webforai").description("CLI tool for ultra-precise HTML to Markdown conversion").version(packageInfo.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(", ")})`, MODES[0]).option("--loader <loader>", `Loader to use (${LOADERS.join(", ")})`, LOADERS[0]).option("--baseUrl <baseUrl>", "Base URL for relative links").option("-o, --stdout", "output to stdout").option("-d, --debug", "output extra debugging information").on("option:debug", () => {
24
- process.env.DEBUG = "true";
25
- }).action(
26
- async (source, outputPath, options) => {
27
- let finalSource;
28
- let sourceIsUrl;
29
- let finalOutputPath;
30
- let finalMode;
31
- let finalLoader = "fetch";
32
- const isOutputFile = !options.stdout;
33
- if (process.env.DEBUG) {
34
- console.info("Arguments and options:");
35
- console.table({
36
- source,
37
- outputPath,
38
- ...options
39
- });
40
- console.info("isOutputFile:", isOutputFile);
150
+
151
+ // src/cli/commands/webforai/index.ts
152
+ import fs5 from "node:fs/promises";
153
+ import path2 from "node:path";
154
+ import { intro, outro, spinner } from "@clack/prompts";
155
+ import pc2 from "picocolors";
156
+
157
+ // src/html-to-mdast.ts
158
+ import { fromHtml } from "hast-util-from-html";
159
+ import { toMdast } from "hast-util-to-mdast";
160
+
161
+ // src/extract-hast/readability.ts
162
+ import { select, selectAll } from "hast-util-select";
163
+ import { toString as hastToString } from "hast-util-to-string";
164
+ import { filter } from "unist-util-filter";
165
+
166
+ // src/extract-hast/utils.ts
167
+ var matchString = (element) => `${element.tagName} ${element.properties.id} ${classnames(element).join(" ")}`;
168
+ var classnames = (element) => {
169
+ if (Array.isArray(element.properties.className)) {
170
+ return element.properties.className;
171
+ }
172
+ return [];
173
+ };
174
+ var isStrInclude = (value, match) => {
175
+ if (typeof value === "string") {
176
+ return value.includes(match);
177
+ }
178
+ return false;
179
+ };
180
+
181
+ // src/extract-hast/readability.ts
182
+ var UNLIKELY_ROLES = ["menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog"];
183
+ var REGEXPS = {
184
+ hidden: /hidden|invisible|fallback-image/i,
185
+ byline: /byline|author|dateline|writtenby|p-author/i,
186
+ specialUnlikelyCandidates: /frb-|uls-menu|language-link/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,
188
+ okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code/i
189
+ };
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"];
192
+ var BASE_MINIMAL_LENGTH = {
193
+ ja: 200,
194
+ en: 500
195
+ };
196
+ var metadataFilter = (node) => {
197
+ return !(["comment", "doctype"].includes(node.type) || node.type === "element" && ["script", "style", "link", "meta", "noscript", "svg", "title"].includes(node.tagName));
198
+ };
199
+ var universalElementFilter = (node) => {
200
+ if (node.type !== "element") {
201
+ return true;
202
+ }
203
+ const element = node;
204
+ if (["aside", "nav"].includes(element.tagName)) {
205
+ return false;
206
+ }
207
+ if (["hidden", "aria-hidden"].some((key) => element.properties[key])) {
208
+ return false;
209
+ }
210
+ if (classnames(element).some((classname) => REGEXPS.hidden.test(classname))) {
211
+ return false;
212
+ }
213
+ if (element.tagName === "dialog") {
214
+ return false;
215
+ }
216
+ if (element.properties.role === "dialog" && element.properties["aria-modal"]) {
217
+ return false;
218
+ }
219
+ if (element.properties.rel === "author" && isStrInclude(element.properties.itemprop, "author")) {
220
+ return false;
221
+ }
222
+ if (REGEXPS.byline.test(matchString(element))) {
223
+ return false;
224
+ }
225
+ if (element.properties.role && UNLIKELY_ROLES.includes(element.properties.role)) {
226
+ return false;
227
+ }
228
+ return true;
229
+ };
230
+ var unlikelyElementFilter = (node) => {
231
+ if (node.type !== "element") {
232
+ return true;
233
+ }
234
+ const element = node;
235
+ if (["body", "article", "main", "section", "h1", "h2", "h3", "h4", "h5", "h6", "p"].includes(element.tagName)) {
236
+ return true;
237
+ }
238
+ const match = matchString(element);
239
+ if (REGEXPS.specialUnlikelyCandidates.test(match)) {
240
+ return false;
241
+ }
242
+ if (REGEXPS.unlikelyCandidates.test(match) && !REGEXPS.okMaybeItsaCandidate.test(match)) {
243
+ return false;
244
+ }
245
+ return true;
246
+ };
247
+ var removeEmptyFilter = (node, _lang) => {
248
+ if (node.type !== "element") {
249
+ return true;
250
+ }
251
+ const element = node;
252
+ if (PARAGRAPH_TAGS.includes(element.tagName)) {
253
+ return true;
254
+ }
255
+ if (element.tagName === "img" && !element.properties.src) {
256
+ return false;
257
+ }
258
+ return true;
259
+ };
260
+ var readabilityExtractHast = (params) => {
261
+ const { hast, lang = "en" } = params;
262
+ const body = select("body", hast) ?? hast;
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];
271
+ let minimalLength = lang in BASE_MINIMAL_LENGTH ? BASE_MINIMAL_LENGTH[lang] : 500;
272
+ if (baseText.length < minimalLength) {
273
+ minimalLength = Math.max(0, baseText.length - 200);
274
+ }
275
+ let extractedTree = baseTree;
276
+ let extractedText = baseText;
277
+ for (const selector of BODY_SELECTORS) {
278
+ const content = { type: "root", children: selectAll(selector, baseFilterd) };
279
+ const contentText = hastToString(content);
280
+ if (contentText.length < 25) {
281
+ continue;
282
+ }
283
+ const links = selectAll("a", content);
284
+ const linkText = links.map((link) => hastToString(link)).join("");
285
+ const linkDensity = linkText.length / contentText.length;
286
+ if (linkDensity > 0.4) {
287
+ continue;
288
+ }
289
+ if (contentText.length > minimalLength) {
290
+ extractedTree = content;
291
+ extractedText = contentText;
292
+ break;
293
+ }
294
+ }
295
+ const finalFilteredTree = filter(extractedTree, (node) => {
296
+ if (!removeEmptyFilter(node, lang)) {
297
+ return false;
298
+ }
299
+ if (!unlikelyElementFilter(node)) {
300
+ return false;
301
+ }
302
+ return true;
303
+ });
304
+ const finalTree = hastToString(finalFilteredTree).length > extractedText.length / 3 ? finalFilteredTree : extractedTree;
305
+ return finalTree;
306
+ };
307
+
308
+ // src/extract-hast/strongly.ts
309
+ import { select as select2 } from "hast-util-select";
310
+ import { filter as filter2 } from "unist-util-filter";
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;
312
+ var stronglyExtractHast = (params) => {
313
+ const { hast } = params;
314
+ const selectors = ["article", "#article", "#content", "main", "div.container"];
315
+ let selectedElement = hast;
316
+ for (const selector of selectors) {
317
+ const element = select2(selector, hast);
318
+ if (element) {
319
+ selectedElement = element;
320
+ break;
321
+ }
322
+ }
323
+ const removedElements = filter2(selectedElement, (node) => {
324
+ if (node.type === "comment") {
325
+ return false;
326
+ }
327
+ if (node.type !== "element") {
328
+ return true;
329
+ }
330
+ const element = node;
331
+ const elementName = `${element.tagName} ${element.properties.id} ${element.properties.className}`;
332
+ if (REMOVE_REGEX.test(elementName)) {
333
+ return false;
334
+ }
335
+ return true;
336
+ });
337
+ return removedElements;
338
+ };
339
+
340
+ // src/extract-hast/index.ts
341
+ var PRESET_EXTRACT_HAST = {
342
+ strongly: stronglyExtractHast,
343
+ readability: readabilityExtractHast
344
+ };
345
+ var DEFAULT_EXTRACT_HAST = ["readability"];
346
+ var extractHast = (params, extractors = DEFAULT_EXTRACT_HAST) => {
347
+ const { hast, lang } = params;
348
+ const _extractors = Array.isArray(extractors) ? extractors : [extractors];
349
+ const extracted = _extractors.reduce((acc, extractor) => {
350
+ if (extractor === false) {
351
+ return acc;
352
+ }
353
+ if (typeof extractor === "string" && extractor in PRESET_EXTRACT_HAST) {
354
+ return PRESET_EXTRACT_HAST[extractor]({ hast: acc, lang });
355
+ }
356
+ if (typeof extractor === "function") {
357
+ return extractor({ hast: acc, lang });
358
+ }
359
+ throw new Error(`Invalid extractor: ${extractor}`);
360
+ }, hast) || hast;
361
+ return extracted;
362
+ };
363
+
364
+ // src/extract-mdast.ts
365
+ import { filter as filter3 } from "unist-util-filter";
366
+ var DECLATION_TYPES = ["blockquote", "strong", "emphasis", "delete"];
367
+ var emptyDeclarationFilter = (node) => {
368
+ if (!DECLATION_TYPES.includes(node.type)) {
369
+ return true;
370
+ }
371
+ if (node.children.length === 0) {
372
+ return false;
373
+ }
374
+ return true;
375
+ };
376
+ var extractMdast = (node) => {
377
+ const extracted = filter3(node, (node2) => {
378
+ if (!emptyDeclarationFilter(node2)) {
379
+ return false;
380
+ }
381
+ return true;
382
+ });
383
+ return extracted;
384
+ };
385
+
386
+ // src/mdast-handlers/custom-a-handler.ts
387
+ import { defaultHandlers } from "hast-util-to-mdast";
388
+ import { toString as hastToString2 } from "hast-util-to-string";
389
+ var customAHandler = (options) => (state, node) => {
390
+ if (options?.asText) {
391
+ const text3 = hastToString2(node);
392
+ if (3 >= text3.length) {
393
+ return void 0;
394
+ }
395
+ const link2 = { type: "text", value: text3 };
396
+ state.patch(node, link2);
397
+ return link2;
398
+ }
399
+ const link = defaultHandlers.a(state, node);
400
+ if (link.children.length > 0) {
401
+ return link;
402
+ }
403
+ return void 0;
404
+ };
405
+
406
+ // src/mdast-handlers/custom-code-handler.ts
407
+ import { toText } from "hast-util-to-text";
408
+ import { trimTrailingLines } from "trim-trailing-lines";
409
+
410
+ // src/utils/detect-code-lang.ts
411
+ var languages = [
412
+ ["bash", [/#!(\/usr)?\/bin\/bash/g, 500], [/\b(if|elif|then|fi|echo)\b|\$/g, 10]],
413
+ ["html", [/<\/?[a-z-]+[^\n>]*>/g, 10], [/^\s+<!DOCTYPE\s+html/g, 500]],
414
+ ["http", [/^(GET|HEAD|POST|PUT|DELETE|PATCH|HTTP)\b/g, 500]],
415
+ [
416
+ "ts",
417
+ [/(import .* from|export \*|export const|const .* = await)/g, 400],
418
+ [
419
+ /\b(console|await|async|function|undefined|export|import|this|class|for|let|const|map|join|require|implements|interface|namespace)\b/g,
420
+ 10
421
+ ]
422
+ ],
423
+ [
424
+ "tsx",
425
+ [/(import .* from|export \*|export const)/g, 400],
426
+ [/\b(react|next|FC)\b/g, 200],
427
+ [
428
+ /\b(console|await|async|function|export|undefined|import|this|class|for|let|const|map|join|require|implements|interface)\b/g,
429
+ 10
430
+ ]
431
+ ],
432
+ ["py", [/\b(def|print|class|and|or|lambda)\b/g, 10]],
433
+ ["sql", [/\b(SELECT|INSERT|FROM)\b/g, 50]],
434
+ ["pl", [/#!(\/usr)?\/bin\/perl/g, 500], [/\b(use|print)\b|\$/g, 10]],
435
+ ["lua", [/#!(\/usr)?\/bin\/lua/g, 500]],
436
+ ["make", [/\b(ifneq|endif|if|elif|then|fi|echo|.PHONY|^[a-z]+ ?:$)\b|\$/gm, 10]],
437
+ ["uri", [/https?:|mailto:|tel:|ftp:/g, 30]],
438
+ ["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]],
439
+ ["diff", [/^[+><-]/gm, 10], [/^@@ ?[-+,0-9 ]+ ?@@/gm, 25]],
440
+ ["md", [/^(>|\t\*|\t\d+.)/gm, 10], [/\[.*\](.*)/g, 10]],
441
+ ["docker", [/^(FROM|ENTRYPOINT|RUN)/gm, 500]],
442
+ ["xml", [/<\/?[a-z-]+[^\n>]*>/g, 10], [/^<\?xml/g, 500]],
443
+ ["c", [/#include\b|\bprintf\s+\(/g, 100]],
444
+ ["rs", [/^\s+(use|fn|mut|match)\b/gm, 100]],
445
+ ["go", [/\b(func|fmt|package)\b/g, 100]],
446
+ ["java", [/^import\s+java/gm, 500]],
447
+ ["asm", [/^(section|global main|extern|\t(call|mov|ret))/gm, 100]],
448
+ ["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]],
449
+ ["json", [/\b(true|false|null|\{})\b|\"[^"]+\":/g, 10]],
450
+ ["yaml", [/^(\s+)?[a-z][a-z0-9]*:/gim, 10]]
451
+ ];
452
+ var detectLanguage = (code) => {
453
+ return languages.map(
454
+ ([lang, ...features]) => [lang, features.reduce((acc, [match, score]) => acc + [...code.matchAll(match)].length * score, 0)]
455
+ ).filter(([_, score]) => score > 20).sort((a, b) => b[1] - a[1])[0]?.[0] || "plain";
456
+ };
457
+
458
+ // src/mdast-handlers/custom-code-handler.ts
459
+ var LANGUAGE_MATCH_REGEX = [/language-(\w+)/, /highlight-source-(\w+)/, /CodeBlock--language-(\w+)/];
460
+ var customCodeHandler = (state, node) => {
461
+ const classNames = node.properties?.className || [];
462
+ const codeValue = trimTrailingLines(toText(node)).trim();
463
+ const classLang = classNames.map((className) => {
464
+ const match = LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find((match2) => match2);
465
+ return match?.[1];
466
+ }).find((className) => className);
467
+ const lang = classLang || detectLanguage(codeValue) || null;
468
+ const result = { type: "code", lang, meta: null, value: codeValue };
469
+ state.patch(node, result);
470
+ return result;
471
+ };
472
+
473
+ // src/mdast-handlers/custom-div-handler.ts
474
+ import { select as select3 } from "hast-util-select";
475
+ import { defaultHandlers as defaultHandlers2 } from "hast-util-to-mdast";
476
+ import { toString as hastToString3 } from "hast-util-to-string";
477
+ import { toText as toText2 } from "hast-util-to-text";
478
+ import { trimTrailingLines as trimTrailingLines2 } from "trim-trailing-lines";
479
+ var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
480
+ var CODE_FILENAME_SELECTORS = "[class*='fileName'],[class*='fileName'],[class*='title'],[class*='Title']";
481
+ var LANGUAGE_MATCH_REGEX2 = [/language-(\w+)/, /highlight-source-(\w+)/, /CodeBlock--language-(\w+)/];
482
+ var findRecursive = (array, condition, maxDepth = 3) => {
483
+ if (maxDepth <= 0) {
484
+ return null;
485
+ }
486
+ for (const value of array) {
487
+ const result = condition(value);
488
+ if (Array.isArray(result)) {
489
+ return findRecursive(result, condition, maxDepth - 1);
490
+ }
491
+ if (result) {
492
+ return value;
493
+ }
494
+ }
495
+ return null;
496
+ };
497
+ var customDivHandler = (state, node) => {
498
+ const classNames = Array.isArray(node.properties.className) ? node.properties.className : [];
499
+ const codeBlock = findRecursive(node.children, (child) => {
500
+ if (child.type !== "element") {
501
+ return false;
502
+ }
503
+ if (child.tagName === "pre") {
504
+ return true;
505
+ }
506
+ return child.children.filter((child2) => child2.type === "element");
507
+ });
508
+ if (codeBlock && classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
509
+ const codeBlockClassNames = codeBlock.type === "element" ? codeBlock.properties.className ?? [] : [];
510
+ const codeValue = trimTrailingLines2(toText2(codeBlock)).trim();
511
+ const filenameElement = select3(CODE_FILENAME_SELECTORS, node);
512
+ const fileLang = filenameElement ? hastToString3(filenameElement).match(/\.(\w+)$/)?.[1] : null;
513
+ const classLang = [...classNames, ...codeBlockClassNames].map((className) => {
514
+ const match = LANGUAGE_MATCH_REGEX2.map((regex) => className.match(regex)).find((match2) => match2);
515
+ return match?.[1];
516
+ }).find((className) => className);
517
+ const lang = fileLang || classLang || detectLanguage(codeValue) || null;
518
+ const result = { type: "code", lang, meta: null, value: codeValue };
519
+ state.patch(node, result);
520
+ return result;
521
+ }
522
+ return defaultHandlers2.div(state, node);
523
+ };
524
+
525
+ // src/mdast-handlers/custom-img-handler.ts
526
+ import { defaultHandlers as defaultHandlers3 } from "hast-util-to-mdast";
527
+ var customImgHandler = (options) => (state, node) => {
528
+ if (options?.hideImage) {
529
+ return void 0;
530
+ }
531
+ return defaultHandlers3.image(state, node);
532
+ };
533
+
534
+ // src/mdast-handlers/custom-table-handler.ts
535
+ import { defaultHandlers as defaultHandlers4 } from "hast-util-to-mdast";
536
+ import { toText as toText3 } from "hast-util-to-text";
537
+ var customTableHandler = (options) => (state, node) => {
538
+ if (options?.asText) {
539
+ const paragraph = { type: "paragraph", children: [{ type: "text", value: toText3(node) }] };
540
+ state.patch(node, paragraph);
541
+ return paragraph;
542
+ }
543
+ return defaultHandlers4.table(state, node);
544
+ };
545
+
546
+ // src/mdast-handlers/math-handler.ts
547
+ import { toHtml } from "hast-util-to-html";
548
+ import { MathMLToLaTeX } from "mathml-to-latex";
549
+ var mathHandler = (state, node) => {
550
+ const mathMl = toHtml(node);
551
+ const latex = MathMLToLaTeX.convert(mathMl);
552
+ const result = { type: "inlineMath", value: latex };
553
+ state.patch(node, result);
554
+ return result;
555
+ };
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
+
599
+ // src/html-to-mdast.ts
600
+ var htmlToMdast = (htmlOrHast, options) => {
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);
605
+ const mdast = toMdast(extractedHast, {
606
+ handlers: {
607
+ math: mathHandler,
608
+ div: customDivHandler,
609
+ pre: customCodeHandler,
610
+ a: customAHandler({ asText: options?.linkAsText }),
611
+ img: customImgHandler({ hideImage: options?.hideImage }),
612
+ table: customTableHandler({ asText: options?.tableAsText })
613
+ }
614
+ });
615
+ const extractedMdast = extractMdast(mdast);
616
+ return extractedMdast;
617
+ };
618
+
619
+ // src/mdast-to-markdown.ts
620
+ import { gfmToMarkdown } from "mdast-util-gfm";
621
+ import { mathToMarkdown } from "mdast-util-math";
622
+ import { toMarkdown } from "mdast-util-to-markdown";
623
+
624
+ // src/link-replacer.ts
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;
41
630
  }
42
631
  try {
43
- finalSource = await text({
44
- message: "Enter the URL or html path to be converted to markdown:",
45
- placeholder: "https://example.com",
46
- initialValue: source,
47
- validate: (value) => {
48
- if (value.trim() === "") {
49
- return "Source is required";
50
- }
51
- if (!isUrl(value)) {
52
- if (!fs.existsSync(value)) {
53
- return "It appears that you are specifying a local file, but the file cannot be found. hint: when specifying a url, start with http or https.";
54
- }
55
- if (fs.statSync(value).isDirectory()) {
56
- return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
57
- }
58
- }
59
- }
60
- });
61
- if (isCancel(finalSource)) {
62
- cancel("Canceled.");
63
- return;
64
- }
65
- sourceIsUrl = isUrl(finalSource);
66
- if (sourceIsUrl) {
67
- finalLoader = await select({
68
- message: "Select loader:",
69
- options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" })),
70
- initialValue: options.loader && LOADERS.includes(options.loader) ? options.loader : "fetch"
71
- });
72
- if (isCancel(finalLoader)) {
73
- cancel("Canceled.");
74
- return;
75
- }
76
- }
77
- if (isOutputFile) {
78
- finalOutputPath = await text({
79
- message: "Enter the output file path:",
80
- placeholder: "output.md",
81
- initialValue: outputPath ? outputPath : sourceIsUrl ? `${urlToFilename(finalSource)}.md` : changeFileExtension(finalSource, "md"),
82
- validate: (value) => {
83
- if (value.trim() === "") {
84
- return "Output path is required";
85
- }
86
- if (fs.existsSync(value) && fs.statSync(value).isDirectory()) {
87
- return "No directory can be specified.";
88
- }
89
- if (!fs.existsSync(value) && value.endsWith("/")) {
90
- return "No directory can be specified.";
91
- }
92
- }
93
- });
94
- if (isCancel(finalOutputPath)) {
95
- cancel("Canceled.");
96
- return;
97
- }
98
- if (fs.existsSync(finalOutputPath)) {
99
- const isOutputFileOverwrite = await confirm({
100
- message: "The file already exists. Overwrite?",
101
- initialValue: false
102
- });
103
- if (isCancel(isOutputFileOverwrite)) {
104
- cancel("Canceled.");
105
- return;
106
- }
107
- if (isOutputFileOverwrite === false) {
108
- finalOutputPath = await text({
109
- message: "Enter the output file path:",
110
- placeholder: "output.md",
111
- initialValue: getNextAvailableFilePath(finalOutputPath),
112
- validate: (value) => {
113
- if (value.trim() === "") {
114
- return "Output path is required";
115
- }
116
- if (fs.existsSync(value)) {
117
- return "The file already exists";
118
- }
119
- if (!fs.existsSync(value) && value.endsWith("/")) {
120
- return "No directory can be specified.";
121
- }
122
- }
123
- });
124
- if (isCancel(finalOutputPath)) {
125
- cancel("Canceled.");
126
- return;
127
- }
128
- }
129
- }
130
- } else {
131
- finalOutputPath = "";
632
+ const absoluteUrl = new URL(url, base).href;
633
+ return `${pre}${absoluteUrl}${post}`;
634
+ } catch {
635
+ return match;
636
+ }
637
+ });
638
+ };
639
+
640
+ // src/utils/mdast-utils.ts
641
+ var warpRoot = (mdast) => {
642
+ if (Array.isArray(mdast)) {
643
+ return { type: "root", children: mdast };
644
+ }
645
+ return mdast;
646
+ };
647
+
648
+ // src/mdast-to-markdown.ts
649
+ var DEFAULT_MDAST_TO_MARKDOWN_OPTIONS = {
650
+ extensions: [gfmToMarkdown(), mathToMarkdown()],
651
+ bullet: "-"
652
+ };
653
+ var mdastToMarkdown = (mdast, options) => {
654
+ const { baseUrl, ...toMarkdownOptions } = { ...DEFAULT_MDAST_TO_MARKDOWN_OPTIONS, ...options };
655
+ let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions).replace(/\*\*\*\*/g, "");
656
+ if (baseUrl) {
657
+ markdown = linkReplacer(markdown, baseUrl);
658
+ }
659
+ return markdown;
660
+ };
661
+
662
+ // src/html-to-markdown.ts
663
+ var htmlToMarkdown = (htmlOrHast, options) => {
664
+ const { baseUrl, formatting: toMarkdownOptions, ...toMdastOptions } = options || {};
665
+ const mdast = htmlToMdast(htmlOrHast, toMdastOptions);
666
+ const markdown = mdastToMarkdown(mdast, { baseUrl, ...toMarkdownOptions });
667
+ return markdown;
668
+ };
669
+
670
+ // src/cli/helpers/inputOutputPath.ts
671
+ import fs2 from "node:fs";
672
+ import { confirm, text } from "@clack/prompts";
673
+
674
+ // src/cli/utils.ts
675
+ import fs from "node:fs";
676
+ import path from "node:path";
677
+ var isUrl = (maybeUrl) => {
678
+ try {
679
+ new URL(maybeUrl);
680
+ return true;
681
+ } catch {
682
+ return false;
683
+ }
684
+ };
685
+ function changeFileExtension(filePath, newExtension) {
686
+ const parsed = path.parse(filePath);
687
+ const ext = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
688
+ return path.format({ ...parsed, ext });
689
+ }
690
+ function urlToFilename(url) {
691
+ try {
692
+ const urlObj = new URL(url);
693
+ const hostname = urlObj.hostname.startsWith("www.") ? urlObj.hostname.slice(4) : urlObj.hostname;
694
+ const domainParts = hostname.split(".").slice(-2).join("-");
695
+ const pathParts = urlObj.pathname.split("/").filter(Boolean);
696
+ const relevantPathParts = pathParts.slice(-2).map(decodeURIComponent).join("-");
697
+ let filename = [domainParts, relevantPathParts].filter(Boolean).join("-");
698
+ filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
699
+ return filename || "output";
700
+ } catch {
701
+ return "output";
702
+ }
703
+ }
704
+ var sourcePathToOutputPath = (sourcePath) => isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
705
+ function getNextAvailableFilePath(filePath) {
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)) {
714
+ counter++;
715
+ newPath = path.join(dir, `${baseName}_${counter}${ext}`);
716
+ }
717
+ return newPath;
718
+ }
719
+
720
+ // src/cli/helpers/assertContinue.ts
721
+ import { cancel, isCancel } from "@clack/prompts";
722
+ function assertContinue(message, cancelMessage = "Canceled.") {
723
+ if (isCancel(message)) {
724
+ cancel(cancelMessage);
725
+ process.exit(1);
726
+ }
727
+ }
728
+
729
+ // src/cli/helpers/inputOutputPath.ts
730
+ var inputOutputPath = async (sourcePath) => {
731
+ const outputPath = await text({
732
+ message: "Enter the output file path:",
733
+ placeholder: "output.md",
734
+ initialValue: sourcePathToOutputPath(sourcePath),
735
+ validate: (value) => {
736
+ if (value.trim() === "") {
737
+ return "Output path is required";
132
738
  }
133
- finalMode = await select({
134
- message: "Select processing mode:",
135
- options: MODES.map((mode) => ({ value: mode, label: mode })),
136
- initialValue: options.mode && MODES.includes(options.mode) ? options.mode : "default"
137
- });
138
- if (isCancel(finalMode)) {
139
- cancel("Canceled.");
140
- return;
739
+ if (fs2.existsSync(value) && fs2.statSync(value).isDirectory()) {
740
+ return "No directory can be specified.";
141
741
  }
142
- } catch (e) {
143
- const error = e;
144
- console.error(pc.red("An error occurred while processing the given value:"), error.message);
145
- if (process.env.DEBUG) {
146
- console.error(error.stack);
742
+ if (!fs2.existsSync(value) && value.endsWith("/")) {
743
+ return "No directory can be specified.";
147
744
  }
148
- process.exit(1);
149
745
  }
150
- if (process.env.DEBUG) {
151
- console.info("Processing with the following options:");
152
- console.table({ finalSource, sourceIsUrl, isOutputFile, finalOutputPath, finalMode, finalLoader });
153
- }
154
- let content;
155
- const loadSpinner = spinner();
156
- try {
157
- if (sourceIsUrl) {
158
- loadSpinner.start("Downloading content.");
159
- if (finalLoader === "playwright") {
160
- content = await playwrightLoadHtml(finalSource);
161
- } else if (finalLoader === "puppeteer") {
162
- content = await puppeteerLoadHtml(finalSource);
163
- } else {
164
- const response = await fetch(finalSource);
165
- content = await response.text();
166
- }
167
- loadSpinner.stop("Content download is complete!");
168
- } else {
169
- loadSpinner.start("Retrieving content.");
170
- if (!fs.existsSync(finalSource)) {
171
- console.error(pc.red("The file does not exist"));
172
- process.exit(1);
173
- }
174
- const stats = fs.statSync(finalSource);
175
- if (!stats.isFile()) {
176
- console.error(pc.red("The source must be a file"));
177
- process.exit(1);
178
- }
179
- content = fs.readFileSync(finalSource, "utf-8");
180
- loadSpinner.stop("Content retrieval is complete!");
746
+ });
747
+ assertContinue(outputPath);
748
+ if (!fs2.existsSync(outputPath)) {
749
+ return outputPath;
750
+ }
751
+ const isOutputFileOverwrite = await confirm({
752
+ message: "The file already exists. Overwrite?",
753
+ initialValue: false
754
+ });
755
+ assertContinue(isOutputFileOverwrite);
756
+ if (isOutputFileOverwrite) {
757
+ return outputPath;
758
+ }
759
+ const escapedOutputPath = await text({
760
+ message: "Enter the output file path:",
761
+ placeholder: "output.md",
762
+ initialValue: getNextAvailableFilePath(outputPath),
763
+ validate: (value) => {
764
+ if (value.trim() === "") {
765
+ return "Output path is required";
181
766
  }
182
- } catch (e) {
183
- const error = e;
184
- loadSpinner.stop(pc.red("An error occurred while retrieving content"));
185
- console.error(error.message);
186
- if (process.env.DEBUG) {
187
- console.error(error.stack);
767
+ if (fs2.existsSync(value)) {
768
+ return "The file already exists";
769
+ }
770
+ if (!fs2.existsSync(value) && value.endsWith("/")) {
771
+ return "No directory can be specified.";
188
772
  }
189
- process.exit(1);
190
- }
191
- if (process.env.DEBUG) {
192
- console.info("Content:");
193
- console.info(content);
194
773
  }
195
- const convertSpinner = spinner();
196
- try {
197
- convertSpinner.start("Converting content to markdown.");
198
- const markdown = htmlToMarkdown(content, {
199
- ...sourceIsUrl || !!options.baseUrl && options.baseUrl.trim() !== "" ? { baseUrl: options.baseUrl || finalSource } : {},
200
- ...finalMode === "ai" ? aiModeOptions : readabilityModeOptions
201
- });
202
- if (process.env.DEBUG) {
203
- console.info("Markdown:");
204
- console.info(markdown);
774
+ });
775
+ assertContinue(escapedOutputPath);
776
+ return escapedOutputPath;
777
+ };
778
+
779
+ // src/cli/helpers/inputSourcePath.ts
780
+ import fs3 from "node:fs";
781
+ import { text as text2 } from "@clack/prompts";
782
+
783
+ // src/cli/constants.ts
784
+ var DEFAULT_PATH = "https://example.com";
785
+ var LOADERS = ["fetch", "playwright"];
786
+ var MODES = ["default", "ai"];
787
+
788
+ // src/cli/helpers/inputSourcePath.ts
789
+ var inputSourcePath = async () => {
790
+ const result = await text2({
791
+ message: "Enter the URL or html path to be converted to markdown:",
792
+ placeholder: DEFAULT_PATH,
793
+ initialValue: "",
794
+ validate: (value) => {
795
+ if (value.trim() === "") {
796
+ return "Source is required";
205
797
  }
206
- if (isOutputFile) {
207
- const directory = path.dirname(finalOutputPath);
208
- if (!fs.existsSync(directory)) {
209
- fs.mkdirSync(directory, { recursive: true });
798
+ if (!isUrl(value)) {
799
+ if (!fs3.existsSync(value)) {
800
+ return "It appears that you are specifying a local file, but the file cannot be found. hint: when specifying a url, start with http or https.";
801
+ }
802
+ if (fs3.statSync(value).isDirectory()) {
803
+ return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
210
804
  }
211
- fs.writeFileSync(finalOutputPath, markdown);
212
- convertSpinner.stop("Markdown conversion is complete!");
213
- outro(`Converted successfully to markdown! Output saved to: ${path.resolve(finalOutputPath)} \u{1F389}`);
214
- } else {
215
- convertSpinner.stop("Markdown conversion is complete!");
216
- outro("Converted successfully to markdown\u{1F389}");
217
- console.log(`
218
-
219
- ${markdown}`);
220
805
  }
221
- } catch (e) {
222
- const error = e;
223
- convertSpinner.stop(pc.red("An error occurred while converting and saving the markdown"));
224
- console.error(error.message);
225
- if (process.env.DEBUG) {
226
- console.error(error.stack);
806
+ }
807
+ });
808
+ assertContinue(result);
809
+ return result;
810
+ };
811
+
812
+ // src/cli/helpers/selectExtractMode.ts
813
+ import { select as select5 } from "@clack/prompts";
814
+ var selectExtractMode = async () => {
815
+ const result = await select5({
816
+ message: "Select processing mode:",
817
+ options: MODES.map((mode) => ({ value: mode, label: mode })),
818
+ initialValue: MODES[0]
819
+ });
820
+ assertContinue(result);
821
+ return result;
822
+ };
823
+
824
+ // src/cli/helpers/selectLoader.ts
825
+ import { select as select6 } from "@clack/prompts";
826
+ var loadersHint = {
827
+ fetch: "Fetch HTML content from the given URL",
828
+ playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
829
+ puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
830
+ };
831
+ var selectLoader = async () => {
832
+ const result = await select6({
833
+ message: "Select loader:",
834
+ initialValue: "fetch",
835
+ options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
836
+ });
837
+ assertContinue(result);
838
+ return result;
839
+ };
840
+
841
+ // src/cli/commands/webforai/loadHtml.ts
842
+ import fs4 from "node:fs/promises";
843
+ import pc from "picocolors";
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();
852
+ };
853
+
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();
227
882
  }
228
- process.exit(1);
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");
229
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
950
+ var aiModeOptions = { linkAsText: true, tableAsText: true, hideImage: true };
951
+ var readabilityModeOptions = { linkAsText: false, tableAsText: false, hideImage: false };
952
+ var webforaiCommand = async (initialPath, options) => {
953
+ intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
954
+ const sourcePath = initialPath ?? await inputSourcePath();
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}`);
960
+ const mode = options.mode ?? await selectExtractMode();
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);
230
972
  }
231
- );
232
- intro(pc.inverse(`webforai CLI version ${packageInfo.version}`));
973
+ options.debug && console.debug(`html: ${html}`);
974
+ const markdown = htmlToMarkdown(html, {
975
+ baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
976
+ ...mode === "ai" ? aiModeOptions : readabilityModeOptions
977
+ });
978
+ options.debug && console.debug(`markdown: ${markdown}`);
979
+ const directory = path2.dirname(outputPath);
980
+ const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
981
+ if (!isDirectoryExists) {
982
+ await fs5.mkdir(directory, { recursive: true });
983
+ }
984
+ await fs5.writeFile(outputPath, markdown);
985
+ outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
986
+ };
987
+
988
+ // src/cli/bin.ts
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}`));
233
992
  program.parse();