@link-assistant/web-capture 2.0.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 (117) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/web-capture.js +2 -5
  3. package/package.json +22 -11
  4. package/src/archive.js +0 -1
  5. package/src/binary-download.js +0 -1
  6. package/src/docx.js +13 -10
  7. package/src/fetch.js +0 -1
  8. package/src/gdocs-images.js +0 -1
  9. package/src/gdocs.js +0 -1
  10. package/src/github.js +0 -1
  11. package/src/lib.js +0 -1
  12. package/src/markdown.js +0 -1
  13. package/src/shared-dialog.js +0 -1
  14. package/src/stream.js +0 -1
  15. package/src/transport.js +23 -5
  16. package/src/txt.js +0 -1
  17. package/.changeset/README.md +0 -8
  18. package/.changeset/config.json +0 -11
  19. package/.dockerignore +0 -6
  20. package/.jscpd.json +0 -19
  21. package/.lenv +0 -8
  22. package/.prettierignore +0 -8
  23. package/.prettierrc +0 -10
  24. package/Dockerfile +0 -52
  25. package/docker-compose.yml +0 -11
  26. package/eslint.config.js +0 -112
  27. package/examples/js/capture_cache.js +0 -12
  28. package/examples/js/engine_comparison.js +0 -39
  29. package/examples/js/html_download.js +0 -23
  30. package/examples/js/image_download.js +0 -36
  31. package/examples/js/markdown_download.js +0 -23
  32. package/examples/js/playwright_example.js +0 -23
  33. package/examples/python/capture_client.py +0 -20
  34. package/examples/python/html_download.py +0 -3
  35. package/examples/python/image_download.py +0 -16
  36. package/examples/python/markdown_download.py +0 -3
  37. package/examples/python/playwright_example.py +0 -22
  38. package/jest.config.mjs +0 -17
  39. package/tests/ci/dependency-freshness.test.js +0 -74
  40. package/tests/ci/detect-code-changes.test.js +0 -187
  41. package/tests/ci/docker-install.test.js +0 -31
  42. package/tests/ci/rust-dockerfile.test.js +0 -54
  43. package/tests/ci/workflow-policy.test.js +0 -79
  44. package/tests/e2e/docker.test.js +0 -186
  45. package/tests/e2e/process.test.js +0 -215
  46. package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
  47. package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
  48. package/tests/fixtures/deepwiki/index.md +0 -1500
  49. package/tests/fixtures/deepwiki/part-1.md +0 -776
  50. package/tests/fixtures/habr-articles.js +0 -45
  51. package/tests/fixtures/heading-numbering.html +0 -19
  52. package/tests/fixtures/list-item-with-br.html +0 -8
  53. package/tests/fixtures/ol-continuous-numbering.html +0 -12
  54. package/tests/fixtures/paragraph-vs-line-break.html +0 -11
  55. package/tests/helpers/nock.js +0 -10
  56. package/tests/helpers/screenshot.js +0 -10
  57. package/tests/helpers/search-fixture.cjs +0 -17
  58. package/tests/helpers/temporary-directory.js +0 -12
  59. package/tests/integration/api-endpoints.test.js +0 -418
  60. package/tests/integration/archive-default-layout.test.js +0 -37
  61. package/tests/integration/browser-engines.test.js +0 -80
  62. package/tests/integration/browser-packaged-path.test.js +0 -45
  63. package/tests/integration/deepwiki-download.test.js +0 -144
  64. package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
  65. package/tests/integration/formalai-contract.test.js +0 -214
  66. package/tests/integration/gdocs-public-doc.test.js +0 -315
  67. package/tests/integration/github-readme.test.js +0 -133
  68. package/tests/integration/google-drive-image.test.js +0 -246
  69. package/tests/integration/habr-article.test.js +0 -337
  70. package/tests/integration/search-endpoint.test.js +0 -88
  71. package/tests/integration/stackoverflow-download.test.js +0 -173
  72. package/tests/integration/wikipedia-download.test.js +0 -134
  73. package/tests/jest.setup.mjs +0 -7
  74. package/tests/mock/index.test.js +0 -225
  75. package/tests/unit/animation.test.js +0 -33
  76. package/tests/unit/batch.test.js +0 -114
  77. package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
  78. package/tests/unit/browser-launch-options.test.js +0 -42
  79. package/tests/unit/browser.test.js +0 -172
  80. package/tests/unit/cache.test.js +0 -182
  81. package/tests/unit/cli.test.js +0 -399
  82. package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
  83. package/tests/unit/convertRelativeUrls.test.js +0 -34
  84. package/tests/unit/extract-images.test.js +0 -259
  85. package/tests/unit/fetch-html.test.js +0 -54
  86. package/tests/unit/figures.test.js +0 -74
  87. package/tests/unit/gdocs-export-headers.test.js +0 -28
  88. package/tests/unit/gdocs-preprocess.test.js +0 -36
  89. package/tests/unit/gdocs.test.js +0 -1490
  90. package/tests/unit/github.test.js +0 -156
  91. package/tests/unit/heading-image.test.js +0 -49
  92. package/tests/unit/heading-numbering.test.js +0 -28
  93. package/tests/unit/html2md-br-in-list-item.test.js +0 -23
  94. package/tests/unit/html2md-ol-numbering.test.js +0 -20
  95. package/tests/unit/html2md.test.js +0 -695
  96. package/tests/unit/image-mode-defaults.test.js +0 -54
  97. package/tests/unit/kreuzberg.test.js +0 -249
  98. package/tests/unit/latex.test.js +0 -123
  99. package/tests/unit/localize-images.test.js +0 -114
  100. package/tests/unit/markdown.test.js +0 -36
  101. package/tests/unit/metadata.test.js +0 -140
  102. package/tests/unit/package-runtime-dependencies.test.js +0 -74
  103. package/tests/unit/paragraph-vs-line-break.test.js +0 -56
  104. package/tests/unit/postprocess.test.js +0 -110
  105. package/tests/unit/proxy-headers.test.js +0 -100
  106. package/tests/unit/publish-verification.test.js +0 -54
  107. package/tests/unit/retry.test.js +0 -111
  108. package/tests/unit/search.test.js +0 -339
  109. package/tests/unit/shared-dialog.test.js +0 -173
  110. package/tests/unit/stackoverflow.test.js +0 -32
  111. package/tests/unit/stream.test.js +0 -83
  112. package/tests/unit/verify.test.js +0 -182
  113. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
  114. package/tests/xpaste/xpaste.e2e.test.js +0 -36
  115. package/tests/xpaste/xpaste.integration.test.js +0 -41
  116. package/tests/xpaste/xpaste.mock.test.js +0 -261
  117. 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![](https://example.invalid/foo.png)\n\nBye.\n`;
9
- const BASE64 = `Hi.\n\n![](data:image/png;base64,${TINY_PNG})\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
- });
@@ -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
- '![alt text](https://example.com/img.png)\n![photo](https://cdn.example.com/photo.jpg)';
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 = '![local](images/figure-1.png)';
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
- '![Figure 1](https://habrastorage.org/r/w1560/getpro/habr/figure.png "Figure 1")';
96
-
97
- const result = await localizeImages(markdown, {
98
- dryRun: true,
99
- filenameGenerator: generateFigureFilename,
100
- });
101
-
102
- expect(result.markdown).toBe('![Figure 1](images/figure-1.png)');
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
- });