@link-assistant/web-capture 1.8.1 → 1.10.0
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 +21 -0
- package/README.md +93 -5
- package/bin/web-capture.js +55 -1
- package/package.json +2 -1
- package/src/index.js +10 -0
- package/src/kreuzberg.js +128 -0
- package/src/lib.js +16 -9
- package/src/markdown.js +49 -1
- package/src/search.js +409 -0
- package/tests/integration/api-endpoints.test.js +63 -0
- package/tests/integration/search-endpoint.test.js +88 -0
- package/tests/unit/kreuzberg.test.js +209 -0
- package/tests/unit/search.test.js +268 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b892585: Add kreuzberg html-to-markdown as high-performance alternative converter with structured results
|
|
8
|
+
|
|
9
|
+
## 1.9.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3f01d90: Add a structured search-provider capture contract (issue #130). New
|
|
14
|
+
`search({ query, provider, limit, captureMode })` library API, `GET /search?q=&provider=&format=json|markdown`
|
|
15
|
+
HTTP endpoint, and `web-capture search "<query>" --provider <provider> --format json|markdown`
|
|
16
|
+
CLI subcommand. Supported providers: `wikipedia` (default, CORS-friendly REST
|
|
17
|
+
API), `duckduckgo`, `google`, `bing`, and `brave` (HTML parsed server-side).
|
|
18
|
+
Results are normalized to a single machine-readable shape —
|
|
19
|
+
`{ query, provider, captureMode, capturedAt, results: [{ rank, title, url, snippet }], diagnostics: { status, blockedByCors, blockedByCaptcha, sourceUrl } }`
|
|
20
|
+
— and blocked or CAPTCHA-gated pages are surfaced through `diagnostics`
|
|
21
|
+
instead of failing silently. The same contract is mirrored in the Rust crate
|
|
22
|
+
so both implementations stay aligned.
|
|
23
|
+
|
|
3
24
|
## 1.8.1
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -88,6 +88,9 @@ web-capture https://example.com --extract-images --images-dir assets -o page.md
|
|
|
88
88
|
# Disable specific features
|
|
89
89
|
web-capture https://example.com --no-extract-latex --no-post-process -o page.md
|
|
90
90
|
|
|
91
|
+
# Structured search-provider capture (JSON by default)
|
|
92
|
+
web-capture search "formal methods" --provider wikipedia
|
|
93
|
+
|
|
91
94
|
# Start as API server
|
|
92
95
|
web-capture --serve
|
|
93
96
|
|
|
@@ -138,11 +141,13 @@ GET /markdown?url=<URL>
|
|
|
138
141
|
|
|
139
142
|
Converts the HTML content of the specified URL to Markdown format. By default, original remote image URLs are preserved and base64 data URIs are stripped (clean single-file output). Use `keepOriginalLinks=false` to strip all images, or `embedImages=true` to keep base64 images inline.
|
|
140
143
|
|
|
141
|
-
| Parameter | Required | Description
|
|
142
|
-
| ------------------- | -------- |
|
|
143
|
-
| `url` | Yes | URL to fetch
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
144
|
+
| Parameter | Required | Description | Default |
|
|
145
|
+
| ------------------- | -------- | ------------------------------------------------------------------------- | -------- |
|
|
146
|
+
| `url` | Yes | URL to fetch | - |
|
|
147
|
+
| `converter` | No | Converter: `turndown` or `kreuzberg` | turndown |
|
|
148
|
+
| `format` | No | Response format: `text` or `json` (`json` requires `converter=kreuzberg`) | text |
|
|
149
|
+
| `embedImages` | No | Keep base64 images inline (`true`/`false`) | `false` |
|
|
150
|
+
| `keepOriginalLinks` | No | Keep original remote URLs, strip base64 | `true` |
|
|
146
151
|
|
|
147
152
|
### Image Endpoint
|
|
148
153
|
|
|
@@ -232,6 +237,50 @@ GET /stream?url=<URL>
|
|
|
232
237
|
|
|
233
238
|
Proxy fetch and streaming proxy endpoints.
|
|
234
239
|
|
|
240
|
+
### Search Endpoint
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
GET /search?q=<QUERY>&provider=<PROVIDER>&format=json|markdown
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Captures structured results from a search provider and returns them in a
|
|
247
|
+
normalized, machine-readable shape. `wikipedia` (the default) uses the
|
|
248
|
+
CORS-friendly REST API; the HTML engines (`duckduckgo`, `google`, `bing`,
|
|
249
|
+
`brave`) are parsed server-side. Blocked or CAPTCHA-gated pages are reported
|
|
250
|
+
through `diagnostics` instead of failing.
|
|
251
|
+
|
|
252
|
+
| Parameter | Required | Description | Default |
|
|
253
|
+
| ---------- | -------- | ---------------------------------------------------- | ----------- |
|
|
254
|
+
| `q` | Yes | Search query (`query` is accepted as an alias) | - |
|
|
255
|
+
| `provider` | No | `wikipedia`, `duckduckgo`, `google`, `bing`, `brave` | `wikipedia` |
|
|
256
|
+
| `limit` | No | Maximum number of results | `10` |
|
|
257
|
+
| `format` | No | Response format: `json` or `markdown` | `json` |
|
|
258
|
+
|
|
259
|
+
Example response (`format=json`):
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"query": "formal methods",
|
|
264
|
+
"provider": "wikipedia",
|
|
265
|
+
"captureMode": "fetch",
|
|
266
|
+
"capturedAt": "2026-05-18T20:30:00.000Z",
|
|
267
|
+
"results": [
|
|
268
|
+
{
|
|
269
|
+
"rank": 1,
|
|
270
|
+
"title": "Formal methods",
|
|
271
|
+
"url": "https://en.wikipedia.org/wiki/Formal_methods",
|
|
272
|
+
"snippet": "mathematically rigorous techniques for the specification..."
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
"diagnostics": {
|
|
276
|
+
"status": 200,
|
|
277
|
+
"blockedByCors": false,
|
|
278
|
+
"blockedByCaptcha": false,
|
|
279
|
+
"sourceUrl": "https://en.wikipedia.org/w/rest.php/v1/search/page?q=formal%20methods&limit=10"
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
235
284
|
## CLI Reference
|
|
236
285
|
|
|
237
286
|
### Server Mode
|
|
@@ -291,6 +340,45 @@ web-capture <url> [options]
|
|
|
291
340
|
| `docx` | Word document with embedded images |
|
|
292
341
|
| `archive` / `zip` | ZIP archive with markdown + local images |
|
|
293
342
|
|
|
343
|
+
### Search Mode
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
web-capture search "<query>" [options]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Captures structured search-provider results. Output defaults to JSON; pass
|
|
350
|
+
`--format markdown` for a human-readable document.
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Search Wikipedia (default provider), JSON output
|
|
354
|
+
web-capture search "formal methods"
|
|
355
|
+
|
|
356
|
+
# Search DuckDuckGo, limit to 5 results
|
|
357
|
+
web-capture search "formal methods" --provider duckduckgo --limit 5
|
|
358
|
+
|
|
359
|
+
# Render the results as Markdown
|
|
360
|
+
web-capture search "formal methods" --format markdown
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| Option | Short | Description | Default |
|
|
364
|
+
| ------------ | ----- | ---------------------------------------------------- | ----------- |
|
|
365
|
+
| `--provider` | | `wikipedia`, `duckduckgo`, `google`, `bing`, `brave` | `wikipedia` |
|
|
366
|
+
| `--limit` | | Maximum number of results | `10` |
|
|
367
|
+
| `--format` | `-f` | Output format: `json` or `markdown` | `json` |
|
|
368
|
+
|
|
369
|
+
The library API exposes the same contract:
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
import { search } from '@link-assistant/web-capture';
|
|
373
|
+
|
|
374
|
+
const result = await search({
|
|
375
|
+
query: 'formal methods',
|
|
376
|
+
provider: 'wikipedia',
|
|
377
|
+
limit: 5,
|
|
378
|
+
});
|
|
379
|
+
console.log(result.results[0].title);
|
|
380
|
+
```
|
|
381
|
+
|
|
294
382
|
## Configuration
|
|
295
383
|
|
|
296
384
|
Configuration values are resolved with the following priority (highest to lowest):
|
package/bin/web-capture.js
CHANGED
|
@@ -40,6 +40,26 @@ const config = makeConfig({
|
|
|
40
40
|
description: 'URL to capture',
|
|
41
41
|
})
|
|
42
42
|
)
|
|
43
|
+
.command(
|
|
44
|
+
'search <query>',
|
|
45
|
+
'Capture structured search-provider results',
|
|
46
|
+
(yargs) =>
|
|
47
|
+
yargs.positional('query', {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'Search query',
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
.option('provider', {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description:
|
|
55
|
+
'Search provider: wikipedia, duckduckgo, google, bing, brave',
|
|
56
|
+
default: 'wikipedia',
|
|
57
|
+
})
|
|
58
|
+
.option('limit', {
|
|
59
|
+
type: 'number',
|
|
60
|
+
description: 'Maximum number of search results',
|
|
61
|
+
default: 10,
|
|
62
|
+
})
|
|
43
63
|
.option('serve', {
|
|
44
64
|
alias: 's',
|
|
45
65
|
type: 'boolean',
|
|
@@ -1234,10 +1254,44 @@ async function captureUrl(url, options) {
|
|
|
1234
1254
|
}
|
|
1235
1255
|
}
|
|
1236
1256
|
|
|
1257
|
+
async function runSearch(query) {
|
|
1258
|
+
const { search, formatSearchAsMarkdown } = await import('../src/search.js');
|
|
1259
|
+
// `--format` defaults to `markdown` for capture mode, but search results are
|
|
1260
|
+
// primarily structured JSON. Only honor the format flag when the user passed
|
|
1261
|
+
// it explicitly; otherwise default search output to JSON.
|
|
1262
|
+
const formatPassedExplicitly = process.argv.some(
|
|
1263
|
+
(arg) => arg === '-f' || arg === '--format' || arg.startsWith('--format=')
|
|
1264
|
+
);
|
|
1265
|
+
const format = (
|
|
1266
|
+
formatPassedExplicitly ? config.format : 'json'
|
|
1267
|
+
).toLowerCase();
|
|
1268
|
+
const result = await search({
|
|
1269
|
+
query,
|
|
1270
|
+
provider: config.provider,
|
|
1271
|
+
limit: config.limit,
|
|
1272
|
+
});
|
|
1273
|
+
if (format === 'markdown' || format === 'md') {
|
|
1274
|
+
console.log(formatSearchAsMarkdown(result));
|
|
1275
|
+
} else {
|
|
1276
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1237
1280
|
async function main() {
|
|
1238
1281
|
const url = getUrlArgument();
|
|
1282
|
+
// The `query` key is only present when the `search <query>` command matched.
|
|
1283
|
+
const isSearch = Object.prototype.hasOwnProperty.call(config, 'query');
|
|
1239
1284
|
|
|
1240
|
-
if (
|
|
1285
|
+
if (isSearch) {
|
|
1286
|
+
// Search mode
|
|
1287
|
+
const query = typeof config.query === 'string' ? config.query : '';
|
|
1288
|
+
if (!query.trim()) {
|
|
1289
|
+
console.error('Error: Missing search query');
|
|
1290
|
+
console.error('Usage: web-capture search "<query>" [--provider <p>]');
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
await runSearch(query);
|
|
1294
|
+
} else if (config.serve) {
|
|
1241
1295
|
// Server mode
|
|
1242
1296
|
await startServer(config.port);
|
|
1243
1297
|
} else if (url) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/web-capture",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"changeset:status": "changeset status --since=origin/main"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@kreuzberg/html-to-markdown-node": "^3.5.5",
|
|
35
36
|
"archiver": "^7.0.1",
|
|
36
37
|
"browser-commander": "^0.8.0",
|
|
37
38
|
"cheerio": "^1.0.0",
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { docxHandler } from './docx.js';
|
|
|
11
11
|
import { animationHandler } from './animation.js';
|
|
12
12
|
import { figuresHandler } from './figures.js';
|
|
13
13
|
import { themedImageHandler } from './themed-image.js';
|
|
14
|
+
import { searchHandler } from './search.js';
|
|
14
15
|
|
|
15
16
|
const app = express();
|
|
16
17
|
const port = process.env.PORT || 3000;
|
|
@@ -27,6 +28,7 @@ app.get('/docx', docxHandler);
|
|
|
27
28
|
app.get('/animation', animationHandler);
|
|
28
29
|
app.get('/figures', figuresHandler);
|
|
29
30
|
app.get('/themed-image', themedImageHandler);
|
|
31
|
+
app.get('/search', searchHandler);
|
|
30
32
|
|
|
31
33
|
// Start the server if this is the main module
|
|
32
34
|
const isMainModule =
|
|
@@ -66,3 +68,11 @@ if (isMainModule) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export { app };
|
|
71
|
+
export {
|
|
72
|
+
search,
|
|
73
|
+
searchHandler,
|
|
74
|
+
parseSearchResults,
|
|
75
|
+
buildSearchUrl,
|
|
76
|
+
formatSearchAsMarkdown,
|
|
77
|
+
SEARCH_PROVIDERS,
|
|
78
|
+
} from './search.js';
|
package/src/kreuzberg.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kreuzberg html-to-markdown integration module.
|
|
3
|
+
*
|
|
4
|
+
* Provides high-performance HTML to Markdown conversion using the
|
|
5
|
+
* @kreuzberg/html-to-markdown-node library (Rust-powered, 150-280 MB/s).
|
|
6
|
+
*
|
|
7
|
+
* This converter is available as an alternative to the default Turndown-based
|
|
8
|
+
* converter, selectable via the `converter=kreuzberg` query parameter.
|
|
9
|
+
*
|
|
10
|
+
* @module kreuzberg
|
|
11
|
+
* @see https://github.com/kreuzberg-dev/html-to-markdown
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { convertRelativeUrls } from './lib.js';
|
|
15
|
+
|
|
16
|
+
let _initPromise = null;
|
|
17
|
+
let _convert = null;
|
|
18
|
+
|
|
19
|
+
async function ensureLoaded() {
|
|
20
|
+
if (_convert) {
|
|
21
|
+
return _convert;
|
|
22
|
+
}
|
|
23
|
+
if (_initPromise) {
|
|
24
|
+
return _initPromise;
|
|
25
|
+
}
|
|
26
|
+
_initPromise = import('@kreuzberg/html-to-markdown-node')
|
|
27
|
+
.then((mod) => {
|
|
28
|
+
_convert = mod.convert || mod.default?.convert || null;
|
|
29
|
+
return _convert;
|
|
30
|
+
})
|
|
31
|
+
.catch(() => {
|
|
32
|
+
_convert = null;
|
|
33
|
+
return null;
|
|
34
|
+
});
|
|
35
|
+
return await _initPromise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toSnakeCase(key) {
|
|
39
|
+
return key.replace(/[A-Z]/g, (match) => `_${match.toLowerCase()}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeStructuredKeys(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.map(normalizeStructuredKeys);
|
|
45
|
+
}
|
|
46
|
+
if (!value || typeof value !== 'object') {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(value).map(([key, entry]) => [
|
|
52
|
+
toSnakeCase(key),
|
|
53
|
+
normalizeStructuredKeys(entry),
|
|
54
|
+
])
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the kreuzberg converter is available.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Promise<boolean>} Whether the converter is available
|
|
62
|
+
*/
|
|
63
|
+
export async function isKreuzbergAvailable() {
|
|
64
|
+
const fn = await ensureLoaded();
|
|
65
|
+
return fn !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert HTML to Markdown using the kreuzberg html-to-markdown library.
|
|
70
|
+
*
|
|
71
|
+
* Returns a structured result with content, metadata, tables, images, and warnings.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} html - HTML content to convert
|
|
74
|
+
* @param {Object} [options] - Conversion options
|
|
75
|
+
* @param {string} [options.baseUrl] - Base URL for resolving relative URLs
|
|
76
|
+
* @param {string} [options.headingStyle='Atx'] - Heading style ('Atx' or 'Setext')
|
|
77
|
+
* @param {string} [options.bulletListMarker] - Bullet character ('-', '*', '+')
|
|
78
|
+
* @param {string} [options.codeBlockStyle] - Code block style ('Fenced' or 'Indented')
|
|
79
|
+
* @returns {Promise<Object>} Structured conversion result
|
|
80
|
+
* @returns {string} result.content - The converted markdown content
|
|
81
|
+
* @returns {Object|null} result.metadata - Extracted metadata (title, links, headings, images, etc.)
|
|
82
|
+
* @returns {Array} result.tables - Extracted table data
|
|
83
|
+
* @returns {Array} result.images - Extracted image data
|
|
84
|
+
* @returns {Array} result.warnings - Non-fatal conversion warnings
|
|
85
|
+
* @throws {Error} If the kreuzberg converter is not available
|
|
86
|
+
*/
|
|
87
|
+
export async function convertWithKreuzberg(html, options = {}) {
|
|
88
|
+
const convert = await ensureLoaded();
|
|
89
|
+
if (!convert) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'Kreuzberg html-to-markdown is not installed. ' +
|
|
92
|
+
'Run: npm install @kreuzberg/html-to-markdown-node'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const convertOptions = {
|
|
97
|
+
headingStyle: 'Atx',
|
|
98
|
+
codeBlockStyle: 'Backticks',
|
|
99
|
+
...options,
|
|
100
|
+
};
|
|
101
|
+
delete convertOptions.baseUrl;
|
|
102
|
+
|
|
103
|
+
const processedHtml = options.baseUrl
|
|
104
|
+
? convertRelativeUrls(html, options.baseUrl)
|
|
105
|
+
: html;
|
|
106
|
+
const result = convert(processedHtml, convertOptions);
|
|
107
|
+
|
|
108
|
+
// Parse the metadata JSON string into an object
|
|
109
|
+
let metadata = null;
|
|
110
|
+
if (result.metadata) {
|
|
111
|
+
try {
|
|
112
|
+
metadata =
|
|
113
|
+
typeof result.metadata === 'string'
|
|
114
|
+
? JSON.parse(result.metadata)
|
|
115
|
+
: result.metadata;
|
|
116
|
+
} catch {
|
|
117
|
+
metadata = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
content: result.content || '',
|
|
123
|
+
metadata: normalizeStructuredKeys(metadata),
|
|
124
|
+
tables: normalizeStructuredKeys(result.tables || []),
|
|
125
|
+
images: normalizeStructuredKeys(result.images || []),
|
|
126
|
+
warnings: result.warnings || [],
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/lib.js
CHANGED
|
@@ -411,6 +411,20 @@ function selectedHtml($, selector) {
|
|
|
411
411
|
return $selected.length ? $.html($selected) : null;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
export function scopeHtmlForMarkdown(html, options = {}) {
|
|
415
|
+
const { contentSelector, bodySelector } = options;
|
|
416
|
+
const $ = cheerio.load(html);
|
|
417
|
+
const bodyHtml = selectedHtml($, bodySelector);
|
|
418
|
+
const contentHtml = selectedHtml($, contentSelector);
|
|
419
|
+
if (!bodyHtml && !contentHtml) {
|
|
420
|
+
return html;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const titleSelector = contentSelector ? `${contentSelector} h1, h1` : 'h1';
|
|
424
|
+
const titleHtml = bodyHtml ? selectedHtml($, titleSelector) : null;
|
|
425
|
+
return [titleHtml, bodyHtml || contentHtml].filter(Boolean).join('\n');
|
|
426
|
+
}
|
|
427
|
+
|
|
414
428
|
// Convert relative URLs to absolute URLs in HTML content
|
|
415
429
|
export function convertRelativeUrls(html, baseUrl) {
|
|
416
430
|
const base = new URL(baseUrl);
|
|
@@ -581,15 +595,8 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
581
595
|
metadata = extractMetadata($);
|
|
582
596
|
}
|
|
583
597
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (bodyHtml || contentHtml) {
|
|
587
|
-
const titleSelector = contentSelector ? `${contentSelector} h1, h1` : 'h1';
|
|
588
|
-
const titleHtml = bodyHtml ? selectedHtml($, titleSelector) : null;
|
|
589
|
-
$ = cheerio.load(
|
|
590
|
-
[titleHtml, bodyHtml || contentHtml].filter(Boolean).join('\n')
|
|
591
|
-
);
|
|
592
|
-
}
|
|
598
|
+
html = scopeHtmlForMarkdown(html, { contentSelector, bodySelector });
|
|
599
|
+
$ = cheerio.load(html);
|
|
593
600
|
|
|
594
601
|
// Remove unwanted elements
|
|
595
602
|
$('style, script, noscript').remove();
|
package/src/markdown.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
fetchHtml,
|
|
3
|
+
convertHtmlToMarkdownEnhanced,
|
|
4
|
+
scopeHtmlForMarkdown,
|
|
5
|
+
} from './lib.js';
|
|
6
|
+
import { convertWithKreuzberg, isKreuzbergAvailable } from './kreuzberg.js';
|
|
2
7
|
import { applyImageMode } from './extract-images.js';
|
|
3
8
|
|
|
4
9
|
export async function markdownHandler(req, res) {
|
|
@@ -6,9 +11,52 @@ export async function markdownHandler(req, res) {
|
|
|
6
11
|
if (!url) {
|
|
7
12
|
return res.status(400).send('Missing `url` parameter');
|
|
8
13
|
}
|
|
14
|
+
|
|
15
|
+
const converter = (req.query.converter || 'turndown').toLowerCase();
|
|
16
|
+
const format = (req.query.format || 'text').toLowerCase();
|
|
9
17
|
const embedImages = req.query.embedImages === 'true';
|
|
18
|
+
|
|
19
|
+
if (!['turndown', 'kreuzberg'].includes(converter)) {
|
|
20
|
+
return res.status(400).send('Unsupported `converter` parameter');
|
|
21
|
+
}
|
|
22
|
+
if (!['text', 'json'].includes(format)) {
|
|
23
|
+
return res.status(400).send('Unsupported `format` parameter');
|
|
24
|
+
}
|
|
25
|
+
if (format === 'json' && converter !== 'kreuzberg') {
|
|
26
|
+
return res
|
|
27
|
+
.status(400)
|
|
28
|
+
.send('`format=json` is only supported with `converter=kreuzberg`');
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
try {
|
|
11
32
|
const html = await fetchHtml(url);
|
|
33
|
+
|
|
34
|
+
if (converter === 'kreuzberg') {
|
|
35
|
+
const available = await isKreuzbergAvailable();
|
|
36
|
+
if (!available) {
|
|
37
|
+
return res
|
|
38
|
+
.status(501)
|
|
39
|
+
.send(
|
|
40
|
+
'Kreuzberg converter not available. Install @kreuzberg/html-to-markdown-node.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const scopedHtml = scopeHtmlForMarkdown(html, {
|
|
45
|
+
contentSelector: req.query.contentSelector,
|
|
46
|
+
bodySelector: req.query.bodySelector,
|
|
47
|
+
});
|
|
48
|
+
const result = await convertWithKreuzberg(scopedHtml, { baseUrl: url });
|
|
49
|
+
const imageResult = await applyImageMode(result.content, {
|
|
50
|
+
mode: embedImages ? 'embed' : 'default',
|
|
51
|
+
});
|
|
52
|
+
result.content = imageResult.markdown;
|
|
53
|
+
|
|
54
|
+
if (format === 'json') {
|
|
55
|
+
return res.json(result);
|
|
56
|
+
}
|
|
57
|
+
return res.type('text/markdown').send(result.content);
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
const { markdown } = convertHtmlToMarkdownEnhanced(html, url, {
|
|
13
61
|
contentSelector: req.query.contentSelector,
|
|
14
62
|
bodySelector: req.query.bodySelector,
|