webforai
Version:
A library that provides a web interface for AI
1,013 lines (971 loc) • 35.7 kB
JavaScript
// src/cli/bin.ts
import { program } from "commander";
// package.json
var package_default = {
name: "webforai",
version: "2.1.1",
description: "A library that provides a web interface for AI",
author: "inaridiy",
license: "Apache-2.0",
keywords: [
"web",
"ai",
"html",
"html2md",
"markdown",
"mdast",
"hast"
],
repository: {
type: "git",
url: "https://github.com/inaridiy/webforai.git"
},
homepage: "https://webforai.dev/",
scripts: {
"copy:package.cjs.json": "pnpm ncp ./package.cjs.json ./dist/cjs/package.json && pnpm ncp ./package.cjs.json ./dist/types/package.json ",
clean: "rimraf dist",
build: "pnpm clean && tsx build.ts && pnpm copy:package.cjs.json",
typecheck: "tsc --noEmit",
prerelease: "pnpm build",
release: "np"
},
files: [
"dist",
"!dist/types/**/*.js"
],
main: "dist/cjs/index.js",
type: "module",
module: "dist/index.js",
types: "dist/types/index.d.ts",
bin: "dist/bin.js",
exports: {
".": {
types: "./dist/types/index.d.ts",
import: "./dist/index.js",
require: "./dist/cjs/index.js"
},
"./types": {
types: "./dist/types/index.d.ts",
import: "./dist/index.js",
require: "./dist/cjs/index.js"
},
"./loaders/playwright": {
types: "./dist/types/loaders/playwright.d.ts",
import: "./dist/loaders/playwright.js",
require: "./dist/cjs/loaders/playwright.js"
},
"./loaders/fetch": {
types: "./dist/types/loaders/fetch.d.ts",
import: "./dist/loaders/fetch.js",
require: "./dist/cjs/loaders/fetch.js"
},
"./loaders/cf-puppeteer": {
types: "./dist/types/loaders/cf-puppeteer.d.ts",
import: "./dist/loaders/cf-puppeteer.js",
require: "./dist/cjs/loaders/cf-puppeteer.js"
},
"./loaders/puppeteer": {
types: "./dist/types/loaders/puppeteer.d.ts",
import: "./dist/loaders/puppeteer.js",
require: "./dist/cjs/loaders/puppeteer.js"
}
},
typesVersions: {
"*": {
types: [
"./dist/types/index.d.ts"
],
"loaders/playwright": [
"./dist/types/loaders/playwright.d.ts"
],
"loaders/cf-puppeteer": [
"./dist/types/loaders/cf-puppeteer.d.ts"
],
"loaders/fetch": [
"./dist/types/loaders/fetch.d.ts"
],
"loaders/puppeteer": [
"./dist/types/loaders/puppeteer.d.ts"
]
}
},
peerDependencies: {
"@cloudflare/puppeteer": ">=0.0.6",
"playwright-core": ">=1.4",
puppeteer: ">=22"
},
peerDependenciesMeta: {
"@cloudflare/puppeteer": {
optional: true
},
"playwright-core": {
optional: false
},
puppeteer: {
optional: true
}
},
dependencies: {
"@clack/prompts": "^0.7.0",
boxen: "^8.0.1",
commander: "^12.1.0",
"hast-util-from-html": "^2.0.3",
"hast-util-select": "^6.0.2",
"hast-util-to-html": "^9.0.3",
"hast-util-to-mdast": "^10.1.0",
"hast-util-to-string": "^3.0.0",
"hast-util-to-text": "^4.0.0",
"mathml-to-latex": "^1.4.1",
"mdast-util-gfm": "^3.0.0",
"mdast-util-math": "^3.0.0",
"mdast-util-to-markdown": "^2.1.0",
picocolors: "^1.0.1",
"trim-trailing-lines": "^2.1.0",
"unist-util-filter": "^5.0.1",
zx: "^8.1.5"
},
devDependencies: {
"@cloudflare/puppeteer": "^0.0.6",
"@tsconfig/recommended": "^1.0.3",
"@types/hast": "^3.0.2",
"@types/mdast": "^4.0.2",
"@types/node": "^20.14.10",
arg: "^5.0.2",
esbuild: "^0.19.11",
"fastest-levenshtein": "^1.0.16",
glob: "^10.3.10",
ncp: "^2.0.0",
np: "^9.2.0",
"playwright-core": "^1.40.1",
puppeteer: "^23.2.2",
rimraf: "^5.0.5",
tsx: "^4.19.1",
typescript: "^5.4.5"
}
};
// src/cli/commands/webforai/index.ts
import fs5 from "node:fs/promises";
import path2 from "node:path";
import { intro, log as log2, outro, spinner } from "@clack/prompts";
import pc2 from "picocolors";
// src/html-to-mdast.ts
import { fromHtml } from "hast-util-from-html";
import { toMdast } from "hast-util-to-mdast";
// src/extract-mdast.ts
import { filter } from "unist-util-filter";
var DECLATION_TYPES = ["blockquote", "strong", "emphasis", "delete"];
var emptyDeclarationFilter = (node) => {
if (!DECLATION_TYPES.includes(node.type)) {
return true;
}
if (node.children.length === 0) {
return false;
}
return true;
};
var extractMdast = (node) => {
const extracted = filter(node, (node2) => {
if (!emptyDeclarationFilter(node2)) {
return false;
}
return true;
});
return extracted;
};
// src/extractors/presets/takumi.ts
import { select, selectAll } from "hast-util-select";
import { toString as hastToString } from "hast-util-to-string";
import { filter as filter2 } from "unist-util-filter";
// src/extractors/presets/utils.ts
var matchString = (element) => `${element.tagName} ${element.properties.id} ${classnames(element).join(" ")}`;
var classnames = (element) => {
if (Array.isArray(element.properties.className)) {
return element.properties.className;
}
return [];
};
var isStrInclude = (value, match) => {
if (typeof value === "string") {
return value.includes(match);
}
return false;
};
// src/extractors/presets/takumi.ts
var UNLIKELY_ROLES = ["menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog"];
var REGEXPS = {
hidden: /hidden|invisible|fallback-image/i,
byline: /byline|author|dateline|writtenby|p-author/i,
specialUnlikelyCandidates: /frb-|uls-menu|language-link/i,
unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|tooltip|disqus|extra|footer|gdpr|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote|speechify-ignore|avatar/i,
okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code/i
};
var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry"];
var PARAGRAPH_TAGS = ["a", "p", "div", "section", "article", "main", "ul", "ol", "li"];
var CONTENTABLE_TAGS = ["article", "main", "section", "h1", "h2", "h3", "h4", "h5", "h6", "p"];
var TEXTABLE_TAGS = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "ul"];
var BASE_MINIMAL_LENGTH = { ja: 200, en: 500 };
var metadataFilter = (node) => {
return !(["comment", "doctype"].includes(node.type) || node.type === "element" && ["script", "style", "link", "meta", "noscript", "svg", "title"].includes(node.tagName));
};
var universalElementFilter = (node) => {
if (node.type !== "element") {
return true;
}
const element = node;
if (["aside", "nav"].includes(element.tagName)) {
return false;
}
if (["hidden", "aria-hidden"].some((key) => element.properties[key])) {
return false;
}
if (classnames(element).some((classname) => REGEXPS.hidden.test(classname))) {
return false;
}
if (element.tagName === "dialog") {
return false;
}
if (element.properties.role === "dialog" && element.properties["aria-modal"]) {
return false;
}
if (element.properties.rel === "author" && isStrInclude(element.properties.itemprop, "author")) {
return false;
}
if (REGEXPS.byline.test(matchString(element))) {
return false;
}
if (element.properties.role && UNLIKELY_ROLES.includes(element.properties.role)) {
return false;
}
return true;
};
var unlikelyElementFilter = (node) => {
if (node.type !== "element") {
return true;
}
const element = node;
if (CONTENTABLE_TAGS.includes(element.tagName)) {
return true;
}
const match = matchString(element);
if (REGEXPS.specialUnlikelyCandidates.test(match)) {
return false;
}
if (REGEXPS.unlikelyCandidates.test(match) && !REGEXPS.okMaybeItsaCandidate.test(match)) {
return false;
}
return true;
};
var removeEmptyFilter = (node, _lang) => {
if (node.type !== "element") {
return true;
}
const element = node;
if (PARAGRAPH_TAGS.includes(element.tagName)) {
return true;
}
if (element.tagName === "img" && !element.properties.src) {
return false;
}
if (TEXTABLE_TAGS.includes(element.tagName) && hastToString(element).length === 0) {
return false;
}
return true;
};
var takumiExtractor = (params) => {
const { hast, lang = "en" } = params;
const body = select("body", hast) ?? hast;
const metadataFilteredHast = filter2(body, (node) => metadataFilter(node));
const metadataFilteredHastText = metadataFilteredHast && hastToString(metadataFilteredHast);
if (!(metadataFilteredHast && metadataFilteredHastText)) {
return body;
}
const baseFilterd = filter2(metadataFilteredHast, (node) => universalElementFilter(node));
const baseFilterdText = baseFilterd ? hastToString(baseFilterd) : "";
const [baseTree, baseText] = baseFilterdText.length > metadataFilteredHastText.length / 3 || baseFilterdText.length > 5e3 ? [baseFilterd, baseFilterdText] : [metadataFilteredHast, metadataFilteredHastText];
let minimalLength = lang in BASE_MINIMAL_LENGTH ? BASE_MINIMAL_LENGTH[lang] : 500;
if (baseText.length < minimalLength) {
minimalLength = Math.max(0, baseText.length - 200);
}
let extractedTree = baseTree;
let extractedText = baseText;
for (const selector of BODY_SELECTORS) {
const content = { type: "root", children: selectAll(selector, baseFilterd) };
const contentText = hastToString(content);
if (contentText.length < 25) {
continue;
}
const links = selectAll("a", content);
const linkText = links.map((link) => hastToString(link)).join("");
const linkDensity = linkText.length / contentText.length;
if (linkDensity > 0.4) {
continue;
}
if (contentText.length > minimalLength) {
extractedTree = content;
extractedText = contentText;
break;
}
}
const finalFilteredTree = filter2(extractedTree, (node) => {
if (!removeEmptyFilter(node, lang)) {
return false;
}
if (!unlikelyElementFilter(node)) {
return false;
}
return true;
});
const finalTree = hastToString(finalFilteredTree).length > extractedText.length / 3 ? finalFilteredTree : extractedTree;
return finalTree;
};
// src/constants.ts
var DEFAULT_EXTRACTORS = [takumiExtractor];
// src/extractors/pipeExtractors.ts
var pipeExtractors = (params, extractors = DEFAULT_EXTRACTORS) => {
const { hast, lang } = params;
const _extractors = Array.isArray(extractors) ? extractors : [extractors];
const extracted = _extractors.reduce((acc, extractor) => {
if (extractor === false) {
return acc;
}
if (typeof extractor === "function") {
return extractor({ hast: acc, lang });
}
throw new Error(`Invalid extractor: ${extractor}`);
}, hast) || hast;
return extracted;
};
// src/extractors/presets/minimal-filter.ts
import { select as select2 } from "hast-util-select";
import { toString as hastToString2 } from "hast-util-to-string";
import { filter as filter3 } from "unist-util-filter";
// src/mdast-handlers/custom-a-handler.ts
import { defaultHandlers } from "hast-util-to-mdast";
import { toString as hastToString3 } from "hast-util-to-string";
var customAHandler = (options) => (state, node) => {
if (options?.asText) {
const text3 = hastToString3(node);
if (3 >= text3.length) {
return void 0;
}
const link2 = { type: "text", value: text3 };
state.patch(node, link2);
return link2;
}
const link = defaultHandlers.a(state, node);
if (link.children.length > 0) {
return link;
}
return void 0;
};
// src/mdast-handlers/custom-code-handler.ts
import { toText } from "hast-util-to-text";
import { trimTrailingLines } from "trim-trailing-lines";
// src/utils/detect-code-lang.ts
var languages = [
["bash", [/#!(\/usr)?\/bin\/bash/g, 500], [/\b(if|elif|then|fi|echo)\b|\$/g, 10]],
["html", [/<\/?[a-z-]+[^\n>]*>/g, 10], [/^\s+<!DOCTYPE\s+html/g, 500]],
["http", [/^(GET|HEAD|POST|PUT|DELETE|PATCH|HTTP)\b/g, 500]],
[
"ts",
[/(import .* from|export \*|export const|const .* = await)/g, 400],
[
/\b(console|await|async|function|undefined|export|import|this|class|for|let|const|map|join|require|implements|interface|namespace)\b/g,
10
]
],
[
"tsx",
[/(import .* from|export \*|export const)/g, 400],
[/\b(react|next|FC)\b/g, 200],
[
/\b(console|await|async|function|export|undefined|import|this|class|for|let|const|map|join|require|implements|interface)\b/g,
10
]
],
["py", [/\b(def|print|class|and|or|lambda)\b/g, 10]],
["sql", [/\b(SELECT|INSERT|FROM)\b/g, 50]],
["pl", [/#!(\/usr)?\/bin\/perl/g, 500], [/\b(use|print)\b|\$/g, 10]],
["lua", [/#!(\/usr)?\/bin\/lua/g, 500]],
["make", [/\b(ifneq|endif|if|elif|then|fi|echo|.PHONY|^[a-z]+ ?:$)\b|\$/gm, 10]],
["uri", [/https?:|mailto:|tel:|ftp:/g, 30]],
["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]],
["diff", [/^[+><-]/gm, 10], [/^@@ ?[-+,0-9 ]+ ?@@/gm, 25]],
["md", [/^(>|\t\*|\t\d+.)/gm, 10], [/\[.*\](.*)/g, 10]],
["docker", [/^(FROM|ENTRYPOINT|RUN)/gm, 500]],
["xml", [/<\/?[a-z-]+[^\n>]*>/g, 10], [/^<\?xml/g, 500]],
["c", [/#include\b|\bprintf\s+\(/g, 100]],
["rs", [/^\s+(use|fn|mut|match)\b/gm, 100]],
["go", [/\b(func|fmt|package)\b/g, 100]],
["java", [/^import\s+java/gm, 500]],
["asm", [/^(section|global main|extern|\t(call|mov|ret))/gm, 100]],
["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]],
["json", [/\b(true|false|null|\{})\b|\"[^"]+\":/g, 10]],
["yaml", [/^(\s+)?[a-z][a-z0-9]*:/gim, 10]]
];
var detectLanguage = (code) => {
return languages.map(
([lang, ...features]) => [lang, features.reduce((acc, [match, score]) => acc + [...code.matchAll(match)].length * score, 0)]
).filter(([_, score]) => score > 20).sort((a, b) => b[1] - a[1])[0]?.[0] || "plain";
};
// src/mdast-handlers/custom-code-handler.ts
var LANGUAGE_MATCH_REGEX = [/language-(\w+)/, /highlight-source-(\w+)/, /CodeBlock--language-(\w+)/];
var customCodeHandler = (state, node) => {
const classNames = node.properties?.className || [];
const codeValue = trimTrailingLines(toText(node)).trim();
const classLang = classNames.map((className) => {
const match = LANGUAGE_MATCH_REGEX.map((regex) => className.match(regex)).find((match2) => match2);
return match?.[1];
}).find((className) => className);
const lang = classLang || detectLanguage(codeValue) || null;
const result = { type: "code", lang, meta: null, value: codeValue };
state.patch(node, result);
return result;
};
// src/mdast-handlers/custom-div-handler.ts
import { select as select3 } from "hast-util-select";
import { defaultHandlers as defaultHandlers2 } from "hast-util-to-mdast";
import { toString as hastToString4 } from "hast-util-to-string";
import { toText as toText2 } from "hast-util-to-text";
import { trimTrailingLines as trimTrailingLines2 } from "trim-trailing-lines";
var CODE_BLOCK_REGEX = /highlight-source|language-|codegroup|codeblock|code-block/i;
var CODE_FILENAME_SELECTORS = "[class*='fileName'],[class*='fileName'],[class*='title'],[class*='Title']";
var LANGUAGE_MATCH_REGEX2 = [/language-(\w+)/, /highlight-source-(\w+)/, /CodeBlock--language-(\w+)/];
var findRecursive = (array, condition, maxDepth = 3) => {
if (maxDepth <= 0) {
return null;
}
for (const value of array) {
const result = condition(value);
if (Array.isArray(result)) {
return findRecursive(result, condition, maxDepth - 1);
}
if (result) {
return value;
}
}
return null;
};
var customDivHandler = (state, node) => {
const classNames = Array.isArray(node.properties.className) ? node.properties.className : [];
const codeBlock = findRecursive(node.children, (child) => {
if (child.type !== "element") {
return false;
}
if (child.tagName === "pre") {
return true;
}
return child.children.filter((child2) => child2.type === "element");
});
if (codeBlock && classNames.some((className) => CODE_BLOCK_REGEX.test(className))) {
const codeBlockClassNames = codeBlock.type === "element" ? codeBlock.properties.className ?? [] : [];
const codeValue = trimTrailingLines2(toText2(codeBlock)).trim();
const filenameElement = select3(CODE_FILENAME_SELECTORS, node);
const fileLang = filenameElement ? hastToString4(filenameElement).match(/\.(\w+)$/)?.[1] : null;
const classLang = [...classNames, ...codeBlockClassNames].map((className) => {
const match = LANGUAGE_MATCH_REGEX2.map((regex) => className.match(regex)).find((match2) => match2);
return match?.[1];
}).find((className) => className);
const lang = fileLang || classLang || detectLanguage(codeValue) || null;
const result = { type: "code", lang, meta: null, value: codeValue };
state.patch(node, result);
return result;
}
return defaultHandlers2.div(state, node);
};
// src/mdast-handlers/custom-img-handler.ts
import { defaultHandlers as defaultHandlers3 } from "hast-util-to-mdast";
var customImgHandler = (options) => (state, node) => {
if (options?.hideImage) {
return void 0;
}
return defaultHandlers3.image(state, node);
};
// src/mdast-handlers/custom-table-handler.ts
import { defaultHandlers as defaultHandlers4 } from "hast-util-to-mdast";
import { toText as toText3 } from "hast-util-to-text";
var customTableHandler = (options) => (state, node) => {
if (options?.asText) {
const paragraph = { type: "paragraph", children: [{ type: "text", value: toText3(node) }] };
state.patch(node, paragraph);
return paragraph;
}
return defaultHandlers4.table(state, node);
};
// src/mdast-handlers/math-handler.ts
import { toHtml } from "hast-util-to-html";
import { MathMLToLaTeX } from "mathml-to-latex";
var mathHandler = (state, node) => {
const mathMl = toHtml(node);
const latex = MathMLToLaTeX.convert(mathMl);
const result = { type: "inlineMath", value: latex };
state.patch(node, result);
return result;
};
// src/utils/hast-utils.ts
import { select as select4, selectAll as selectAll2 } from "hast-util-select";
var getLangFromHast = (node) => {
const html = select4("html", node);
if (html && typeof html.properties.lang === "string") {
return html.properties.lang;
}
if (node.type !== "element") {
return;
}
const element = node;
if (element.tagName !== "html") {
return;
}
const langAttr = element.properties.lang || element.properties["xml:lang"];
if (langAttr) {
return langAttr;
}
return void 0;
};
var getLangFromStr = (str) => {
const match = str.match(/lang=["']([^"']+)["']/);
if (match) {
return match[1];
}
return void 0;
};
var getUrlFromHast = (node) => {
if (node.type !== "element") {
return void 0;
}
const metaTagAttributes = ["og:url", "twitter:url"];
const metaTags = selectAll2("meta", node);
for (const meta of metaTags) {
const property = meta.properties.property || meta.properties.name;
if (typeof property === "string" && metaTagAttributes.includes(property)) {
return typeof meta.properties.content === "string" ? meta.properties.content : void 0;
}
}
return void 0;
};
// src/html-to-mdast.ts
var htmlToMdast = (htmlOrHast, options) => {
const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
const url = defaultUrl || getUrlFromHast(hast);
const extractedHast = pipeExtractors({ hast, lang, url }, extractors);
const mdast = toMdast(extractedHast, {
handlers: {
math: mathHandler,
div: customDivHandler,
pre: customCodeHandler,
a: customAHandler({ asText: options?.linkAsText }),
img: customImgHandler({ hideImage: options?.hideImage }),
table: customTableHandler({ asText: options?.tableAsText })
}
});
const extractedMdast = extractMdast(mdast);
return extractedMdast;
};
// src/mdast-to-markdown.ts
import { gfmToMarkdown } from "mdast-util-gfm";
import { mathToMarkdown } from "mdast-util-math";
import { toMarkdown } from "mdast-util-to-markdown";
// src/link-replacer.ts
var linkReplacer = (markdown, base) => {
const regex = /(!?\[.*?\]\()([^)\s]+)(\))/g;
return markdown.replace(regex, (match, pre, url, post) => {
if (/^(https?:|#)/.test(url)) {
return match;
}
try {
const absoluteUrl = new URL(url, base).href;
return `${pre}${absoluteUrl}${post}`;
} catch {
return match;
}
});
};
// src/utils/mdast-utils.ts
var warpRoot = (mdast) => {
if (Array.isArray(mdast)) {
return { type: "root", children: mdast };
}
return mdast;
};
// src/mdast-to-markdown.ts
var DEFAULT_MDAST_TO_MARKDOWN_OPTIONS = {
extensions: [gfmToMarkdown(), mathToMarkdown()],
bullet: "-"
};
var mdastToMarkdown = (mdast, options) => {
const { baseUrl, ...toMarkdownOptions } = { ...DEFAULT_MDAST_TO_MARKDOWN_OPTIONS, ...options };
let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions).replace(/\*\*\*\*/g, "");
if (baseUrl) {
markdown = linkReplacer(markdown, baseUrl);
}
return markdown;
};
// src/html-to-markdown.ts
var htmlToMarkdown = (htmlOrHast, options) => {
const { baseUrl, formatting: toMarkdownOptions, ...toMdastOptions } = options || {};
const mdast = htmlToMdast(htmlOrHast, toMdastOptions);
const markdown = mdastToMarkdown(mdast, { baseUrl, ...toMarkdownOptions });
return markdown;
};
// src/cli/helpers/inputOutputPath.ts
import fs2 from "node:fs";
import { confirm, text } from "@clack/prompts";
// src/cli/utils.ts
import fs from "node:fs";
import path from "node:path";
var isUrl = (maybeUrl) => {
try {
new URL(maybeUrl);
return true;
} catch {
return false;
}
};
function changeFileExtension(filePath, newExtension) {
const parsedPath = filePath.split("/");
const fileName = parsedPath[parsedPath.length - 1];
const formattedNewExtension = newExtension.startsWith(".") ? newExtension : `.${newExtension}`;
if (fileName.startsWith(".")) {
const parts = fileName.split(".");
if (parts.length === 2) {
return parsedPath.slice(0, -1).concat(`${fileName}${formattedNewExtension}`).join("/");
}
parts[parts.length - 1] = newExtension.replace(/^\./, "");
return parsedPath.slice(0, -1).concat(parts.join(".")).join("/");
}
const lastDotIndex = fileName.lastIndexOf(".");
const baseName = lastDotIndex !== -1 ? fileName.slice(0, lastDotIndex) : fileName;
const newFileName = `${baseName}${formattedNewExtension}`;
parsedPath[parsedPath.length - 1] = newFileName;
return parsedPath.join("/");
}
function urlToFilename(url) {
try {
const urlObj = new URL(url);
const domainParts = urlObj.hostname.split(".").reverse().reduce((acc, part, index) => {
if (index === 0) {
return acc;
}
if (acc.length >= 2) {
return acc;
}
if (part === "www") {
return acc;
}
return [part, ...acc];
}, []);
const domainString = domainParts.reverse().join("-");
const pathParts = urlObj.pathname.split("/").filter(Boolean);
const relevantPathParts = pathParts.slice(-2);
const pathString = relevantPathParts.map((part) => decodeURIComponent(part)).join("-");
let filename = [domainString, pathString].filter(Boolean).join("-");
filename = filename.toLowerCase().replace(/[<>:"/\\|?*\x00-\x1F]/g, "").replace(/[\s.]+/g, "-").replace(/^-+|-+$/g, "");
return filename || "output";
} catch {
return "output";
}
}
var sourcePathToOutputPath = (sourcePath) => {
return isUrl(sourcePath) ? `${urlToFilename(sourcePath)}.md` : changeFileExtension(sourcePath, "md");
};
function getNextAvailableFilePath(filePath) {
const parsedPath = path.parse(filePath);
const directory = parsedPath.dir;
const fullName = parsedPath.base;
const [firstPart, ...restParts] = fullName.split(".");
const restName = restParts.length > 0 ? `.${restParts.join(".")}` : "";
const baseName = firstPart.replace(/_\d+$/, "");
let counter = 1;
let nextFilePath = filePath;
while (fs.existsSync(nextFilePath)) {
const match = firstPart.match(/_(\d+)$/);
if (match) {
counter = Number.parseInt(match[1], 10) + 1;
}
const newName = `${baseName}_${counter}${restName}`;
nextFilePath = path.join(directory, newName);
counter++;
}
return nextFilePath;
}
// src/cli/helpers/assertContinue.ts
import { cancel, isCancel } from "@clack/prompts";
function assertContinue(message, cancelMessage = "Canceled.") {
if (isCancel(message)) {
cancel(cancelMessage);
process.exit(1);
}
}
// src/cli/helpers/inputOutputPath.ts
var inputOutputPath = async (sourcePath) => {
const outputPath = await text({
message: "Enter the output file path:",
placeholder: "output.md",
initialValue: sourcePathToOutputPath(sourcePath),
validate: (value) => {
if (value.trim() === "") {
return "Output path is required";
}
if (fs2.existsSync(value) && fs2.statSync(value).isDirectory()) {
return "No directory can be specified.";
}
if (!fs2.existsSync(value) && value.endsWith("/")) {
return "No directory can be specified.";
}
}
});
assertContinue(outputPath);
if (!fs2.existsSync(outputPath)) {
return outputPath;
}
const isOutputFileOverwrite = await confirm({
message: "The file already exists. Overwrite?",
initialValue: false
});
assertContinue(isOutputFileOverwrite);
if (isOutputFileOverwrite) {
return outputPath;
}
const escapedOutputPath = await text({
message: "Enter the output file path:",
placeholder: "output.md",
initialValue: getNextAvailableFilePath(outputPath),
validate: (value) => {
if (value.trim() === "") {
return "Output path is required";
}
if (fs2.existsSync(value)) {
return "The file already exists";
}
if (!fs2.existsSync(value) && value.endsWith("/")) {
return "No directory can be specified.";
}
}
});
assertContinue(escapedOutputPath);
return escapedOutputPath;
};
// src/cli/helpers/inputSourcePath.ts
import fs3 from "node:fs";
import { text as text2 } from "@clack/prompts";
// src/cli/constants.ts
var DEFAULT_PATH = "https://example.com";
var LOADERS = ["fetch", "playwright"];
var MODES = ["default", "ai"];
// src/cli/helpers/inputSourcePath.ts
var inputSourcePath = async () => {
const result = await text2({
message: "Enter the URL or html path to be converted to markdown:",
placeholder: DEFAULT_PATH,
initialValue: "",
validate: (value) => {
if (value.trim() === "") {
return "Source is required";
}
if (!isUrl(value)) {
if (!fs3.existsSync(value)) {
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.";
}
if (fs3.statSync(value).isDirectory()) {
return "You are specifying a local file, but you cannot specify a directory. hint: when specifying a url, start with http or https.";
}
}
}
});
assertContinue(result);
return result;
};
// src/cli/helpers/selectExtractMode.ts
import { select as select5 } from "@clack/prompts";
var selectExtractMode = async () => {
const result = await select5({
message: "Select processing mode:",
options: MODES.map((mode) => ({ value: mode, label: mode })),
initialValue: MODES[0]
});
assertContinue(result);
return result;
};
// src/cli/helpers/selectLoader.ts
import { select as select6 } from "@clack/prompts";
var loadersHint = {
fetch: "Fetch HTML content from the given URL",
playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
};
var selectLoader = async () => {
const result = await select6({
message: "Select loader:",
initialValue: "fetch",
options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
});
assertContinue(result);
return result;
};
// src/cli/commands/webforai/loadHtml.ts
import fs4 from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { log } from "@clack/prompts";
import boxen from "boxen";
import pc from "picocolors";
import { chromium as chromium2 } from "playwright-core";
// src/loaders/fetch.ts
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";
var loadHtml = async (url, userAgent = USER_AGENT) => {
const response = await fetch(url, { headers: { "User-Agent": userAgent } });
return response.text();
};
// src/loaders/playwright.ts
import { chromium, devices } from "playwright-core";
var SUPER_BYPASS_DEVICE = {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
hasTouch: false,
isMobile: false,
javaScriptEnabled: true,
locale: "en-US",
timezoneId: "America/New_York"
};
var loadHtml2 = async (url, options) => {
const { browser, waitUntil, timeout, superBypassMode } = options ?? {};
const _browser = browser ?? await chromium.launch({ headless: true });
const context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
if (superBypassMode) {
await context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
});
}
const page = await context.newPage();
if (superBypassMode) {
await page.route("**/*.js", (route) => {
if (route.request().url().includes("captcha-delivery")) {
return route.abort();
}
return route.continue();
});
}
await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
await page.evaluate(() => {
const elements = document.querySelectorAll("*");
for (const element of elements) {
const rect = element.getBoundingClientRect();
element.setAttribute("data-rwidth", rect.width.toString());
element.setAttribute("data-rheight", rect.height.toString());
}
});
const html = await page.content();
await page.close();
if (browser) {
await context.close();
} else {
await _browser.close();
}
return html;
};
// src/cli/commands/webforai/loadHtml.ts
var checkPlaywrightAvailable = async () => {
const path3 = chromium2.executablePath();
try {
await fs4.access(path3);
return true;
} catch {
return false;
}
};
var getPlaywrightVersion = async () => {
const path3 = await import.meta.resolve("playwright-core/package.json");
const pwPackageJson = await fs4.readFile(fileURLToPath(path3), "utf-8").then((res) => JSON.parse(res.toString())).catch(() => null);
return pwPackageJson?.version;
};
var loadHtml3 = async (sourcePath, loader, options) => {
if (loader === "local") {
options.debug && log.info(`Loading HTML from local file: ${sourcePath}`);
const content = await fs4.readFile(sourcePath, "utf-8");
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
return content;
}
if (loader === "fetch") {
options.debug && log.info(`Loading HTML from URL: ${sourcePath}`);
const content = await loadHtml(sourcePath);
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
return content;
}
if (loader === "playwright") {
options.debug && log.info(`Loading HTML from playwright: ${sourcePath}`);
const isPlaywrightAvailable = await checkPlaywrightAvailable();
options.debug && log.info(`Playwright available: ${isPlaywrightAvailable}`);
const pwVersion = await getPlaywrightVersion();
if (!isPlaywrightAvailable) {
const message = [
pc.bold("Error: Playwright is not available"),
"",
"To use the Playwright loader, please install Playwright by running:",
"",
` npx playwright@${pwVersion} install chromium`,
"",
"Hint 1: If you receive a warning like this:",
` "WARNING: It looks like you are running 'npx playwright install' without first installing your project's dependencies."`,
"You can safely ignore this warning.",
"",
"Hint 2: If you encounter the following message:",
` "Host system is missing dependencies to run browsers."`,
"You should install the necessary dependencies by executing:",
"",
` sudo npx playwright@${pwVersion} install-deps`
];
log.error(boxen(message.join("\n"), { padding: 1, borderStyle: "round" }));
throw new Error("Playwright is not available");
}
const content = await loadHtml2(sourcePath);
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
return content;
}
throw new Error(`Unsupported loader: ${loader}`);
};
// src/cli/commands/webforai/index.ts
var aiModeOptions = { linkAsText: true, tableAsText: true, hideImage: true };
var readabilityModeOptions = { linkAsText: false, tableAsText: false, hideImage: false };
var webforaiCommand = async (initialPath, options) => {
intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
const sourcePath = initialPath ?? await inputSourcePath();
options.debug && log2.info(`sourcePath: ${sourcePath}`);
const loader = isUrl(sourcePath) ? options.loader ?? await selectLoader() : "local";
options.debug && log2.info(`loader: ${loader}`);
const outputPath = options.output ?? await inputOutputPath(sourcePath);
options.debug && log2.info(`outputPath: ${outputPath}`);
const mode = options.mode ?? await selectExtractMode();
options.debug && log2.info(`mode: ${mode}`);
let html;
const s = spinner();
try {
s.start("Loading content...");
html = await loadHtml3(sourcePath, loader, { debug: options.debug });
s.stop(pc2.green("Content loaded!"));
} catch (error) {
s.stop(pc2.red("Content loading failed!"));
console.error(error);
process.exit(1);
}
options.debug && log2.info(`html: ${html}`);
const markdown = htmlToMarkdown(html, {
baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
...mode === "ai" ? aiModeOptions : readabilityModeOptions
});
options.debug && log2.info(`markdown: ${markdown}`);
const directory = path2.dirname(outputPath);
const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
if (!isDirectoryExists) {
await fs5.mkdir(directory, { recursive: true });
}
await fs5.writeFile(outputPath, markdown);
outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
};
// src/cli/bin.ts
program.name("webforai").description("CLI tool for ultra-precise HTML to Markdown conversion").version(package_default.version, "-v, --version", "output the current version");
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);
program.parse();