@link-assistant/web-capture 1.7.26 → 1.7.27

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 1.7.27
4
+
5
+ ### Patch Changes
6
+
7
+ - 9cbb4c7: Pin the default `--format archive` layout contract so the zip always contains exactly `document.md` + `document.html` + `images/` (issue #113). The markdown-format archive endpoint previously bundled only `document.md` and `images/`, omitting the reference `document.html`; it now includes it on every path. Adds a reusable `buildArchiveFromHtml(html, baseUrl)` helper and an integration test that pins the layout so a future refactor cannot regress it.
8
+
3
9
  ## 1.7.26
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.7.26",
3
+ "version": "1.7.27",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -65,7 +65,8 @@
65
65
  "markdown-it": "^14.1.1",
66
66
  "nock": "^13.5.4",
67
67
  "prettier": "^3.6.2",
68
- "supertest": "^6.3.4"
68
+ "supertest": "^6.3.4",
69
+ "unzipper": "^0.12.3"
69
70
  },
70
71
  "type": "module",
71
72
  "keywords": [
package/src/archive.js CHANGED
@@ -170,14 +170,17 @@ export async function archiveHandler(req, res) {
170
170
  }
171
171
 
172
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
- }
173
+ appendMarkdownAndImages(archive, markdown);
174
+ } else {
175
+ archive.append(markdown, { name: 'document.md' });
178
176
  }
179
177
 
180
- archive.append(markdown, { name: 'document.md' });
178
+ // document.html — the source the markdown was derived from, for
179
+ // reference only, so the default archive layout (document.md +
180
+ // document.html + images/) is identical across capture paths (issue #113).
181
+ archive.append(prettyPrintHtml(convertRelativeUrls(html, absoluteUrl)), {
182
+ name: 'document.html',
183
+ });
181
184
  }
182
185
 
183
186
  // Download and add images if local mode
@@ -207,6 +210,57 @@ export async function archiveHandler(req, res) {
207
210
  }
208
211
  }
209
212
 
213
+ /**
214
+ * Extract base64 images from `markdown` into the archive's `images/` folder and
215
+ * append the rewritten markdown as `document.md`. Shared by the archive
216
+ * endpoint and {@link buildArchiveFromHtml} so the default layout stays
217
+ * identical across capture paths.
218
+ *
219
+ * @param {import('archiver').Archiver} archive - The archive being built
220
+ * @param {string} markdown - Markdown content (may contain base64 data URIs)
221
+ */
222
+ function appendMarkdownAndImages(archive, markdown) {
223
+ const { markdown: rewritten, images } = extractBase64ToBuffers(
224
+ markdown,
225
+ 'images'
226
+ );
227
+ for (const img of images) {
228
+ archive.append(img.buffer, { name: `images/${img.filename}` });
229
+ }
230
+ archive.append(rewritten, { name: 'document.md' });
231
+ }
232
+
233
+ /**
234
+ * Build a self-contained ZIP archive (as a Buffer) from raw HTML, matching the
235
+ * default `--format archive` layout contract (issue #113):
236
+ *
237
+ * - `document.md` — markdown referencing images by relative `images/` path
238
+ * - `document.html` — the source HTML the markdown was derived from (reference)
239
+ * - `images/` — every inline base64 image as a separate file
240
+ *
241
+ * @param {string} html - Source HTML to convert
242
+ * @param {string} baseUrl - Base URL for resolving relative links
243
+ * @returns {Promise<Buffer>} the finalized ZIP archive bytes
244
+ */
245
+ export async function buildArchiveFromHtml(html, baseUrl) {
246
+ const archive = archiver('zip', { zlib: { level: 9 } });
247
+ const chunks = [];
248
+ archive.on('data', (chunk) => chunks.push(chunk));
249
+ const finished = new Promise((resolve, reject) => {
250
+ archive.on('end', resolve);
251
+ archive.on('error', reject);
252
+ });
253
+
254
+ appendMarkdownAndImages(archive, convertHtmlToMarkdown(html, baseUrl));
255
+ archive.append(prettyPrintHtml(convertRelativeUrls(html, baseUrl)), {
256
+ name: 'document.html',
257
+ });
258
+
259
+ await archive.finalize();
260
+ await finished;
261
+ return Buffer.concat(chunks);
262
+ }
263
+
210
264
  function guessImageExtension(url) {
211
265
  const pathname = new URL(url).pathname.toLowerCase();
212
266
  if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) {
@@ -9,6 +9,7 @@
9
9
  import { jest } from '@jest/globals';
10
10
  import request from 'supertest';
11
11
  import nock from 'nock';
12
+ import unzipper from 'unzipper';
12
13
  import { app } from '../../src/index.js';
13
14
 
14
15
  jest.setTimeout(60000);
@@ -95,6 +96,14 @@ describe('API Endpoint Tests', () => {
95
96
  // ZIP signature: PK (0x50 0x4B)
96
97
  expect(res.body[0]).toBe(0x50);
97
98
  expect(res.body[1]).toBe(0x4b);
99
+
100
+ // Default archive layout contract (issue #113): the markdown archive must
101
+ // bundle document.md AND document.html for reference.
102
+ const dir = await unzipper.Open.buffer(res.body);
103
+ const names = dir.files.map((f) => f.path);
104
+ expect(names).toEqual(
105
+ expect.arrayContaining(['document.md', 'document.html'])
106
+ );
98
107
  });
99
108
 
100
109
  it('returns a ZIP archive with HTML format', async () => {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pin the default `--format archive` layout contract (issue #113):
3
+ * the zip contains exactly `document.md` + `document.html` + `images/`.
4
+ */
5
+ import unzipper from 'unzipper';
6
+ import { buildArchiveFromHtml } from '../../src/archive.js';
7
+
8
+ const TINY_PNG =
9
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
10
+ const FIXTURE_HTML = `<!doctype html><html><body>
11
+ <h1>Hi</h1><p>Para 1.</p>
12
+ <p><img alt="" src="data:image/png;base64,${TINY_PNG}"></p>
13
+ <p>Para 2.</p>
14
+ </body></html>`;
15
+
16
+ it('default archive contains document.md, document.html, and images/ folder', async () => {
17
+ const buf = await buildArchiveFromHtml(
18
+ FIXTURE_HTML,
19
+ 'https://example.invalid/'
20
+ );
21
+ const dir = await unzipper.Open.buffer(buf);
22
+ const names = dir.files.map((f) => f.path);
23
+
24
+ expect(names).toEqual(
25
+ expect.arrayContaining(['document.md', 'document.html'])
26
+ );
27
+ expect(names.some((n) => n.startsWith('images/'))).toBe(true);
28
+
29
+ const mdEntry = dir.files.find((f) => f.path === 'document.md');
30
+ const md = (await mdEntry.buffer()).toString();
31
+ expect(md).toMatch(/images\//);
32
+ expect(md).not.toMatch(/data:image/);
33
+
34
+ const htmlEntry = dir.files.find((f) => f.path === 'document.html');
35
+ const html = (await htmlEntry.buffer()).toString();
36
+ expect(html).toMatch(/<h1>/);
37
+ });