@link-assistant/web-capture 1.12.0 → 2.0.1

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -1
  3. package/bin/web-capture.js +5 -7
  4. package/package.json +32 -18
  5. package/src/archive.js +1 -2
  6. package/src/binary-download.js +13 -0
  7. package/src/docx.js +14 -11
  8. package/src/fetch.js +1 -2
  9. package/src/figures.js +2 -12
  10. package/src/gdocs-images.js +0 -1
  11. package/src/gdocs.js +0 -1
  12. package/src/github.js +0 -1
  13. package/src/lib.js +1 -2
  14. package/src/localize-images.js +2 -12
  15. package/src/markdown.js +0 -1
  16. package/src/shared-dialog.js +0 -1
  17. package/src/stream.js +0 -1
  18. package/src/transport.js +23 -5
  19. package/src/txt.js +0 -1
  20. package/.changeset/README.md +0 -8
  21. package/.changeset/config.json +0 -11
  22. package/.dockerignore +0 -6
  23. package/.jscpd.json +0 -19
  24. package/.lenv +0 -8
  25. package/.prettierignore +0 -8
  26. package/.prettierrc +0 -10
  27. package/Dockerfile +0 -52
  28. package/babel.config.cjs +0 -13
  29. package/docker-compose.yml +0 -11
  30. package/eslint.config.js +0 -112
  31. package/examples/js/capture_cache.js +0 -12
  32. package/examples/js/engine_comparison.js +0 -39
  33. package/examples/js/html_download.js +0 -23
  34. package/examples/js/image_download.js +0 -36
  35. package/examples/js/markdown_download.js +0 -23
  36. package/examples/js/playwright_example.js +0 -23
  37. package/examples/python/html_download.py +0 -15
  38. package/examples/python/image_download.py +0 -20
  39. package/examples/python/markdown_download.py +0 -15
  40. package/examples/python/playwright_example.py +0 -22
  41. package/jest.config.mjs +0 -20
  42. package/tests/ci/dependency-freshness.test.js +0 -74
  43. package/tests/ci/detect-code-changes.test.js +0 -174
  44. package/tests/ci/docker-install.test.js +0 -31
  45. package/tests/ci/rust-dockerfile.test.js +0 -54
  46. package/tests/ci/workflow-policy.test.js +0 -79
  47. package/tests/e2e/docker.test.js +0 -186
  48. package/tests/e2e/process.test.js +0 -215
  49. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  50. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  51. package/tests/fixtures/deepwiki/index.md +0 -1500
  52. package/tests/fixtures/deepwiki/part-1.md +0 -776
  53. package/tests/fixtures/habr-articles.js +0 -45
  54. package/tests/fixtures/heading-numbering.html +0 -19
  55. package/tests/fixtures/list-item-with-br.html +0 -8
  56. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  57. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  58. package/tests/helpers/nock.js +0 -10
  59. package/tests/helpers/search-fixture.cjs +0 -17
  60. package/tests/helpers/temporary-directory.js +0 -12
  61. package/tests/integration/api-endpoints.test.js +0 -418
  62. package/tests/integration/archive-default-layout.test.js +0 -37
  63. package/tests/integration/browser-engines.test.js +0 -128
  64. package/tests/integration/browser-packaged-path.test.js +0 -45
  65. package/tests/integration/deepwiki-download.test.js +0 -144
  66. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  67. package/tests/integration/formalai-contract.test.js +0 -214
  68. package/tests/integration/gdocs-public-doc.test.js +0 -315
  69. package/tests/integration/github-readme.test.js +0 -137
  70. package/tests/integration/google-drive-image.test.js +0 -246
  71. package/tests/integration/habr-article.test.js +0 -337
  72. package/tests/integration/search-endpoint.test.js +0 -88
  73. package/tests/integration/stackoverflow-download.test.js +0 -173
  74. package/tests/integration/wikipedia-download.test.js +0 -142
  75. package/tests/jest.setup.mjs +0 -7
  76. package/tests/mock/index.test.js +0 -225
  77. package/tests/unit/animation.test.js +0 -33
  78. package/tests/unit/batch.test.js +0 -114
  79. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  80. package/tests/unit/browser-launch-options.test.js +0 -42
  81. package/tests/unit/browser.test.js +0 -172
  82. package/tests/unit/cache.test.js +0 -182
  83. package/tests/unit/cli.test.js +0 -399
  84. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  85. package/tests/unit/convertRelativeUrls.test.js +0 -34
  86. package/tests/unit/extract-images.test.js +0 -259
  87. package/tests/unit/fetch-html.test.js +0 -54
  88. package/tests/unit/figures.test.js +0 -74
  89. package/tests/unit/gdocs-export-headers.test.js +0 -28
  90. package/tests/unit/gdocs-preprocess.test.js +0 -36
  91. package/tests/unit/gdocs.test.js +0 -1490
  92. package/tests/unit/github.test.js +0 -156
  93. package/tests/unit/heading-image.test.js +0 -49
  94. package/tests/unit/heading-numbering.test.js +0 -28
  95. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  96. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  97. package/tests/unit/html2md.test.js +0 -695
  98. package/tests/unit/image-mode-defaults.test.js +0 -54
  99. package/tests/unit/kreuzberg.test.js +0 -249
  100. package/tests/unit/latex.test.js +0 -123
  101. package/tests/unit/localize-images.test.js +0 -114
  102. package/tests/unit/markdown.test.js +0 -36
  103. package/tests/unit/metadata.test.js +0 -140
  104. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  105. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  106. package/tests/unit/postprocess.test.js +0 -110
  107. package/tests/unit/proxy-headers.test.js +0 -100
  108. package/tests/unit/publish-verification.test.js +0 -57
  109. package/tests/unit/retry.test.js +0 -111
  110. package/tests/unit/search.test.js +0 -339
  111. package/tests/unit/shared-dialog.test.js +0 -173
  112. package/tests/unit/stackoverflow.test.js +0 -32
  113. package/tests/unit/stream.test.js +0 -85
  114. package/tests/unit/verify.test.js +0 -182
  115. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  116. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  117. package/tests/xpaste/xpaste.integration.test.js +0 -41
  118. package/tests/xpaste/xpaste.mock.test.js +0 -261
  119. package/tests/xpaste/xpaste.unit.test.js +0 -119
@@ -1,45 +0,0 @@
1
- /**
2
- * Configuration for Habr test articles.
3
- *
4
- * These are the same three articles archived in the link-foundation/meta-theory repo.
5
- * Each entry provides article metadata for use in integration tests.
6
- */
7
-
8
- export const HABR_ARTICLES = {
9
- '0.0.0': {
10
- version: '0.0.0',
11
- title: 'Math introduction to Deep Theory',
12
- url: 'https://habr.com/en/companies/deepfoundation/articles/658705/',
13
- language: 'en',
14
- expectedFigures: 12,
15
- },
16
- '0.0.1': {
17
- version: '0.0.1',
18
- title: 'Глубокая Теория Связей 0.0.1',
19
- titleEnglish: 'Deep Theory of Links 0.0.1',
20
- url: 'https://habr.com/ru/companies/deepfoundation/articles/804617/',
21
- language: 'ru',
22
- expectedFigures: 10,
23
- },
24
- '0.0.2': {
25
- version: '0.0.2',
26
- title: 'The Links Theory 0.0.2',
27
- url: 'https://habr.com/en/articles/895896/',
28
- language: 'en',
29
- expectedFigures: 13,
30
- },
31
- };
32
-
33
- export function getArticle(version) {
34
- const article = HABR_ARTICLES[version];
35
- if (!article) {
36
- throw new Error(
37
- `Unknown article version: ${version}. Available: ${Object.keys(HABR_ARTICLES).join(', ')}`
38
- );
39
- }
40
- return article;
41
- }
42
-
43
- export function getAllArticles() {
44
- return Object.values(HABR_ARTICLES);
45
- }
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <body>
4
- <ol>
5
- <li>
6
- <h4><strong>13. Top-level numbered heading in source</strong></h4>
7
- </li>
8
- </ol>
9
-
10
- <h5><strong>13.1 First subsection heading</strong></h5>
11
- <p>Where it activates: ...</p>
12
-
13
- <h5><strong>13.2 Second subsection heading</strong></h5>
14
- <p>Where it activates: ...</p>
15
-
16
- <h5><strong>13.3 Third subsection heading</strong></h5>
17
- <p>Where it activates: ...</p>
18
- </body>
19
- </html>
@@ -1,8 +0,0 @@
1
- <ol>
2
- <li>
3
- <strong>Glossary entry.</strong><span> Some lead text.<br /></span
4
- ><strong>TAG1</strong><span> – Definition one.<br /></span
5
- ><strong>TAG2</strong><span> – Definition two.<br /></span
6
- ><strong>TAG3</strong><span> – Definition three.</span>
7
- </li>
8
- </ol>
@@ -1,12 +0,0 @@
1
- <ol>
2
- <li><strong>First</strong></li>
3
- <li><strong>Second</strong></li>
4
- </ol>
5
- <p>Some prose between lists.</p>
6
- <ol>
7
- <li><strong>Third</strong></li>
8
- <li><strong>Fourth</strong></li>
9
- </ol>
10
- <ol start="13">
11
- <li><strong>Top-level numbered heading.</strong></li>
12
- </ol>
@@ -1,11 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <body>
4
- <p>
5
- <span>Caption A:</span><br />
6
- <img alt="" src="data:image/png;base64,iVBORw0KGgo=" /><br /><br />
7
- <span>Caption B:</span><br />
8
- <img alt="" src="data:image/png;base64,iVBORw0KGgo=" />
9
- </p>
10
- </body>
11
- </html>
@@ -1,10 +0,0 @@
1
- import nock from 'nock';
2
-
3
- // Nock 15 interceptors survive Jest VM contexts unless explicitly restored.
4
- // https://github.com/nock/nock#memory-issues-with-jest
5
- afterAll(() => {
6
- nock.cleanAll();
7
- nock.restore();
8
- });
9
-
10
- export default nock;
@@ -1,17 +0,0 @@
1
- const { default: nock } = require('nock');
2
-
3
- nock('https://en.wikipedia.org')
4
- .persist()
5
- .get('/w/rest.php/v1/search/page')
6
- .query(true)
7
- .reply(200, {
8
- pages: [
9
- {
10
- id: 1,
11
- key: 'Formal_methods',
12
- title: 'Formal methods',
13
- excerpt: 'the study of <b>formal</b> methods',
14
- description: 'rigorous techniques',
15
- },
16
- ],
17
- });
@@ -1,12 +0,0 @@
1
- import { mkdtemp, rm } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import path from 'node:path';
4
-
5
- export async function withTemporaryDirectory(prefix, operation) {
6
- const directory = await mkdtemp(path.join(tmpdir(), prefix));
7
- try {
8
- return await operation(directory);
9
- } finally {
10
- await rm(directory, { recursive: true, force: true });
11
- }
12
- }
@@ -1,418 +0,0 @@
1
- /**
2
- * Integration tests for the API endpoints.
3
- *
4
- * Tests /image, /archive, /pdf, /docx endpoints with various options.
5
- * Uses nock to mock external requests for fast, reliable unit-level testing.
6
- * These tests validate endpoint behavior and response formats.
7
- */
8
-
9
- import { jest } from '@jest/globals';
10
- import request from 'supertest';
11
- import nock from '../helpers/nock.js';
12
- import unzipper from 'unzipper';
13
- import { app } from '../../src/index.js';
14
- import { isKreuzbergAvailable } from '../../src/kreuzberg.js';
15
-
16
- jest.setTimeout(60000);
17
-
18
- const MOCK_HTML = `<!DOCTYPE html>
19
- <html><head><title>Test</title>
20
- <link rel="stylesheet" href="https://example.com/style.css">
21
- </head>
22
- <body>
23
- <h1>Test Page</h1>
24
- <p>Hello world</p>
25
- <img src="https://example.com/image.png" alt="test">
26
- </body></html>`;
27
-
28
- function mockGithubRepository(owner = 'octocat', repo = 'Hello-World') {
29
- const readme = '# Hello World\n\nThis README came from GitHub.';
30
- nock('https://api.github.com')
31
- .get(`/repos/${owner}/${repo}`)
32
- .reply(200, {
33
- full_name: `${owner}/${repo}`,
34
- description: 'A friendly test repository',
35
- html_url: `https://github.com/${owner}/${repo}`,
36
- default_branch: 'master',
37
- language: 'JavaScript',
38
- stargazers_count: 42,
39
- forks_count: 7,
40
- open_issues_count: 3,
41
- license: { spdx_id: 'MIT' },
42
- topics: ['demo', 'capture'],
43
- })
44
- .get(`/repos/${owner}/${repo}/readme`)
45
- .query({ ref: 'master' })
46
- .reply(200, {
47
- name: 'README.md',
48
- path: 'README.md',
49
- encoding: 'base64',
50
- content: Buffer.from(readme, 'utf8').toString('base64'),
51
- html_url: `https://github.com/${owner}/${repo}/blob/master/README.md`,
52
- })
53
- .get(`/repos/${owner}/${repo}/contents`)
54
- .query({ ref: 'master' })
55
- .reply(200, [
56
- {
57
- name: 'src',
58
- path: 'src',
59
- type: 'dir',
60
- html_url: `https://github.com/${owner}/${repo}/tree/master/src`,
61
- },
62
- {
63
- name: 'README.md',
64
- path: 'README.md',
65
- type: 'file',
66
- size: readme.length,
67
- html_url: `https://github.com/${owner}/${repo}/blob/master/README.md`,
68
- },
69
- ]);
70
- }
71
-
72
- afterEach(() => {
73
- nock.cleanAll();
74
- });
75
-
76
- describe('API Endpoint Tests', () => {
77
- describe('unsupported routes', () => {
78
- it('does not expose /gdocs as an output-format endpoint', async () => {
79
- await request(app)
80
- .get('/gdocs')
81
- .query({
82
- url: 'https://docs.google.com/document/d/test-doc/edit',
83
- })
84
- .expect(404);
85
- });
86
- });
87
-
88
- describe('GET /image', () => {
89
- it('rejects invalid format', async () => {
90
- await request(app)
91
- .get('/image')
92
- .query({ url: 'https://example.com/test', format: 'bmp' })
93
- .expect(400);
94
- });
95
-
96
- it('returns 400 without url', async () => {
97
- await request(app).get('/image').expect(400);
98
- });
99
- });
100
-
101
- describe('GET /markdown', () => {
102
- it('returns markdown', async () => {
103
- nock('https://example.com').get('/md-test').reply(200, MOCK_HTML, {
104
- 'content-type': 'text/html',
105
- });
106
-
107
- const res = await request(app)
108
- .get('/markdown')
109
- .query({ url: 'https://example.com/md-test' })
110
- .expect(200);
111
-
112
- expect(res.headers['content-type']).toMatch(/text\/markdown/);
113
- expect(res.text).toContain('Test Page');
114
- expect(res.text).toContain('Hello world');
115
- });
116
-
117
- it('returns a compact GitHub repository markdown snapshot', async () => {
118
- mockGithubRepository();
119
-
120
- const res = await request(app)
121
- .get('/markdown')
122
- .query({ url: 'https://github.com/octocat/Hello-World' })
123
- .expect(200);
124
-
125
- expect(res.headers['content-type']).toMatch(/text\/markdown/);
126
- expect(res.text).toContain('# octocat/Hello-World');
127
- expect(res.text).toContain('## Repository');
128
- expect(res.text).toContain('## Files');
129
- expect(res.text).toContain('- [src/](');
130
- expect(res.text).toContain('## README.md');
131
- expect(res.text).toContain('This README came from GitHub.');
132
- expect(res.text).not.toContain('Skip to content');
133
- expect(nock.isDone()).toBe(true);
134
- });
135
-
136
- it('returns a compact GitHub repository markdown snapshot for kreuzberg text format', async () => {
137
- mockGithubRepository();
138
-
139
- const res = await request(app)
140
- .get('/markdown')
141
- .query({
142
- url: 'https://github.com/octocat/Hello-World',
143
- converter: 'kreuzberg',
144
- format: 'text',
145
- })
146
- .expect(200);
147
-
148
- expect(res.headers['content-type']).toMatch(/text\/markdown/);
149
- expect(res.text).toContain('# octocat/Hello-World');
150
- expect(res.text).toContain('## Files');
151
- expect(res.text).toContain('This README came from GitHub.');
152
- expect(nock.isDone()).toBe(true);
153
- });
154
-
155
- it('rejects unsupported converter names', async () => {
156
- await request(app)
157
- .get('/markdown')
158
- .query({
159
- url: 'https://example.com/md-test',
160
- converter: 'unknown',
161
- })
162
- .expect(400);
163
- });
164
-
165
- it('rejects JSON format without the kreuzberg converter', async () => {
166
- await request(app)
167
- .get('/markdown')
168
- .query({
169
- url: 'https://example.com/md-test',
170
- format: 'json',
171
- })
172
- .expect(400);
173
- });
174
-
175
- it('returns structured kreuzberg JSON with selectors and absolute links', async () => {
176
- if (!(await isKreuzbergAvailable())) {
177
- return;
178
- }
179
-
180
- nock('https://example.com')
181
- .get('/kreuzberg-json')
182
- .reply(
183
- 200,
184
- `<!DOCTYPE html><html><body>
185
- <main>
186
- <h1>Article Title</h1>
187
- <nav>Navigation</nav>
188
- <article><p>Wanted body with <a href="/about">relative link</a>.</p></article>
189
- </main>
190
- </body></html>`,
191
- { 'content-type': 'text/html' }
192
- );
193
-
194
- const res = await request(app)
195
- .get('/markdown')
196
- .query({
197
- url: 'https://example.com/kreuzberg-json',
198
- converter: 'kreuzberg',
199
- format: 'json',
200
- contentSelector: 'main',
201
- bodySelector: 'article',
202
- })
203
- .expect(200);
204
-
205
- expect(res.headers['content-type']).toMatch(/application\/json/);
206
- expect(res.body).toHaveProperty('content');
207
- expect(res.body).toHaveProperty('metadata');
208
- expect(res.body).toHaveProperty('tables');
209
- expect(res.body).toHaveProperty('images');
210
- expect(res.body).toHaveProperty('warnings');
211
- expect(res.body.content).toContain('Article Title');
212
- expect(res.body.content).toContain('Wanted body');
213
- expect(res.body.content).toContain('https://example.com/about');
214
- expect(res.body.content).not.toContain('Navigation');
215
- });
216
- });
217
-
218
- describe('GET /txt', () => {
219
- it('returns a compact GitHub repository plain-text snapshot', async () => {
220
- mockGithubRepository();
221
-
222
- const res = await request(app)
223
- .get('/txt')
224
- .query({ url: 'https://github.com/octocat/Hello-World' })
225
- .expect(200);
226
-
227
- expect(res.headers['content-type']).toMatch(/text\/plain/);
228
- expect(res.headers['content-disposition']).toContain(
229
- 'filename="octocat-Hello-World.txt"'
230
- );
231
- expect(res.text).toContain('Repository: octocat/Hello-World');
232
- expect(res.text).toContain('Files:');
233
- expect(res.text).toContain('- src/');
234
- expect(res.text).toContain('README.md:');
235
- expect(res.text).toContain('This README came from GitHub.');
236
- expect(nock.isDone()).toBe(true);
237
- });
238
- });
239
-
240
- describe('GET /archive', () => {
241
- it('returns a ZIP archive with remote images (markdown format)', async () => {
242
- nock('https://example.com').get('/archive-test').reply(200, MOCK_HTML, {
243
- 'content-type': 'text/html',
244
- });
245
-
246
- const res = await request(app)
247
- .get('/archive')
248
- .query({
249
- url: 'https://example.com/archive-test',
250
- localImages: 'false',
251
- documentFormat: 'markdown',
252
- })
253
- .buffer(true)
254
- .parse((res, callback) => {
255
- const chunks = [];
256
- res.on('data', (chunk) => chunks.push(chunk));
257
- res.on('end', () => callback(null, Buffer.concat(chunks)));
258
- })
259
- .expect(200);
260
-
261
- expect(res.body).toBeInstanceOf(Buffer);
262
- expect(res.body.length).toBeGreaterThan(50);
263
- // ZIP signature: PK (0x50 0x4B)
264
- expect(res.body[0]).toBe(0x50);
265
- expect(res.body[1]).toBe(0x4b);
266
-
267
- // Default archive layout contract (issue #113): the markdown archive must
268
- // bundle document.md AND document.html for reference.
269
- const dir = await unzipper.Open.buffer(res.body);
270
- const names = dir.files.map((f) => f.path);
271
- expect(names).toEqual(
272
- expect.arrayContaining(['document.md', 'document.html'])
273
- );
274
- });
275
-
276
- it('returns a ZIP archive with HTML format', async () => {
277
- nock('https://example.com')
278
- .get('/archive-html-test')
279
- .reply(200, MOCK_HTML, {
280
- 'content-type': 'text/html',
281
- });
282
-
283
- const res = await request(app)
284
- .get('/archive')
285
- .query({
286
- url: 'https://example.com/archive-html-test',
287
- localImages: 'false',
288
- documentFormat: 'html',
289
- })
290
- .buffer(true)
291
- .parse((res, callback) => {
292
- const chunks = [];
293
- res.on('data', (chunk) => chunks.push(chunk));
294
- res.on('end', () => callback(null, Buffer.concat(chunks)));
295
- })
296
- .expect(200);
297
-
298
- expect(res.body).toBeInstanceOf(Buffer);
299
- expect(res.body.length).toBeGreaterThan(50);
300
- // ZIP signature
301
- expect(res.body[0]).toBe(0x50);
302
- expect(res.body[1]).toBe(0x4b);
303
- });
304
-
305
- it('returns a ZIP archive with local images and folders', async () => {
306
- const mockPng = Buffer.from([
307
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
308
- ]);
309
-
310
- nock('https://example.com')
311
- .get('/archive-local-test')
312
- .reply(200, MOCK_HTML, { 'content-type': 'text/html' });
313
-
314
- nock('https://example.com')
315
- .get('/image.png')
316
- .reply(200, mockPng, { 'content-type': 'image/png' });
317
-
318
- const res = await request(app)
319
- .get('/archive')
320
- .query({
321
- url: 'https://example.com/archive-local-test',
322
- localImages: 'true',
323
- })
324
- .buffer(true)
325
- .parse((res, callback) => {
326
- const chunks = [];
327
- res.on('data', (chunk) => chunks.push(chunk));
328
- res.on('end', () => callback(null, Buffer.concat(chunks)));
329
- })
330
- .expect(200);
331
-
332
- expect(res.body).toBeInstanceOf(Buffer);
333
- expect(res.body.length).toBeGreaterThan(50);
334
- // ZIP signature
335
- expect(res.body[0]).toBe(0x50);
336
- expect(res.body[1]).toBe(0x4b);
337
- });
338
-
339
- it('returns a ZIP archive with HTML format and local CSS', async () => {
340
- const mockCss = 'body { background: #fff; }';
341
- const mockPng = Buffer.from([
342
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
343
- ]);
344
-
345
- nock('https://example.com')
346
- .get('/archive-html-local')
347
- .reply(200, MOCK_HTML, { 'content-type': 'text/html' });
348
-
349
- nock('https://example.com')
350
- .get('/style.css')
351
- .reply(200, mockCss, { 'content-type': 'text/css' });
352
-
353
- nock('https://example.com')
354
- .get('/image.png')
355
- .reply(200, mockPng, { 'content-type': 'image/png' });
356
-
357
- const res = await request(app)
358
- .get('/archive')
359
- .query({
360
- url: 'https://example.com/archive-html-local',
361
- localImages: 'true',
362
- documentFormat: 'html',
363
- })
364
- .buffer(true)
365
- .parse((res, callback) => {
366
- const chunks = [];
367
- res.on('data', (chunk) => chunks.push(chunk));
368
- res.on('end', () => callback(null, Buffer.concat(chunks)));
369
- })
370
- .expect(200);
371
-
372
- expect(res.body).toBeInstanceOf(Buffer);
373
- expect(res.body.length).toBeGreaterThan(50);
374
- // ZIP signature
375
- expect(res.body[0]).toBe(0x50);
376
- expect(res.body[1]).toBe(0x4b);
377
- });
378
-
379
- it('returns 400 without url', async () => {
380
- await request(app).get('/archive').expect(400);
381
- });
382
- });
383
-
384
- describe('GET /docx', () => {
385
- it('returns a DOCX document', async () => {
386
- nock('https://example.com').get('/docx-test').reply(200, MOCK_HTML, {
387
- 'content-type': 'text/html',
388
- });
389
-
390
- const res = await request(app)
391
- .get('/docx')
392
- .query({ url: 'https://example.com/docx-test' })
393
- .buffer(true)
394
- .parse((res, callback) => {
395
- const chunks = [];
396
- res.on('data', (chunk) => chunks.push(chunk));
397
- res.on('end', () => callback(null, Buffer.concat(chunks)));
398
- })
399
- .expect(200);
400
-
401
- expect(res.body).toBeInstanceOf(Buffer);
402
- expect(res.body.length).toBeGreaterThan(100);
403
- // DOCX is a ZIP file
404
- expect(res.body[0]).toBe(0x50);
405
- expect(res.body[1]).toBe(0x4b);
406
- });
407
-
408
- it('returns 400 without url', async () => {
409
- await request(app).get('/docx').expect(400);
410
- });
411
- });
412
-
413
- describe('GET /pdf', () => {
414
- it('returns 400 without url', async () => {
415
- await request(app).get('/pdf').expect(400);
416
- });
417
- });
418
- });
@@ -1,37 +0,0 @@
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
- });