@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.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
+
});
|