@link-assistant/web-capture 1.7.4 → 1.7.6
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 +12 -0
- package/README.md +7 -4
- package/bin/web-capture.js +370 -117
- package/package.json +2 -1
- package/src/browser.js +15 -0
- package/src/gdocs.js +828 -124
- package/src/index.js +0 -2
- package/tests/integration/api-endpoints.test.js +11 -0
- package/tests/unit/cli.test.js +1 -0
- package/tests/unit/gdocs.test.js +161 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.7.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Support Google Docs capture selection through `--capture browser` editor-model parsing, public export API capture, and authenticated Docs REST API capture.
|
|
8
|
+
|
|
9
|
+
## 1.7.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- dfaf007: Remove the unsupported `/gdocs` HTTP API route from the JavaScript and Rust server surfaces while keeping Google Docs URL capture through normal output formats.
|
|
14
|
+
|
|
3
15
|
## 1.7.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -92,11 +92,14 @@ web-capture --serve --port 8080
|
|
|
92
92
|
### Google Docs
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
# Capture
|
|
96
|
-
web-capture https://docs.google.com/document/d/DOC_ID/edit
|
|
95
|
+
# Capture the live editor model as Markdown (default --capture browser)
|
|
96
|
+
web-capture https://docs.google.com/document/d/DOC_ID/edit --capture browser
|
|
97
97
|
|
|
98
|
-
# Capture
|
|
99
|
-
web-capture https://docs.google.com/document/d/DOC_ID/edit --
|
|
98
|
+
# Capture via the public export endpoint
|
|
99
|
+
web-capture https://docs.google.com/document/d/DOC_ID/edit --capture api
|
|
100
|
+
|
|
101
|
+
# Capture via the Google Docs REST API
|
|
102
|
+
web-capture https://docs.google.com/document/d/DOC_ID/edit --capture api --apiToken YOUR_TOKEN
|
|
100
103
|
```
|
|
101
104
|
|
|
102
105
|
## API Endpoints (Server Mode)
|
package/bin/web-capture.js
CHANGED
|
@@ -8,6 +8,23 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { URL } from 'node:url';
|
|
10
10
|
import { makeConfig } from 'lino-arguments';
|
|
11
|
+
import makeLog from 'log-lazy';
|
|
12
|
+
|
|
13
|
+
function makeVerboseLog(enabled) {
|
|
14
|
+
return makeLog({
|
|
15
|
+
level: enabled ? 'all' : 'none',
|
|
16
|
+
log: {
|
|
17
|
+
fatal: console.error,
|
|
18
|
+
error: console.error,
|
|
19
|
+
warn: console.error,
|
|
20
|
+
info: console.error,
|
|
21
|
+
debug: console.error,
|
|
22
|
+
verbose: console.error,
|
|
23
|
+
trace: console.error,
|
|
24
|
+
silly: console.error,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
// Create configuration using lino-arguments pattern
|
|
13
30
|
const config = makeConfig({
|
|
@@ -230,7 +247,7 @@ const config = makeConfig({
|
|
|
230
247
|
'Capture private Google Doc with API token'
|
|
231
248
|
)
|
|
232
249
|
.epilogue(
|
|
233
|
-
'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
|
|
250
|
+
'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'
|
|
234
251
|
)
|
|
235
252
|
.strict(),
|
|
236
253
|
lenv: {
|
|
@@ -271,7 +288,6 @@ async function startServer(port) {
|
|
|
271
288
|
console.log(` GET /docx?url=<URL> - DOCX with embedded images`);
|
|
272
289
|
console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
|
|
273
290
|
console.log(` GET /stream?url=<URL> - Stream content`);
|
|
274
|
-
console.log(` GET /gdocs?url=<URL> - Capture Google Docs document`);
|
|
275
291
|
console.log('');
|
|
276
292
|
console.log('Press Ctrl+C to stop the server');
|
|
277
293
|
resolve(server);
|
|
@@ -308,6 +324,82 @@ function deriveOutputPath(absoluteUrl, ext, dataDir) {
|
|
|
308
324
|
return path.join(dir, `document.${ext}`);
|
|
309
325
|
}
|
|
310
326
|
|
|
327
|
+
function archiveExtension(archiveFormat) {
|
|
328
|
+
if (archiveFormat === 'tar.gz' || archiveFormat === 'gz') {
|
|
329
|
+
return 'tar.gz';
|
|
330
|
+
}
|
|
331
|
+
if (archiveFormat === '7z') {
|
|
332
|
+
return '7z';
|
|
333
|
+
}
|
|
334
|
+
if (archiveFormat === 'tar') {
|
|
335
|
+
return 'tar';
|
|
336
|
+
}
|
|
337
|
+
return 'zip';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function writeGoogleDocsTextOutput({
|
|
341
|
+
content,
|
|
342
|
+
absoluteUrl,
|
|
343
|
+
ext,
|
|
344
|
+
explicitOutput,
|
|
345
|
+
dataDir,
|
|
346
|
+
label,
|
|
347
|
+
}) {
|
|
348
|
+
const output =
|
|
349
|
+
explicitOutput === '-'
|
|
350
|
+
? null
|
|
351
|
+
: explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
|
|
352
|
+
if (output) {
|
|
353
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
354
|
+
fs.writeFileSync(output, content, 'utf-8');
|
|
355
|
+
console.error(`${label} saved to: ${output}`);
|
|
356
|
+
} else {
|
|
357
|
+
process.stdout.write(content);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function writeGoogleDocsArchive({
|
|
362
|
+
archiveResult,
|
|
363
|
+
absoluteUrl,
|
|
364
|
+
explicitOutput,
|
|
365
|
+
dataDir,
|
|
366
|
+
archiveFormat,
|
|
367
|
+
prettyHtml,
|
|
368
|
+
}) {
|
|
369
|
+
const { default: archiver } = await import('archiver');
|
|
370
|
+
const { prettyPrintHtml } = await import('../src/lib.js');
|
|
371
|
+
const output =
|
|
372
|
+
explicitOutput === '-'
|
|
373
|
+
? null
|
|
374
|
+
: explicitOutput ||
|
|
375
|
+
deriveOutputPath(absoluteUrl, archiveExtension(archiveFormat), dataDir);
|
|
376
|
+
const archiverFn = archiver.default ? archiver.default : archiver;
|
|
377
|
+
const archive = archiverFn('zip', { zlib: { level: 9 } });
|
|
378
|
+
archive.append(archiveResult.markdown, { name: 'document.md' });
|
|
379
|
+
archive.append(
|
|
380
|
+
prettyHtml ? prettyPrintHtml(archiveResult.html) : archiveResult.html,
|
|
381
|
+
{ name: 'document.html' }
|
|
382
|
+
);
|
|
383
|
+
for (const img of archiveResult.images || []) {
|
|
384
|
+
archive.append(img.data, { name: `images/${img.filename}` });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (output) {
|
|
388
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
389
|
+
const outStream = fs.createWriteStream(output);
|
|
390
|
+
archive.pipe(outStream);
|
|
391
|
+
await archive.finalize();
|
|
392
|
+
await new Promise((resolve) => outStream.on('close', resolve));
|
|
393
|
+
console.error(`Google Doc (archive) saved to: ${output}`);
|
|
394
|
+
} else {
|
|
395
|
+
const { PassThrough } = await import('stream');
|
|
396
|
+
const passthrough = new PassThrough();
|
|
397
|
+
archive.pipe(passthrough);
|
|
398
|
+
passthrough.pipe(process.stdout);
|
|
399
|
+
await archive.finalize();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
311
403
|
async function captureUrl(url, options) {
|
|
312
404
|
const {
|
|
313
405
|
format,
|
|
@@ -326,7 +418,10 @@ async function captureUrl(url, options) {
|
|
|
326
418
|
imagesDir,
|
|
327
419
|
dataDir,
|
|
328
420
|
keepOriginalLinks,
|
|
421
|
+
capture,
|
|
422
|
+
verbose,
|
|
329
423
|
} = options;
|
|
424
|
+
const log = makeVerboseLog(verbose);
|
|
330
425
|
|
|
331
426
|
// Ensure URL is absolute
|
|
332
427
|
let absoluteUrl = url;
|
|
@@ -346,142 +441,299 @@ async function captureUrl(url, options) {
|
|
|
346
441
|
const { fetchHtml, convertToUtf8, convertRelativeUrls } =
|
|
347
442
|
await import('../src/lib.js');
|
|
348
443
|
const { createBrowser } = await import('../src/browser.js');
|
|
349
|
-
const {
|
|
350
|
-
|
|
444
|
+
const {
|
|
445
|
+
isGoogleDocsUrl,
|
|
446
|
+
fetchGoogleDoc,
|
|
447
|
+
fetchGoogleDocAsMarkdown,
|
|
448
|
+
fetchGoogleDocFromDocsApi,
|
|
449
|
+
captureGoogleDocWithBrowser,
|
|
450
|
+
selectGoogleDocsCaptureMethod,
|
|
451
|
+
} = await import('../src/gdocs.js');
|
|
351
452
|
const { stripBase64Images } = await import('../src/extract-images.js');
|
|
352
453
|
|
|
353
454
|
const normalizedFormat = format.toLowerCase();
|
|
455
|
+
log.debug(() => ({
|
|
456
|
+
event: 'capture.start',
|
|
457
|
+
url: absoluteUrl,
|
|
458
|
+
format: normalizedFormat,
|
|
459
|
+
capture,
|
|
460
|
+
engine,
|
|
461
|
+
hasApiToken: Boolean(options.apiToken),
|
|
462
|
+
}));
|
|
354
463
|
|
|
355
|
-
//
|
|
464
|
+
// Google Docs capture honors --capture:
|
|
465
|
+
// - browser: load /edit and extract DOCS_modelChunk
|
|
466
|
+
// - api without token: public export endpoint
|
|
467
|
+
// - api with token: docs.googleapis.com REST API
|
|
356
468
|
if (isGoogleDocsUrl(absoluteUrl)) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
469
|
+
const apiToken = options.apiToken;
|
|
470
|
+
const gdocsMethod = selectGoogleDocsCaptureMethod(capture, apiToken);
|
|
471
|
+
const modelFormats = new Set([
|
|
472
|
+
'archive',
|
|
473
|
+
'zip',
|
|
474
|
+
'markdown',
|
|
475
|
+
'md',
|
|
476
|
+
'html',
|
|
477
|
+
'txt',
|
|
478
|
+
'text',
|
|
479
|
+
]);
|
|
480
|
+
log.debug(() => ({
|
|
481
|
+
event: 'gdocs.capture.selected',
|
|
482
|
+
url: absoluteUrl,
|
|
483
|
+
method: gdocsMethod,
|
|
484
|
+
format: normalizedFormat,
|
|
485
|
+
modelFormat: modelFormats.has(normalizedFormat),
|
|
486
|
+
hasApiToken: Boolean(apiToken),
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
if (
|
|
490
|
+
gdocsMethod === 'browser-model' &&
|
|
491
|
+
!modelFormats.has(normalizedFormat)
|
|
492
|
+
) {
|
|
493
|
+
log.debug(() => ({
|
|
494
|
+
event: 'gdocs.capture.fallback-browser-pipeline',
|
|
495
|
+
reason: 'requested format is not supported by editor model renderer',
|
|
496
|
+
format: normalizedFormat,
|
|
497
|
+
}));
|
|
498
|
+
// Screenshot/PDF/DOCX formats should use the regular browser pipeline below.
|
|
499
|
+
} else if (gdocsMethod === 'browser-model') {
|
|
500
|
+
try {
|
|
501
|
+
const result = await captureGoogleDocWithBrowser(absoluteUrl, {
|
|
502
|
+
engine,
|
|
373
503
|
apiToken,
|
|
504
|
+
log,
|
|
374
505
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
506
|
+
log.debug(() => ({
|
|
507
|
+
event: 'gdocs.capture.browser-model.rendered',
|
|
508
|
+
documentId: result.documentId,
|
|
509
|
+
blocks: result.capture?.blocks?.length || 0,
|
|
510
|
+
tables: result.capture?.tables?.length || 0,
|
|
511
|
+
images: result.capture?.images?.length || 0,
|
|
512
|
+
markdownBytes: Buffer.byteLength(result.markdown || ''),
|
|
513
|
+
htmlBytes: Buffer.byteLength(result.html || ''),
|
|
514
|
+
textBytes: Buffer.byteLength(result.text || ''),
|
|
515
|
+
}));
|
|
516
|
+
if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
|
|
517
|
+
await writeGoogleDocsArchive({
|
|
518
|
+
archiveResult: { ...result, images: [] },
|
|
519
|
+
absoluteUrl,
|
|
520
|
+
explicitOutput,
|
|
521
|
+
dataDir,
|
|
522
|
+
archiveFormat: options.archiveFormat || 'zip',
|
|
523
|
+
prettyHtml: options.prettyHtml,
|
|
392
524
|
});
|
|
393
|
-
for (const img of archiveResult.images) {
|
|
394
|
-
archive.append(img.data, { name: `images/${img.filename}` });
|
|
395
|
-
}
|
|
396
|
-
await archive.finalize();
|
|
397
|
-
await new Promise((resolve) => outStream.on('close', resolve));
|
|
398
|
-
console.error(`Google Doc (archive) saved to: ${output}`);
|
|
399
525
|
} else {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
526
|
+
const rendered =
|
|
527
|
+
normalizedFormat === 'html'
|
|
528
|
+
? result.html
|
|
529
|
+
: normalizedFormat === 'txt' || normalizedFormat === 'text'
|
|
530
|
+
? result.text
|
|
531
|
+
: result.markdown;
|
|
532
|
+
const ext =
|
|
533
|
+
normalizedFormat === 'html'
|
|
534
|
+
? 'html'
|
|
535
|
+
: normalizedFormat === 'txt' || normalizedFormat === 'text'
|
|
536
|
+
? 'txt'
|
|
537
|
+
: 'md';
|
|
538
|
+
writeGoogleDocsTextOutput({
|
|
539
|
+
content: rendered,
|
|
540
|
+
absoluteUrl,
|
|
541
|
+
ext,
|
|
542
|
+
explicitOutput,
|
|
543
|
+
dataDir,
|
|
544
|
+
label: `Google Doc (${gdocsMethod})`,
|
|
415
545
|
});
|
|
416
|
-
for (const img of archiveResult.images) {
|
|
417
|
-
archive.append(img.data, { name: `images/${img.filename}` });
|
|
418
|
-
}
|
|
419
|
-
await archive.finalize();
|
|
420
546
|
}
|
|
421
|
-
|
|
422
|
-
|
|
547
|
+
return;
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
} else if (gdocsMethod === 'docs-api') {
|
|
553
|
+
try {
|
|
554
|
+
const result = await fetchGoogleDocFromDocsApi(absoluteUrl, {
|
|
423
555
|
apiToken,
|
|
556
|
+
log,
|
|
424
557
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
558
|
+
log.debug(() => ({
|
|
559
|
+
event: 'gdocs.capture.docs-api.rendered',
|
|
560
|
+
documentId: result.documentId,
|
|
561
|
+
markdownBytes: Buffer.byteLength(result.markdown || ''),
|
|
562
|
+
htmlBytes: Buffer.byteLength(result.html || ''),
|
|
563
|
+
textBytes: Buffer.byteLength(result.text || ''),
|
|
564
|
+
}));
|
|
565
|
+
if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
|
|
566
|
+
await writeGoogleDocsArchive({
|
|
567
|
+
archiveResult: { ...result, images: [] },
|
|
568
|
+
absoluteUrl,
|
|
569
|
+
explicitOutput,
|
|
570
|
+
dataDir,
|
|
571
|
+
archiveFormat: options.archiveFormat || 'zip',
|
|
572
|
+
prettyHtml: options.prettyHtml,
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
const rendered =
|
|
576
|
+
normalizedFormat === 'html'
|
|
577
|
+
? result.html
|
|
578
|
+
: normalizedFormat === 'txt' || normalizedFormat === 'text'
|
|
579
|
+
? result.text
|
|
580
|
+
: result.markdown;
|
|
581
|
+
const ext =
|
|
582
|
+
normalizedFormat === 'html'
|
|
583
|
+
? 'html'
|
|
584
|
+
: normalizedFormat === 'txt' || normalizedFormat === 'text'
|
|
585
|
+
? 'txt'
|
|
586
|
+
: 'md';
|
|
587
|
+
writeGoogleDocsTextOutput({
|
|
588
|
+
content: rendered,
|
|
589
|
+
absoluteUrl,
|
|
590
|
+
ext,
|
|
591
|
+
explicitOutput,
|
|
592
|
+
dataDir,
|
|
593
|
+
label: `Google Doc (${gdocsMethod})`,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
} catch (err) {
|
|
598
|
+
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
try {
|
|
603
|
+
const { extractAndSaveImages } =
|
|
604
|
+
await import('../src/extract-images.js');
|
|
605
|
+
if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
|
|
606
|
+
const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
|
|
607
|
+
const { default: archiver } = await import('archiver');
|
|
608
|
+
const archiveFormat = options.archiveFormat || 'zip';
|
|
609
|
+
const ext =
|
|
610
|
+
archiveFormat === 'tar.gz' || archiveFormat === 'gz'
|
|
611
|
+
? 'tar.gz'
|
|
612
|
+
: archiveFormat === '7z'
|
|
613
|
+
? '7z'
|
|
614
|
+
: archiveFormat === 'tar'
|
|
615
|
+
? 'tar'
|
|
616
|
+
: 'zip';
|
|
617
|
+
const archiveResult = await fetchGoogleDocAsArchive(absoluteUrl, {
|
|
618
|
+
apiToken,
|
|
619
|
+
log,
|
|
620
|
+
});
|
|
621
|
+
const output =
|
|
622
|
+
explicitOutput === '-'
|
|
623
|
+
? null
|
|
624
|
+
: explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
|
|
625
|
+
if (output) {
|
|
626
|
+
const outStream = fs.createWriteStream(output);
|
|
627
|
+
const archive = archiver.default
|
|
628
|
+
? archiver.default('zip', { zlib: { level: 9 } })
|
|
629
|
+
: archiver('zip', { zlib: { level: 9 } });
|
|
630
|
+
archive.pipe(outStream);
|
|
631
|
+
const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
|
|
632
|
+
archive.append(archiveResult.markdown, { name: 'document.md' });
|
|
633
|
+
const htmlContent = options.prettyHtml
|
|
634
|
+
? ppHtml(archiveResult.html)
|
|
635
|
+
: archiveResult.html;
|
|
636
|
+
archive.append(htmlContent, {
|
|
637
|
+
name: 'document.html',
|
|
638
|
+
});
|
|
639
|
+
for (const img of archiveResult.images) {
|
|
640
|
+
archive.append(img.data, { name: `images/${img.filename}` });
|
|
440
641
|
}
|
|
642
|
+
await archive.finalize();
|
|
643
|
+
await new Promise((resolve) => outStream.on('close', resolve));
|
|
644
|
+
console.error(`Google Doc (archive) saved to: ${output}`);
|
|
441
645
|
} else {
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
646
|
+
const { PassThrough } = await import('stream');
|
|
647
|
+
const passthrough = new PassThrough();
|
|
648
|
+
const archiverMod = await import('archiver');
|
|
649
|
+
const archiverFn =
|
|
650
|
+
archiverMod.default?.default || archiverMod.default;
|
|
651
|
+
const archive = archiverFn('zip', { zlib: { level: 9 } });
|
|
652
|
+
archive.pipe(passthrough);
|
|
653
|
+
passthrough.pipe(process.stdout);
|
|
654
|
+
const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
|
|
655
|
+
archive.append(archiveResult.markdown, { name: 'document.md' });
|
|
656
|
+
const htmlContent2 = options.prettyHtml
|
|
657
|
+
? ppHtml2(archiveResult.html)
|
|
658
|
+
: archiveResult.html;
|
|
659
|
+
archive.append(htmlContent2, {
|
|
660
|
+
name: 'document.html',
|
|
445
661
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
console.error(
|
|
449
|
-
`Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
|
|
450
|
-
);
|
|
662
|
+
for (const img of archiveResult.images) {
|
|
663
|
+
archive.append(img.data, { name: `images/${img.filename}` });
|
|
451
664
|
}
|
|
665
|
+
await archive.finalize();
|
|
666
|
+
}
|
|
667
|
+
} else if (
|
|
668
|
+
normalizedFormat === 'markdown' ||
|
|
669
|
+
normalizedFormat === 'md'
|
|
670
|
+
) {
|
|
671
|
+
const result = await fetchGoogleDocAsMarkdown(absoluteUrl, {
|
|
672
|
+
apiToken,
|
|
673
|
+
log,
|
|
674
|
+
});
|
|
675
|
+
let { markdown } = result;
|
|
676
|
+
const output =
|
|
677
|
+
explicitOutput === '-'
|
|
678
|
+
? null
|
|
679
|
+
: explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
|
|
680
|
+
if (output) {
|
|
681
|
+
if (embedImages) {
|
|
682
|
+
// Keep base64 data URIs inline
|
|
683
|
+
} else if (keepOriginalLinks) {
|
|
684
|
+
const strip = stripBase64Images(markdown);
|
|
685
|
+
if (strip.stripped > 0) {
|
|
686
|
+
markdown = strip.markdown;
|
|
687
|
+
console.error(
|
|
688
|
+
`Stripped ${strip.stripped} base64 images (keeping original links)`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
const outputDir = path.dirname(output);
|
|
693
|
+
const extracted = extractAndSaveImages(markdown, outputDir, {
|
|
694
|
+
imagesDir: options.imagesDir || 'images',
|
|
695
|
+
});
|
|
696
|
+
if (extracted.extracted > 0) {
|
|
697
|
+
markdown = extracted.markdown;
|
|
698
|
+
console.error(
|
|
699
|
+
`Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
704
|
+
fs.writeFileSync(output, markdown, 'utf-8');
|
|
705
|
+
console.error(`Google Doc Markdown saved to: ${output}`);
|
|
706
|
+
} else {
|
|
707
|
+
process.stdout.write(markdown);
|
|
452
708
|
}
|
|
453
|
-
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
454
|
-
fs.writeFileSync(output, markdown, 'utf-8');
|
|
455
|
-
console.error(`Google Doc Markdown saved to: ${output}`);
|
|
456
|
-
} else {
|
|
457
|
-
process.stdout.write(markdown);
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
const gdocsFormat =
|
|
461
|
-
normalizedFormat === 'png' || normalizedFormat === 'image'
|
|
462
|
-
? 'html'
|
|
463
|
-
: normalizedFormat;
|
|
464
|
-
const result = await fetchGoogleDoc(absoluteUrl, {
|
|
465
|
-
format: gdocsFormat,
|
|
466
|
-
apiToken,
|
|
467
|
-
});
|
|
468
|
-
const output =
|
|
469
|
-
explicitOutput === '-'
|
|
470
|
-
? null
|
|
471
|
-
: explicitOutput ||
|
|
472
|
-
deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
|
|
473
|
-
if (output) {
|
|
474
|
-
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
475
|
-
fs.writeFileSync(output, result.content, 'utf-8');
|
|
476
|
-
console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
|
|
477
709
|
} else {
|
|
478
|
-
|
|
710
|
+
const gdocsFormat =
|
|
711
|
+
normalizedFormat === 'png' || normalizedFormat === 'image'
|
|
712
|
+
? 'html'
|
|
713
|
+
: normalizedFormat;
|
|
714
|
+
const result = await fetchGoogleDoc(absoluteUrl, {
|
|
715
|
+
format: gdocsFormat,
|
|
716
|
+
apiToken,
|
|
717
|
+
log,
|
|
718
|
+
});
|
|
719
|
+
const output =
|
|
720
|
+
explicitOutput === '-'
|
|
721
|
+
? null
|
|
722
|
+
: explicitOutput ||
|
|
723
|
+
deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
|
|
724
|
+
if (output) {
|
|
725
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
726
|
+
fs.writeFileSync(output, result.content, 'utf-8');
|
|
727
|
+
console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
|
|
728
|
+
} else {
|
|
729
|
+
process.stdout.write(result.content);
|
|
730
|
+
}
|
|
479
731
|
}
|
|
732
|
+
return;
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
735
|
+
process.exit(1);
|
|
480
736
|
}
|
|
481
|
-
return;
|
|
482
|
-
} catch (err) {
|
|
483
|
-
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
484
|
-
process.exit(1);
|
|
485
737
|
}
|
|
486
738
|
}
|
|
487
739
|
|
|
@@ -894,6 +1146,7 @@ async function main() {
|
|
|
894
1146
|
archiveFormat: config.archiveFormat,
|
|
895
1147
|
keepOriginalLinks: config.keepOriginalLinks,
|
|
896
1148
|
prettyHtml: !config.noPrettyHtml,
|
|
1149
|
+
capture: config.capture,
|
|
897
1150
|
});
|
|
898
1151
|
} else {
|
|
899
1152
|
// No arguments - show error
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/web-capture",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.6",
|
|
4
4
|
"description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"he": "^1.2.0",
|
|
41
41
|
"iconv-lite": "^0.6.3",
|
|
42
42
|
"lino-arguments": "^0.3.0",
|
|
43
|
+
"log-lazy": "^1.0.4",
|
|
43
44
|
"node-fetch": "^2.7.0",
|
|
44
45
|
"playwright": "^1.49.0",
|
|
45
46
|
"puppeteer": "^24.8.2",
|
package/src/browser.js
CHANGED
|
@@ -172,6 +172,20 @@ function createPageAdapter(rawPage, commander, engineType) {
|
|
|
172
172
|
await commander.destroy();
|
|
173
173
|
await rawPage.close();
|
|
174
174
|
},
|
|
175
|
+
async addInitScript(fn) {
|
|
176
|
+
if (engineType === 'playwright' && rawPage.addInitScript) {
|
|
177
|
+
await rawPage.addInitScript(fn);
|
|
178
|
+
} else if (rawPage.evaluateOnNewDocument) {
|
|
179
|
+
await rawPage.evaluateOnNewDocument(fn);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
async waitForTimeout(ms) {
|
|
183
|
+
if (rawPage.waitForTimeout) {
|
|
184
|
+
await rawPage.waitForTimeout(ms);
|
|
185
|
+
} else {
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
}
|
|
188
|
+
},
|
|
175
189
|
|
|
176
190
|
// --- browser-commander v0.8.0+: PDF generation ---
|
|
177
191
|
// Usage: await page.pdf({ pdfOptions: { format: 'A4', printBackground: true } })
|
|
@@ -211,6 +225,7 @@ function createPageAdapter(rawPage, commander, engineType) {
|
|
|
211
225
|
|
|
212
226
|
// Engine type identifier
|
|
213
227
|
type: engineType,
|
|
228
|
+
rawPage,
|
|
214
229
|
};
|
|
215
230
|
}
|
|
216
231
|
|