@link-assistant/web-capture 2.0.0 → 2.0.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.
- package/CHANGELOG.md +17 -0
- package/bin/web-capture.js +2 -5
- package/package.json +36 -23
- package/src/archive.js +0 -1
- package/src/binary-download.js +0 -1
- package/src/docx.js +13 -10
- package/src/fetch.js +0 -1
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +0 -1
- package/src/markdown.js +0 -1
- package/src/shared-dialog.js +0 -1
- package/src/stream.js +0 -1
- package/src/transport.js +23 -5
- package/src/txt.js +0 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.dockerignore +0 -6
- package/.jscpd.json +0 -19
- package/.lenv +0 -8
- package/.prettierignore +0 -8
- package/.prettierrc +0 -10
- package/Dockerfile +0 -52
- package/docker-compose.yml +0 -11
- package/eslint.config.js +0 -112
- package/examples/js/capture_cache.js +0 -12
- package/examples/js/engine_comparison.js +0 -39
- package/examples/js/html_download.js +0 -23
- package/examples/js/image_download.js +0 -36
- package/examples/js/markdown_download.js +0 -23
- package/examples/js/playwright_example.js +0 -23
- package/examples/python/capture_client.py +0 -20
- package/examples/python/html_download.py +0 -3
- package/examples/python/image_download.py +0 -16
- package/examples/python/markdown_download.py +0 -3
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -17
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -187
- package/tests/ci/docker-install.test.js +0 -31
- package/tests/ci/rust-dockerfile.test.js +0 -54
- package/tests/ci/workflow-policy.test.js +0 -79
- package/tests/e2e/docker.test.js +0 -186
- package/tests/e2e/process.test.js +0 -215
- package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
- package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
- package/tests/fixtures/deepwiki/index.md +0 -1500
- package/tests/fixtures/deepwiki/part-1.md +0 -776
- package/tests/fixtures/habr-articles.js +0 -45
- package/tests/fixtures/heading-numbering.html +0 -19
- package/tests/fixtures/list-item-with-br.html +0 -8
- package/tests/fixtures/ol-continuous-numbering.html +0 -12
- package/tests/fixtures/paragraph-vs-line-break.html +0 -11
- package/tests/helpers/nock.js +0 -10
- package/tests/helpers/screenshot.js +0 -10
- package/tests/helpers/search-fixture.cjs +0 -17
- package/tests/helpers/temporary-directory.js +0 -12
- package/tests/integration/api-endpoints.test.js +0 -418
- package/tests/integration/archive-default-layout.test.js +0 -37
- package/tests/integration/browser-engines.test.js +0 -80
- package/tests/integration/browser-packaged-path.test.js +0 -45
- package/tests/integration/deepwiki-download.test.js +0 -144
- package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
- package/tests/integration/formalai-contract.test.js +0 -214
- package/tests/integration/gdocs-public-doc.test.js +0 -315
- package/tests/integration/github-readme.test.js +0 -133
- package/tests/integration/google-drive-image.test.js +0 -246
- package/tests/integration/habr-article.test.js +0 -337
- package/tests/integration/search-endpoint.test.js +0 -88
- package/tests/integration/stackoverflow-download.test.js +0 -173
- package/tests/integration/wikipedia-download.test.js +0 -134
- package/tests/jest.setup.mjs +0 -7
- package/tests/mock/index.test.js +0 -225
- package/tests/unit/animation.test.js +0 -33
- package/tests/unit/batch.test.js +0 -114
- package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
- package/tests/unit/browser-launch-options.test.js +0 -42
- package/tests/unit/browser.test.js +0 -172
- package/tests/unit/cache.test.js +0 -182
- package/tests/unit/cli.test.js +0 -399
- package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
- package/tests/unit/convertRelativeUrls.test.js +0 -34
- package/tests/unit/extract-images.test.js +0 -259
- package/tests/unit/fetch-html.test.js +0 -54
- package/tests/unit/figures.test.js +0 -74
- package/tests/unit/gdocs-export-headers.test.js +0 -28
- package/tests/unit/gdocs-preprocess.test.js +0 -36
- package/tests/unit/gdocs.test.js +0 -1490
- package/tests/unit/github.test.js +0 -156
- package/tests/unit/heading-image.test.js +0 -49
- package/tests/unit/heading-numbering.test.js +0 -28
- package/tests/unit/html2md-br-in-list-item.test.js +0 -23
- package/tests/unit/html2md-ol-numbering.test.js +0 -20
- package/tests/unit/html2md.test.js +0 -695
- package/tests/unit/image-mode-defaults.test.js +0 -54
- package/tests/unit/kreuzberg.test.js +0 -249
- package/tests/unit/latex.test.js +0 -123
- package/tests/unit/localize-images.test.js +0 -114
- package/tests/unit/markdown.test.js +0 -36
- package/tests/unit/metadata.test.js +0 -140
- package/tests/unit/package-runtime-dependencies.test.js +0 -74
- package/tests/unit/paragraph-vs-line-break.test.js +0 -56
- package/tests/unit/postprocess.test.js +0 -110
- package/tests/unit/proxy-headers.test.js +0 -100
- package/tests/unit/publish-verification.test.js +0 -54
- package/tests/unit/retry.test.js +0 -111
- package/tests/unit/search.test.js +0 -339
- package/tests/unit/shared-dialog.test.js +0 -173
- package/tests/unit/stackoverflow.test.js +0 -32
- package/tests/unit/stream.test.js +0 -83
- package/tests/unit/verify.test.js +0 -182
- package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
- package/tests/xpaste/xpaste.e2e.test.js +0 -36
- package/tests/xpaste/xpaste.integration.test.js +0 -41
- package/tests/xpaste/xpaste.mock.test.js +0 -261
- 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>
|
package/tests/helpers/nock.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { expect } from '@jest/globals';
|
|
2
|
-
|
|
3
|
-
// PNG magic number (89 50 4E 47 0D 0A 1A 0A).
|
|
4
|
-
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
5
|
-
|
|
6
|
-
export function expectPngScreenshot(screenshot, minimumLength = 1000) {
|
|
7
|
-
expect(screenshot).toBeInstanceOf(Uint8Array);
|
|
8
|
-
expect(screenshot.length).toBeGreaterThan(minimumLength);
|
|
9
|
-
expect(Buffer.from(screenshot).subarray(0, 8)).toEqual(PNG_SIGNATURE);
|
|
10
|
-
}
|
|
@@ -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
|
-
});
|