@link-assistant/web-capture 1.10.6 → 1.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/kreuzberg.js +13 -1
- package/tests/helpers/search-fixture.cjs +17 -0
- package/tests/integration/formalai-contract.test.js +214 -0
- package/tests/unit/cli.test.js +104 -7
- package/tests/unit/kreuzberg.test.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.10.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8cee5e4: Keep JS/Rust kreuzberg parity for the html-to-markdown 3.6 image dimensions
|
|
8
|
+
change (issue #137). Export `normalizeStructuredKeys` and add regression tests
|
|
9
|
+
asserting inline image `dimensions` are exposed as `{ width, height }`, mirroring
|
|
10
|
+
the Rust `inline_image_to_json` fix.
|
|
11
|
+
|
|
12
|
+
## 1.10.7
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 5311a23: Document the FormalAI-compatible capture/search contract and add smoke tests for stable HTTP and CLI response shapes.
|
|
17
|
+
|
|
3
18
|
## 1.10.6
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -127,6 +127,8 @@ parsing. For very large or slow documents, tune the wait with
|
|
|
127
127
|
## API Endpoints (Server Mode)
|
|
128
128
|
|
|
129
129
|
Start the server with `web-capture --serve` and use the endpoints below.
|
|
130
|
+
FormalAI integration should use the stable HTTP/CLI contract documented in
|
|
131
|
+
[`../docs/formalai-contract.md`](../docs/formalai-contract.md).
|
|
130
132
|
|
|
131
133
|
### HTML Endpoint
|
|
132
134
|
|
package/package.json
CHANGED
package/src/kreuzberg.js
CHANGED
|
@@ -39,7 +39,19 @@ function toSnakeCase(key) {
|
|
|
39
39
|
return key.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Recursively normalize object keys to snake_case.
|
|
44
|
+
*
|
|
45
|
+
* Used to give the structured `metadata`, `tables`, and `images` payloads a
|
|
46
|
+
* stable shape regardless of the casing the native binding emits. In
|
|
47
|
+
* particular, inline image `dimensions` are exposed as `{ width, height }` —
|
|
48
|
+
* mirroring the Rust `inline_image_to_json` mapping after the
|
|
49
|
+
* html-to-markdown 3.6 `ImageDimensions` change (see issue #137).
|
|
50
|
+
*
|
|
51
|
+
* @param {*} value - Value to normalize
|
|
52
|
+
* @returns {*} The value with all object keys converted to snake_case
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeStructuredKeys(value) {
|
|
43
55
|
if (Array.isArray(value)) {
|
|
44
56
|
return value.map(normalizeStructuredKeys);
|
|
45
57
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const nock = require('nock');
|
|
2
|
+
|
|
3
|
+
nock('https://en.wikipedia.org')
|
|
4
|
+
.persist()
|
|
5
|
+
.get('/w/rest.php/v1/search/page')
|
|
6
|
+
.query(true)
|
|
7
|
+
.reply(200, {
|
|
8
|
+
pages: [
|
|
9
|
+
{
|
|
10
|
+
id: 1,
|
|
11
|
+
key: 'Formal_methods',
|
|
12
|
+
title: 'Formal methods',
|
|
13
|
+
excerpt: 'the study of <b>formal</b> methods',
|
|
14
|
+
description: 'rigorous techniques',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for the FormalAI-facing HTTP contract (issue #135).
|
|
3
|
+
*
|
|
4
|
+
* These tests intentionally assert the stable response shapes documented in
|
|
5
|
+
* docs/formalai-contract.md rather than endpoint internals.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import request from 'supertest';
|
|
9
|
+
import nock from 'nock';
|
|
10
|
+
import unzipper from 'unzipper';
|
|
11
|
+
import { app, SEARCH_PROVIDERS } from '../../src/index.js';
|
|
12
|
+
|
|
13
|
+
const HTML = `<!doctype html>
|
|
14
|
+
<html>
|
|
15
|
+
<head><title>FormalAI fixture</title></head>
|
|
16
|
+
<body><h1>FormalAI Fixture</h1><p>Stable HTTP shape.</p></body>
|
|
17
|
+
</html>`;
|
|
18
|
+
|
|
19
|
+
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
20
|
+
|
|
21
|
+
const WIKI_JSON = {
|
|
22
|
+
pages: [
|
|
23
|
+
{
|
|
24
|
+
id: 1,
|
|
25
|
+
key: 'Formal_methods',
|
|
26
|
+
title: 'Formal methods',
|
|
27
|
+
excerpt: 'the study of <b>formal</b> methods',
|
|
28
|
+
description: 'rigorous techniques',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function parseBinary(res, callback) {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
36
|
+
res.on('end', () => callback(null, Buffer.concat(chunks)));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
nock.cleanAll();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('FormalAI HTTP contract (#135)', () => {
|
|
44
|
+
it('returns stable text artifact shapes for /html, /txt, and /markdown', async () => {
|
|
45
|
+
nock('https://formalai.example').get('/page').reply(200, HTML, {
|
|
46
|
+
'content-type': 'text/html; charset=utf-8',
|
|
47
|
+
});
|
|
48
|
+
const html = await request(app)
|
|
49
|
+
.get('/html')
|
|
50
|
+
.query({ url: 'https://formalai.example/page' })
|
|
51
|
+
.expect(200)
|
|
52
|
+
.expect('Content-Type', /text\/html/);
|
|
53
|
+
expect(html.text).toContain('<h1>FormalAI Fixture</h1>');
|
|
54
|
+
|
|
55
|
+
nock('https://formalai.example')
|
|
56
|
+
.get('/text')
|
|
57
|
+
.reply(200, 'FormalAI plain text', {
|
|
58
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
59
|
+
});
|
|
60
|
+
const text = await request(app)
|
|
61
|
+
.get('/txt')
|
|
62
|
+
.query({ url: 'https://formalai.example/text' })
|
|
63
|
+
.expect(200)
|
|
64
|
+
.expect('Content-Type', /text\/plain/);
|
|
65
|
+
expect(text.headers['content-disposition']).toContain('.txt');
|
|
66
|
+
expect(text.text).toBe('FormalAI plain text');
|
|
67
|
+
|
|
68
|
+
nock('https://formalai.example').get('/markdown').reply(200, HTML, {
|
|
69
|
+
'content-type': 'text/html; charset=utf-8',
|
|
70
|
+
});
|
|
71
|
+
const markdown = await request(app)
|
|
72
|
+
.get('/markdown')
|
|
73
|
+
.query({ url: 'https://formalai.example/markdown' })
|
|
74
|
+
.expect(200)
|
|
75
|
+
.expect('Content-Type', /text\/markdown/);
|
|
76
|
+
expect(markdown.text).toContain('FormalAI Fixture');
|
|
77
|
+
expect(markdown.text).toContain('Stable HTTP shape.');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('preserves upstream status and content for /fetch and /stream', async () => {
|
|
81
|
+
nock('https://formalai.example')
|
|
82
|
+
.get('/fetch-source')
|
|
83
|
+
.reply(203, 'fetch body', {
|
|
84
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
85
|
+
});
|
|
86
|
+
const fetched = await request(app)
|
|
87
|
+
.get('/fetch')
|
|
88
|
+
.query({ url: 'https://formalai.example/fetch-source' })
|
|
89
|
+
.expect(203)
|
|
90
|
+
.expect('Content-Type', /text\/plain/);
|
|
91
|
+
expect(fetched.text).toBe('fetch body');
|
|
92
|
+
|
|
93
|
+
nock('https://formalai.example')
|
|
94
|
+
.get('/stream-source')
|
|
95
|
+
.reply(206, 'stream body', {
|
|
96
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
97
|
+
});
|
|
98
|
+
const streamed = await request(app)
|
|
99
|
+
.get('/stream')
|
|
100
|
+
.query({ url: 'https://formalai.example/stream-source' })
|
|
101
|
+
.expect(206)
|
|
102
|
+
.expect('Content-Type', /text\/plain/);
|
|
103
|
+
expect(streamed.text).toBe('stream body');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns stable binary artifact shapes for /image and /archive', async () => {
|
|
107
|
+
nock('https://drive.google.com')
|
|
108
|
+
.get('/uc')
|
|
109
|
+
.query({ export: 'download', id: 'formalaiimage' })
|
|
110
|
+
.reply(200, PNG, { 'content-type': 'image/png' });
|
|
111
|
+
const image = await request(app)
|
|
112
|
+
.get('/image')
|
|
113
|
+
.query({
|
|
114
|
+
url: 'https://drive.google.com/file/d/formalaiimage/view',
|
|
115
|
+
})
|
|
116
|
+
.buffer(true)
|
|
117
|
+
.parse(parseBinary)
|
|
118
|
+
.expect(200)
|
|
119
|
+
.expect('Content-Type', /image\/png/);
|
|
120
|
+
expect(image.headers['content-disposition']).toContain(
|
|
121
|
+
'google-drive-formalaiimage.png'
|
|
122
|
+
);
|
|
123
|
+
expect(image.body.subarray(0, 8)).toEqual(PNG);
|
|
124
|
+
|
|
125
|
+
nock('https://formalai.example').get('/archive').reply(200, HTML, {
|
|
126
|
+
'content-type': 'text/html; charset=utf-8',
|
|
127
|
+
});
|
|
128
|
+
const archive = await request(app)
|
|
129
|
+
.get('/archive')
|
|
130
|
+
.query({
|
|
131
|
+
url: 'https://formalai.example/archive',
|
|
132
|
+
localImages: 'false',
|
|
133
|
+
})
|
|
134
|
+
.buffer(true)
|
|
135
|
+
.parse(parseBinary)
|
|
136
|
+
.expect(200)
|
|
137
|
+
.expect('Content-Type', /application\/zip/);
|
|
138
|
+
expect(archive.body[0]).toBe(0x50);
|
|
139
|
+
expect(archive.body[1]).toBe(0x4b);
|
|
140
|
+
const zip = await unzipper.Open.buffer(archive.body);
|
|
141
|
+
expect(zip.files.map((file) => file.path)).toEqual(
|
|
142
|
+
expect.arrayContaining(['document.md', 'document.html'])
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns normalized /search JSON diagnostics and the documented provider catalog', async () => {
|
|
147
|
+
expect(SEARCH_PROVIDERS).toEqual([
|
|
148
|
+
'wikipedia',
|
|
149
|
+
'duckduckgo',
|
|
150
|
+
'google',
|
|
151
|
+
'bing',
|
|
152
|
+
'brave',
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
nock('https://en.wikipedia.org')
|
|
156
|
+
.get('/w/rest.php/v1/search/page')
|
|
157
|
+
.query(true)
|
|
158
|
+
.reply(200, WIKI_JSON);
|
|
159
|
+
|
|
160
|
+
const res = await request(app)
|
|
161
|
+
.get('/search')
|
|
162
|
+
.query({ q: 'formal-ai', provider: 'wikipedia', limit: 1 })
|
|
163
|
+
.expect(200)
|
|
164
|
+
.expect('Content-Type', /application\/json/);
|
|
165
|
+
|
|
166
|
+
expect(res.body).toMatchObject({
|
|
167
|
+
query: 'formal-ai',
|
|
168
|
+
provider: 'wikipedia',
|
|
169
|
+
captureMode: 'fetch',
|
|
170
|
+
results: [
|
|
171
|
+
{
|
|
172
|
+
rank: 1,
|
|
173
|
+
title: 'Formal methods',
|
|
174
|
+
url: 'https://en.wikipedia.org/wiki/Formal_methods',
|
|
175
|
+
snippet: 'the study of formal methods',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
diagnostics: {
|
|
179
|
+
status: 200,
|
|
180
|
+
blockedByCors: false,
|
|
181
|
+
blockedByCaptcha: false,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
expect(res.body.capturedAt).toEqual(expect.any(String));
|
|
185
|
+
expect(res.body.diagnostics.sourceUrl).toContain('en.wikipedia.org');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('keeps normalized /search diagnostics when provider capture fails', async () => {
|
|
189
|
+
nock('https://en.wikipedia.org')
|
|
190
|
+
.get('/w/rest.php/v1/search/page')
|
|
191
|
+
.query(true)
|
|
192
|
+
.replyWithError(new Error('provider offline'));
|
|
193
|
+
|
|
194
|
+
const res = await request(app)
|
|
195
|
+
.get('/search')
|
|
196
|
+
.query({ q: 'formal-ai', provider: 'wikipedia', limit: 1 })
|
|
197
|
+
.expect(200)
|
|
198
|
+
.expect('Content-Type', /application\/json/);
|
|
199
|
+
|
|
200
|
+
expect(res.body).toMatchObject({
|
|
201
|
+
query: 'formal-ai',
|
|
202
|
+
provider: 'wikipedia',
|
|
203
|
+
captureMode: 'fetch',
|
|
204
|
+
results: [],
|
|
205
|
+
diagnostics: {
|
|
206
|
+
status: 0,
|
|
207
|
+
blockedByCors: false,
|
|
208
|
+
blockedByCaptcha: false,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
expect(res.body.diagnostics.sourceUrl).toContain('en.wikipedia.org');
|
|
212
|
+
expect(res.body.diagnostics.error).toContain('provider offline');
|
|
213
|
+
});
|
|
214
|
+
});
|
package/tests/unit/cli.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Unit tests for CLI argument parsing and functionality
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import http from 'node:http';
|
|
4
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { dirname, join, resolve } from 'path';
|
|
7
7
|
import packageJson from '../../package.json' with { type: 'json' };
|
|
@@ -9,11 +9,12 @@ import packageJson from '../../package.json' with { type: 'json' };
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
const cliPath = resolve(__dirname, '../../bin/web-capture.js');
|
|
12
|
+
const searchFixturePath = resolve(__dirname, '../helpers/search-fixture.cjs');
|
|
12
13
|
|
|
13
14
|
// Helper function to run CLI and capture output
|
|
14
15
|
function runCli(args, options = {}) {
|
|
15
16
|
return new Promise((resolve, reject) => {
|
|
16
|
-
const proc = spawn(
|
|
17
|
+
const proc = spawn(process.execPath, [cliPath, ...args], {
|
|
17
18
|
cwd: options.cwd || dirname(cliPath),
|
|
18
19
|
env: { ...process.env, ...options.env },
|
|
19
20
|
});
|
|
@@ -36,13 +37,14 @@ function runCli(args, options = {}) {
|
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function startFixtureServer(
|
|
40
|
+
function startFixtureServer({
|
|
41
|
+
body = '<!doctype html><html><body><h1>Issue 68</h1><p>Captured from positional URL.</p></body></html>',
|
|
42
|
+
contentType = 'text/html; charset=utf-8',
|
|
43
|
+
} = {}) {
|
|
40
44
|
return new Promise((resolve, reject) => {
|
|
41
45
|
const server = http.createServer((req, res) => {
|
|
42
|
-
res.writeHead(200, { 'Content-Type':
|
|
43
|
-
res.end(
|
|
44
|
-
'<!doctype html><html><body><h1>Issue 68</h1><p>Captured from positional URL.</p></body></html>'
|
|
45
|
-
);
|
|
46
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
47
|
+
res.end(body);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
server.on('error', reject);
|
|
@@ -255,4 +257,99 @@ describe('CLI', () => {
|
|
|
255
257
|
expect(result.stdout).toContain('-o');
|
|
256
258
|
});
|
|
257
259
|
});
|
|
260
|
+
|
|
261
|
+
describe('FormalAI-compatible CLI contract', () => {
|
|
262
|
+
test('writes HTML, Markdown, and text captures to stdout with -o -', async () => {
|
|
263
|
+
const { server, url } = await startFixtureServer({
|
|
264
|
+
body: '<!doctype html><html><body><h1>FormalAI Fixture</h1><p>Stable CLI shape.</p></body></html>',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const html = await runCli([url, '--format', 'html', '--output', '-']);
|
|
269
|
+
expect(html.code).toBe(0);
|
|
270
|
+
expect(html.stdout).toContain('<h1>FormalAI Fixture</h1>');
|
|
271
|
+
|
|
272
|
+
const markdown = await runCli([
|
|
273
|
+
url,
|
|
274
|
+
'--format',
|
|
275
|
+
'markdown',
|
|
276
|
+
'--output',
|
|
277
|
+
'-',
|
|
278
|
+
]);
|
|
279
|
+
expect(markdown.code).toBe(0);
|
|
280
|
+
expect(markdown.stdout).toContain('FormalAI Fixture');
|
|
281
|
+
expect(markdown.stdout).toContain('Stable CLI shape.');
|
|
282
|
+
|
|
283
|
+
const text = await runCli([url, '--format', 'txt', '--output', '-']);
|
|
284
|
+
expect(text.code).toBe(0);
|
|
285
|
+
expect(text.stdout).toContain('FormalAI Fixture');
|
|
286
|
+
} finally {
|
|
287
|
+
await stopFixtureServer(server);
|
|
288
|
+
}
|
|
289
|
+
}, 20000);
|
|
290
|
+
|
|
291
|
+
test('writes archive captures as ZIP files for binary CLI consumers', async () => {
|
|
292
|
+
const outputDir = mkdtempSync(join(process.cwd(), 'formalai-cli-'));
|
|
293
|
+
const outputPath = join(outputDir, 'capture.zip');
|
|
294
|
+
const { server, url } = await startFixtureServer({
|
|
295
|
+
body: '<!doctype html><html><body><h1>FormalAI Archive</h1></body></html>',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const result = await runCli([
|
|
300
|
+
url,
|
|
301
|
+
'--archive',
|
|
302
|
+
'zip',
|
|
303
|
+
'--output',
|
|
304
|
+
outputPath,
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
expect(result.code).toBe(0);
|
|
308
|
+
const zipBytes = readFileSync(outputPath);
|
|
309
|
+
expect(zipBytes.length).toBeGreaterThan(50);
|
|
310
|
+
expect(zipBytes[0]).toBe(0x50);
|
|
311
|
+
expect(zipBytes[1]).toBe(0x4b);
|
|
312
|
+
} finally {
|
|
313
|
+
await stopFixtureServer(server);
|
|
314
|
+
rmSync(outputDir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
}, 20000);
|
|
317
|
+
|
|
318
|
+
test('emits normalized search JSON from the search subcommand', async () => {
|
|
319
|
+
const nodeOptions = [
|
|
320
|
+
process.env.NODE_OPTIONS,
|
|
321
|
+
`--require=${searchFixturePath}`,
|
|
322
|
+
]
|
|
323
|
+
.filter(Boolean)
|
|
324
|
+
.join(' ');
|
|
325
|
+
const result = await runCli(
|
|
326
|
+
['search', 'formal-ai', '--provider', 'wikipedia', '--limit', '1'],
|
|
327
|
+
{
|
|
328
|
+
env: { NODE_OPTIONS: nodeOptions },
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(result.code).toBe(0);
|
|
333
|
+
const body = JSON.parse(result.stdout);
|
|
334
|
+
expect(body).toMatchObject({
|
|
335
|
+
query: 'formal-ai',
|
|
336
|
+
provider: 'wikipedia',
|
|
337
|
+
captureMode: 'fetch',
|
|
338
|
+
});
|
|
339
|
+
expect(body.results).toEqual([
|
|
340
|
+
{
|
|
341
|
+
rank: 1,
|
|
342
|
+
title: 'Formal methods',
|
|
343
|
+
url: 'https://en.wikipedia.org/wiki/Formal_methods',
|
|
344
|
+
snippet: 'the study of formal methods',
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
expect(body.diagnostics).toMatchObject({
|
|
348
|
+
status: 200,
|
|
349
|
+
blockedByCors: false,
|
|
350
|
+
blockedByCaptcha: false,
|
|
351
|
+
});
|
|
352
|
+
expect(body.diagnostics.sourceUrl).toContain('en.wikipedia.org');
|
|
353
|
+
}, 20000);
|
|
354
|
+
});
|
|
258
355
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
convertWithKreuzberg,
|
|
3
3
|
isKreuzbergAvailable,
|
|
4
|
+
normalizeStructuredKeys,
|
|
4
5
|
} from '../../src/kreuzberg.js';
|
|
5
6
|
|
|
6
7
|
let available;
|
|
@@ -207,3 +208,42 @@ describe('kreuzberg html-to-markdown integration', () => {
|
|
|
207
208
|
expect(result.content).toContain('Item 3');
|
|
208
209
|
});
|
|
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
|
+
});
|