webforai 1.5.1 → 1.6.1
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.
- package/dist/bin.js +258 -278
- package/dist/cjs/extract-hast/index.js +4 -3
- package/dist/cjs/extract-hast/readability.js +28 -61
- package/dist/cjs/extract-hast/strongly.js +2 -1
- package/dist/cjs/extract-hast/utils.js +0 -15
- package/dist/cjs/html-to-mdast.js +6 -6
- package/dist/cjs/link-replacer.js +10 -5
- package/dist/cjs/loaders/playwright.js +36 -7
- package/dist/cjs/{cli/constants.js → mdast-handlers/custom-br-handler.js} +9 -12
- package/dist/cjs/mdast-to-markdown.js +1 -1
- package/dist/cjs/utils/hast-utils.js +71 -0
- package/dist/extract-hast/index.js +4 -3
- package/dist/extract-hast/readability.js +29 -62
- package/dist/extract-hast/strongly.js +2 -1
- package/dist/extract-hast/utils.js +0 -14
- package/dist/html-to-mdast.js +6 -6
- package/dist/link-replacer.js +10 -5
- package/dist/loaders/playwright.js +36 -7
- package/dist/mdast-handlers/custom-br-handler.js +8 -0
- package/dist/mdast-to-markdown.js +1 -1
- package/dist/types/cli/bin.js.map +1 -1
- package/dist/types/cli/commands/webforai/index.d.ts +6 -0
- package/dist/types/cli/commands/webforai/index.js.map +1 -0
- package/dist/types/cli/commands/webforai/loadHtml.d.ts +3 -0
- package/dist/types/cli/commands/webforai/loadHtml.js.map +1 -0
- package/dist/types/extract-hast/index.d.ts +9 -4
- package/dist/types/extract-hast/index.js.map +1 -1
- package/dist/types/extract-hast/readability.d.ts +2 -1
- package/dist/types/extract-hast/readability.js.map +1 -1
- package/dist/types/extract-hast/strongly.d.ts +2 -1
- package/dist/types/extract-hast/strongly.js.map +1 -1
- package/dist/types/extract-hast/utils.d.ts +0 -1
- package/dist/types/extract-hast/utils.js.map +1 -1
- package/dist/types/html-to-mdast.d.ts +4 -0
- package/dist/types/html-to-mdast.js.map +1 -1
- package/dist/types/link-replacer.d.ts +1 -1
- package/dist/types/link-replacer.js.map +1 -1
- package/dist/types/loaders/playwright.d.ts +8 -2
- package/dist/types/loaders/playwright.js.map +1 -1
- package/dist/types/mdast-handlers/custom-br-handler.d.ts +2 -0
- package/dist/types/mdast-handlers/custom-br-handler.js.map +1 -0
- package/dist/types/mdast-to-markdown.js.map +1 -1
- package/dist/types/utils/hast-utils.d.ts +4 -0
- package/dist/types/utils/hast-utils.js.map +1 -0
- package/dist/utils/hast-utils.js +46 -0
- package/package.json +22 -10
- package/dist/cjs/cli/bin.js +0 -34
- package/dist/cjs/cli/commands/handlers/index.js +0 -84
- package/dist/cjs/cli/commands/handlers/loadHtml.js +0 -127
- package/dist/cjs/cli/helpers/assertContinue.js +0 -34
- package/dist/cjs/cli/helpers/inputOutputPath.js +0 -89
- package/dist/cjs/cli/helpers/inputSourcePath.js +0 -64
- package/dist/cjs/cli/helpers/selectExtractMode.js +0 -39
- package/dist/cjs/cli/helpers/selectLoader.js +0 -44
- package/dist/cjs/cli/utils.js +0 -122
- package/dist/cjs/html-to-markdown.test.js +0 -118
- package/dist/cjs/link-replacer.test.js +0 -47
- package/dist/cjs/loaders/fetch.test.js +0 -9
- package/dist/cjs/loaders/playwright.test.js +0 -17
- package/dist/cjs/loaders/puppeteer.test.js +0 -17
- package/dist/cli/bin.js +0 -12
- package/dist/cli/commands/handlers/index.js +0 -51
- package/dist/cli/commands/handlers/loadHtml.js +0 -94
- package/dist/cli/constants.js +0 -9
- package/dist/cli/helpers/assertContinue.js +0 -11
- package/dist/cli/helpers/inputOutputPath.js +0 -56
- package/dist/cli/helpers/inputSourcePath.js +0 -31
- package/dist/cli/helpers/selectExtractMode.js +0 -16
- package/dist/cli/helpers/selectLoader.js +0 -21
- package/dist/cli/utils.js +0 -85
- package/dist/html-to-markdown.test.js +0 -118
- package/dist/link-replacer.test.js +0 -47
- package/dist/loaders/fetch.test.js +0 -9
- package/dist/loaders/playwright.test.js +0 -17
- package/dist/loaders/puppeteer.test.js +0 -17
- package/dist/types/cli/commands/handlers/index.d.ts +0 -13
- package/dist/types/cli/commands/handlers/index.js.map +0 -1
- package/dist/types/cli/commands/handlers/loadHtml.d.ts +0 -2
- package/dist/types/cli/commands/handlers/loadHtml.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,64 +1,12 @@
|
|
|
1
|
-
|
|
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";
|
|
55
4
|
import { program } from "commander";
|
|
56
|
-
import pc2 from "picocolors";
|
|
57
5
|
|
|
58
6
|
// package.json
|
|
59
7
|
var package_default = {
|
|
60
8
|
name: "webforai",
|
|
61
|
-
version: "1.
|
|
9
|
+
version: "1.6.1",
|
|
62
10
|
description: "A library that provides a web interface for AI",
|
|
63
11
|
author: "inaridiy",
|
|
64
12
|
license: "MIT",
|
|
@@ -146,20 +94,31 @@ var package_default = {
|
|
|
146
94
|
},
|
|
147
95
|
peerDependencies: {
|
|
148
96
|
"@cloudflare/puppeteer": ">=0.0.6",
|
|
149
|
-
playwright: ">=1.4",
|
|
97
|
+
"playwright-core": ">=1.4",
|
|
150
98
|
puppeteer: ">=22"
|
|
151
99
|
},
|
|
100
|
+
peerDependenciesMeta: {
|
|
101
|
+
"@cloudflare/puppeteer": {
|
|
102
|
+
optional: true
|
|
103
|
+
},
|
|
104
|
+
"playwright-core": {
|
|
105
|
+
optional: false
|
|
106
|
+
},
|
|
107
|
+
puppeteer: {
|
|
108
|
+
optional: true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
152
111
|
dependencies: {
|
|
153
112
|
"@clack/prompts": "^0.7.0",
|
|
113
|
+
boxen: "^8.0.1",
|
|
154
114
|
commander: "^12.1.0",
|
|
155
|
-
"
|
|
156
|
-
"hast-util-
|
|
157
|
-
"hast-util-
|
|
158
|
-
"hast-util-to-html": "^9.0.0",
|
|
115
|
+
"hast-util-from-html": "^2.0.3",
|
|
116
|
+
"hast-util-select": "^6.0.2",
|
|
117
|
+
"hast-util-to-html": "^9.0.3",
|
|
159
118
|
"hast-util-to-mdast": "^10.1.0",
|
|
160
119
|
"hast-util-to-string": "^3.0.0",
|
|
161
120
|
"hast-util-to-text": "^4.0.0",
|
|
162
|
-
"mathml-to-latex": "^1.
|
|
121
|
+
"mathml-to-latex": "^1.4.1",
|
|
163
122
|
"mdast-util-gfm": "^3.0.0",
|
|
164
123
|
"mdast-util-math": "^3.0.0",
|
|
165
124
|
"mdast-util-to-markdown": "^2.1.0",
|
|
@@ -173,24 +132,26 @@ var package_default = {
|
|
|
173
132
|
"@tsconfig/recommended": "^1.0.3",
|
|
174
133
|
"@types/hast": "^3.0.2",
|
|
175
134
|
"@types/mdast": "^4.0.2",
|
|
176
|
-
"@types/node": "^20.
|
|
135
|
+
"@types/node": "^20.14.10",
|
|
177
136
|
arg: "^5.0.2",
|
|
178
137
|
esbuild: "^0.19.11",
|
|
138
|
+
"fastest-levenshtein": "^1.0.16",
|
|
179
139
|
glob: "^10.3.10",
|
|
180
140
|
ncp: "^2.0.0",
|
|
181
141
|
np: "^9.2.0",
|
|
182
|
-
playwright: "^1.40.1",
|
|
142
|
+
"playwright-core": "^1.40.1",
|
|
183
143
|
puppeteer: "^23.2.2",
|
|
184
144
|
rimraf: "^5.0.5",
|
|
185
|
-
tsx: "^4.
|
|
145
|
+
tsx: "^4.19.1",
|
|
186
146
|
typescript: "^5.4.5"
|
|
187
147
|
}
|
|
188
148
|
};
|
|
189
149
|
|
|
190
|
-
// src/cli/commands/
|
|
191
|
-
import fs5 from "node:fs";
|
|
150
|
+
// src/cli/commands/webforai/index.ts
|
|
151
|
+
import fs5 from "node:fs/promises";
|
|
192
152
|
import path2 from "node:path";
|
|
193
|
-
import { outro } from "@clack/prompts";
|
|
153
|
+
import { intro, log as log2, outro, spinner } from "@clack/prompts";
|
|
154
|
+
import pc2 from "picocolors";
|
|
194
155
|
|
|
195
156
|
// src/html-to-mdast.ts
|
|
196
157
|
import { fromHtml } from "hast-util-from-html";
|
|
@@ -215,19 +176,6 @@ var isStrInclude = (value, match) => {
|
|
|
215
176
|
}
|
|
216
177
|
return false;
|
|
217
178
|
};
|
|
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
179
|
|
|
232
180
|
// src/extract-hast/readability.ts
|
|
233
181
|
var UNLIKELY_ROLES = ["menu", "menubar", "complementary", "navigation", "alert", "alertdialog", "dialog"];
|
|
@@ -235,35 +183,24 @@ var REGEXPS = {
|
|
|
235
183
|
hidden: /hidden|invisible|fallback-image/i,
|
|
236
184
|
byline: /byline|author|dateline|writtenby|p-author/i,
|
|
237
185
|
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,
|
|
186
|
+
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
187
|
okMaybeItsaCandidate: /and|article|body|column|content|main|shadow|code/i
|
|
240
188
|
};
|
|
241
|
-
var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry"
|
|
242
|
-
var PARAGRAPH_TAGS = ["p", "div", "section", "article", "main", "ul", "ol", "li"];
|
|
189
|
+
var BODY_SELECTORS = ["article", "#article", ".article_body", ".article-body", "#content", ".entry"];
|
|
190
|
+
var PARAGRAPH_TAGS = ["a", "p", "div", "section", "article", "main", "ul", "ol", "li"];
|
|
243
191
|
var BASE_MINIMAL_LENGTH = {
|
|
244
192
|
ja: 200,
|
|
245
193
|
en: 500
|
|
246
194
|
};
|
|
247
|
-
var
|
|
248
|
-
|
|
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
|
-
}
|
|
195
|
+
var metadataFilter = (node) => {
|
|
196
|
+
return !(["comment", "doctype"].includes(node.type) || node.type === "element" && ["script", "style", "link", "meta", "noscript", "svg", "title"].includes(node.tagName));
|
|
259
197
|
};
|
|
260
|
-
var metadataFilter = (node) => !["comment", "doctype"].includes(node.type);
|
|
261
198
|
var universalElementFilter = (node) => {
|
|
262
199
|
if (node.type !== "element") {
|
|
263
200
|
return true;
|
|
264
201
|
}
|
|
265
202
|
const element = node;
|
|
266
|
-
if (["
|
|
203
|
+
if (["aside", "nav"].includes(element.tagName)) {
|
|
267
204
|
return false;
|
|
268
205
|
}
|
|
269
206
|
if (["hidden", "aria-hidden"].some((key) => element.properties[key])) {
|
|
@@ -294,10 +231,7 @@ var unlikelyElementFilter = (node) => {
|
|
|
294
231
|
return true;
|
|
295
232
|
}
|
|
296
233
|
const element = node;
|
|
297
|
-
if (["body", "article", "main", "section", "
|
|
298
|
-
return true;
|
|
299
|
-
}
|
|
300
|
-
if (hasAncestors(element, ["table", "code"], 3)) {
|
|
234
|
+
if (["body", "article", "main", "section", "h1", "h2", "h3", "h4", "h5", "h6", "p"].includes(element.tagName)) {
|
|
301
235
|
return true;
|
|
302
236
|
}
|
|
303
237
|
const match = matchString(element);
|
|
@@ -309,55 +243,36 @@ var unlikelyElementFilter = (node) => {
|
|
|
309
243
|
}
|
|
310
244
|
return true;
|
|
311
245
|
};
|
|
312
|
-
var
|
|
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) => {
|
|
246
|
+
var removeEmptyFilter = (node, _lang) => {
|
|
323
247
|
if (node.type !== "element") {
|
|
324
248
|
return true;
|
|
325
249
|
}
|
|
326
250
|
const element = node;
|
|
327
|
-
if (
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
if (isImageLink(element)) {
|
|
251
|
+
if (PARAGRAPH_TAGS.includes(element.tagName)) {
|
|
331
252
|
return true;
|
|
332
253
|
}
|
|
333
|
-
|
|
334
|
-
if (text3.length < 10) {
|
|
254
|
+
if (element.tagName === "img" && !element.properties.src) {
|
|
335
255
|
return false;
|
|
336
256
|
}
|
|
337
257
|
return true;
|
|
338
258
|
};
|
|
339
|
-
var readabilityExtractHast = (
|
|
340
|
-
const lang =
|
|
259
|
+
var readabilityExtractHast = (params) => {
|
|
260
|
+
const { hast, lang = "en" } = params;
|
|
341
261
|
const body = select("body", hast) ?? hast;
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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];
|
|
262
|
+
const metadataFilteredHast = filter(body, (node) => metadataFilter(node));
|
|
263
|
+
const metadataFilteredHastText = metadataFilteredHast && hastToString(metadataFilteredHast);
|
|
264
|
+
if (!(metadataFilteredHast && metadataFilteredHastText)) {
|
|
265
|
+
return body;
|
|
266
|
+
}
|
|
267
|
+
const baseFilterd = filter(metadataFilteredHast, (node) => universalElementFilter(node));
|
|
268
|
+
const baseFilterdText = baseFilterd ? hastToString(baseFilterd) : "";
|
|
269
|
+
const [baseTree, baseText] = baseFilterdText.length > metadataFilteredHastText.length / 3 || baseFilterdText.length > 5e3 ? [baseFilterd, baseFilterdText] : [metadataFilteredHast, metadataFilteredHastText];
|
|
357
270
|
let minimalLength = lang in BASE_MINIMAL_LENGTH ? BASE_MINIMAL_LENGTH[lang] : 500;
|
|
358
271
|
if (baseText.length < minimalLength) {
|
|
359
272
|
minimalLength = Math.max(0, baseText.length - 200);
|
|
360
273
|
}
|
|
274
|
+
let extractedTree = baseTree;
|
|
275
|
+
let extractedText = baseText;
|
|
361
276
|
for (const selector of BODY_SELECTORS) {
|
|
362
277
|
const content = { type: "root", children: selectAll(selector, baseFilterd) };
|
|
363
278
|
const contentText = hastToString(content);
|
|
@@ -366,18 +281,18 @@ var readabilityExtractHast = (hast) => {
|
|
|
366
281
|
}
|
|
367
282
|
const links = selectAll("a", content);
|
|
368
283
|
const linkText = links.map((link) => hastToString(link)).join("");
|
|
369
|
-
const linkDensity = linkText.length /
|
|
284
|
+
const linkDensity = linkText.length / contentText.length;
|
|
370
285
|
if (linkDensity > 0.4) {
|
|
371
286
|
continue;
|
|
372
287
|
}
|
|
373
288
|
if (contentText.length > minimalLength) {
|
|
374
|
-
|
|
375
|
-
|
|
289
|
+
extractedTree = content;
|
|
290
|
+
extractedText = contentText;
|
|
376
291
|
break;
|
|
377
292
|
}
|
|
378
293
|
}
|
|
379
|
-
const finalFilteredTree = filter(
|
|
380
|
-
if (!removeEmptyFilter(node)) {
|
|
294
|
+
const finalFilteredTree = filter(extractedTree, (node) => {
|
|
295
|
+
if (!removeEmptyFilter(node, lang)) {
|
|
381
296
|
return false;
|
|
382
297
|
}
|
|
383
298
|
if (!unlikelyElementFilter(node)) {
|
|
@@ -385,7 +300,7 @@ var readabilityExtractHast = (hast) => {
|
|
|
385
300
|
}
|
|
386
301
|
return true;
|
|
387
302
|
});
|
|
388
|
-
const finalTree = hastToString(finalFilteredTree).length >
|
|
303
|
+
const finalTree = hastToString(finalFilteredTree).length > extractedText.length / 3 ? finalFilteredTree : extractedTree;
|
|
389
304
|
return finalTree;
|
|
390
305
|
};
|
|
391
306
|
|
|
@@ -393,7 +308,8 @@ var readabilityExtractHast = (hast) => {
|
|
|
393
308
|
import { select as select2 } from "hast-util-select";
|
|
394
309
|
import { filter as filter2 } from "unist-util-filter";
|
|
395
310
|
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 = (
|
|
311
|
+
var stronglyExtractHast = (params) => {
|
|
312
|
+
const { hast } = params;
|
|
397
313
|
const selectors = ["article", "#article", "#content", "main", "div.container"];
|
|
398
314
|
let selectedElement = hast;
|
|
399
315
|
for (const selector of selectors) {
|
|
@@ -426,17 +342,18 @@ var PRESET_EXTRACT_HAST = {
|
|
|
426
342
|
readability: readabilityExtractHast
|
|
427
343
|
};
|
|
428
344
|
var DEFAULT_EXTRACT_HAST = ["readability"];
|
|
429
|
-
var extractHast = (
|
|
345
|
+
var extractHast = (params, extractors = DEFAULT_EXTRACT_HAST) => {
|
|
346
|
+
const { hast, lang } = params;
|
|
430
347
|
const _extractors = Array.isArray(extractors) ? extractors : [extractors];
|
|
431
348
|
const extracted = _extractors.reduce((acc, extractor) => {
|
|
432
349
|
if (extractor === false) {
|
|
433
350
|
return acc;
|
|
434
351
|
}
|
|
435
352
|
if (typeof extractor === "string" && extractor in PRESET_EXTRACT_HAST) {
|
|
436
|
-
return PRESET_EXTRACT_HAST[extractor](acc);
|
|
353
|
+
return PRESET_EXTRACT_HAST[extractor]({ hast: acc, lang });
|
|
437
354
|
}
|
|
438
355
|
if (typeof extractor === "function") {
|
|
439
|
-
return extractor(acc);
|
|
356
|
+
return extractor({ hast: acc, lang });
|
|
440
357
|
}
|
|
441
358
|
throw new Error(`Invalid extractor: ${extractor}`);
|
|
442
359
|
}, hast) || hast;
|
|
@@ -625,10 +542,6 @@ var customTableHandler = (options) => (state, node) => {
|
|
|
625
542
|
return defaultHandlers4.table(state, node);
|
|
626
543
|
};
|
|
627
544
|
|
|
628
|
-
// src/mdast-handlers/empty-handler.ts
|
|
629
|
-
var emptyHandler = () => {
|
|
630
|
-
};
|
|
631
|
-
|
|
632
545
|
// src/mdast-handlers/math-handler.ts
|
|
633
546
|
import { toHtml } from "hast-util-to-html";
|
|
634
547
|
import { MathMLToLaTeX } from "mathml-to-latex";
|
|
@@ -640,11 +553,54 @@ var mathHandler = (state, node) => {
|
|
|
640
553
|
return result;
|
|
641
554
|
};
|
|
642
555
|
|
|
556
|
+
// src/utils/hast-utils.ts
|
|
557
|
+
import { select as select4, selectAll as selectAll2 } from "hast-util-select";
|
|
558
|
+
var getLangFromHast = (node) => {
|
|
559
|
+
const html = select4("html", node);
|
|
560
|
+
if (html && typeof html.properties.lang === "string") {
|
|
561
|
+
return html.properties.lang;
|
|
562
|
+
}
|
|
563
|
+
if (node.type !== "element") {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const element = node;
|
|
567
|
+
if (element.tagName !== "html") {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const langAttr = element.properties.lang || element.properties["xml:lang"];
|
|
571
|
+
if (langAttr) {
|
|
572
|
+
return langAttr;
|
|
573
|
+
}
|
|
574
|
+
return void 0;
|
|
575
|
+
};
|
|
576
|
+
var getLangFromStr = (str) => {
|
|
577
|
+
const match = str.match(/lang=["']([^"']+)["']/);
|
|
578
|
+
if (match) {
|
|
579
|
+
return match[1];
|
|
580
|
+
}
|
|
581
|
+
return void 0;
|
|
582
|
+
};
|
|
583
|
+
var getUrlFromHast = (node) => {
|
|
584
|
+
if (node.type !== "element") {
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
const metaTagAttributes = ["og:url", "twitter:url"];
|
|
588
|
+
const metaTags = selectAll2("meta", node);
|
|
589
|
+
for (const meta of metaTags) {
|
|
590
|
+
const property = meta.properties.property || meta.properties.name;
|
|
591
|
+
if (typeof property === "string" && metaTagAttributes.includes(property)) {
|
|
592
|
+
return typeof meta.properties.content === "string" ? meta.properties.content : void 0;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return void 0;
|
|
596
|
+
};
|
|
597
|
+
|
|
643
598
|
// src/html-to-mdast.ts
|
|
644
599
|
var htmlToMdast = (htmlOrHast, options) => {
|
|
645
|
-
const { extractors } = options || {};
|
|
646
|
-
const hast = typeof htmlOrHast === "string" ? fromHtml(htmlOrHast, { fragment: true }) : htmlOrHast;
|
|
647
|
-
const
|
|
600
|
+
const { extractors, url: defaultUrl, lang: defaultLang } = options || {};
|
|
601
|
+
const [lang, hast] = typeof htmlOrHast === "string" ? [defaultLang || getLangFromStr(htmlOrHast), fromHtml(htmlOrHast, { fragment: true })] : [defaultLang || getLangFromHast(htmlOrHast), htmlOrHast];
|
|
602
|
+
const url = defaultUrl || getUrlFromHast(hast);
|
|
603
|
+
const extractedHast = extractHast({ hast, lang, url }, extractors);
|
|
648
604
|
const mdast = toMdast(extractedHast, {
|
|
649
605
|
handlers: {
|
|
650
606
|
math: mathHandler,
|
|
@@ -652,8 +608,7 @@ var htmlToMdast = (htmlOrHast, options) => {
|
|
|
652
608
|
pre: customCodeHandler,
|
|
653
609
|
a: customAHandler({ asText: options?.linkAsText }),
|
|
654
610
|
img: customImgHandler({ hideImage: options?.hideImage }),
|
|
655
|
-
table: customTableHandler({ asText: options?.tableAsText })
|
|
656
|
-
br: emptyHandler
|
|
611
|
+
table: customTableHandler({ asText: options?.tableAsText })
|
|
657
612
|
}
|
|
658
613
|
});
|
|
659
614
|
const extractedMdast = extractMdast(mdast);
|
|
@@ -666,13 +621,18 @@ import { mathToMarkdown } from "mdast-util-math";
|
|
|
666
621
|
import { toMarkdown } from "mdast-util-to-markdown";
|
|
667
622
|
|
|
668
623
|
// src/link-replacer.ts
|
|
669
|
-
var linkReplacer = (markdown,
|
|
670
|
-
const regex = /(!?\[.*?\]\()(
|
|
671
|
-
return markdown.replace(regex, (match, pre,
|
|
672
|
-
if (
|
|
624
|
+
var linkReplacer = (markdown, base) => {
|
|
625
|
+
const regex = /(!?\[.*?\]\()([^)\s]+)(\))/g;
|
|
626
|
+
return markdown.replace(regex, (match, pre, url, post) => {
|
|
627
|
+
if (/^(https?:|#)/.test(url)) {
|
|
628
|
+
return match;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const absoluteUrl = new URL(url, base).href;
|
|
632
|
+
return `${pre}${absoluteUrl}${post}`;
|
|
633
|
+
} catch {
|
|
673
634
|
return match;
|
|
674
635
|
}
|
|
675
|
-
return `${pre}${new URL(thisUrl, link).href}${post}`;
|
|
676
636
|
});
|
|
677
637
|
};
|
|
678
638
|
|
|
@@ -691,7 +651,7 @@ var DEFAULT_MDAST_TO_MARKDOWN_OPTIONS = {
|
|
|
691
651
|
};
|
|
692
652
|
var mdastToMarkdown = (mdast, options) => {
|
|
693
653
|
const { baseUrl, ...toMarkdownOptions } = { ...DEFAULT_MDAST_TO_MARKDOWN_OPTIONS, ...options };
|
|
694
|
-
let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions);
|
|
654
|
+
let markdown = toMarkdown(warpRoot(mdast), toMarkdownOptions).replace(/\*\*\*\*/g, "");
|
|
695
655
|
if (baseUrl) {
|
|
696
656
|
markdown = linkReplacer(markdown, baseUrl);
|
|
697
657
|
}
|
|
@@ -882,9 +842,9 @@ var inputSourcePath = async () => {
|
|
|
882
842
|
};
|
|
883
843
|
|
|
884
844
|
// src/cli/helpers/selectExtractMode.ts
|
|
885
|
-
import { select as
|
|
845
|
+
import { select as select5 } from "@clack/prompts";
|
|
886
846
|
var selectExtractMode = async () => {
|
|
887
|
-
const result = await
|
|
847
|
+
const result = await select5({
|
|
888
848
|
message: "Select processing mode:",
|
|
889
849
|
options: MODES.map((mode) => ({ value: mode, label: mode })),
|
|
890
850
|
initialValue: MODES[0]
|
|
@@ -894,14 +854,14 @@ var selectExtractMode = async () => {
|
|
|
894
854
|
};
|
|
895
855
|
|
|
896
856
|
// src/cli/helpers/selectLoader.ts
|
|
897
|
-
import { select as
|
|
857
|
+
import { select as select6 } from "@clack/prompts";
|
|
898
858
|
var loadersHint = {
|
|
899
859
|
fetch: "Fetch HTML content from the given URL",
|
|
900
860
|
playwright: "Retrieve HTML content after rendering using Playwright; Playwright must be installed in advance.",
|
|
901
861
|
puppeteer: "Retrieve HTML content after rendering using Puppeteer; Puppeteer must be installed in advance."
|
|
902
862
|
};
|
|
903
863
|
var selectLoader = async () => {
|
|
904
|
-
const result = await
|
|
864
|
+
const result = await select6({
|
|
905
865
|
message: "Select loader:",
|
|
906
866
|
initialValue: "fetch",
|
|
907
867
|
options: LOADERS.map((mode) => ({ value: mode, label: mode, hint: loadersHint[mode] || "" }))
|
|
@@ -910,140 +870,160 @@ var selectLoader = async () => {
|
|
|
910
870
|
return result;
|
|
911
871
|
};
|
|
912
872
|
|
|
913
|
-
// src/cli/commands/
|
|
873
|
+
// src/cli/commands/webforai/loadHtml.ts
|
|
914
874
|
import fs4 from "node:fs/promises";
|
|
915
|
-
import {
|
|
875
|
+
import { log } from "@clack/prompts";
|
|
876
|
+
import boxen from "boxen";
|
|
916
877
|
import pc from "picocolors";
|
|
917
|
-
import {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
}
|
|
878
|
+
import { chromium as chromium2 } from "playwright-core";
|
|
879
|
+
|
|
880
|
+
// src/loaders/fetch.ts
|
|
881
|
+
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";
|
|
882
|
+
var loadHtml = async (url, userAgent = USER_AGENT) => {
|
|
883
|
+
const response = await fetch(url, { headers: { "User-Agent": userAgent } });
|
|
884
|
+
return response.text();
|
|
941
885
|
};
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
loadSpinner.stop("Content retrieval is complete!");
|
|
955
|
-
return content;
|
|
886
|
+
|
|
887
|
+
// src/loaders/playwright.ts
|
|
888
|
+
import { chromium, devices } from "playwright-core";
|
|
889
|
+
var SUPER_BYPASS_DEVICE = {
|
|
890
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
891
|
+
viewport: { width: 1920, height: 1080 },
|
|
892
|
+
deviceScaleFactor: 1,
|
|
893
|
+
hasTouch: false,
|
|
894
|
+
isMobile: false,
|
|
895
|
+
javaScriptEnabled: true,
|
|
896
|
+
locale: "en-US",
|
|
897
|
+
timezoneId: "America/New_York"
|
|
956
898
|
};
|
|
957
|
-
var
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
|
|
899
|
+
var loadHtml2 = async (url, options) => {
|
|
900
|
+
const { browser, waitUntil, timeout, superBypassMode } = options ?? {};
|
|
901
|
+
const _browser = browser ?? await chromium.launch({ headless: true });
|
|
902
|
+
const context = await _browser.newContext(superBypassMode ? SUPER_BYPASS_DEVICE : devices["Desktop Chrome"]);
|
|
903
|
+
if (superBypassMode) {
|
|
904
|
+
await context.addInitScript(() => {
|
|
905
|
+
Object.defineProperty(navigator, "webdriver", { get: () => void 0 });
|
|
906
|
+
Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"] });
|
|
907
|
+
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
const page = await context.newPage();
|
|
911
|
+
if (superBypassMode) {
|
|
912
|
+
await page.route("**/*.js", (route) => {
|
|
913
|
+
if (route.request().url().includes("captcha-delivery")) {
|
|
914
|
+
return route.abort();
|
|
915
|
+
}
|
|
916
|
+
return route.continue();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
await page.goto(url, { waitUntil: waitUntil ?? "load", timeout });
|
|
920
|
+
const html = await page.content();
|
|
921
|
+
await page.close();
|
|
922
|
+
if (browser) {
|
|
923
|
+
await context.close();
|
|
924
|
+
} else {
|
|
925
|
+
await _browser.close();
|
|
926
|
+
}
|
|
927
|
+
return html;
|
|
963
928
|
};
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
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;
|
|
929
|
+
|
|
930
|
+
// src/cli/commands/webforai/loadHtml.ts
|
|
931
|
+
var checkPlaywrightAvailable = async () => {
|
|
932
|
+
const path3 = chromium2.executablePath();
|
|
933
|
+
const isAvailable = await fs4.access(path3).catch(() => false);
|
|
934
|
+
return !!isAvailable;
|
|
982
935
|
};
|
|
983
|
-
var
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
936
|
+
var getPlaywrightVersion = async () => {
|
|
937
|
+
const path3 = await import.meta.resolve("playwright-core/package.json");
|
|
938
|
+
const pwPackageJson = await fs4.readFile(path3.replace("file://", ""), "utf-8").then((res) => JSON.parse(res.toString())).catch(() => null);
|
|
939
|
+
return pwPackageJson?.version;
|
|
940
|
+
};
|
|
941
|
+
var loadHtml3 = async (sourcePath, loader, options) => {
|
|
942
|
+
if (loader === "local") {
|
|
943
|
+
options.debug && log.info(`Loading HTML from local file: ${sourcePath}`);
|
|
944
|
+
const content = await fs4.readFile(sourcePath, "utf-8");
|
|
945
|
+
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
|
|
946
|
+
return content;
|
|
947
|
+
}
|
|
948
|
+
if (loader === "fetch") {
|
|
949
|
+
options.debug && log.info(`Loading HTML from URL: ${sourcePath}`);
|
|
950
|
+
const content = await loadHtml(sourcePath);
|
|
951
|
+
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
|
|
952
|
+
return content;
|
|
953
|
+
}
|
|
954
|
+
if (loader === "playwright") {
|
|
955
|
+
options.debug && log.info(`Loading HTML from playwright: ${sourcePath}`);
|
|
956
|
+
const isPlaywrightAvailable = await checkPlaywrightAvailable();
|
|
957
|
+
options.debug && log.info(`Playwright available: ${isPlaywrightAvailable}`);
|
|
958
|
+
const pwVersion = await getPlaywrightVersion();
|
|
959
|
+
if (!isPlaywrightAvailable) {
|
|
960
|
+
const message = [
|
|
961
|
+
pc.bold("Error: Playwright is not available"),
|
|
962
|
+
"",
|
|
963
|
+
"To use the Playwright loader, please install Playwright by running:",
|
|
964
|
+
"",
|
|
965
|
+
` npx playwright@${pwVersion} install chromium`,
|
|
966
|
+
"",
|
|
967
|
+
"Hint 1: If you receive a warning like this:",
|
|
968
|
+
` "WARNING: It looks like you are running 'npx playwright install' without first installing your project's dependencies."`,
|
|
969
|
+
"You can safely ignore this warning.",
|
|
970
|
+
"",
|
|
971
|
+
"Hint 2: If you encounter the following message:",
|
|
972
|
+
` "Host system is missing dependencies to run browsers."`,
|
|
973
|
+
"You should install the necessary dependencies by executing:",
|
|
974
|
+
"",
|
|
975
|
+
` sudo npx playwright@${pwVersion} install-deps`
|
|
976
|
+
];
|
|
977
|
+
log.error(boxen(message.join("\n"), { padding: 1, borderStyle: "round" }));
|
|
978
|
+
throw new Error("Playwright is not available");
|
|
979
|
+
}
|
|
980
|
+
const content = await loadHtml2(sourcePath);
|
|
981
|
+
options.debug && log.info(`HTML loaded: ${content.slice(0, 100)}`);
|
|
982
|
+
return content;
|
|
1002
983
|
}
|
|
984
|
+
throw new Error(`Unsupported loader: ${loader}`);
|
|
1003
985
|
};
|
|
1004
986
|
|
|
1005
|
-
// src/cli/commands/
|
|
987
|
+
// src/cli/commands/webforai/index.ts
|
|
1006
988
|
var aiModeOptions = { linkAsText: true, tableAsText: true, hideImage: true };
|
|
1007
989
|
var readabilityModeOptions = { linkAsText: false, tableAsText: false, hideImage: false };
|
|
1008
|
-
var webforaiCommand = async (initialPath,
|
|
990
|
+
var webforaiCommand = async (initialPath, options) => {
|
|
991
|
+
intro(pc2.bold(pc2.green(`webforai CLI version ${package_default.version}`)));
|
|
1009
992
|
const sourcePath = initialPath ?? await inputSourcePath();
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
console.debug(`loader: ${loader}`);
|
|
1016
|
-
}
|
|
1017
|
-
const outputPath = initialOutputPath ?? await inputOutputPath(sourcePath);
|
|
1018
|
-
if (options.debug) {
|
|
1019
|
-
console.debug(`outputPath: ${outputPath}`);
|
|
1020
|
-
}
|
|
993
|
+
options.debug && log2.info(`sourcePath: ${sourcePath}`);
|
|
994
|
+
const loader = isUrl(sourcePath) ? options.loader ?? await selectLoader() : "local";
|
|
995
|
+
options.debug && log2.info(`loader: ${loader}`);
|
|
996
|
+
const outputPath = options.output ?? await inputOutputPath(sourcePath);
|
|
997
|
+
options.debug && log2.info(`outputPath: ${outputPath}`);
|
|
1021
998
|
const mode = options.mode ?? await selectExtractMode();
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
999
|
+
options.debug && log2.info(`mode: ${mode}`);
|
|
1000
|
+
let html;
|
|
1001
|
+
const s = spinner();
|
|
1002
|
+
try {
|
|
1003
|
+
s.start("Loading content...");
|
|
1004
|
+
html = await loadHtml3(sourcePath, loader, { debug: options.debug });
|
|
1005
|
+
s.stop(pc2.green("Content loaded!"));
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
s.stop(pc2.red("Content loading failed!"));
|
|
1008
|
+
console.error(error);
|
|
1009
|
+
process.exit(1);
|
|
1028
1010
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1011
|
+
options.debug && log2.info(`html: ${html}`);
|
|
1012
|
+
const markdown = htmlToMarkdown(html, {
|
|
1013
|
+
baseUrl: isUrl(sourcePath) ? sourcePath : void 0,
|
|
1031
1014
|
...mode === "ai" ? aiModeOptions : readabilityModeOptions
|
|
1032
1015
|
});
|
|
1033
|
-
|
|
1034
|
-
console.debug(`markdown: ${markdown}`);
|
|
1035
|
-
}
|
|
1016
|
+
options.debug && log2.info(`markdown: ${markdown}`);
|
|
1036
1017
|
const directory = path2.dirname(outputPath);
|
|
1037
|
-
|
|
1038
|
-
|
|
1018
|
+
const isDirectoryExists = await fs5.stat(directory).then((stat) => stat.isDirectory());
|
|
1019
|
+
if (!isDirectoryExists) {
|
|
1020
|
+
await fs5.mkdir(directory, { recursive: true });
|
|
1039
1021
|
}
|
|
1040
|
-
fs5.
|
|
1041
|
-
outro("Markdown
|
|
1022
|
+
await fs5.writeFile(outputPath, markdown);
|
|
1023
|
+
outro(pc2.green(`${pc2.bold("Done!")} Markdown saved to ${outputPath}`));
|
|
1042
1024
|
};
|
|
1043
1025
|
|
|
1044
1026
|
// 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")
|
|
1046
|
-
|
|
1047
|
-
}).action(webforaiCommand);
|
|
1048
|
-
intro(pc2.inverse(`webforai CLI version ${package_default.version}`));
|
|
1027
|
+
program.name("webforai").description("CLI tool for ultra-precise HTML to Markdown conversion").version(package_default.version, "-v, --version", "output the current version");
|
|
1028
|
+
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);
|
|
1049
1029
|
program.parse();
|