@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,142 +0,0 @@
1
- /**
2
- * Integration tests for downloading the Wikipedia page (issue #8).
3
- *
4
- * Verifies that web-capture can actually download the Wikipedia article about
5
- * Wikipedia itself (https://en.wikipedia.org/wiki/Wikipedia) as both Markdown
6
- * and an image (PNG screenshot) using every supported browser engine
7
- * (Puppeteer and Playwright).
8
- *
9
- * These tests hit the live Wikipedia servers. Set WIKIPEDIA_INTEGRATION=true to
10
- * run them. In CI they are enabled via the workflow environment variable. When
11
- * the variable is unset the suite is skipped so default/offline runs stay
12
- * deterministic.
13
- *
14
- * Navigation uses retry with exponential backoff to absorb transient network
15
- * failures, mirroring the Habr integration suite.
16
- */
17
-
18
- import { jest } from '@jest/globals';
19
- import { createBrowser } from '../../src/browser.js';
20
- import { convertHtmlToMarkdown } from '../../src/lib.js';
21
- import { retry } from '../../src/retry.js';
22
-
23
- const WIKIPEDIA_URL = 'https://en.wikipedia.org/wiki/Wikipedia';
24
-
25
- // PNG magic number (89 50 4E 47 0D 0A 1A 0A).
26
- const PNG_SIGNATURE = Buffer.from([
27
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
28
- ]);
29
-
30
- const SKIP_LIVE =
31
- !process.env.WIKIPEDIA_INTEGRATION ||
32
- process.env.WIKIPEDIA_INTEGRATION === 'false';
33
- const describeIfLive = SKIP_LIVE ? describe.skip : describe;
34
-
35
- // These tests hit live servers and may take a while.
36
- jest.setTimeout(120000);
37
-
38
- // Navigate with retry + exponential backoff, then give the page a moment to
39
- // settle so late-loading content and images are present before we capture.
40
- async function navigateWithRetry(page, url) {
41
- await retry(
42
- async () => {
43
- await page.goto(url, {
44
- waitUntil: 'networkidle0',
45
- timeout: 60000,
46
- });
47
- },
48
- {
49
- retries: 3,
50
- baseDelay: 2000,
51
- onRetry: (err, attempt, delay) => {
52
- console.log(
53
- `Navigation retry ${attempt} for ${url} after ${delay}ms: ${err.message}`
54
- );
55
- },
56
- }
57
- );
58
- await new Promise((resolve) => setTimeout(resolve, 5000));
59
- }
60
-
61
- // Run the full matrix (Markdown + image) once per supported engine.
62
- describe.each(['puppeteer', 'playwright'])(
63
- 'Wikipedia Page Download (%s engine)',
64
- (engine) => {
65
- describeIfLive(`${engine}`, () => {
66
- let browser;
67
- let page;
68
-
69
- beforeAll(async () => {
70
- browser = await createBrowser(engine);
71
- page = await browser.newPage();
72
- await page.setViewport({ width: 1280, height: 800 });
73
- await navigateWithRetry(page, WIKIPEDIA_URL);
74
- });
75
-
76
- afterAll(async () => {
77
- if (browser) {
78
- await browser.close();
79
- }
80
- });
81
-
82
- it('downloads the Wikipedia page as Markdown', async () => {
83
- const html = await page.content();
84
- expect(html).toMatch(/<html/i);
85
- expect(html.length).toBeGreaterThan(1000);
86
-
87
- const markdown = convertHtmlToMarkdown(html, WIKIPEDIA_URL);
88
-
89
- // Markdown should carry the page's real content...
90
- expect(markdown).toContain('Wikipedia');
91
- expect(markdown.length).toBeGreaterThan(500);
92
- // ...and should contain markdown structure (headings and/or links)...
93
- expect(markdown).toMatch(/(^#{1,6}\s)|(\[.*?\]\(.*?\))/m);
94
- // ...but no longer be raw HTML document scaffolding.
95
- expect(markdown).not.toMatch(/<html/i);
96
- expect(markdown).not.toMatch(/<head[\s>]/i);
97
- });
98
-
99
- it('downloads the Wikipedia page as an image (PNG screenshot)', async () => {
100
- const screenshot = await page.screenshot({ type: 'png' });
101
-
102
- expect(screenshot).toBeInstanceOf(Buffer);
103
- expect(screenshot.length).toBeGreaterThan(1000);
104
- // Verify the PNG signature so we know it is a real image.
105
- expect(screenshot.slice(0, 8)).toEqual(PNG_SIGNATURE);
106
- });
107
- });
108
- }
109
- );
110
-
111
- describeIfLive('Wikipedia Page Download (engine parity)', () => {
112
- it('both engines download Markdown and an image for the Wikipedia page', async () => {
113
- const engines = ['puppeteer', 'playwright'];
114
- const results = [];
115
-
116
- for (const engine of engines) {
117
- const browser = await createBrowser(engine);
118
- try {
119
- const page = await browser.newPage();
120
- await page.setViewport({ width: 1280, height: 800 });
121
- await navigateWithRetry(page, WIKIPEDIA_URL);
122
-
123
- const html = await page.content();
124
- const markdown = convertHtmlToMarkdown(html, WIKIPEDIA_URL);
125
- const screenshot = await page.screenshot({ type: 'png' });
126
-
127
- results.push({ engine, markdown, screenshot });
128
- } finally {
129
- await browser.close();
130
- }
131
- }
132
-
133
- for (const { markdown, screenshot } of results) {
134
- // Markdown captured from both engines.
135
- expect(markdown).toContain('Wikipedia');
136
- expect(markdown.length).toBeGreaterThan(500);
137
- // Image captured from both engines.
138
- expect(screenshot.slice(0, 8)).toEqual(PNG_SIGNATURE);
139
- expect(screenshot.length).toBeGreaterThan(1000);
140
- }
141
- }, 180000);
142
- });
@@ -1,7 +0,0 @@
1
- import { jest } from '@jest/globals';
2
-
3
- // Increase timeout for tests that might take longer
4
- jest.setTimeout(10000);
5
-
6
- // Suppress console.error during tests
7
- global.console.error = jest.fn();
@@ -1,225 +0,0 @@
1
- import request from 'supertest';
2
- import nock from '../helpers/nock.js';
3
- import { jest } from '@jest/globals';
4
-
5
- let app;
6
-
7
- beforeAll(async () => {
8
- app = (await import('../../src/index.js')).app;
9
- });
10
-
11
- describe('Web Capture Microservice', () => {
12
- beforeEach(() => {
13
- nock.cleanAll();
14
- jest.clearAllMocks();
15
- });
16
-
17
- describe('GET /html', () => {
18
- const testUrl = 'https://example.com';
19
- const testHtml = '<html><body><h1>Test Page</h1></body></html>';
20
-
21
- it('should return HTML content when URL is provided', async () => {
22
- nock(testUrl).get('/').reply(200, testHtml);
23
-
24
- const response = await request(app).get('/html').query({ url: testUrl });
25
-
26
- expect(response.status).toBe(200);
27
- expect(response.type).toBe('text/html');
28
- expect(response.text).toBe(testHtml);
29
- });
30
-
31
- it('should return 400 when URL is missing', async () => {
32
- const response = await request(app).get('/html');
33
-
34
- expect(response.status).toBe(400);
35
- expect(response.text).toBe('Missing `url` parameter');
36
- });
37
-
38
- it('should return 500 when fetch fails', async () => {
39
- nock(testUrl).get('/').replyWithError('Network error');
40
-
41
- const response = await request(app).get('/html').query({ url: testUrl });
42
-
43
- expect(response.status).toBe(500);
44
- expect(response.text).toBe('Error fetching HTML');
45
- });
46
- });
47
-
48
- describe('GET /markdown', () => {
49
- const testUrl = 'https://example.com';
50
- const testHtml =
51
- '<html><body><h1>Test Page</h1><p>Some text</p></body></html>';
52
- const expectedMarkdown = '# Test Page\n\nSome text';
53
-
54
- it('should convert HTML to Markdown when URL is provided', async () => {
55
- nock(testUrl).get('/').reply(200, testHtml);
56
-
57
- const response = await request(app)
58
- .get('/markdown')
59
- .query({ url: testUrl });
60
-
61
- expect(response.status).toBe(200);
62
- expect(response.type).toBe('text/markdown');
63
- expect(response.text).toBe(expectedMarkdown);
64
- });
65
-
66
- it('should remove CSS from the markdown output', async () => {
67
- const htmlWithCss = `
68
- <html>
69
- <head>
70
- <style>
71
- body { background-color: #f0f0f2; }
72
- div { width: 600px; }
73
- </style>
74
- </head>
75
- <body>
76
- <h1>Test Page</h1>
77
- <p>Some text</p>
78
- </body>
79
- </html>
80
- `;
81
-
82
- nock(testUrl).get('/').reply(200, htmlWithCss);
83
-
84
- const response = await request(app)
85
- .get('/markdown')
86
- .query({ url: testUrl });
87
-
88
- expect(response.status).toBe(200);
89
- expect(response.type).toBe('text/markdown');
90
- expect(response.text).not.toContain('background-color');
91
- expect(response.text).not.toContain('width: 600px');
92
- expect(response.text).toContain('Test Page');
93
- expect(response.text).toContain('Some text');
94
- });
95
-
96
- it('should return 400 when URL is missing', async () => {
97
- const response = await request(app).get('/markdown');
98
-
99
- expect(response.status).toBe(400);
100
- expect(response.text).toBe('Missing `url` parameter');
101
- });
102
-
103
- it('should return 500 when fetch fails', async () => {
104
- nock(testUrl).get('/').replyWithError('Network error');
105
-
106
- const response = await request(app)
107
- .get('/markdown')
108
- .query({ url: testUrl });
109
-
110
- expect(response.status).toBe(500);
111
- expect(response.text).toBe('Error converting to Markdown');
112
- });
113
- });
114
-
115
- describe('GET /image', () => {
116
- const testUrl = 'https://example.com';
117
- // Buffer for PNG image mock
118
- // eslint-disable-next-line no-unused-vars
119
- const mockBuffer = Buffer.from([
120
- 0x89,
121
- 0x50,
122
- 0x4e,
123
- 0x47,
124
- 0x0d,
125
- 0x0a,
126
- 0x1a,
127
- 0x0a, // PNG signature
128
- 0x00,
129
- 0x00,
130
- 0x00,
131
- 0x0d,
132
- 0x49,
133
- 0x48,
134
- 0x44,
135
- 0x52, // IHDR chunk
136
- // ... (rest can be arbitrary for test)
137
- 0x00,
138
- 0x00,
139
- 0x00,
140
- 0x00,
141
- 0x49,
142
- 0x45,
143
- 0x4e,
144
- 0x44,
145
- 0xae,
146
- 0x42,
147
- 0x60,
148
- 0x82, // IEND chunk
149
- ]);
150
-
151
- it('should return PNG image when URL is provided', async () => {
152
- const response = await request(app).get('/image').query({ url: testUrl });
153
-
154
- expect(response.status).toBe(200);
155
- expect(response.type).toBe('image/png');
156
- const pngSignature = Buffer.from([
157
- 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
158
- ]);
159
- expect(response.body.slice(0, 8)).toEqual(pngSignature);
160
- }, 60000);
161
-
162
- it('should return 400 when URL is missing', async () => {
163
- const response = await request(app).get('/image');
164
-
165
- expect(response.status).toBe(400);
166
- expect(response.text).toBe('Missing `url` parameter');
167
- });
168
- });
169
-
170
- describe('GET /stream', () => {
171
- it('should stream content from the given URL', async () => {
172
- const testUrl = 'https://example.com';
173
- const testHtml = '<html><body><h1>Streamed Page</h1></body></html>';
174
- nock(testUrl).get('/').reply(200, testHtml, {
175
- 'content-type': 'text/html',
176
- });
177
- const response = await request(app).get(`/stream?url=${testUrl}`);
178
- expect(response.status).toBe(200);
179
- expect(response.text).toMatch(/<html/i);
180
- });
181
-
182
- it('should return 400 when URL is missing', async () => {
183
- const response = await request(app).get('/stream');
184
- expect(response.status).toBe(400);
185
- expect(response.text).toBe('Missing `url` parameter');
186
- });
187
-
188
- it('should return 500 when fetch fails', async () => {
189
- const testUrl = 'https://nonexistent.example.com';
190
- const response = await request(app).get(`/stream?url=${testUrl}`);
191
- expect(response.status).toBe(500);
192
- expect(response.text).toBe('Error proxying content');
193
- });
194
- });
195
-
196
- describe('GET /fetch', () => {
197
- const testUrl = 'https://example.com';
198
- const testHtml = '<html><body><h1>Test Page</h1></body></html>';
199
-
200
- it('should return content when URL is provided', async () => {
201
- nock(testUrl).get('/').reply(200, testHtml);
202
-
203
- const response = await request(app).get('/fetch').query({ url: testUrl });
204
-
205
- expect(response.status).toBe(200);
206
- expect(response.text).toBe(testHtml);
207
- });
208
-
209
- it('should return 400 when URL is missing', async () => {
210
- const response = await request(app).get('/fetch');
211
-
212
- expect(response.status).toBe(400);
213
- expect(response.text).toBe('Missing `url` parameter');
214
- });
215
-
216
- it('should return 500 when fetch fails', async () => {
217
- nock(testUrl).get('/').replyWithError('Network error');
218
-
219
- const response = await request(app).get('/fetch').query({ url: testUrl });
220
-
221
- expect(response.status).toBe(500);
222
- expect(response.text).toBe('Error fetching content');
223
- });
224
- });
225
- });
@@ -1,33 +0,0 @@
1
- import { compareFrames } from '../../src/animation.js';
2
-
3
- describe('animation module', () => {
4
- describe('compareFrames', () => {
5
- it('returns 1.0 for identical frames', () => {
6
- const frame = Buffer.from([1, 2, 3, 4, 5]);
7
- expect(compareFrames(frame, frame)).toBe(1.0);
8
- });
9
-
10
- it('returns 0 for completely different frames', () => {
11
- const frame1 = Buffer.from([0, 0, 0, 0]);
12
- const frame2 = Buffer.from([255, 255, 255, 255]);
13
- expect(compareFrames(frame1, frame2)).toBe(0);
14
- });
15
-
16
- it('returns 0 for different sized frames', () => {
17
- const frame1 = Buffer.from([1, 2, 3]);
18
- const frame2 = Buffer.from([1, 2]);
19
- expect(compareFrames(frame1, frame2)).toBe(0);
20
- });
21
-
22
- it('returns 0 for null frames', () => {
23
- expect(compareFrames(null, Buffer.from([1]))).toBe(0);
24
- expect(compareFrames(Buffer.from([1]), null)).toBe(0);
25
- });
26
-
27
- it('calculates partial similarity', () => {
28
- const frame1 = Buffer.from([1, 2, 3, 4]);
29
- const frame2 = Buffer.from([1, 2, 0, 0]);
30
- expect(compareFrames(frame1, frame2)).toBe(0.5);
31
- });
32
- });
33
- });
@@ -1,114 +0,0 @@
1
- import {
2
- getArticle,
3
- getAllVersions,
4
- getAllArticles,
5
- createConfigFromUrls,
6
- validateConfig,
7
- } from '../../src/batch.js';
8
-
9
- describe('batch module', () => {
10
- const sampleConfig = {
11
- articles: {
12
- '0.0.0': {
13
- url: 'https://example.com/article-1',
14
- title: 'First Article',
15
- language: 'en',
16
- },
17
- '0.0.1': {
18
- url: 'https://example.com/article-2',
19
- title: 'Second Article',
20
- language: 'ru',
21
- },
22
- },
23
- defaults: {
24
- archivePath: 'archive',
25
- markdownFile: 'document.md',
26
- imagesDir: 'images',
27
- hasLocalImages: true,
28
- },
29
- };
30
-
31
- describe('getArticle', () => {
32
- it('returns article config by version', () => {
33
- const article = getArticle(sampleConfig, '0.0.0');
34
- expect(article.url).toBe('https://example.com/article-1');
35
- expect(article.title).toBe('First Article');
36
- });
37
-
38
- it('merges defaults into article config', () => {
39
- const article = getArticle(sampleConfig, '0.0.0');
40
- expect(article.markdownFile).toBe('document.md');
41
- expect(article.hasLocalImages).toBe(true);
42
- });
43
-
44
- it('throws for unknown version', () => {
45
- expect(() => getArticle(sampleConfig, '9.9.9')).toThrow(
46
- /Unknown article version/
47
- );
48
- });
49
- });
50
-
51
- describe('getAllVersions', () => {
52
- it('returns all version keys', () => {
53
- const versions = getAllVersions(sampleConfig);
54
- expect(versions).toEqual(['0.0.0', '0.0.1']);
55
- });
56
- });
57
-
58
- describe('getAllArticles', () => {
59
- it('returns all articles with defaults merged', () => {
60
- const articles = getAllArticles(sampleConfig);
61
- expect(articles).toHaveLength(2);
62
- expect(articles[0].markdownFile).toBe('document.md');
63
- expect(articles[1].language).toBe('ru');
64
- });
65
- });
66
-
67
- describe('createConfigFromUrls', () => {
68
- it('creates config from URL list', () => {
69
- const config = createConfigFromUrls([
70
- 'https://example.com/a',
71
- 'https://example.com/b',
72
- ]);
73
- expect(Object.keys(config.articles)).toHaveLength(2);
74
- expect(config.articles['1'].url).toBe('https://example.com/a');
75
- expect(config.articles['2'].url).toBe('https://example.com/b');
76
- });
77
-
78
- it('sets default archive paths', () => {
79
- const config = createConfigFromUrls(['https://habr.com/article/123']);
80
- expect(config.articles['1'].archivePath).toContain('habr-com');
81
- expect(config.articles['1'].markdownFile).toBe('document.md');
82
- });
83
- });
84
-
85
- describe('validateConfig', () => {
86
- it('validates correct config', () => {
87
- const result = validateConfig(sampleConfig);
88
- expect(result.valid).toBe(true);
89
- expect(result.errors).toHaveLength(0);
90
- });
91
-
92
- it('rejects config without articles', () => {
93
- const result = validateConfig({});
94
- expect(result.valid).toBe(false);
95
- expect(result.errors[0]).toContain('articles');
96
- });
97
-
98
- it('rejects article without url', () => {
99
- const result = validateConfig({
100
- articles: { 1: { title: 'No URL' } },
101
- });
102
- expect(result.valid).toBe(false);
103
- expect(result.errors[0]).toContain('missing required "url"');
104
- });
105
-
106
- it('rejects article with invalid url', () => {
107
- const result = validateConfig({
108
- articles: { 1: { url: 'not-a-url' } },
109
- });
110
- expect(result.valid).toBe(false);
111
- expect(result.errors[0]).toContain('invalid URL');
112
- });
113
- });
114
- });
@@ -1,54 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import {
5
- normalizeGoogleDocsExportMarkdown,
6
- preprocessGoogleDocsExportHtml,
7
- } from '../../src/gdocs.js';
8
- import { convertHtmlToMarkdown } from '../../src/lib.js';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const HTML = fs.readFileSync(
13
- path.join(__dirname, '..', 'fixtures', 'bold-coalesce-and-leakage.html'),
14
- 'utf8'
15
- );
16
-
17
- describe('bold-run coalescing and block boundary handling (issue #120)', () => {
18
- let md;
19
- beforeAll(() => {
20
- const preprocessed = preprocessGoogleDocsExportHtml(HTML);
21
- md = normalizeGoogleDocsExportMarkdown(
22
- convertHtmlToMarkdown(preprocessed.html)
23
- );
24
- });
25
-
26
- it('A: coalesces adjacent bold spans into a single bold run', () => {
27
- expect(md).toMatch(/\*\*13\.1 First subsection\*\*/);
28
- expect(md).not.toMatch(/\*\*13\.1\*\*\s+\*\*First subsection\*\*/);
29
- });
30
-
31
- it('B: never emits an empty "****" pair between adjacent bold runs', () => {
32
- expect(md).not.toMatch(/\*{4}/);
33
- });
34
-
35
- it('C: closes bold at block-level boundaries (<br>, <img>)', () => {
36
- // No single bold run may contain an image. CommonMark bold cannot span
37
- // a blank line (`\n\n`), so the inner content is restricted accordingly.
38
- const strongRe = /\*\*((?:(?!\*\*|\n\n)[\s\S])+?)\*\*/g;
39
- const imageRe = /!\[[^\]]*\]\([^)]+\)/;
40
- for (const match of md.matchAll(strongRe)) {
41
- expect(imageRe.test(match[1])).toBe(false);
42
- }
43
- expect(md).toMatch(/\*\*Caption A:\*\*/);
44
- expect(md).toMatch(/\*\*Caption B:\*\*/);
45
- });
46
-
47
- it('D: every "**" opener has a matching closer (balanced asterisks)', () => {
48
- const stripped = md
49
- .replace(/`[^`]*`/g, '')
50
- .replace(/!\[[^\]]*\]\([^)]*\)/g, '');
51
- const count = (stripped.match(/\*\*/g) || []).length;
52
- expect(count % 2).toBe(0);
53
- });
54
- });
@@ -1,42 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- import path from 'node:path';
3
- import { withTemporaryDirectory } from '../helpers/temporary-directory.js';
4
-
5
- const initialPage = { close: jest.fn(), bringToFront: jest.fn() };
6
- const browser = { pages: jest.fn(() => [initialPage]), close: jest.fn() };
7
- const launch = jest.fn(async () => browser);
8
- const launchPersistentContext = jest.fn(async () => browser);
9
- jest.unstable_mockModule('puppeteer', () => ({ default: { launch } }));
10
- jest.unstable_mockModule('playwright', () => ({
11
- chromium: { launchPersistentContext },
12
- }));
13
- const { createBrowser } = await import('../../src/browser.js');
14
-
15
- beforeEach(() => {
16
- jest.clearAllMocks();
17
- });
18
-
19
- test.each(['puppeteer', 'playwright'])(
20
- '%s forwards packaged executablePath and channel to the engine',
21
- (engine) =>
22
- withTemporaryDirectory('launch-options-', async (directory) => {
23
- const options = {
24
- executablePath: path.join(directory, 'packaged-chromium'),
25
- channel: 'chrome',
26
- userDataDir: directory,
27
- };
28
- const handle = await createBrowser(engine, options);
29
- try {
30
- const observed =
31
- engine === 'puppeteer'
32
- ? launch.mock.calls[0][0]
33
- : launchPersistentContext.mock.calls[0][1];
34
- expect(observed).toMatchObject({
35
- executablePath: options.executablePath,
36
- channel: options.channel,
37
- });
38
- } finally {
39
- await handle.close();
40
- }
41
- })
42
- );