@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,261 +0,0 @@
1
- import request from 'supertest';
2
- import nock from '../helpers/nock.js';
3
- import { jest } from '@jest/globals';
4
- import unzipper from 'unzipper';
5
-
6
- let app;
7
-
8
- beforeAll(async () => {
9
- app = (await import('../../src/index.js')).app;
10
- });
11
-
12
- describe('xpaste.pro integration tests', () => {
13
- beforeEach(() => {
14
- nock.cleanAll();
15
- jest.clearAllMocks();
16
- });
17
-
18
- describe('GET /txt with xpaste.pro URL', () => {
19
- const testUrl = 'https://xpaste.pro/p/t4q0Lsp0';
20
- // Using actual data from https://xpaste.pro/p/t4q0Lsp0 (captured on 2025-11-14)
21
- const testText = `# 1
22
- #
23
- # Time: 210707 15:39:36
24
- # User@Host: 1703313381[1703313381] @ [136.243.53.188] Id: 1138102510
25
- # Schema: xxxxxx Last_errno: 0 Killed: 0
26
- # Query_time: 2.182754 Lock_time: 0.000120 Rows_sent: 0 Rows_examined: 324036 Rows_affected: 0
27
- # Bytes_sent: 20494
28
- SET timestamp=1625661576;
29
- SELECT f.*, t.*, p.*, u.*, tt.mark_time AS topic_mark_time, ft.mark_time AS forum_mark_time FROM (phpbb_posts p CROSS JOIN phpbb_users u CROSS JOIN phpbb_topics t) LEFT JOIN phpbb_forums f ON (t.forum_id = f.forum_id) LEFT JOIN phpbb_topics_track tt ON (t.topic_id = tt.topic_id AND tt.user_id = 659822) LEFT JOIN phpbb_forums_track ft ON (f.forum_id = ft.forum_id AND ft.user_id = 659822) WHERE p.topic_id = t.topic_id
30
- AND p.poster_id = u.user_id
31
- AND p.post_time > 1625660099
32
- AND p.forum_id = 326
33
-
34
-
35
-
36
- AND p.post_approved = 1 ORDER BY t.topic_last_post_time DESC, p.post_time
37
- LIMIT 100;`;
38
-
39
- it('should fetch and return text content from xpaste.pro', async () => {
40
- nock('https://xpaste.pro')
41
- .get('/p/t4q0Lsp0/raw')
42
- .reply(200, testText, { 'content-type': 'text/plain; charset=utf-8' });
43
-
44
- const response = await request(app).get('/txt').query({ url: testUrl });
45
-
46
- expect(response.status).toBe(200);
47
- expect(response.type).toBe('text/plain');
48
- expect(response.text).toBe(testText);
49
- expect(response.headers['content-disposition']).toContain('attachment');
50
- expect(response.headers['content-disposition']).toContain('.txt');
51
- });
52
-
53
- it('should return 400 when URL is missing', async () => {
54
- const response = await request(app).get('/txt');
55
-
56
- expect(response.status).toBe(400);
57
- expect(response.text).toBe('Missing `url` parameter');
58
- });
59
-
60
- it('should return 500 when fetch fails', async () => {
61
- nock('https://xpaste.pro')
62
- .get('/p/t4q0Lsp0/raw')
63
- .replyWithError('Network error');
64
-
65
- const response = await request(app).get('/txt').query({ url: testUrl });
66
-
67
- expect(response.status).toBe(500);
68
- expect(response.text).toBe('Error fetching text content');
69
- });
70
- });
71
-
72
- describe('GET /markdown with xpaste.pro URL (small content)', () => {
73
- const testUrl = 'https://xpaste.pro/p/t4q0Lsp0';
74
- const rawText = `# 1
75
- #
76
- # Time: 210707 15:39:36
77
- SET timestamp=1625661576;
78
- SELECT * FROM phpbb_posts WHERE topic_id = 123;`;
79
- // Mock HTML that resembles actual xpaste.pro page structure
80
- const testHtml = `<!DOCTYPE html>
81
- <html>
82
- <head><title>xPaste</title></head>
83
- <body>
84
- <h1>Xpaste</h1>
85
- <div>
86
- <strong>Формат:</strong> text<br>
87
- <strong>Время создания:</strong> 07.07.2021, 12:48 UTC<br>
88
- <strong>Будет удалена:</strong> 05.07.2031, 12:48 UTC
89
- </div>
90
- <pre># 1
91
- #
92
- # Time: 210707 15:39:36
93
- # User@Host: 1703313381[1703313381] @ [136.243.53.188] Id: 1138102510
94
- # Schema: xxxxxx Last_errno: 0 Killed: 0
95
- # Query_time: 2.182754 Lock_time: 0.000120 Rows_sent: 0 Rows_examined: 324036 Rows_affected: 0
96
- SET timestamp=1625661576;
97
- SELECT * FROM phpbb_posts WHERE topic_id = 123;</pre>
98
- <footer>
99
- <a href="https://xpaste.pro/">xPaste</a>
100
- <p>Упакуем пароль или код в cсылку для передачи</p>
101
- <p>Сделано в <a href="https://southbridge.io/">Southbridge</a></p>
102
- </footer>
103
- </body>
104
- </html>`;
105
-
106
- it('should convert HTML to markdown with all page elements when content is less than 1500 lines', async () => {
107
- nock('https://xpaste.pro')
108
- .get('/p/t4q0Lsp0')
109
- .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
110
- nock('https://xpaste.pro')
111
- .get('/p/t4q0Lsp0/raw')
112
- .reply(200, rawText, { 'content-type': 'text/plain; charset=utf-8' });
113
-
114
- const response = await request(app)
115
- .get('/markdown')
116
- .query({ url: testUrl });
117
-
118
- expect(response.status).toBe(200);
119
- expect(response.type).toBe('text/markdown');
120
-
121
- // Verify it includes page metadata (visible in screenshot)
122
- expect(response.text).toContain('Xpaste');
123
- expect(response.text).toContain('Формат:');
124
- expect(response.text).toContain('text');
125
- expect(response.text).toContain('07.07.2021');
126
-
127
- // Verify it includes the SQL query content
128
- expect(response.text).toContain('1703313381');
129
- // Note: markdown escapes special characters like * and _
130
- expect(response.text).toMatch(/SELECT.*FROM phpbb.*posts/);
131
-
132
- // Verify it includes footer elements (visible in screenshot)
133
- expect(response.text).toContain('Southbridge');
134
- expect(response.text).toContain('Упакуем пароль или код');
135
-
136
- // Verify the raw text file is embedded as a named markdown block.
137
- expect(response.text).toContain('## xpaste-pro-t4q0Lsp0.txt');
138
- expect(response.text).toContain('```text\n# 1\n#');
139
- });
140
-
141
- it('should normalize raw xpaste URLs before markdown conversion', async () => {
142
- nock('https://xpaste.pro')
143
- .get('/p/t4q0Lsp0')
144
- .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
145
- nock('https://xpaste.pro')
146
- .get('/p/t4q0Lsp0/raw')
147
- .reply(200, rawText, { 'content-type': 'text/plain; charset=utf-8' });
148
-
149
- const response = await request(app)
150
- .get('/markdown')
151
- .query({ url: `${testUrl}/raw` });
152
-
153
- expect(response.status).toBe(200);
154
- expect(response.type).toBe('text/markdown');
155
- expect(response.text).toContain('Формат:');
156
- expect(response.text).toContain('## xpaste-pro-t4q0Lsp0.txt');
157
- });
158
- });
159
-
160
- describe('GET /markdown with xpaste.pro URL (large content)', () => {
161
- const testUrl = 'https://xpaste.pro/p/largefile';
162
-
163
- it('should create a zip archive when content is 1500 lines or more', async () => {
164
- // Create HTML content that will result in exactly 1500 lines of markdown
165
- const largeContent = Array(1480).fill('<p>Line of text</p>').join('\n');
166
- const largeHtml = `<!DOCTYPE html>
167
- <html><head><title>Large File</title></head><body>${largeContent}</body></html>`;
168
-
169
- nock('https://xpaste.pro')
170
- .get('/p/largefile')
171
- .reply(200, largeHtml, { 'content-type': 'text/html; charset=utf-8' });
172
- nock('https://xpaste.pro')
173
- .get('/p/largefile/raw')
174
- .reply(200, largeContent.replace(/<\/?p>/g, ''), {
175
- 'content-type': 'text/plain; charset=utf-8',
176
- });
177
-
178
- const response = await request(app)
179
- .get('/markdown')
180
- .query({ url: testUrl })
181
- .buffer(true)
182
- .parse((res, callback) => {
183
- const chunks = [];
184
- res.on('data', (chunk) => chunks.push(chunk));
185
- res.on('end', () => callback(null, Buffer.concat(chunks)));
186
- });
187
-
188
- expect(response.status).toBe(200);
189
- expect(response.type).toBe('application/zip');
190
- expect(response.headers['content-disposition']).toContain('attachment');
191
- expect(response.headers['content-disposition']).toContain(
192
- 'largefile.zip'
193
- );
194
-
195
- const dir = await unzipper.Open.buffer(response.body);
196
- const names = dir.files.map((file) => file.path);
197
- expect(names).toEqual(
198
- expect.arrayContaining([
199
- 'index.md',
200
- 'xpaste-pro-largefile.md',
201
- 'xpaste-pro-largefile.txt',
202
- ])
203
- );
204
-
205
- const index = await dir.files
206
- .find((file) => file.path === 'index.md')
207
- .buffer();
208
- const rawTextFile = await dir.files
209
- .find((file) => file.path === 'xpaste-pro-largefile.txt')
210
- .buffer();
211
- expect(index.toString()).toContain(
212
- '[xpaste-pro-largefile.txt](xpaste-pro-largefile.txt)'
213
- );
214
- expect(rawTextFile.toString()).toContain('Line of text');
215
- });
216
-
217
- it('should create a zip archive for content with more than 1500 lines', async () => {
218
- // Create HTML content that will result in more than 1500 lines of markdown
219
- const largeContent = Array(1600).fill('<p>Line of text</p>').join('\n');
220
- const largeHtml = `<!DOCTYPE html>
221
- <html><head><title>Large File</title></head><body>${largeContent}</body></html>`;
222
-
223
- nock('https://xpaste.pro')
224
- .get('/p/largefile')
225
- .reply(200, largeHtml, { 'content-type': 'text/html; charset=utf-8' });
226
- nock('https://xpaste.pro')
227
- .get('/p/largefile/raw')
228
- .reply(200, largeContent.replace(/<\/?p>/g, ''), {
229
- 'content-type': 'text/plain; charset=utf-8',
230
- });
231
-
232
- const response = await request(app)
233
- .get('/markdown')
234
- .query({ url: testUrl });
235
-
236
- expect(response.status).toBe(200);
237
- expect(response.type).toBe('application/zip');
238
- });
239
- });
240
-
241
- describe('GET /markdown with non-xpaste.pro URL', () => {
242
- const testUrl = 'https://example.com';
243
- const testHtml =
244
- '<html><body><h1>Test Page</h1><p>Regular HTML content</p></body></html>';
245
-
246
- it('should process regular HTML URLs normally', async () => {
247
- nock(testUrl).get('/').reply(200, testHtml);
248
-
249
- const response = await request(app)
250
- .get('/markdown')
251
- .query({ url: testUrl });
252
-
253
- expect(response.status).toBe(200);
254
- expect(response.type).toBe('text/markdown');
255
- expect(response.text).toContain('Test Page');
256
- expect(response.text).toContain('Regular HTML content');
257
- // Should not be a zip file
258
- expect(response.headers['content-type']).not.toBe('application/zip');
259
- });
260
- });
261
- });
@@ -1,119 +0,0 @@
1
- import {
2
- appendTextPasteMarkdownAttachment,
3
- getTextPasteFilename,
4
- isTextPasteUrl,
5
- normalizeUrlForTextContent,
6
- normalizeUrlForTextPage,
7
- } from '../../src/lib.js';
8
-
9
- describe('xpaste.pro URL handling utilities', () => {
10
- describe('normalizeUrlForTextContent', () => {
11
- it('should convert xpaste.pro URL to raw endpoint', () => {
12
- const url = 'https://xpaste.pro/p/t4q0Lsp0';
13
- const normalized = normalizeUrlForTextContent(url);
14
- expect(normalized).toBe('https://xpaste.pro/p/t4q0Lsp0/raw');
15
- });
16
-
17
- it('should convert localized xpaste.pro URLs to localized raw endpoints', () => {
18
- expect(
19
- normalizeUrlForTextContent('https://xpaste.pro/ru/p/t4q0Lsp0')
20
- ).toBe('https://xpaste.pro/ru/p/t4q0Lsp0/raw');
21
- expect(
22
- normalizeUrlForTextContent('https://xpaste.pro/en/p/t4q0Lsp0')
23
- ).toBe('https://xpaste.pro/en/p/t4q0Lsp0/raw');
24
- });
25
-
26
- it('should not modify xpaste.pro URL that already has /raw', () => {
27
- const url = 'https://xpaste.pro/p/t4q0Lsp0/raw';
28
- const normalized = normalizeUrlForTextContent(url);
29
- expect(normalized).toBe('https://xpaste.pro/p/t4q0Lsp0/raw');
30
- });
31
-
32
- it('should not modify non-xpaste.pro URLs', () => {
33
- const url = 'https://example.com/page';
34
- const normalized = normalizeUrlForTextContent(url);
35
- expect(normalized).toBe(url);
36
- });
37
-
38
- it('should handle invalid URLs gracefully', () => {
39
- const url = 'not-a-valid-url';
40
- const normalized = normalizeUrlForTextContent(url);
41
- expect(normalized).toBe(url);
42
- });
43
- });
44
-
45
- describe('normalizeUrlForTextPage', () => {
46
- it('should convert xpaste.pro raw URLs back to visual page URLs', () => {
47
- expect(normalizeUrlForTextPage('https://xpaste.pro/p/t4q0Lsp0/raw')).toBe(
48
- 'https://xpaste.pro/p/t4q0Lsp0'
49
- );
50
- expect(
51
- normalizeUrlForTextPage('https://xpaste.pro/ru/p/t4q0Lsp0/raw')
52
- ).toBe('https://xpaste.pro/ru/p/t4q0Lsp0');
53
- });
54
-
55
- it('should not modify non-xpaste.pro URLs', () => {
56
- const url = 'https://example.com/page';
57
- expect(normalizeUrlForTextPage(url)).toBe(url);
58
- });
59
- });
60
-
61
- describe('isTextPasteUrl', () => {
62
- it('should return true for xpaste.pro paste URLs', () => {
63
- expect(isTextPasteUrl('https://xpaste.pro/p/t4q0Lsp0')).toBe(true);
64
- expect(isTextPasteUrl('https://xpaste.pro/p/abc123')).toBe(true);
65
- expect(isTextPasteUrl('https://xpaste.pro/ru/p/t4q0Lsp0')).toBe(true);
66
- expect(isTextPasteUrl('https://xpaste.pro/en/p/t4q0Lsp0')).toBe(true);
67
- });
68
-
69
- it('should return false for non-xpaste.pro URLs', () => {
70
- expect(isTextPasteUrl('https://example.com')).toBe(false);
71
- expect(isTextPasteUrl('https://pastebin.com/xyz')).toBe(false);
72
- });
73
-
74
- it('should return false for xpaste.pro URLs without /p/ path', () => {
75
- expect(isTextPasteUrl('https://xpaste.pro')).toBe(false);
76
- expect(isTextPasteUrl('https://xpaste.pro/about')).toBe(false);
77
- expect(isTextPasteUrl('https://xpaste.pro/foo/p/t4q0Lsp0')).toBe(false);
78
- expect(isTextPasteUrl('https://xpaste.pro/p/t4q0Lsp0/raw/extra')).toBe(
79
- false
80
- );
81
- });
82
-
83
- it('should return false for invalid URLs', () => {
84
- expect(isTextPasteUrl('not-a-url')).toBe(false);
85
- });
86
- });
87
-
88
- describe('getTextPasteFilename', () => {
89
- it('should derive a canonical text filename for paste URLs', () => {
90
- expect(getTextPasteFilename('https://xpaste.pro/p/t4q0Lsp0')).toBe(
91
- 'xpaste-pro-t4q0Lsp0.txt'
92
- );
93
- expect(getTextPasteFilename('https://xpaste.pro/ru/p/t4q0Lsp0/raw')).toBe(
94
- 'xpaste-pro-t4q0Lsp0.txt'
95
- );
96
- });
97
-
98
- it('should return null for non-paste URLs', () => {
99
- expect(getTextPasteFilename('https://example.com/page')).toBeNull();
100
- });
101
- });
102
-
103
- describe('appendTextPasteMarkdownAttachment', () => {
104
- it('should embed raw text as a named fenced attachment block', () => {
105
- const markdown = '# Page\n\nVisible content';
106
- const rawText = 'first line\n```inside paste```\nlast line';
107
- const result = appendTextPasteMarkdownAttachment(
108
- markdown,
109
- 'https://xpaste.pro/p/t4q0Lsp0',
110
- rawText
111
- );
112
-
113
- expect(result).toContain('## xpaste-pro-t4q0Lsp0.txt');
114
- expect(result).toContain(
115
- '````text\nfirst line\n```inside paste```\nlast line\n````'
116
- );
117
- });
118
- });
119
- });