@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.
- package/CHANGELOG.md +17 -0
- package/README.md +2 -1
- package/bin/web-capture.js +5 -7
- package/package.json +32 -18
- package/src/archive.js +1 -2
- package/src/binary-download.js +13 -0
- package/src/docx.js +14 -11
- package/src/fetch.js +1 -2
- package/src/figures.js +2 -12
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +1 -2
- package/src/localize-images.js +2 -12
- 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/babel.config.cjs +0 -13
- 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/html_download.py +0 -15
- package/examples/python/image_download.py +0 -20
- package/examples/python/markdown_download.py +0 -15
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -20
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -174
- 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/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 -128
- 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 -137
- 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 -142
- 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 -57
- 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 -85
- 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,54 +0,0 @@
|
|
|
1
|
-
import { applyImageMode } from '../../src/extract-images.js';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
|
|
6
|
-
const TINY_PNG =
|
|
7
|
-
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
|
|
8
|
-
const REMOTE = `Hi.\n\n\n\nBye.\n`;
|
|
9
|
-
const BASE64 = `Hi.\n\n\n\nBye.\n`;
|
|
10
|
-
|
|
11
|
-
describe('image-mode contract', () => {
|
|
12
|
-
let tmp;
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'imode-'));
|
|
15
|
-
});
|
|
16
|
-
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
17
|
-
|
|
18
|
-
it('default mode keeps remote URLs', async () => {
|
|
19
|
-
const r = await applyImageMode(REMOTE, { mode: 'default' });
|
|
20
|
-
expect(r.markdown).toContain('https://example.invalid/foo.png');
|
|
21
|
-
expect(r.markdown).not.toMatch(/images\//);
|
|
22
|
-
expect(r.markdown).not.toMatch(/data:image/);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('--extract-images writes files', async () => {
|
|
26
|
-
const r = await applyImageMode(BASE64, {
|
|
27
|
-
mode: 'extract',
|
|
28
|
-
dir: tmp,
|
|
29
|
-
subdir: 'images',
|
|
30
|
-
});
|
|
31
|
-
expect(r.markdown).toMatch(/images\/image-/);
|
|
32
|
-
expect(fs.readdirSync(path.join(tmp, 'images'))).toHaveLength(1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('--embed-images keeps base64', async () => {
|
|
36
|
-
const r = await applyImageMode(BASE64, { mode: 'embed' });
|
|
37
|
-
expect(r.markdown).toContain('data:image');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('flags have observable effect on every input', async () => {
|
|
41
|
-
for (const src of [REMOTE, BASE64]) {
|
|
42
|
-
const a = await applyImageMode(src, { mode: 'default' });
|
|
43
|
-
const b = await applyImageMode(src, { mode: 'embed' });
|
|
44
|
-
const c = await applyImageMode(src, {
|
|
45
|
-
mode: 'extract',
|
|
46
|
-
dir: tmp,
|
|
47
|
-
subdir: 'images',
|
|
48
|
-
});
|
|
49
|
-
expect(
|
|
50
|
-
new Set([a.markdown, b.markdown, c.markdown]).size
|
|
51
|
-
).toBeGreaterThan(1);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
convertWithKreuzberg,
|
|
3
|
-
isKreuzbergAvailable,
|
|
4
|
-
normalizeStructuredKeys,
|
|
5
|
-
} from '../../src/kreuzberg.js';
|
|
6
|
-
|
|
7
|
-
let available;
|
|
8
|
-
|
|
9
|
-
beforeAll(async () => {
|
|
10
|
-
available = await isKreuzbergAvailable();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
function skipIfUnavailable() {
|
|
14
|
-
if (!available) {
|
|
15
|
-
return 'kreuzberg native binding not installed';
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('kreuzberg html-to-markdown integration', () => {
|
|
21
|
-
it('reports availability correctly', async () => {
|
|
22
|
-
expect(typeof available).toBe('boolean');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('converts basic HTML to Markdown', async () => {
|
|
26
|
-
const skip = skipIfUnavailable();
|
|
27
|
-
if (skip) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const result = await convertWithKreuzberg(
|
|
31
|
-
'<h1>Hello World</h1><p>This is a test.</p>'
|
|
32
|
-
);
|
|
33
|
-
expect(result.content).toContain('# Hello World');
|
|
34
|
-
expect(result.content).toContain('This is a test.');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('returns structured result with all fields', async () => {
|
|
38
|
-
const skip = skipIfUnavailable();
|
|
39
|
-
if (skip) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const result = await convertWithKreuzberg('<p>Test</p>');
|
|
43
|
-
expect(result).toHaveProperty('content');
|
|
44
|
-
expect(result).toHaveProperty('metadata');
|
|
45
|
-
expect(result).toHaveProperty('tables');
|
|
46
|
-
expect(result).toHaveProperty('images');
|
|
47
|
-
expect(result).toHaveProperty('warnings');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('extracts metadata from HTML', async () => {
|
|
51
|
-
const skip = skipIfUnavailable();
|
|
52
|
-
if (skip) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const html = `<html>
|
|
56
|
-
<head>
|
|
57
|
-
<title>Test Page</title>
|
|
58
|
-
<meta property="og:description" content="A test page">
|
|
59
|
-
</head>
|
|
60
|
-
<body><h1>Hello</h1></body>
|
|
61
|
-
</html>`;
|
|
62
|
-
const result = await convertWithKreuzberg(html);
|
|
63
|
-
expect(result.metadata).toBeTruthy();
|
|
64
|
-
expect(result.metadata.document.title).toBe('Test Page');
|
|
65
|
-
expect(result.metadata.document.open_graph.description).toBe('A test page');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('extracts headings from metadata', async () => {
|
|
69
|
-
const skip = skipIfUnavailable();
|
|
70
|
-
if (skip) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const html = '<h1>First</h1><h2>Second</h2><h3>Third</h3>';
|
|
74
|
-
const result = await convertWithKreuzberg(html);
|
|
75
|
-
expect(result.metadata).toBeTruthy();
|
|
76
|
-
expect(result.metadata.headers.length).toBe(3);
|
|
77
|
-
expect(result.metadata.headers[0].text).toBe('First');
|
|
78
|
-
expect(result.metadata.headers[0].level).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('extracts links from metadata', async () => {
|
|
82
|
-
const skip = skipIfUnavailable();
|
|
83
|
-
if (skip) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const html =
|
|
87
|
-
'<a href="https://example.com">Example</a><a href="mailto:test@test.com">Email</a>';
|
|
88
|
-
const result = await convertWithKreuzberg(html);
|
|
89
|
-
expect(result.metadata).toBeTruthy();
|
|
90
|
-
expect(result.metadata.links.length).toBeGreaterThanOrEqual(1);
|
|
91
|
-
const externalLink = result.metadata.links.find(
|
|
92
|
-
(l) => l.href === 'https://example.com'
|
|
93
|
-
);
|
|
94
|
-
expect(externalLink).toBeTruthy();
|
|
95
|
-
expect(externalLink.text).toBe('Example');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('converts tables to Markdown', async () => {
|
|
99
|
-
const skip = skipIfUnavailable();
|
|
100
|
-
if (skip) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const html = `<table>
|
|
104
|
-
<thead><tr><th>Name</th><th>Value</th></tr></thead>
|
|
105
|
-
<tbody><tr><td>A</td><td>1</td></tr><tr><td>B</td><td>2</td></tr></tbody>
|
|
106
|
-
</table>`;
|
|
107
|
-
const result = await convertWithKreuzberg(html);
|
|
108
|
-
expect(result.content).toContain('| Name | Value |');
|
|
109
|
-
expect(result.content).toMatch(/\| A\s+\| 1\s+\|/);
|
|
110
|
-
expect(result.content).toMatch(/\| B\s+\| 2\s+\|/);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('converts links correctly', async () => {
|
|
114
|
-
const skip = skipIfUnavailable();
|
|
115
|
-
if (skip) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const html = '<a href="https://example.com">Click here</a>';
|
|
119
|
-
const result = await convertWithKreuzberg(html);
|
|
120
|
-
expect(result.content).toContain('[Click here](https://example.com)');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('resolves relative links with baseUrl', async () => {
|
|
124
|
-
const skip = skipIfUnavailable();
|
|
125
|
-
if (skip) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const html = '<a href="/about">About</a>';
|
|
129
|
-
const result = await convertWithKreuzberg(html, {
|
|
130
|
-
baseUrl: 'https://example.com/docs/page',
|
|
131
|
-
});
|
|
132
|
-
expect(result.content).toContain('[About](https://example.com/about)');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('converts bold and italic correctly', async () => {
|
|
136
|
-
const skip = skipIfUnavailable();
|
|
137
|
-
if (skip) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const html = '<p>This is <strong>bold</strong> and <em>italic</em></p>';
|
|
141
|
-
const result = await convertWithKreuzberg(html);
|
|
142
|
-
expect(result.content).toContain('**bold**');
|
|
143
|
-
expect(result.content).toContain('*italic*');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('converts code blocks correctly', async () => {
|
|
147
|
-
const skip = skipIfUnavailable();
|
|
148
|
-
if (skip) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const html = '<pre><code class="language-js">const x = 1;</code></pre>';
|
|
152
|
-
const result = await convertWithKreuzberg(html);
|
|
153
|
-
expect(result.content).toContain('```');
|
|
154
|
-
expect(result.content).toContain('const x = 1;');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('handles empty HTML gracefully', async () => {
|
|
158
|
-
const skip = skipIfUnavailable();
|
|
159
|
-
if (skip) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const result = await convertWithKreuzberg('');
|
|
163
|
-
expect(result.content).toBeDefined();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('removes script tags from output', async () => {
|
|
167
|
-
const skip = skipIfUnavailable();
|
|
168
|
-
if (skip) {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const html = '<p>Content</p><script>alert("xss")</script>';
|
|
172
|
-
const result = await convertWithKreuzberg(html);
|
|
173
|
-
expect(result.content).toContain('Content');
|
|
174
|
-
expect(result.content).not.toContain('alert');
|
|
175
|
-
expect(result.content).not.toContain('script');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('removes style tags from output', async () => {
|
|
179
|
-
const skip = skipIfUnavailable();
|
|
180
|
-
if (skip) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const html = '<style>body { color: red; }</style><p>Content</p>';
|
|
184
|
-
const result = await convertWithKreuzberg(html);
|
|
185
|
-
expect(result.content).toContain('Content');
|
|
186
|
-
expect(result.content).not.toContain('color: red');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('handles GFM strikethrough', async () => {
|
|
190
|
-
const skip = skipIfUnavailable();
|
|
191
|
-
if (skip) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const html = '<p>This is <del>deleted</del> text</p>';
|
|
195
|
-
const result = await convertWithKreuzberg(html);
|
|
196
|
-
expect(result.content).toContain('~~deleted~~');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('handles lists correctly', async () => {
|
|
200
|
-
const skip = skipIfUnavailable();
|
|
201
|
-
if (skip) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const html = '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
|
|
205
|
-
const result = await convertWithKreuzberg(html);
|
|
206
|
-
expect(result.content).toContain('Item 1');
|
|
207
|
-
expect(result.content).toContain('Item 2');
|
|
208
|
-
expect(result.content).toContain('Item 3');
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('kreuzberg structured key normalization (issue #137 parity)', () => {
|
|
213
|
-
// Mirrors the Rust `inline_image_to_json` regression tests: after the
|
|
214
|
-
// html-to-markdown 3.6 change, inline image dimensions are a structured
|
|
215
|
-
// `{ width, height }` value rather than a tuple. The normalization pipeline
|
|
216
|
-
// that backs `result.images` must preserve those `width`/`height` keys.
|
|
217
|
-
it('preserves width/height keys on inline image dimensions', () => {
|
|
218
|
-
const images = normalizeStructuredKeys([
|
|
219
|
-
{
|
|
220
|
-
format: 'png',
|
|
221
|
-
filename: 'pixel.png',
|
|
222
|
-
description: 'one pixel',
|
|
223
|
-
dimensions: { width: 800, height: 600 },
|
|
224
|
-
source: 'ImgDataUri',
|
|
225
|
-
},
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
|
-
expect(images).toHaveLength(1);
|
|
229
|
-
expect(images[0].dimensions).toEqual({ width: 800, height: 600 });
|
|
230
|
-
expect(images[0].format).toBe('png');
|
|
231
|
-
expect(images[0].filename).toBe('pixel.png');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('leaves images without dimensions untouched', () => {
|
|
235
|
-
const images = normalizeStructuredKeys([
|
|
236
|
-
{ format: 'svg', source: 'SvgElement', dimensions: null },
|
|
237
|
-
]);
|
|
238
|
-
|
|
239
|
-
expect(images[0].dimensions).toBeNull();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('snake_cases nested structured keys', () => {
|
|
243
|
-
const normalized = normalizeStructuredKeys({
|
|
244
|
-
openGraph: { siteName: 'Example' },
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
expect(normalized).toEqual({ open_graph: { site_name: 'Example' } });
|
|
248
|
-
});
|
|
249
|
-
});
|
package/tests/unit/latex.test.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import * as cheerio from 'cheerio';
|
|
2
|
-
import {
|
|
3
|
-
extractHabrFormula,
|
|
4
|
-
extractKatexFormula,
|
|
5
|
-
extractMathJaxFormula,
|
|
6
|
-
isFormulaImage,
|
|
7
|
-
isMathElement,
|
|
8
|
-
extractFormula,
|
|
9
|
-
} from '../../src/latex.js';
|
|
10
|
-
|
|
11
|
-
describe('latex module', () => {
|
|
12
|
-
describe('extractHabrFormula', () => {
|
|
13
|
-
it('extracts LaTeX from Habr formula img source attribute', () => {
|
|
14
|
-
const $ = cheerio.load(
|
|
15
|
-
'<img class="formula" source=" L \\to L^2 " alt="formula">'
|
|
16
|
-
);
|
|
17
|
-
const el = $('img')[0];
|
|
18
|
-
expect(extractHabrFormula($, el)).toBe('L \\to L^2');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('falls back to alt text when source is missing', () => {
|
|
22
|
-
const $ = cheerio.load('<img class="formula" alt=" x + y ">');
|
|
23
|
-
const el = $('img')[0];
|
|
24
|
-
expect(extractHabrFormula($, el)).toBe('x + y');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('returns null when no source or alt', () => {
|
|
28
|
-
const $ = cheerio.load('<img class="formula">');
|
|
29
|
-
const el = $('img')[0];
|
|
30
|
-
expect(extractHabrFormula($, el)).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('extractKatexFormula', () => {
|
|
35
|
-
it('extracts from annotation element', () => {
|
|
36
|
-
const $ = cheerio.load(
|
|
37
|
-
'<span class="katex"><annotation encoding="application/x-tex">E = mc^2</annotation></span>'
|
|
38
|
-
);
|
|
39
|
-
const el = $('span.katex')[0];
|
|
40
|
-
expect(extractKatexFormula($, el)).toBe('E = mc^2');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('extracts from data-tex attribute', () => {
|
|
44
|
-
const $ = cheerio.load(
|
|
45
|
-
'<span class="math" data-tex="\\alpha + \\beta"></span>'
|
|
46
|
-
);
|
|
47
|
-
const el = $('span.math')[0];
|
|
48
|
-
expect(extractKatexFormula($, el)).toBe('\\alpha + \\beta');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns null when no TeX source found', () => {
|
|
52
|
-
const $ = cheerio.load('<span class="katex">rendered</span>');
|
|
53
|
-
const el = $('span.katex')[0];
|
|
54
|
-
expect(extractKatexFormula($, el)).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('extractMathJaxFormula', () => {
|
|
59
|
-
it('extracts from data-tex attribute', () => {
|
|
60
|
-
const $ = cheerio.load(
|
|
61
|
-
'<mjx-container data-tex="\\sum_{i=1}^{n}"></mjx-container>'
|
|
62
|
-
);
|
|
63
|
-
const el = $('mjx-container')[0];
|
|
64
|
-
expect(extractMathJaxFormula($, el)).toBe('\\sum_{i=1}^{n}');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('isFormulaImage', () => {
|
|
69
|
-
it('identifies img.formula elements', () => {
|
|
70
|
-
const $ = cheerio.load('<img class="formula" source="x^2">');
|
|
71
|
-
expect(isFormulaImage($, $('img')[0])).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('identifies img with source attribute', () => {
|
|
75
|
-
const $ = cheerio.load('<img source="x^2">');
|
|
76
|
-
expect(isFormulaImage($, $('img')[0])).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('rejects regular images', () => {
|
|
80
|
-
const $ = cheerio.load('<img src="photo.jpg" alt="photo">');
|
|
81
|
-
expect(isFormulaImage($, $('img')[0])).toBe(false);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('isMathElement', () => {
|
|
86
|
-
it('identifies katex elements', () => {
|
|
87
|
-
const $ = cheerio.load('<span class="katex">x</span>');
|
|
88
|
-
expect(isMathElement($, $('span')[0])).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('identifies math elements', () => {
|
|
92
|
-
const $ = cheerio.load('<span class="math">x</span>');
|
|
93
|
-
expect(isMathElement($, $('span')[0])).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('identifies mjx-container', () => {
|
|
97
|
-
const $ = cheerio.load('<mjx-container>x</mjx-container>');
|
|
98
|
-
expect(isMathElement($, $('mjx-container')[0])).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('rejects regular elements', () => {
|
|
102
|
-
const $ = cheerio.load('<span class="text">x</span>');
|
|
103
|
-
expect(isMathElement($, $('span')[0])).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('extractFormula', () => {
|
|
108
|
-
it('extracts from Habr formula image', () => {
|
|
109
|
-
const $ = cheerio.load('<img class="formula" source="L \\to L^2">');
|
|
110
|
-
expect(extractFormula($, $('img')[0])).toBe('L \\to L^2');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('extracts from KaTeX element', () => {
|
|
114
|
-
const $ = cheerio.load('<span class="katex" data-tex="\\alpha"></span>');
|
|
115
|
-
expect(extractFormula($, $('span')[0])).toBe('\\alpha');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('returns null for regular elements', () => {
|
|
119
|
-
const $ = cheerio.load('<p>text</p>');
|
|
120
|
-
expect(extractFormula($, $('p')[0])).toBeNull();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
extractImageReferences,
|
|
3
|
-
getExtensionFromUrl,
|
|
4
|
-
generateLocalFilename,
|
|
5
|
-
generateFigureFilename,
|
|
6
|
-
localizeImages,
|
|
7
|
-
} from '../../src/localize-images.js';
|
|
8
|
-
|
|
9
|
-
describe('localize-images module', () => {
|
|
10
|
-
describe('extractImageReferences', () => {
|
|
11
|
-
it('extracts markdown image references', () => {
|
|
12
|
-
const md =
|
|
13
|
-
'\n';
|
|
14
|
-
const images = extractImageReferences(md);
|
|
15
|
-
expect(images).toHaveLength(2);
|
|
16
|
-
expect(images[0].altText).toBe('alt text');
|
|
17
|
-
expect(images[0].url).toBe('https://example.com/img.png');
|
|
18
|
-
expect(images[1].altText).toBe('photo');
|
|
19
|
-
expect(images[1].url).toBe('https://cdn.example.com/photo.jpg');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('ignores local image references', () => {
|
|
23
|
-
const md = '';
|
|
24
|
-
const images = extractImageReferences(md);
|
|
25
|
-
expect(images).toHaveLength(0);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('ignores non-image links', () => {
|
|
29
|
-
const md = '[link text](https://example.com)';
|
|
30
|
-
const images = extractImageReferences(md);
|
|
31
|
-
expect(images).toHaveLength(0);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('returns empty array for no images', () => {
|
|
35
|
-
const images = extractImageReferences('plain text');
|
|
36
|
-
expect(images).toHaveLength(0);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('getExtensionFromUrl', () => {
|
|
41
|
-
it('detects .png extension', () => {
|
|
42
|
-
expect(getExtensionFromUrl('https://example.com/image.png')).toBe('.png');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('detects .jpg extension', () => {
|
|
46
|
-
expect(getExtensionFromUrl('https://example.com/photo.jpg')).toBe('.jpg');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('detects .gif extension', () => {
|
|
50
|
-
expect(getExtensionFromUrl('https://example.com/anim.gif')).toBe('.gif');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('handles query parameters', () => {
|
|
54
|
-
expect(
|
|
55
|
-
getExtensionFromUrl('https://example.com/image.png?w=800&h=600')
|
|
56
|
-
).toBe('.png');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('defaults to .png for unknown extension', () => {
|
|
60
|
-
expect(getExtensionFromUrl('https://example.com/image')).toBe('.png');
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe('generateLocalFilename', () => {
|
|
65
|
-
it('generates zero-padded filename', () => {
|
|
66
|
-
expect(generateLocalFilename('https://example.com/img.png', 0)).toBe(
|
|
67
|
-
'image-01.png'
|
|
68
|
-
);
|
|
69
|
-
expect(generateLocalFilename('https://example.com/img.jpg', 9)).toBe(
|
|
70
|
-
'image-10.jpg'
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('preserves file extension from URL', () => {
|
|
75
|
-
expect(generateLocalFilename('https://example.com/photo.gif', 2)).toBe(
|
|
76
|
-
'image-03.gif'
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('generateFigureFilename', () => {
|
|
82
|
-
it('generates figure-numbered filenames while preserving extensions', () => {
|
|
83
|
-
expect(
|
|
84
|
-
generateFigureFilename('https://habrastorage.org/getpro/habr/a.png', 0)
|
|
85
|
-
).toBe('figure-1.png');
|
|
86
|
-
expect(
|
|
87
|
-
generateFigureFilename('https://habrastorage.org/getpro/habr/a.jpg', 9)
|
|
88
|
-
).toBe('figure-10.jpg');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('localizeImages', () => {
|
|
93
|
-
it('accepts a custom filename generator for archive-compatible paths', async () => {
|
|
94
|
-
const markdown =
|
|
95
|
-
'';
|
|
96
|
-
|
|
97
|
-
const result = await localizeImages(markdown, {
|
|
98
|
-
dryRun: true,
|
|
99
|
-
filenameGenerator: generateFigureFilename,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(result.markdown).toBe('');
|
|
103
|
-
expect(result.metadata).toEqual([
|
|
104
|
-
{
|
|
105
|
-
index: 1,
|
|
106
|
-
originalUrl:
|
|
107
|
-
'https://habrastorage.org/r/w1560/getpro/habr/figure.png',
|
|
108
|
-
altText: 'Figure 1',
|
|
109
|
-
localPath: 'images/figure-1.png',
|
|
110
|
-
},
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { shouldRenderHtmlWithBrowser } from '../../src/markdown.js';
|
|
2
|
-
|
|
3
|
-
describe('markdown rendering decisions', () => {
|
|
4
|
-
it('uses browser rendering for JavaScript-heavy HTML shells', () => {
|
|
5
|
-
const html = `<!doctype html>
|
|
6
|
-
<html>
|
|
7
|
-
<head><script src="/_next/static/app.js"></script></head>
|
|
8
|
-
<body><div id="__next"></div></body>
|
|
9
|
-
</html>`;
|
|
10
|
-
|
|
11
|
-
expect(shouldRenderHtmlWithBrowser(html)).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('keeps plain HTML on the direct fetch path', () => {
|
|
15
|
-
const html = `<!doctype html>
|
|
16
|
-
<html>
|
|
17
|
-
<head><title>Static page</title></head>
|
|
18
|
-
<body><main><h1>Static content</h1></main></body>
|
|
19
|
-
</html>`;
|
|
20
|
-
|
|
21
|
-
expect(shouldRenderHtmlWithBrowser(html)).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('keeps static pages with incidental scripts on the direct fetch path', () => {
|
|
25
|
-
const html = `<!doctype html>
|
|
26
|
-
<html>
|
|
27
|
-
<head>
|
|
28
|
-
<title>Static page</title>
|
|
29
|
-
<script src="/analytics.js"></script>
|
|
30
|
-
</head>
|
|
31
|
-
<body><main><h1>Static content</h1></main></body>
|
|
32
|
-
</html>`;
|
|
33
|
-
|
|
34
|
-
expect(shouldRenderHtmlWithBrowser(html)).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|