@link-assistant/web-capture 1.4.2 → 1.7.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.
@@ -5,6 +5,7 @@
5
5
  // 2. Capture mode: web-capture <url> [options]
6
6
 
7
7
  import fs from 'fs';
8
+ import path from 'path';
8
9
  import { URL } from 'node:url';
9
10
  import { makeConfig } from 'lino-arguments';
10
11
 
@@ -33,8 +34,8 @@ const config = makeConfig({
33
34
  alias: 'f',
34
35
  type: 'string',
35
36
  description:
36
- 'Output format: html, markdown, md, image, png, jpeg, pdf, docx, archive',
37
- default: 'html',
37
+ 'Output format: markdown, md, html, image, png, jpeg, pdf, docx, archive',
38
+ default: 'markdown',
38
39
  })
39
40
  .option('theme', {
40
41
  alias: 't',
@@ -89,35 +90,65 @@ const config = makeConfig({
89
90
  type: 'string',
90
91
  description: 'Path to .lenv configuration file',
91
92
  })
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
93
  .option('extractLatex', {
99
94
  type: 'boolean',
100
95
  description:
101
- 'Extract LaTeX formulas from img.formula, KaTeX, MathJax (default: true when --enhanced)',
102
- default: true,
96
+ 'Extract LaTeX formulas from img.formula, KaTeX, MathJax (default: true). Use --no-extract-latex to disable.',
97
+ default: getenv('WEB_CAPTURE_EXTRACT_LATEX', true),
103
98
  })
104
99
  .option('extractMetadata', {
105
100
  type: 'boolean',
106
101
  description:
107
- 'Extract article metadata (author, date, hubs, tags) (default: true when --enhanced)',
108
- default: true,
102
+ 'Extract article metadata (author, date, hubs, tags) (default: true). Use --no-extract-metadata to disable.',
103
+ default: getenv('WEB_CAPTURE_EXTRACT_METADATA', true),
109
104
  })
110
105
  .option('postProcess', {
111
106
  type: 'boolean',
112
107
  description:
113
- 'Apply post-processing (unicode normalization, LaTeX spacing) (default: true when --enhanced)',
114
- default: true,
108
+ 'Apply post-processing (unicode normalization, LaTeX spacing) (default: true). Use --no-post-process to disable.',
109
+ default: getenv('WEB_CAPTURE_POST_PROCESS', true),
115
110
  })
116
111
  .option('detectCodeLanguage', {
117
112
  type: 'boolean',
118
113
  description:
119
- 'Detect and correct code block languages (default: true when --enhanced)',
120
- default: true,
114
+ 'Detect and correct code block languages (default: true). Use --no-detect-code-language to disable.',
115
+ default: getenv('WEB_CAPTURE_DETECT_CODE_LANGUAGE', true),
116
+ })
117
+ .option('embedImages', {
118
+ type: 'boolean',
119
+ description:
120
+ 'Keep images as inline base64 data URIs instead of extracting to files (default: false). Use --embed-images to enable.',
121
+ default: getenv('WEB_CAPTURE_EMBED_IMAGES', false),
122
+ })
123
+ .option('imagesDir', {
124
+ type: 'string',
125
+ description:
126
+ 'Directory name for extracted images, relative to output file (default: images)',
127
+ default: getenv('WEB_CAPTURE_IMAGES_DIR', 'images'),
128
+ })
129
+ .option('dataDir', {
130
+ type: 'string',
131
+ description:
132
+ 'Base directory for auto-derived output paths when -o is omitted ' +
133
+ '(default: ./data/web-capture)',
134
+ default: getenv('WEB_CAPTURE_DATA_DIR', './data/web-capture'),
135
+ })
136
+ .option('archive', {
137
+ type: 'string',
138
+ description:
139
+ 'Create archive output. Formats: zip (default), 7z, tar.gz (alias gz), tar. Use without value for zip.',
140
+ })
141
+ .option('noExtractImages', {
142
+ type: 'boolean',
143
+ description: 'Alias for --embed-images: keep images inline as base64',
144
+ default: false,
145
+ })
146
+ .option('keepOriginalLinks', {
147
+ type: 'boolean',
148
+ description:
149
+ 'Keep original remote image URLs instead of downloading or extracting. ' +
150
+ 'Base64 data URIs are stripped (no original URL to restore).',
151
+ default: getenv('WEB_CAPTURE_KEEP_ORIGINAL_LINKS', false),
121
152
  })
122
153
  .option('dualTheme', {
123
154
  type: 'boolean',
@@ -150,6 +181,12 @@ const config = makeConfig({
150
181
  'API token for authenticated capture (e.g., Google Docs private documents). Can also be set via API_TOKEN env variable.',
151
182
  default: getenv('API_TOKEN', undefined),
152
183
  })
184
+ .option('noPrettyHtml', {
185
+ type: 'boolean',
186
+ description:
187
+ 'Disable HTML pretty-printing in archive output (output minified HTML instead of indented).',
188
+ default: getenv('WEB_CAPTURE_NO_PRETTY_HTML', false),
189
+ })
153
190
  .help('help')
154
191
  .alias('help', 'h')
155
192
  .version()
@@ -241,10 +278,20 @@ async function startServer(port) {
241
278
  });
242
279
  }
243
280
 
281
+ function deriveOutputPath(absoluteUrl, ext, dataDir) {
282
+ const parsed = new URL(absoluteUrl);
283
+ const host = parsed.hostname;
284
+ let urlPath = parsed.pathname.replace(/\/+$/, '') || '';
285
+ urlPath = urlPath.replace(/^\//, '');
286
+ const dir = path.join(dataDir, host, urlPath);
287
+ fs.mkdirSync(dir, { recursive: true });
288
+ return path.join(dir, `document.${ext}`);
289
+ }
290
+
244
291
  async function captureUrl(url, options) {
245
292
  const {
246
293
  format,
247
- output,
294
+ output: explicitOutput,
248
295
  engine,
249
296
  theme,
250
297
  width,
@@ -254,6 +301,11 @@ async function captureUrl(url, options) {
254
301
  // localImages is used by archive format
255
302
  // eslint-disable-next-line no-unused-vars
256
303
  localImages,
304
+ embedImages,
305
+ // eslint-disable-next-line no-unused-vars
306
+ imagesDir,
307
+ dataDir,
308
+ keepOriginalLinks,
257
309
  } = options;
258
310
 
259
311
  // Ensure URL is absolute
@@ -271,15 +323,12 @@ async function captureUrl(url, options) {
271
323
  }
272
324
 
273
325
  // Import required modules
274
- const {
275
- fetchHtml,
276
- convertHtmlToMarkdown,
277
- convertToUtf8,
278
- convertRelativeUrls,
279
- } = await import('../src/lib.js');
326
+ const { fetchHtml, convertToUtf8, convertRelativeUrls } =
327
+ await import('../src/lib.js');
280
328
  const { createBrowser } = await import('../src/browser.js');
281
329
  const { isGoogleDocsUrl, fetchGoogleDoc, fetchGoogleDocAsMarkdown } =
282
330
  await import('../src/gdocs.js');
331
+ const { stripBase64Images } = await import('../src/extract-images.js');
283
332
 
284
333
  const normalizedFormat = format.toLowerCase();
285
334
 
@@ -287,15 +336,105 @@ async function captureUrl(url, options) {
287
336
  if (isGoogleDocsUrl(absoluteUrl)) {
288
337
  try {
289
338
  const apiToken = options.apiToken;
290
- if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
339
+ const { extractAndSaveImages } = await import('../src/extract-images.js');
340
+ if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
341
+ const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
342
+ const { default: archiver } = await import('archiver');
343
+ const archiveFormat = options.archiveFormat || 'zip';
344
+ const ext =
345
+ archiveFormat === 'tar.gz' || archiveFormat === 'gz'
346
+ ? 'tar.gz'
347
+ : archiveFormat === '7z'
348
+ ? '7z'
349
+ : archiveFormat === 'tar'
350
+ ? 'tar'
351
+ : 'zip';
352
+ const archiveResult = await fetchGoogleDocAsArchive(absoluteUrl, {
353
+ apiToken,
354
+ });
355
+ const output =
356
+ explicitOutput === '-'
357
+ ? null
358
+ : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
359
+ if (output) {
360
+ const outStream = fs.createWriteStream(output);
361
+ const archive = archiver.default
362
+ ? archiver.default('zip', { zlib: { level: 9 } })
363
+ : archiver('zip', { zlib: { level: 9 } });
364
+ archive.pipe(outStream);
365
+ const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
366
+ archive.append(archiveResult.markdown, { name: 'document.md' });
367
+ const htmlContent = options.prettyHtml
368
+ ? ppHtml(archiveResult.html)
369
+ : archiveResult.html;
370
+ archive.append(htmlContent, {
371
+ name: 'document.html',
372
+ });
373
+ for (const img of archiveResult.images) {
374
+ archive.append(img.data, { name: `images/${img.filename}` });
375
+ }
376
+ await archive.finalize();
377
+ await new Promise((resolve) => outStream.on('close', resolve));
378
+ console.error(`Google Doc (archive) saved to: ${output}`);
379
+ } else {
380
+ const { PassThrough } = await import('stream');
381
+ const passthrough = new PassThrough();
382
+ const archiverMod = await import('archiver');
383
+ const archiverFn =
384
+ archiverMod.default?.default || archiverMod.default;
385
+ const archive = archiverFn('zip', { zlib: { level: 9 } });
386
+ archive.pipe(passthrough);
387
+ passthrough.pipe(process.stdout);
388
+ const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
389
+ archive.append(archiveResult.markdown, { name: 'document.md' });
390
+ const htmlContent2 = options.prettyHtml
391
+ ? ppHtml2(archiveResult.html)
392
+ : archiveResult.html;
393
+ archive.append(htmlContent2, {
394
+ name: 'document.html',
395
+ });
396
+ for (const img of archiveResult.images) {
397
+ archive.append(img.data, { name: `images/${img.filename}` });
398
+ }
399
+ await archive.finalize();
400
+ }
401
+ } else if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
291
402
  const result = await fetchGoogleDocAsMarkdown(absoluteUrl, {
292
403
  apiToken,
293
404
  });
405
+ let { markdown } = result;
406
+ const output =
407
+ explicitOutput === '-'
408
+ ? null
409
+ : explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
294
410
  if (output) {
295
- fs.writeFileSync(output, result.markdown, 'utf-8');
411
+ if (embedImages) {
412
+ // Keep base64 data URIs inline
413
+ } else if (keepOriginalLinks) {
414
+ const strip = stripBase64Images(markdown);
415
+ if (strip.stripped > 0) {
416
+ markdown = strip.markdown;
417
+ console.error(
418
+ `Stripped ${strip.stripped} base64 images (keeping original links)`
419
+ );
420
+ }
421
+ } else {
422
+ const outputDir = path.dirname(output);
423
+ const extracted = extractAndSaveImages(markdown, outputDir, {
424
+ imagesDir: options.imagesDir || 'images',
425
+ });
426
+ if (extracted.extracted > 0) {
427
+ markdown = extracted.markdown;
428
+ console.error(
429
+ `Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
430
+ );
431
+ }
432
+ }
433
+ fs.mkdirSync(path.dirname(output), { recursive: true });
434
+ fs.writeFileSync(output, markdown, 'utf-8');
296
435
  console.error(`Google Doc Markdown saved to: ${output}`);
297
436
  } else {
298
- process.stdout.write(result.markdown);
437
+ process.stdout.write(markdown);
299
438
  }
300
439
  } else {
301
440
  const gdocsFormat =
@@ -306,7 +445,13 @@ async function captureUrl(url, options) {
306
445
  format: gdocsFormat,
307
446
  apiToken,
308
447
  });
448
+ const output =
449
+ explicitOutput === '-'
450
+ ? null
451
+ : explicitOutput ||
452
+ deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
309
453
  if (output) {
454
+ fs.mkdirSync(path.dirname(output), { recursive: true });
310
455
  fs.writeFileSync(output, result.content, 'utf-8');
311
456
  console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
312
457
  } else {
@@ -346,7 +491,7 @@ async function captureUrl(url, options) {
346
491
  fullPage,
347
492
  });
348
493
  const outPath =
349
- output ||
494
+ explicitOutput ||
350
495
  `${new URL(absoluteUrl).hostname.replace(/\./g, '_')}_${Date.now()}.jpg`;
351
496
  fs.writeFileSync(outPath, buffer);
352
497
  console.error(`JPEG screenshot saved to: ${outPath}`);
@@ -376,7 +521,7 @@ async function captureUrl(url, options) {
376
521
  printBackground: true,
377
522
  margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
378
523
  });
379
- const outPath = output || 'page.pdf';
524
+ const outPath = explicitOutput || 'page.pdf';
380
525
  fs.writeFileSync(outPath, pdfBuffer);
381
526
  console.error(`PDF saved to: ${outPath}`);
382
527
  } finally {
@@ -413,7 +558,7 @@ async function captureUrl(url, options) {
413
558
  }
414
559
  const doc = new Document({ sections: [{ children }] });
415
560
  const buffer = await Packer.toBuffer(doc);
416
- const outPath = output || 'page.docx';
561
+ const outPath = explicitOutput || 'page.docx';
417
562
  fs.writeFileSync(outPath, buffer);
418
563
  console.error(`DOCX saved to: ${outPath}`);
419
564
  } else if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
@@ -435,7 +580,7 @@ async function captureUrl(url, options) {
435
580
 
436
581
  const docFormat = options.documentFormat === 'html' ? 'html' : 'markdown';
437
582
  const outPath =
438
- output ||
583
+ explicitOutput ||
439
584
  `${new URL(absoluteUrl).hostname.replace(/\./g, '-')}-archive.zip`;
440
585
  const outStream = fs.createWriteStream(outPath);
441
586
  const archive = archiver.default
@@ -460,7 +605,11 @@ async function captureUrl(url, options) {
460
605
 
461
606
  // Build image map for local downloads
462
607
  const imageMap = new Map();
463
- if (options.localImages && uniqueImages.length > 0) {
608
+ if (
609
+ options.localImages &&
610
+ !keepOriginalLinks &&
611
+ uniqueImages.length > 0
612
+ ) {
464
613
  let idx = 1;
465
614
  for (const imgUrl of uniqueImages) {
466
615
  const ext = imgUrl.match(/\.(jpe?g|gif|webp|svg|png)/i)?.[1] || 'png';
@@ -488,7 +637,11 @@ async function captureUrl(url, options) {
488
637
  }
489
638
  });
490
639
  }
491
- archive.append($out.html(), { name: 'article.html' });
640
+ const { prettyPrintHtml } = await import('../src/lib.js');
641
+ const formattedHtml = options.prettyHtml
642
+ ? prettyPrintHtml($out.html())
643
+ : $out.html();
644
+ archive.append(formattedHtml, { name: 'document.html' });
492
645
  } else {
493
646
  let markdown = convertHtmlToMarkdown(html, absoluteUrl);
494
647
  if (imageMap.size > 0) {
@@ -496,7 +649,7 @@ async function captureUrl(url, options) {
496
649
  markdown = markdown.split(remoteUrl).join(localPath);
497
650
  }
498
651
  }
499
- archive.append(markdown, { name: 'article.md' });
652
+ archive.append(markdown, { name: 'document.md' });
500
653
  }
501
654
 
502
655
  // Download images
@@ -522,24 +675,49 @@ async function captureUrl(url, options) {
522
675
  await new Promise((resolve) => outStream.on('close', resolve));
523
676
  console.error(`Archive saved to: ${outPath}`);
524
677
  } else if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
525
- // Markdown format
678
+ // Markdown format — enhanced conversion is now the default
526
679
  const html = await fetchHtml(absoluteUrl);
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);
680
+ const { convertHtmlToMarkdownEnhanced } = await import('../src/lib.js');
681
+ const { extractAndSaveImages } = await import('../src/extract-images.js');
682
+ const result = convertHtmlToMarkdownEnhanced(html, absoluteUrl, {
683
+ extractLatex: options.extractLatex,
684
+ extractMetadata: options.extractMetadata,
685
+ postProcess: options.postProcess,
686
+ detectCodeLanguage: options.detectCodeLanguage,
687
+ });
688
+ let markdown = result.markdown;
689
+
690
+ const output =
691
+ explicitOutput === '-'
692
+ ? null
693
+ : explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
694
+ if (output) {
695
+ if (embedImages) {
696
+ // Keep base64 data URIs inline
697
+ } else if (keepOriginalLinks) {
698
+ const strip = stripBase64Images(markdown);
699
+ if (strip.stripped > 0) {
700
+ markdown = strip.markdown;
701
+ console.error(
702
+ `Stripped ${strip.stripped} base64 images (keeping original links)`
703
+ );
704
+ }
705
+ } else {
706
+ const outputDir = path.dirname(output);
707
+ const extracted = extractAndSaveImages(markdown, outputDir, {
708
+ imagesDir: options.imagesDir || 'images',
709
+ });
710
+ if (extracted.extracted > 0) {
711
+ markdown = extracted.markdown;
712
+ console.error(
713
+ `Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
714
+ );
715
+ }
716
+ }
540
717
  }
541
718
 
542
719
  if (output) {
720
+ fs.mkdirSync(path.dirname(output), { recursive: true });
543
721
  fs.writeFileSync(output, markdown, 'utf-8');
544
722
  console.error(`Markdown saved to: ${output}`);
545
723
  } else {
@@ -571,9 +749,9 @@ async function captureUrl(url, options) {
571
749
 
572
750
  const buffer = await page.screenshot({ type: 'png' });
573
751
 
574
- if (output) {
575
- fs.writeFileSync(output, buffer);
576
- console.error(`Screenshot saved to: ${output}`);
752
+ if (explicitOutput) {
753
+ fs.writeFileSync(explicitOutput, buffer);
754
+ console.error(`Screenshot saved to: ${explicitOutput}`);
577
755
  } else {
578
756
  // Generate default filename based on URL
579
757
  const urlObj = new URL(absoluteUrl);
@@ -627,7 +805,12 @@ async function captureUrl(url, options) {
627
805
  resultHtml = convertRelativeUrls(utf8Html, absoluteUrl);
628
806
  }
629
807
 
808
+ const output =
809
+ explicitOutput === '-'
810
+ ? null
811
+ : explicitOutput || deriveOutputPath(absoluteUrl, 'html', dataDir);
630
812
  if (output) {
813
+ fs.mkdirSync(path.dirname(output), { recursive: true });
631
814
  fs.writeFileSync(output, resultHtml, 'utf-8');
632
815
  console.error(`HTML saved to: ${output}`);
633
816
  } else {
@@ -648,6 +831,26 @@ async function main() {
648
831
  // Server mode
649
832
  await startServer(config.port);
650
833
  } else if (url) {
834
+ // --no-extract-images is an alias for --embed-images
835
+ if (config.noExtractImages) {
836
+ config.embedImages = true;
837
+ }
838
+ // --archive flag overrides format
839
+ if (config.archive !== undefined) {
840
+ const archiveFormat =
841
+ config.archive === true || config.archive === ''
842
+ ? 'zip'
843
+ : config.archive;
844
+ const validFormats = ['zip', '7z', 'tar.gz', 'gz', 'tar'];
845
+ if (!validFormats.includes(archiveFormat)) {
846
+ console.error(
847
+ `Error: Unsupported archive format "${archiveFormat}". Supported: ${validFormats.join(', ')}`
848
+ );
849
+ process.exit(1);
850
+ }
851
+ config.archiveFormat = archiveFormat === 'gz' ? 'tar.gz' : archiveFormat;
852
+ config.format = 'archive';
853
+ }
651
854
  // Capture mode
652
855
  await captureUrl(url, {
653
856
  format: config.format,
@@ -660,13 +863,18 @@ async function main() {
660
863
  fullPage: config.fullPage,
661
864
  localImages: config.localImages,
662
865
  documentFormat: config.documentFormat,
663
- enhanced: config.enhanced,
664
866
  extractLatex: config.extractLatex,
665
867
  extractMetadata: config.extractMetadata,
666
868
  postProcess: config.postProcess,
667
869
  detectCodeLanguage: config.detectCodeLanguage,
668
870
  dualTheme: config.dualTheme,
669
871
  apiToken: config.apiToken,
872
+ embedImages: config.embedImages,
873
+ imagesDir: config.imagesDir,
874
+ dataDir: config.dataDir,
875
+ archiveFormat: config.archiveFormat,
876
+ keepOriginalLinks: config.keepOriginalLinks,
877
+ prettyHtml: !config.noPrettyHtml,
670
878
  });
671
879
  } else {
672
880
  // No arguments - show error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.4.2",
3
+ "version": "1.7.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": {
@@ -73,6 +73,10 @@
73
73
  "microservice"
74
74
  ],
75
75
  "license": "Unlicense",
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "https://github.com/link-assistant/web-capture"
79
+ },
76
80
  "engines": {
77
81
  "node": ">=22.0.0 <23.0.0"
78
82
  },
package/src/archive.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * ZIP archive handler.
3
3
  *
4
4
  * Downloads a web page as a self-contained ZIP archive containing:
5
- * - article.md or article.html (document with either remote or local asset links)
5
+ * - document.md or document.html (document with either remote or local asset links)
6
6
  * - images/ (directory of downloaded images, when localImages=true)
7
7
  * - css/ (directory for stylesheets, when documentFormat=html and localImages=true)
8
8
  * - js/ (directory for scripts, when documentFormat=html and localImages=true)
@@ -23,8 +23,10 @@ import {
23
23
  fetchHtml,
24
24
  convertHtmlToMarkdown,
25
25
  convertRelativeUrls,
26
+ prettyPrintHtml,
26
27
  } from './lib.js';
27
28
  import { retry } from './retry.js';
29
+ import { extractBase64ToBuffers } from './extract-images.js';
28
30
 
29
31
  export async function archiveHandler(req, res) {
30
32
  const url = req.query.url;
@@ -32,7 +34,11 @@ export async function archiveHandler(req, res) {
32
34
  return res.status(400).send('Missing `url` parameter');
33
35
  }
34
36
 
35
- const localImages = req.query.localImages !== 'false'; // default true
37
+ const keepOriginalLinks = req.query.keepOriginalLinks === 'true';
38
+ const localImages = keepOriginalLinks
39
+ ? false
40
+ : req.query.localImages !== 'false';
41
+ const embedImages = req.query.embedImages === 'true';
36
42
  const documentFormat =
37
43
  req.query.documentFormat === 'html' ? 'html' : 'markdown';
38
44
 
@@ -135,8 +141,8 @@ export async function archiveHandler(req, res) {
135
141
  });
136
142
  }
137
143
 
138
- outputHtml = $out.html();
139
- archive.append(outputHtml, { name: 'article.html' });
144
+ outputHtml = prettyPrintHtml($out.html());
145
+ archive.append(outputHtml, { name: 'document.html' });
140
146
 
141
147
  // Download and add CSS files
142
148
  for (const { url: cssUrl, localPath } of cssFiles) {
@@ -158,13 +164,20 @@ export async function archiveHandler(req, res) {
158
164
  let markdown = convertHtmlToMarkdown(html, absoluteUrl);
159
165
 
160
166
  if (localImages && imageMap.size > 0) {
161
- // Rewrite image URLs in markdown to local paths
162
167
  for (const [remoteUrl, localPath] of imageMap) {
163
168
  markdown = markdown.split(remoteUrl).join(localPath);
164
169
  }
165
170
  }
166
171
 
167
- archive.append(markdown, { name: 'article.md' });
172
+ if (!embedImages && !keepOriginalLinks) {
173
+ const b64 = extractBase64ToBuffers(markdown, 'images');
174
+ markdown = b64.markdown;
175
+ for (const img of b64.images) {
176
+ archive.append(img.buffer, { name: `images/${img.filename}` });
177
+ }
178
+ }
179
+
180
+ archive.append(markdown, { name: 'document.md' });
168
181
  }
169
182
 
170
183
  // Download and add images if local mode
package/src/batch.js CHANGED
@@ -139,9 +139,9 @@ export function createConfigFromUrls(urls, defaults = {}) {
139
139
  url,
140
140
  title: `Article ${id}`,
141
141
  archivePath: `archive/${hostname.replace(/\./g, '-')}/${id}`,
142
- markdownFile: 'article.md',
143
- screenshotLightFile: 'article-light.png',
144
- screenshotDarkFile: 'article-dark.png',
142
+ markdownFile: 'document.md',
143
+ screenshotLightFile: 'document-light.png',
144
+ screenshotDarkFile: 'document-dark.png',
145
145
  imagesDir: 'images',
146
146
  hasLocalImages: true,
147
147
  };