@link-assistant/web-capture 1.10.1 → 1.10.3

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.
@@ -0,0 +1,97 @@
1
+ import {
2
+ formatGithubRepositoryMarkdown,
3
+ formatGithubRepositoryText,
4
+ getGithubRepositoryTextFilename,
5
+ isGithubRepositoryUrl,
6
+ parseGithubRepositoryUrl,
7
+ } from '../../src/github.js';
8
+
9
+ const SNAPSHOT = {
10
+ sourceUrl: 'https://github.com/octocat/Hello-World',
11
+ defaultBranch: 'master',
12
+ repository: {
13
+ full_name: 'octocat/Hello-World',
14
+ description: 'A friendly test repository',
15
+ html_url: 'https://github.com/octocat/Hello-World',
16
+ default_branch: 'master',
17
+ language: 'JavaScript',
18
+ stargazers_count: 42,
19
+ forks_count: 7,
20
+ open_issues_count: 3,
21
+ license: { spdx_id: 'MIT' },
22
+ topics: ['demo', 'capture'],
23
+ },
24
+ tree: [
25
+ {
26
+ name: 'src',
27
+ path: 'src',
28
+ type: 'dir',
29
+ html_url: 'https://github.com/octocat/Hello-World/tree/master/src',
30
+ },
31
+ {
32
+ name: 'README.md',
33
+ path: 'README.md',
34
+ type: 'file',
35
+ size: 37,
36
+ html_url: 'https://github.com/octocat/Hello-World/blob/master/README.md',
37
+ },
38
+ ],
39
+ readme: {
40
+ path: 'README.md',
41
+ content: '# Hello World\n\nThis is the README.',
42
+ },
43
+ };
44
+
45
+ describe('GitHub repository URLs', () => {
46
+ it('detects plain GitHub repository pages', () => {
47
+ expect(
48
+ parseGithubRepositoryUrl('https://github.com/octocat/Hello-World')
49
+ ).toEqual({
50
+ owner: 'octocat',
51
+ repo: 'Hello-World',
52
+ fullName: 'octocat/Hello-World',
53
+ htmlUrl: 'https://github.com/octocat/Hello-World',
54
+ });
55
+ expect(
56
+ isGithubRepositoryUrl('https://github.com/octocat/Hello-World')
57
+ ).toBe(true);
58
+ expect(
59
+ isGithubRepositoryUrl('https://github.com/octocat/Hello-World/issues')
60
+ ).toBe(false);
61
+ expect(
62
+ isGithubRepositoryUrl('https://example.com/octocat/Hello-World')
63
+ ).toBe(false);
64
+ });
65
+
66
+ it('derives repository text filenames', () => {
67
+ expect(
68
+ getGithubRepositoryTextFilename('https://github.com/octocat/Hello-World')
69
+ ).toBe('octocat-Hello-World.txt');
70
+ });
71
+ });
72
+
73
+ describe('GitHub repository snapshot formatting', () => {
74
+ it('formats a compact markdown snapshot with metadata, file tree, and README', () => {
75
+ const markdown = formatGithubRepositoryMarkdown(SNAPSHOT);
76
+
77
+ expect(markdown).toContain('# octocat/Hello-World');
78
+ expect(markdown).toContain('> A friendly test repository');
79
+ expect(markdown).toContain('- Default branch: `master`');
80
+ expect(markdown).toContain('- [src/](');
81
+ expect(markdown).toContain('- [README.md](');
82
+ expect(markdown).toContain('## README.md');
83
+ expect(markdown).toContain('# Hello World');
84
+ });
85
+
86
+ it('formats a compact plain-text snapshot with metadata, file tree, and README', () => {
87
+ const text = formatGithubRepositoryText(SNAPSHOT);
88
+
89
+ expect(text).toContain('Repository: octocat/Hello-World');
90
+ expect(text).toContain('Description: A friendly test repository');
91
+ expect(text).toContain('Files:');
92
+ expect(text).toContain('- src/');
93
+ expect(text).toContain('- README.md');
94
+ expect(text).toContain('README.md:');
95
+ expect(text).toContain('This is the README.');
96
+ });
97
+ });
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Integration tests to verify that markdown extraction from xpaste.pro
3
+ * includes ALL text visible in screenshots of the same page
4
+ *
5
+ * This ensures 100% coverage of visual content extraction as required in issue #15
6
+ */
7
+
8
+ import request from 'supertest';
9
+ import nock from 'nock';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ let app;
17
+
18
+ beforeAll(async () => {
19
+ app = (await import('../../src/index.js')).app;
20
+ });
21
+
22
+ describe('xpaste.pro screenshot-to-markdown content matching', () => {
23
+ beforeEach(() => {
24
+ nock.cleanAll();
25
+ });
26
+
27
+ describe('GET /markdown for t4q0Lsp0', () => {
28
+ const testUrl = 'https://xpaste.pro/p/t4q0Lsp0';
29
+
30
+ // Load the actual HTML page content that was saved
31
+ const htmlPath = path.join(
32
+ __dirname,
33
+ '../../../tests/xpaste/data/t4q0Lsp0-page.html'
34
+ );
35
+ const rawTextPath = path.join(
36
+ __dirname,
37
+ '../../../tests/xpaste/data/t4q0Lsp0-actual-content.txt'
38
+ );
39
+ let testHtml;
40
+ let rawText;
41
+
42
+ beforeAll(() => {
43
+ if (fs.existsSync(htmlPath)) {
44
+ testHtml = fs.readFileSync(htmlPath, 'utf-8');
45
+ }
46
+ if (fs.existsSync(rawTextPath)) {
47
+ rawText = fs.readFileSync(rawTextPath, 'utf-8');
48
+ }
49
+ });
50
+
51
+ beforeEach(() => {
52
+ if (rawText) {
53
+ nock('https://xpaste.pro')
54
+ .persist()
55
+ .get('/p/t4q0Lsp0/raw')
56
+ .reply(200, rawText, {
57
+ 'content-type': 'text/plain; charset=utf-8',
58
+ });
59
+ }
60
+ });
61
+
62
+ it('should include all header/metadata text visible in screenshot', async () => {
63
+ if (!testHtml) {
64
+ console.warn('Skipping test - HTML file not found');
65
+ return;
66
+ }
67
+
68
+ nock('https://xpaste.pro')
69
+ .get('/p/t4q0Lsp0')
70
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
71
+
72
+ const response = await request(app)
73
+ .get('/markdown')
74
+ .query({ url: testUrl });
75
+
76
+ expect(response.status).toBe(200);
77
+ expect(response.type).toBe('text/markdown');
78
+
79
+ // Verify page title/branding (top of screenshot)
80
+ expect(response.text).toContain('Xpaste');
81
+ expect(response.text).toContain('xPaste');
82
+
83
+ // Verify metadata section (visible in screenshot)
84
+ expect(response.text).toContain('Формат');
85
+ expect(response.text).toContain('text');
86
+ expect(response.text).toContain('Время создания');
87
+ expect(response.text).toContain('07.07.2021');
88
+ expect(response.text).toContain('12:48 UTC');
89
+ expect(response.text).toContain('Будет удалена');
90
+ expect(response.text).toContain('05.07.2031');
91
+ });
92
+
93
+ it('should include all SQL query content visible in screenshot', async () => {
94
+ if (!testHtml) {
95
+ console.warn('Skipping test - HTML file not found');
96
+ return;
97
+ }
98
+
99
+ nock('https://xpaste.pro')
100
+ .get('/p/t4q0Lsp0')
101
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
102
+
103
+ const response = await request(app)
104
+ .get('/markdown')
105
+ .query({ url: testUrl });
106
+
107
+ expect(response.status).toBe(200);
108
+
109
+ // Verify all 4 SQL queries visible in screenshot are present
110
+
111
+ // Query #1 metadata (lines 1-8 in screenshot)
112
+ expect(response.text).toContain('# 1');
113
+ expect(response.text).toContain('Time: 210707 15:39:36');
114
+ expect(response.text).toMatch(
115
+ /User@Host:\s+1703313381\[1703313381\]\s+@\s+\[136\.243\.53\.188\]/
116
+ );
117
+ expect(response.text).toContain('Id: 1138102510');
118
+ expect(response.text).toContain('Schema: xxxxxx');
119
+ expect(response.text).toContain('Query_time: 2.182754');
120
+ expect(response.text).toContain('Rows_examined: 324036');
121
+
122
+ // Query #2 metadata (lines 19-28 in screenshot)
123
+ expect(response.text).toContain('# 2');
124
+ expect(response.text).toContain('Time: 210707 15:38:11');
125
+ expect(response.text).toContain('Id: 1138100591');
126
+ expect(response.text).toContain('Query_time: 9.259469');
127
+ expect(response.text).toContain('DELETE FROM phpbb_post_revisions');
128
+ expect(response.text).toContain('WHERE post_id = 10472158');
129
+
130
+ // Query #3 metadata (lines 30-46 in screenshot)
131
+ expect(response.text).toContain('# 3');
132
+ expect(response.text).toContain('Time: 210707 15:32:18');
133
+ expect(response.text).toContain('Id: 1138094492');
134
+ expect(response.text).toContain('Query_time: 1.113014');
135
+ expect(response.text).toContain('Rows_examined: 521597');
136
+
137
+ // Query #4 metadata (lines 48-64 in screenshot)
138
+ expect(response.text).toContain('# 4');
139
+ expect(response.text).toContain('Time: 210707 15:27:56');
140
+ expect(response.text).toContain('Id: 1138089558');
141
+ expect(response.text).toContain('Query_time: 4.682828');
142
+ expect(response.text).toContain('Rows_examined: 2457916');
143
+
144
+ // Verify SQL statements contain key elements
145
+ expect(response.text).toContain('SELECT f.*, t.*, p.*, u.*');
146
+ expect(response.text).toContain('phpbb_posts');
147
+ expect(response.text).toContain('phpbb_users');
148
+ expect(response.text).toContain('phpbb_topics');
149
+ expect(response.text).toContain('phpbb_forums');
150
+ });
151
+
152
+ it('should include all footer text visible in screenshot', async () => {
153
+ if (!testHtml) {
154
+ console.warn('Skipping test - HTML file not found');
155
+ return;
156
+ }
157
+
158
+ nock('https://xpaste.pro')
159
+ .get('/p/t4q0Lsp0')
160
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
161
+
162
+ const response = await request(app)
163
+ .get('/markdown')
164
+ .query({ url: testUrl });
165
+
166
+ expect(response.status).toBe(200);
167
+
168
+ // Verify footer elements (bottom of screenshot)
169
+ expect(response.text).toContain('xPaste');
170
+ expect(response.text).toContain('Упакуем пароль или код в');
171
+ expect(response.text).toMatch(/cсылку для передачи/); // Note: 'c' can be different Unicode char
172
+ expect(response.text).toContain('Сделано в');
173
+ expect(response.text).toContain('Southbridge');
174
+ expect(response.text).toContain('Справка');
175
+ expect(response.text).toContain('Политика конфиденциальности');
176
+ });
177
+
178
+ it('should include language selector links visible in screenshot', async () => {
179
+ if (!testHtml) {
180
+ console.warn('Skipping test - HTML file not found');
181
+ return;
182
+ }
183
+
184
+ nock('https://xpaste.pro')
185
+ .get('/p/t4q0Lsp0')
186
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
187
+
188
+ const response = await request(app)
189
+ .get('/markdown')
190
+ .query({ url: testUrl });
191
+
192
+ expect(response.status).toBe(200);
193
+
194
+ // Verify language links (top right of screenshot)
195
+ expect(response.text).toContain('Ru');
196
+ expect(response.text).toContain('En');
197
+ });
198
+
199
+ it('should include RAW link visible in screenshot', async () => {
200
+ if (!testHtml) {
201
+ console.warn('Skipping test - HTML file not found');
202
+ return;
203
+ }
204
+
205
+ nock('https://xpaste.pro')
206
+ .get('/p/t4q0Lsp0')
207
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
208
+
209
+ const response = await request(app)
210
+ .get('/markdown')
211
+ .query({ url: testUrl });
212
+
213
+ expect(response.status).toBe(200);
214
+
215
+ // Verify RAW button (visible in screenshot)
216
+ expect(response.text).toContain('RAW');
217
+ });
218
+
219
+ it('should embed the raw text file as a markdown text block', async () => {
220
+ if (!testHtml || !rawText) {
221
+ console.warn('Skipping test - fixture files not found');
222
+ return;
223
+ }
224
+
225
+ nock('https://xpaste.pro')
226
+ .get('/p/t4q0Lsp0')
227
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
228
+
229
+ const response = await request(app)
230
+ .get('/markdown')
231
+ .query({ url: testUrl });
232
+
233
+ expect(response.status).toBe(200);
234
+ expect(response.type).toBe('text/markdown');
235
+ expect(response.text).toContain('## xpaste-pro-t4q0Lsp0.txt');
236
+ expect(response.text).toContain('```text');
237
+ expect(response.text).toContain(
238
+ rawText.split('\n')[0].replace(/\r/g, '')
239
+ );
240
+ expect(response.text).toContain('DELETE FROM phpbb_post_revisions');
241
+ });
242
+
243
+ it('should verify correct element ordering - heading and languages at top', async () => {
244
+ if (!testHtml) {
245
+ console.warn('Skipping test - HTML file not found');
246
+ return;
247
+ }
248
+
249
+ nock('https://xpaste.pro')
250
+ .get('/p/t4q0Lsp0')
251
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
252
+
253
+ const response = await request(app)
254
+ .get('/markdown')
255
+ .query({ url: testUrl });
256
+
257
+ expect(response.status).toBe(200);
258
+
259
+ const lines = response.text.split('\n');
260
+
261
+ // Find line numbers of key elements
262
+ const headingLine = lines.findIndex((l) => l.includes('Упакуем пароль'));
263
+ const languageLine = lines.findIndex(
264
+ (l) => l.includes('[Ru]') || l.includes('[En]')
265
+ );
266
+ const formatLine = lines.findIndex((l) => l.includes('Формат:'));
267
+ const firstQueryLine = lines.findIndex(
268
+ (l) => l.includes('# 1') && l.includes('#')
269
+ );
270
+
271
+ // Verify heading comes before metadata (as shown in screenshot)
272
+ expect(headingLine).toBeGreaterThan(0);
273
+ expect(headingLine).toBeLessThan(formatLine);
274
+
275
+ // Verify languages come before metadata (as shown in screenshot)
276
+ expect(languageLine).toBeGreaterThan(0);
277
+ expect(languageLine).toBeLessThan(formatLine);
278
+
279
+ // Verify heading comes before main content
280
+ expect(headingLine).toBeLessThan(firstQueryLine);
281
+
282
+ // Verify metadata comes before main content
283
+ expect(formatLine).toBeLessThan(firstQueryLine);
284
+
285
+ console.log(
286
+ `✓ Element ordering verified: heading (line ${headingLine + 1}) -> languages (line ${languageLine + 1}) -> metadata (line ${formatLine + 1}) -> content (line ${firstQueryLine + 1})`
287
+ );
288
+ });
289
+
290
+ it('should verify markdown extraction matches screenshot - comprehensive check', async () => {
291
+ if (!testHtml) {
292
+ console.warn('Skipping test - HTML file not found');
293
+ return;
294
+ }
295
+
296
+ nock('https://xpaste.pro')
297
+ .get('/p/t4q0Lsp0')
298
+ .reply(200, testHtml, { 'content-type': 'text/html; charset=utf-8' });
299
+
300
+ const response = await request(app)
301
+ .get('/markdown')
302
+ .query({ url: testUrl });
303
+
304
+ expect(response.status).toBe(200);
305
+
306
+ // Define all key text elements visible in the screenshot
307
+ const requiredElements = [
308
+ // Header/branding
309
+ 'Xpaste',
310
+
311
+ // Metadata section
312
+ 'Формат:',
313
+ 'text',
314
+ 'Время создания:',
315
+ '07.07.2021, 12:48 UTC',
316
+ 'Будет удалена:',
317
+ '05.07.2031, 12:48 UTC',
318
+
319
+ // Query #1
320
+ '# 1',
321
+ 'Time: 210707 15:39:36',
322
+ 'User@Host: 1703313381[1703313381]',
323
+ '[136.243.53.188]',
324
+ 'Id: 1138102510',
325
+ 'Query_time: 2.182754',
326
+
327
+ // Query #2
328
+ '# 2',
329
+ 'Time: 210707 15:38:11',
330
+ 'Id: 1138100591',
331
+ 'DELETE FROM phpbb_post_revisions',
332
+
333
+ // Query #3
334
+ '# 3',
335
+ 'Time: 210707 15:32:18',
336
+ 'Id: 1138094492',
337
+
338
+ // Query #4
339
+ '# 4',
340
+ 'Time: 210707 15:27:56',
341
+ 'Id: 1138089558',
342
+
343
+ // Footer
344
+ 'Упакуем пароль',
345
+ 'Southbridge',
346
+ 'Справка',
347
+ 'Политика конфиденциальности',
348
+
349
+ // Language links
350
+ 'Ru',
351
+ 'En',
352
+
353
+ // RAW button
354
+ 'RAW',
355
+ ];
356
+
357
+ // Verify ALL elements are present
358
+ const missingElements = [];
359
+ requiredElements.forEach((element) => {
360
+ if (!response.text.includes(element)) {
361
+ missingElements.push(element);
362
+ }
363
+ });
364
+
365
+ expect(missingElements).toEqual([]);
366
+
367
+ // Log success with coverage stats
368
+ if (missingElements.length === 0) {
369
+ console.log(
370
+ `✓ All ${requiredElements.length} screenshot text elements found in markdown`
371
+ );
372
+ }
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,36 @@
1
+ import getPort from 'get-port';
2
+ import nock from 'nock';
3
+ import { app } from '../../src/index.js';
4
+
5
+ describe('xpaste.pro HTTP e2e', () => {
6
+ let server;
7
+
8
+ afterEach(async () => {
9
+ nock.cleanAll();
10
+ if (server) {
11
+ await new Promise((resolve) => server.close(resolve));
12
+ server = null;
13
+ }
14
+ });
15
+
16
+ it('serves raw paste text through the real HTTP server', async () => {
17
+ const pasteText = 'console.log("xpaste fixture");\n';
18
+ nock('https://xpaste.pro')
19
+ .get('/p/js123/raw')
20
+ .reply(200, pasteText, { 'content-type': 'text/plain; charset=utf-8' });
21
+
22
+ const port = await getPort();
23
+ server = app.listen(port);
24
+
25
+ const response = await fetch(
26
+ `http://127.0.0.1:${port}/txt?url=${encodeURIComponent('https://xpaste.pro/p/js123')}`
27
+ );
28
+
29
+ expect(response.status).toBe(200);
30
+ expect(response.headers.get('content-type')).toMatch(/text\/plain/);
31
+ expect(response.headers.get('content-disposition')).toContain(
32
+ 'xpaste-pro-js123.txt'
33
+ );
34
+ expect(await response.text()).toBe(pasteText);
35
+ });
36
+ });
@@ -0,0 +1,41 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { convertHtmlToMarkdownEnhanced } from '../../src/lib.js';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const DATA_DIR = path.join(__dirname, '../../../tests/xpaste/data');
8
+
9
+ describe('xpaste.pro fixture integration', () => {
10
+ it('converts the captured page fixture with the same visual ordering as the screenshot', () => {
11
+ const html = fs.readFileSync(
12
+ path.join(DATA_DIR, 't4q0Lsp0-page.html'),
13
+ 'utf-8'
14
+ );
15
+ const { markdown } = convertHtmlToMarkdownEnhanced(
16
+ html,
17
+ 'https://xpaste.pro/p/t4q0Lsp0',
18
+ {
19
+ extractMetadata: false,
20
+ postProcess: false,
21
+ }
22
+ );
23
+
24
+ const headingIndex = markdown.indexOf('Упакуем пароль');
25
+ const languageIndexes = ['[Ru]', '[En]']
26
+ .map((label) => markdown.indexOf(label))
27
+ .filter((index) => index >= 0);
28
+ const languageIndex = Math.min(...languageIndexes);
29
+ const formatIndex = markdown.indexOf('Формат:');
30
+ const firstQueryIndex = markdown.indexOf('# 1');
31
+
32
+ expect(headingIndex).toBeGreaterThanOrEqual(0);
33
+ expect(languageIndexes).not.toHaveLength(0);
34
+ expect(languageIndex).toBeGreaterThanOrEqual(0);
35
+ expect(formatIndex).toBeGreaterThanOrEqual(0);
36
+ expect(firstQueryIndex).toBeGreaterThanOrEqual(0);
37
+ expect(headingIndex).toBeLessThan(formatIndex);
38
+ expect(languageIndex).toBeLessThan(formatIndex);
39
+ expect(formatIndex).toBeLessThan(firstQueryIndex);
40
+ });
41
+ });