@link-assistant/web-capture 1.7.5 → 1.7.7
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 +369 -115
- package/package.json +2 -1
- package/src/browser.js +15 -0
- package/src/gdocs.js +827 -12
- package/src/lib.js +9 -1
- package/src/localize-images.js +17 -2
- package/tests/unit/gdocs.test.js +161 -0
- package/tests/unit/html2md.test.js +45 -1
- package/tests/unit/localize-images.test.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.7.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9034d01: Add Habr archive-compatible markdown controls for figure-numbered image localization and preserved code block whitespace.
|
|
8
|
+
|
|
9
|
+
## 1.7.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Support Google Docs capture selection through `--capture browser` editor-model parsing, public export API capture, and authenticated Docs REST API capture.
|
|
14
|
+
|
|
3
15
|
## 1.7.5
|
|
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({
|
|
@@ -307,6 +324,82 @@ function deriveOutputPath(absoluteUrl, ext, dataDir) {
|
|
|
307
324
|
return path.join(dir, `document.${ext}`);
|
|
308
325
|
}
|
|
309
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
|
+
|
|
310
403
|
async function captureUrl(url, options) {
|
|
311
404
|
const {
|
|
312
405
|
format,
|
|
@@ -325,7 +418,10 @@ async function captureUrl(url, options) {
|
|
|
325
418
|
imagesDir,
|
|
326
419
|
dataDir,
|
|
327
420
|
keepOriginalLinks,
|
|
421
|
+
capture,
|
|
422
|
+
verbose,
|
|
328
423
|
} = options;
|
|
424
|
+
const log = makeVerboseLog(verbose);
|
|
329
425
|
|
|
330
426
|
// Ensure URL is absolute
|
|
331
427
|
let absoluteUrl = url;
|
|
@@ -345,142 +441,299 @@ async function captureUrl(url, options) {
|
|
|
345
441
|
const { fetchHtml, convertToUtf8, convertRelativeUrls } =
|
|
346
442
|
await import('../src/lib.js');
|
|
347
443
|
const { createBrowser } = await import('../src/browser.js');
|
|
348
|
-
const {
|
|
349
|
-
|
|
444
|
+
const {
|
|
445
|
+
isGoogleDocsUrl,
|
|
446
|
+
fetchGoogleDoc,
|
|
447
|
+
fetchGoogleDocAsMarkdown,
|
|
448
|
+
fetchGoogleDocFromDocsApi,
|
|
449
|
+
captureGoogleDocWithBrowser,
|
|
450
|
+
selectGoogleDocsCaptureMethod,
|
|
451
|
+
} = await import('../src/gdocs.js');
|
|
350
452
|
const { stripBase64Images } = await import('../src/extract-images.js');
|
|
351
453
|
|
|
352
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
|
+
}));
|
|
353
463
|
|
|
354
|
-
//
|
|
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
|
|
355
468
|
if (isGoogleDocsUrl(absoluteUrl)) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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,
|
|
372
503
|
apiToken,
|
|
504
|
+
log,
|
|
373
505
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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,
|
|
391
524
|
});
|
|
392
|
-
for (const img of archiveResult.images) {
|
|
393
|
-
archive.append(img.data, { name: `images/${img.filename}` });
|
|
394
|
-
}
|
|
395
|
-
await archive.finalize();
|
|
396
|
-
await new Promise((resolve) => outStream.on('close', resolve));
|
|
397
|
-
console.error(`Google Doc (archive) saved to: ${output}`);
|
|
398
525
|
} else {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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})`,
|
|
414
545
|
});
|
|
415
|
-
for (const img of archiveResult.images) {
|
|
416
|
-
archive.append(img.data, { name: `images/${img.filename}` });
|
|
417
|
-
}
|
|
418
|
-
await archive.finalize();
|
|
419
546
|
}
|
|
420
|
-
|
|
421
|
-
|
|
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, {
|
|
422
555
|
apiToken,
|
|
556
|
+
log,
|
|
423
557
|
});
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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}` });
|
|
439
641
|
}
|
|
642
|
+
await archive.finalize();
|
|
643
|
+
await new Promise((resolve) => outStream.on('close', resolve));
|
|
644
|
+
console.error(`Google Doc (archive) saved to: ${output}`);
|
|
440
645
|
} else {
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
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',
|
|
444
661
|
});
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
console.error(
|
|
448
|
-
`Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
|
|
449
|
-
);
|
|
662
|
+
for (const img of archiveResult.images) {
|
|
663
|
+
archive.append(img.data, { name: `images/${img.filename}` });
|
|
450
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);
|
|
451
708
|
}
|
|
452
|
-
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
453
|
-
fs.writeFileSync(output, markdown, 'utf-8');
|
|
454
|
-
console.error(`Google Doc Markdown saved to: ${output}`);
|
|
455
|
-
} else {
|
|
456
|
-
process.stdout.write(markdown);
|
|
457
|
-
}
|
|
458
|
-
} else {
|
|
459
|
-
const gdocsFormat =
|
|
460
|
-
normalizedFormat === 'png' || normalizedFormat === 'image'
|
|
461
|
-
? 'html'
|
|
462
|
-
: normalizedFormat;
|
|
463
|
-
const result = await fetchGoogleDoc(absoluteUrl, {
|
|
464
|
-
format: gdocsFormat,
|
|
465
|
-
apiToken,
|
|
466
|
-
});
|
|
467
|
-
const output =
|
|
468
|
-
explicitOutput === '-'
|
|
469
|
-
? null
|
|
470
|
-
: explicitOutput ||
|
|
471
|
-
deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
|
|
472
|
-
if (output) {
|
|
473
|
-
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
474
|
-
fs.writeFileSync(output, result.content, 'utf-8');
|
|
475
|
-
console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
|
|
476
709
|
} else {
|
|
477
|
-
|
|
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
|
+
}
|
|
478
731
|
}
|
|
732
|
+
return;
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
735
|
+
process.exit(1);
|
|
479
736
|
}
|
|
480
|
-
return;
|
|
481
|
-
} catch (err) {
|
|
482
|
-
console.error(`Error capturing Google Doc: ${err.message}`);
|
|
483
|
-
process.exit(1);
|
|
484
737
|
}
|
|
485
738
|
}
|
|
486
739
|
|
|
@@ -893,6 +1146,7 @@ async function main() {
|
|
|
893
1146
|
archiveFormat: config.archiveFormat,
|
|
894
1147
|
keepOriginalLinks: config.keepOriginalLinks,
|
|
895
1148
|
prettyHtml: !config.noPrettyHtml,
|
|
1149
|
+
capture: config.capture,
|
|
896
1150
|
});
|
|
897
1151
|
} else {
|
|
898
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.7",
|
|
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
|
|