@link-assistant/web-capture 1.1.2 → 1.4.2
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/CHANGELOG.md +17 -14
- package/README.md +256 -146
- package/bin/web-capture.js +403 -6
- package/package.json +11 -4
- package/src/animation.js +283 -0
- package/src/archive.js +212 -0
- package/src/batch.js +180 -0
- package/src/browser.js +150 -111
- package/src/docx.js +182 -0
- package/src/figures.js +208 -0
- package/src/gdocs.js +333 -0
- package/src/image.js +65 -8
- package/src/index.js +15 -0
- package/src/latex.js +127 -0
- package/src/lib.js +226 -1
- package/src/localize-images.js +202 -0
- package/src/metadata.js +333 -0
- package/src/pdf.js +76 -0
- package/src/popups.js +171 -0
- package/src/postprocess.js +265 -0
- package/src/retry.js +48 -0
- package/src/themed-image.js +167 -0
- package/src/verify.js +312 -0
- package/tests/e2e/docker.test.js +51 -12
- package/tests/e2e/process.test.js +34 -10
- package/tests/fixtures/habr-articles.js +45 -0
- package/tests/integration/api-endpoints.test.js +231 -0
- package/tests/integration/habr-article.test.js +337 -0
- package/tests/mock/index.test.js +5 -3
- package/tests/unit/animation.test.js +33 -0
- package/tests/unit/batch.test.js +114 -0
- package/tests/unit/browser.test.js +38 -2
- package/tests/unit/figures.test.js +74 -0
- package/tests/unit/gdocs.test.js +203 -0
- package/tests/unit/latex.test.js +123 -0
- package/tests/unit/localize-images.test.js +78 -0
- package/tests/unit/metadata.test.js +140 -0
- package/tests/unit/postprocess.test.js +108 -0
- package/tests/unit/retry.test.js +111 -0
- package/tests/unit/verify.test.js +182 -0
- package/.github/workflows/release.yml +0 -294
- package/ARCHITECTURE.md +0 -982
- package/LICENSE +0 -24
- package/apply.sh +0 -166
- package/docs/market-analysis-2025-05-13.md +0 -64
- package/scripts/changeset-version.mjs +0 -38
- package/scripts/create-github-release.mjs +0 -93
- package/scripts/create-manual-changeset.mjs +0 -88
- package/scripts/format-github-release.mjs +0 -83
- package/scripts/format-release-notes.mjs +0 -218
- package/scripts/instant-version-bump.mjs +0 -121
- package/scripts/publish-to-npm.mjs +0 -128
- package/scripts/setup-npm.mjs +0 -37
- package/scripts/validate-changeset.mjs +0 -106
- package/scripts/version-and-commit.mjs +0 -237
package/bin/web-capture.js
CHANGED
|
@@ -32,9 +32,47 @@ const config = makeConfig({
|
|
|
32
32
|
.option('format', {
|
|
33
33
|
alias: 'f',
|
|
34
34
|
type: 'string',
|
|
35
|
-
description:
|
|
35
|
+
description:
|
|
36
|
+
'Output format: html, markdown, md, image, png, jpeg, pdf, docx, archive',
|
|
36
37
|
default: 'html',
|
|
37
38
|
})
|
|
39
|
+
.option('theme', {
|
|
40
|
+
alias: 't',
|
|
41
|
+
type: 'string',
|
|
42
|
+
description:
|
|
43
|
+
'Color scheme for screenshots/PDF: light, dark, no-preference',
|
|
44
|
+
})
|
|
45
|
+
.option('width', {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'Viewport width in pixels (default: 1280)',
|
|
48
|
+
default: 1280,
|
|
49
|
+
})
|
|
50
|
+
.option('height', {
|
|
51
|
+
type: 'number',
|
|
52
|
+
description: 'Viewport height in pixels (default: 800)',
|
|
53
|
+
default: 800,
|
|
54
|
+
})
|
|
55
|
+
.option('quality', {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'JPEG quality 0-100 (default: 80, only for jpeg format)',
|
|
58
|
+
default: 80,
|
|
59
|
+
})
|
|
60
|
+
.option('fullPage', {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
description: 'Capture full scrollable page (default: false)',
|
|
63
|
+
default: false,
|
|
64
|
+
})
|
|
65
|
+
.option('localImages', {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'Download images locally in archive mode (default: true)',
|
|
68
|
+
default: true,
|
|
69
|
+
})
|
|
70
|
+
.option('documentFormat', {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description:
|
|
73
|
+
'Document format in archive mode: markdown (default) or html',
|
|
74
|
+
default: 'markdown',
|
|
75
|
+
})
|
|
38
76
|
.option('output', {
|
|
39
77
|
alias: 'o',
|
|
40
78
|
type: 'string',
|
|
@@ -51,6 +89,67 @@ const config = makeConfig({
|
|
|
51
89
|
type: 'string',
|
|
52
90
|
description: 'Path to .lenv configuration file',
|
|
53
91
|
})
|
|
92
|
+
.option('enhanced', {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description:
|
|
95
|
+
'Use enhanced markdown conversion with LaTeX extraction, metadata, and post-processing (default: false)',
|
|
96
|
+
default: false,
|
|
97
|
+
})
|
|
98
|
+
.option('extractLatex', {
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
description:
|
|
101
|
+
'Extract LaTeX formulas from img.formula, KaTeX, MathJax (default: true when --enhanced)',
|
|
102
|
+
default: true,
|
|
103
|
+
})
|
|
104
|
+
.option('extractMetadata', {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description:
|
|
107
|
+
'Extract article metadata (author, date, hubs, tags) (default: true when --enhanced)',
|
|
108
|
+
default: true,
|
|
109
|
+
})
|
|
110
|
+
.option('postProcess', {
|
|
111
|
+
type: 'boolean',
|
|
112
|
+
description:
|
|
113
|
+
'Apply post-processing (unicode normalization, LaTeX spacing) (default: true when --enhanced)',
|
|
114
|
+
default: true,
|
|
115
|
+
})
|
|
116
|
+
.option('detectCodeLanguage', {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description:
|
|
119
|
+
'Detect and correct code block languages (default: true when --enhanced)',
|
|
120
|
+
default: true,
|
|
121
|
+
})
|
|
122
|
+
.option('dualTheme', {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description:
|
|
125
|
+
'Capture both light and dark theme screenshots (default: false)',
|
|
126
|
+
default: false,
|
|
127
|
+
})
|
|
128
|
+
.option('configFile', {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Path to batch configuration file (JSON or MJS)',
|
|
131
|
+
})
|
|
132
|
+
.option('all', {
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
description: 'Process all articles in batch configuration',
|
|
135
|
+
default: false,
|
|
136
|
+
})
|
|
137
|
+
.option('dryRun', {
|
|
138
|
+
type: 'boolean',
|
|
139
|
+
description: 'Show what would be done without making changes',
|
|
140
|
+
default: false,
|
|
141
|
+
})
|
|
142
|
+
.option('verbose', {
|
|
143
|
+
type: 'boolean',
|
|
144
|
+
description: 'Show detailed output',
|
|
145
|
+
default: false,
|
|
146
|
+
})
|
|
147
|
+
.option('apiToken', {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description:
|
|
150
|
+
'API token for authenticated capture (e.g., Google Docs private documents). Can also be set via API_TOKEN env variable.',
|
|
151
|
+
default: getenv('API_TOKEN', undefined),
|
|
152
|
+
})
|
|
54
153
|
.help('help')
|
|
55
154
|
.alias('help', 'h')
|
|
56
155
|
.version()
|
|
@@ -68,12 +167,26 @@ const config = makeConfig({
|
|
|
68
167
|
'web-capture https://example.com --format markdown --output page.md',
|
|
69
168
|
'Capture URL as Markdown to file'
|
|
70
169
|
)
|
|
170
|
+
.option('capture', {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description:
|
|
173
|
+
'Capture method: browser (default, uses Puppeteer/Playwright) or api (direct HTTP fetch, for Google Docs etc.)',
|
|
174
|
+
default: 'browser',
|
|
175
|
+
})
|
|
71
176
|
.example(
|
|
72
177
|
'web-capture https://example.com --format png --engine playwright -o screenshot.png',
|
|
73
178
|
'Capture screenshot using Playwright'
|
|
74
179
|
)
|
|
180
|
+
.example(
|
|
181
|
+
'web-capture https://docs.google.com/document/d/DOC_ID/edit --format markdown',
|
|
182
|
+
'Capture Google Doc as Markdown (API-based, public doc)'
|
|
183
|
+
)
|
|
184
|
+
.example(
|
|
185
|
+
'web-capture https://docs.google.com/document/d/DOC_ID/edit --format markdown --apiToken YOUR_TOKEN',
|
|
186
|
+
'Capture private Google Doc with API token'
|
|
187
|
+
)
|
|
75
188
|
.epilogue(
|
|
76
|
-
'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE>
|
|
189
|
+
'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&format=png|jpeg&theme=light|dark Screenshot\n GET /archive?url=<URL>&localImages=true&documentFormat=markdown|html ZIP archive\n GET /pdf?url=<URL>&theme=light|dark PDF with embedded images\n GET /docx?url=<URL> DOCX with embedded images\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy\n GET /gdocs?url=<URL>&format=markdown|html|txt Google Docs capture'
|
|
77
190
|
)
|
|
78
191
|
.strict(),
|
|
79
192
|
lenv: {
|
|
@@ -93,9 +206,15 @@ async function startServer(port) {
|
|
|
93
206
|
console.log('Available endpoints:');
|
|
94
207
|
console.log(` GET /html?url=<URL> - Render page as HTML`);
|
|
95
208
|
console.log(` GET /markdown?url=<URL> - Convert page to Markdown`);
|
|
96
|
-
console.log(` GET /image?url=<URL> - Screenshot
|
|
209
|
+
console.log(` GET /image?url=<URL> - Screenshot (PNG/JPEG)`);
|
|
210
|
+
console.log(
|
|
211
|
+
` GET /archive?url=<URL> - ZIP archive with markdown + images`
|
|
212
|
+
);
|
|
213
|
+
console.log(` GET /pdf?url=<URL> - PDF with embedded images`);
|
|
214
|
+
console.log(` GET /docx?url=<URL> - DOCX with embedded images`);
|
|
97
215
|
console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
|
|
98
216
|
console.log(` GET /stream?url=<URL> - Stream content`);
|
|
217
|
+
console.log(` GET /gdocs?url=<URL> - Capture Google Docs document`);
|
|
99
218
|
console.log('');
|
|
100
219
|
console.log('Press Ctrl+C to stop the server');
|
|
101
220
|
resolve(server);
|
|
@@ -123,7 +242,19 @@ async function startServer(port) {
|
|
|
123
242
|
}
|
|
124
243
|
|
|
125
244
|
async function captureUrl(url, options) {
|
|
126
|
-
const {
|
|
245
|
+
const {
|
|
246
|
+
format,
|
|
247
|
+
output,
|
|
248
|
+
engine,
|
|
249
|
+
theme,
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
quality,
|
|
253
|
+
fullPage,
|
|
254
|
+
// localImages is used by archive format
|
|
255
|
+
// eslint-disable-next-line no-unused-vars
|
|
256
|
+
localImages,
|
|
257
|
+
} = options;
|
|
127
258
|
|
|
128
259
|
// Ensure URL is absolute
|
|
129
260
|
let absoluteUrl = url;
|
|
@@ -147,14 +278,266 @@ async function captureUrl(url, options) {
|
|
|
147
278
|
convertRelativeUrls,
|
|
148
279
|
} = await import('../src/lib.js');
|
|
149
280
|
const { createBrowser } = await import('../src/browser.js');
|
|
281
|
+
const { isGoogleDocsUrl, fetchGoogleDoc, fetchGoogleDocAsMarkdown } =
|
|
282
|
+
await import('../src/gdocs.js');
|
|
150
283
|
|
|
151
284
|
const normalizedFormat = format.toLowerCase();
|
|
152
285
|
|
|
286
|
+
// Auto-detect Google Docs URLs and use API-based capture
|
|
287
|
+
if (isGoogleDocsUrl(absoluteUrl)) {
|
|
288
|
+
try {
|
|
289
|
+
const apiToken = options.apiToken;
|
|
290
|
+
if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
|
|
291
|
+
const result = await fetchGoogleDocAsMarkdown(absoluteUrl, {
|
|
292
|
+
apiToken,
|
|
293
|
+
});
|
|
294
|
+
if (output) {
|
|
295
|
+
fs.writeFileSync(output, result.markdown, 'utf-8');
|
|
296
|
+
console.error(`Google Doc Markdown saved to: ${output}`);
|
|
297
|
+
} else {
|
|
298
|
+
process.stdout.write(result.markdown);
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
const gdocsFormat =
|
|
302
|
+
normalizedFormat === 'png' || normalizedFormat === 'image'
|
|
303
|
+
? 'html'
|
|
304
|
+
: normalizedFormat;
|
|
305
|
+
const result = await fetchGoogleDoc(absoluteUrl, {
|
|
306
|
+
format: gdocsFormat,
|
|
307
|
+
apiToken,
|
|
308
|
+
});
|
|
309
|
+
if (output) {
|
|
310
|
+
fs.writeFileSync(output, result.content, 'utf-8');
|
|
311
|
+
console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
|
|
312
|
+
} else {
|
|
313
|
+
process.stdout.write(result.content);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
153
323
|
try {
|
|
154
|
-
if (normalizedFormat === '
|
|
324
|
+
if (normalizedFormat === 'jpeg') {
|
|
325
|
+
// JPEG screenshot
|
|
326
|
+
const { createBrowser } = await import('../src/browser.js');
|
|
327
|
+
const { dismissPopups, scrollToLoadContent } =
|
|
328
|
+
await import('../src/popups.js');
|
|
329
|
+
const browserOpts = theme ? { colorScheme: theme } : {};
|
|
330
|
+
const browser = await createBrowser(engine, browserOpts);
|
|
331
|
+
try {
|
|
332
|
+
const page = await browser.newPage();
|
|
333
|
+
await page.setViewport({ width, height });
|
|
334
|
+
await page.goto(absoluteUrl, {
|
|
335
|
+
waitUntil: 'networkidle0',
|
|
336
|
+
timeout: 30000,
|
|
337
|
+
});
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
339
|
+
if (fullPage) {
|
|
340
|
+
await scrollToLoadContent(page);
|
|
341
|
+
}
|
|
342
|
+
await dismissPopups(page);
|
|
343
|
+
const buffer = await page.screenshot({
|
|
344
|
+
type: 'jpeg',
|
|
345
|
+
quality,
|
|
346
|
+
fullPage,
|
|
347
|
+
});
|
|
348
|
+
const outPath =
|
|
349
|
+
output ||
|
|
350
|
+
`${new URL(absoluteUrl).hostname.replace(/\./g, '_')}_${Date.now()}.jpg`;
|
|
351
|
+
fs.writeFileSync(outPath, buffer);
|
|
352
|
+
console.error(`JPEG screenshot saved to: ${outPath}`);
|
|
353
|
+
} finally {
|
|
354
|
+
await browser.close();
|
|
355
|
+
}
|
|
356
|
+
} else if (normalizedFormat === 'pdf') {
|
|
357
|
+
// PDF export
|
|
358
|
+
const { createBrowser } = await import('../src/browser.js');
|
|
359
|
+
const { dismissPopups, scrollToLoadContent } =
|
|
360
|
+
await import('../src/popups.js');
|
|
361
|
+
const browserOpts = theme ? { colorScheme: theme } : {};
|
|
362
|
+
const browser = await createBrowser(engine, browserOpts);
|
|
363
|
+
try {
|
|
364
|
+
const page = await browser.newPage();
|
|
365
|
+
await page.setViewport({ width, height });
|
|
366
|
+
await page.goto(absoluteUrl, {
|
|
367
|
+
waitUntil: 'networkidle0',
|
|
368
|
+
timeout: 30000,
|
|
369
|
+
});
|
|
370
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
371
|
+
await scrollToLoadContent(page);
|
|
372
|
+
await dismissPopups(page);
|
|
373
|
+
const rawPage = page.rawPage || page;
|
|
374
|
+
const pdfBuffer = await rawPage.pdf({
|
|
375
|
+
format: 'A4',
|
|
376
|
+
printBackground: true,
|
|
377
|
+
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
|
|
378
|
+
});
|
|
379
|
+
const outPath = output || 'page.pdf';
|
|
380
|
+
fs.writeFileSync(outPath, pdfBuffer);
|
|
381
|
+
console.error(`PDF saved to: ${outPath}`);
|
|
382
|
+
} finally {
|
|
383
|
+
await browser.close();
|
|
384
|
+
}
|
|
385
|
+
} else if (normalizedFormat === 'docx') {
|
|
386
|
+
// DOCX export – delegate to the docx handler logic
|
|
387
|
+
const { fetchHtml, convertRelativeUrls } = await import('../src/lib.js');
|
|
388
|
+
const cheerio = await import('cheerio');
|
|
389
|
+
// eslint-disable-next-line no-unused-vars
|
|
390
|
+
const { Document, Packer, Paragraph, TextRun, HeadingLevel } =
|
|
391
|
+
await import('docx');
|
|
392
|
+
const html = await fetchHtml(absoluteUrl);
|
|
393
|
+
const absHtml = convertRelativeUrls(html, absoluteUrl);
|
|
394
|
+
const $ = cheerio.load(absHtml);
|
|
395
|
+
$('style, script, noscript, nav, footer, header').remove();
|
|
396
|
+
const children = [];
|
|
397
|
+
const titleText =
|
|
398
|
+
$('h1').first().text().trim() || $('title').text().trim();
|
|
399
|
+
if (titleText) {
|
|
400
|
+
children.push(
|
|
401
|
+
new Paragraph({ text: titleText, heading: HeadingLevel.TITLE })
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const body = $('article').length ? $('article') : $('body');
|
|
405
|
+
for (const el of body.children().toArray()) {
|
|
406
|
+
const text = $(el).text().trim();
|
|
407
|
+
if (text) {
|
|
408
|
+
children.push(new Paragraph({ text }));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (children.length === 0) {
|
|
412
|
+
children.push(new Paragraph({ text: 'No content extracted.' }));
|
|
413
|
+
}
|
|
414
|
+
const doc = new Document({ sections: [{ children }] });
|
|
415
|
+
const buffer = await Packer.toBuffer(doc);
|
|
416
|
+
const outPath = output || 'page.docx';
|
|
417
|
+
fs.writeFileSync(outPath, buffer);
|
|
418
|
+
console.error(`DOCX saved to: ${outPath}`);
|
|
419
|
+
} else if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
|
|
420
|
+
// ZIP archive
|
|
421
|
+
const { default: archiver } = await import('archiver');
|
|
422
|
+
const { fetchHtml, convertHtmlToMarkdown, convertRelativeUrls } =
|
|
423
|
+
await import('../src/lib.js');
|
|
424
|
+
const { retry } = await import('../src/retry.js');
|
|
425
|
+
const cheerio = await import('cheerio');
|
|
426
|
+
const nodeFetch = await import('node-fetch');
|
|
427
|
+
|
|
428
|
+
const html = await retry(() => fetchHtml(absoluteUrl), {
|
|
429
|
+
retries: 3,
|
|
430
|
+
baseDelay: 1000,
|
|
431
|
+
onRetry: (err, attempt) => {
|
|
432
|
+
console.error(`Retry ${attempt} fetching page: ${err.message}`);
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const docFormat = options.documentFormat === 'html' ? 'html' : 'markdown';
|
|
437
|
+
const outPath =
|
|
438
|
+
output ||
|
|
439
|
+
`${new URL(absoluteUrl).hostname.replace(/\./g, '-')}-archive.zip`;
|
|
440
|
+
const outStream = fs.createWriteStream(outPath);
|
|
441
|
+
const archive = archiver.default
|
|
442
|
+
? archiver.default('zip', { zlib: { level: 9 } })
|
|
443
|
+
: archiver('zip', { zlib: { level: 9 } });
|
|
444
|
+
archive.pipe(outStream);
|
|
445
|
+
|
|
446
|
+
// Collect images
|
|
447
|
+
const $ = cheerio.load(html);
|
|
448
|
+
const images = [];
|
|
449
|
+
$('img').each(function () {
|
|
450
|
+
const src = $(this).attr('src');
|
|
451
|
+
if (src && !src.startsWith('data:') && !src.startsWith('blob:')) {
|
|
452
|
+
try {
|
|
453
|
+
images.push(new URL(src, absoluteUrl).href);
|
|
454
|
+
} catch {
|
|
455
|
+
/* skip */
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
const uniqueImages = [...new Set(images)];
|
|
460
|
+
|
|
461
|
+
// Build image map for local downloads
|
|
462
|
+
const imageMap = new Map();
|
|
463
|
+
if (options.localImages && uniqueImages.length > 0) {
|
|
464
|
+
let idx = 1;
|
|
465
|
+
for (const imgUrl of uniqueImages) {
|
|
466
|
+
const ext = imgUrl.match(/\.(jpe?g|gif|webp|svg|png)/i)?.[1] || 'png';
|
|
467
|
+
imageMap.set(imgUrl, `images/image-${idx}.${ext}`);
|
|
468
|
+
idx++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (docFormat === 'html') {
|
|
473
|
+
const outputHtml = convertRelativeUrls(html, absoluteUrl);
|
|
474
|
+
const $out = cheerio.load(outputHtml);
|
|
475
|
+
$out('script, noscript').remove();
|
|
476
|
+
if (imageMap.size > 0) {
|
|
477
|
+
$out('img').each(function () {
|
|
478
|
+
const src = $out(this).attr('src');
|
|
479
|
+
if (src) {
|
|
480
|
+
try {
|
|
481
|
+
const resolved = new URL(src, absoluteUrl).href;
|
|
482
|
+
if (imageMap.has(resolved)) {
|
|
483
|
+
$out(this).attr('src', imageMap.get(resolved));
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
/* skip */
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
archive.append($out.html(), { name: 'article.html' });
|
|
492
|
+
} else {
|
|
493
|
+
let markdown = convertHtmlToMarkdown(html, absoluteUrl);
|
|
494
|
+
if (imageMap.size > 0) {
|
|
495
|
+
for (const [remoteUrl, localPath] of imageMap) {
|
|
496
|
+
markdown = markdown.split(remoteUrl).join(localPath);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
archive.append(markdown, { name: 'article.md' });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Download images
|
|
503
|
+
if (imageMap.size > 0) {
|
|
504
|
+
for (const [imgUrl, localPath] of imageMap) {
|
|
505
|
+
try {
|
|
506
|
+
const fetchFn = nodeFetch.default || nodeFetch;
|
|
507
|
+
const resp = await retry(() => fetchFn(imgUrl), {
|
|
508
|
+
retries: 2,
|
|
509
|
+
baseDelay: 500,
|
|
510
|
+
});
|
|
511
|
+
if (resp.ok) {
|
|
512
|
+
const buffer = await resp.buffer();
|
|
513
|
+
archive.append(buffer, { name: localPath });
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
/* skip failed image downloads */
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
await archive.finalize();
|
|
522
|
+
await new Promise((resolve) => outStream.on('close', resolve));
|
|
523
|
+
console.error(`Archive saved to: ${outPath}`);
|
|
524
|
+
} else if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
|
|
155
525
|
// Markdown format
|
|
156
526
|
const html = await fetchHtml(absoluteUrl);
|
|
157
|
-
|
|
527
|
+
let markdown;
|
|
528
|
+
|
|
529
|
+
if (options.enhanced) {
|
|
530
|
+
const { convertHtmlToMarkdownEnhanced } = await import('../src/lib.js');
|
|
531
|
+
const result = convertHtmlToMarkdownEnhanced(html, absoluteUrl, {
|
|
532
|
+
extractLatex: options.extractLatex,
|
|
533
|
+
extractMetadata: options.extractMetadata,
|
|
534
|
+
postProcess: options.postProcess,
|
|
535
|
+
detectCodeLanguage: options.detectCodeLanguage,
|
|
536
|
+
});
|
|
537
|
+
markdown = result.markdown;
|
|
538
|
+
} else {
|
|
539
|
+
markdown = convertHtmlToMarkdown(html, absoluteUrl);
|
|
540
|
+
}
|
|
158
541
|
|
|
159
542
|
if (output) {
|
|
160
543
|
fs.writeFileSync(output, markdown, 'utf-8');
|
|
@@ -270,6 +653,20 @@ async function main() {
|
|
|
270
653
|
format: config.format,
|
|
271
654
|
output: config.output,
|
|
272
655
|
engine: config.engine,
|
|
656
|
+
theme: config.theme,
|
|
657
|
+
width: config.width,
|
|
658
|
+
height: config.height,
|
|
659
|
+
quality: config.quality,
|
|
660
|
+
fullPage: config.fullPage,
|
|
661
|
+
localImages: config.localImages,
|
|
662
|
+
documentFormat: config.documentFormat,
|
|
663
|
+
enhanced: config.enhanced,
|
|
664
|
+
extractLatex: config.extractLatex,
|
|
665
|
+
extractMetadata: config.extractMetadata,
|
|
666
|
+
postProcess: config.postProcess,
|
|
667
|
+
detectCodeLanguage: config.detectCodeLanguage,
|
|
668
|
+
dualTheme: config.dualTheme,
|
|
669
|
+
apiToken: config.apiToken,
|
|
273
670
|
});
|
|
274
671
|
} else {
|
|
275
672
|
// No arguments - show error
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/web-capture",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,23 +27,30 @@
|
|
|
27
27
|
"check": "npm run lint && npm run format:check && npm run check:duplication",
|
|
28
28
|
"prepare": "husky || true",
|
|
29
29
|
"changeset": "changeset",
|
|
30
|
-
"changeset:version": "node scripts/changeset-version.mjs",
|
|
30
|
+
"changeset:version": "node ../scripts/changeset-version.mjs",
|
|
31
31
|
"changeset:publish": "changeset publish",
|
|
32
32
|
"changeset:status": "changeset status --since=origin/main"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"archiver": "^7.0.1",
|
|
36
|
+
"browser-commander": "^0.8.0",
|
|
35
37
|
"cheerio": "^1.0.0",
|
|
38
|
+
"docx": "^9.6.1",
|
|
36
39
|
"express": "^4.18.2",
|
|
37
|
-
"
|
|
40
|
+
"he": "^1.2.0",
|
|
41
|
+
"iconv-lite": "^0.6.3",
|
|
42
|
+
"lino-arguments": "^0.3.0",
|
|
38
43
|
"node-fetch": "^2.7.0",
|
|
39
44
|
"playwright": "^1.49.0",
|
|
40
45
|
"puppeteer": "^24.8.2",
|
|
46
|
+
"puppeteer-core": "^24.40.0",
|
|
41
47
|
"turndown": "^7.1.1"
|
|
42
48
|
},
|
|
43
49
|
"devDependencies": {
|
|
44
50
|
"@babel/core": "^7.24.0",
|
|
45
51
|
"@babel/preset-env": "^7.24.0",
|
|
46
52
|
"@changesets/cli": "^2.29.7",
|
|
53
|
+
"@types/archiver": "^7.0.0",
|
|
47
54
|
"babel-jest": "^29.7.0",
|
|
48
55
|
"eslint": "^9.38.0",
|
|
49
56
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -65,7 +72,7 @@
|
|
|
65
72
|
"screenshot",
|
|
66
73
|
"microservice"
|
|
67
74
|
],
|
|
68
|
-
"license": "
|
|
75
|
+
"license": "Unlicense",
|
|
69
76
|
"engines": {
|
|
70
77
|
"node": ">=22.0.0 <23.0.0"
|
|
71
78
|
},
|