@link-assistant/web-capture 1.7.7 → 1.7.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 +7 -0
- package/bin/web-capture.js +16 -1
- package/package.json +1 -1
- package/src/lib.js +23 -1
- package/src/markdown.js +5 -2
- package/tests/unit/cli.test.js +23 -1
- package/tests/unit/html2md.test.js +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.7.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e951b65: Fix JavaScript CLI version output when run from another npm project.
|
|
8
|
+
- aa4888d: Add content selector options for article-only Markdown capture while preserving full-page metadata extraction.
|
|
9
|
+
|
|
3
10
|
## 1.7.7
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/bin/web-capture.js
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'path';
|
|
|
9
9
|
import { URL } from 'node:url';
|
|
10
10
|
import { makeConfig } from 'lino-arguments';
|
|
11
11
|
import makeLog from 'log-lazy';
|
|
12
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
12
13
|
|
|
13
14
|
function makeVerboseLog(enabled) {
|
|
14
15
|
return makeLog({
|
|
@@ -138,6 +139,18 @@ const config = makeConfig({
|
|
|
138
139
|
'Detect and correct code block languages (default: true). Use --no-detect-code-language to disable.',
|
|
139
140
|
default: getenv('WEB_CAPTURE_DETECT_CODE_LANGUAGE', true),
|
|
140
141
|
})
|
|
142
|
+
.option('contentSelector', {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description:
|
|
145
|
+
'CSS selector used to scope markdown conversion while preserving full-page metadata extraction.',
|
|
146
|
+
default: getenv('WEB_CAPTURE_CONTENT_SELECTOR', undefined),
|
|
147
|
+
})
|
|
148
|
+
.option('bodySelector', {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description:
|
|
151
|
+
'CSS selector for article body markdown; prepends the selected article title when available.',
|
|
152
|
+
default: getenv('WEB_CAPTURE_BODY_SELECTOR', undefined),
|
|
153
|
+
})
|
|
141
154
|
.option('embedImages', {
|
|
142
155
|
type: 'boolean',
|
|
143
156
|
description:
|
|
@@ -213,7 +226,7 @@ const config = makeConfig({
|
|
|
213
226
|
})
|
|
214
227
|
.help('help')
|
|
215
228
|
.alias('help', 'h')
|
|
216
|
-
.version()
|
|
229
|
+
.version(packageJson.version)
|
|
217
230
|
.alias('version', 'v')
|
|
218
231
|
.example('web-capture --serve', 'Start API server on port 3000')
|
|
219
232
|
.example(
|
|
@@ -956,6 +969,8 @@ async function captureUrl(url, options) {
|
|
|
956
969
|
extractMetadata: options.extractMetadata,
|
|
957
970
|
postProcess: options.postProcess,
|
|
958
971
|
detectCodeLanguage: options.detectCodeLanguage,
|
|
972
|
+
contentSelector: options.contentSelector,
|
|
973
|
+
bodySelector: options.bodySelector,
|
|
959
974
|
});
|
|
960
975
|
let markdown = result.markdown;
|
|
961
976
|
|
package/package.json
CHANGED
package/src/lib.js
CHANGED
|
@@ -171,6 +171,14 @@ export function convertHtmlToMarkdown(html, baseUrl) {
|
|
|
171
171
|
return he.decode(turndown.turndown($.html())).replace(/\u00A0/g, ' ');
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function selectedHtml($, selector) {
|
|
175
|
+
if (!selector) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const $selected = $(selector).first();
|
|
179
|
+
return $selected.length ? $.html($selected) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
// Convert relative URLs to absolute URLs in HTML content
|
|
175
183
|
export function convertRelativeUrls(html, baseUrl) {
|
|
176
184
|
const base = new URL(baseUrl);
|
|
@@ -313,6 +321,8 @@ export function convertRelativeUrls(html, baseUrl) {
|
|
|
313
321
|
* @param {boolean} [options.postProcess=true] - Apply post-processing pipeline
|
|
314
322
|
* @param {boolean} [options.detectCodeLanguage=true] - Detect/correct code languages
|
|
315
323
|
* @param {boolean} [options.preserveCodeWhitespace=false] - Keep original whitespace inside code blocks
|
|
324
|
+
* @param {string} [options.contentSelector] - CSS selector to scope Markdown conversion
|
|
325
|
+
* @param {string} [options.bodySelector] - CSS selector appended after the selected article title
|
|
316
326
|
* @returns {Object} Result with { markdown, metadata }
|
|
317
327
|
*/
|
|
318
328
|
export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
@@ -322,6 +332,8 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
322
332
|
postProcess = true,
|
|
323
333
|
detectCodeLanguage = true,
|
|
324
334
|
preserveCodeWhitespace = false,
|
|
335
|
+
contentSelector,
|
|
336
|
+
bodySelector,
|
|
325
337
|
} = options;
|
|
326
338
|
|
|
327
339
|
// Ensure all URLs are absolute before Markdown conversion
|
|
@@ -329,7 +341,7 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
329
341
|
html = convertRelativeUrls(html, baseUrl);
|
|
330
342
|
}
|
|
331
343
|
|
|
332
|
-
|
|
344
|
+
let $ = cheerio.load(html);
|
|
333
345
|
|
|
334
346
|
// Extract metadata before cleaning
|
|
335
347
|
let metadata = null;
|
|
@@ -337,6 +349,16 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
337
349
|
metadata = extractMetadata($);
|
|
338
350
|
}
|
|
339
351
|
|
|
352
|
+
const bodyHtml = selectedHtml($, bodySelector);
|
|
353
|
+
const contentHtml = selectedHtml($, contentSelector);
|
|
354
|
+
if (bodyHtml || contentHtml) {
|
|
355
|
+
const titleSelector = contentSelector ? `${contentSelector} h1, h1` : 'h1';
|
|
356
|
+
const titleHtml = bodyHtml ? selectedHtml($, titleSelector) : null;
|
|
357
|
+
$ = cheerio.load(
|
|
358
|
+
[titleHtml, bodyHtml || contentHtml].filter(Boolean).join('\n')
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
340
362
|
// Remove unwanted elements
|
|
341
363
|
$('style, script, noscript').remove();
|
|
342
364
|
$('*').each(function () {
|
package/src/markdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchHtml,
|
|
1
|
+
import { fetchHtml, convertHtmlToMarkdownEnhanced } from './lib.js';
|
|
2
2
|
import { stripBase64Images } from './extract-images.js';
|
|
3
3
|
|
|
4
4
|
export async function markdownHandler(req, res) {
|
|
@@ -9,7 +9,10 @@ export async function markdownHandler(req, res) {
|
|
|
9
9
|
const embedImages = req.query.embedImages === 'true';
|
|
10
10
|
try {
|
|
11
11
|
const html = await fetchHtml(url);
|
|
12
|
-
let markdown =
|
|
12
|
+
let { markdown } = convertHtmlToMarkdownEnhanced(html, url, {
|
|
13
|
+
contentSelector: req.query.contentSelector,
|
|
14
|
+
bodySelector: req.query.bodySelector,
|
|
15
|
+
});
|
|
13
16
|
if (!embedImages) {
|
|
14
17
|
const strip = stripBase64Images(markdown);
|
|
15
18
|
markdown = strip.markdown;
|
package/tests/unit/cli.test.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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
5
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { dirname, resolve } from 'path';
|
|
6
|
+
import { dirname, join, resolve } from 'path';
|
|
7
|
+
import packageJson from '../../package.json' with { type: 'json' };
|
|
6
8
|
|
|
7
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
10
|
const __dirname = dirname(__filename);
|
|
@@ -108,6 +110,26 @@ describe('CLI', () => {
|
|
|
108
110
|
// yargs outputs just the version number
|
|
109
111
|
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
test('shows web-capture version when run from another npm project', async () => {
|
|
115
|
+
const callerProjectDir = mkdtempSync(join(process.cwd(), 'caller-'));
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(callerProjectDir, 'package.json'),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
name: 'caller-project',
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await runCli(['--version'], { cwd: callerProjectDir });
|
|
126
|
+
|
|
127
|
+
expect(result.code).toBe(0);
|
|
128
|
+
expect(result.stdout.trim()).toBe(packageJson.version);
|
|
129
|
+
} finally {
|
|
130
|
+
rmSync(callerProjectDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
111
133
|
});
|
|
112
134
|
|
|
113
135
|
describe('URL validation', () => {
|
|
@@ -612,6 +612,47 @@ describe('convertHtmlToMarkdown', () => {
|
|
|
612
612
|
});
|
|
613
613
|
});
|
|
614
614
|
|
|
615
|
+
describe('convertHtmlToMarkdownEnhanced content selectors', () => {
|
|
616
|
+
it('scopes Habr-shaped article markdown while keeping full-page metadata', () => {
|
|
617
|
+
const html = `
|
|
618
|
+
<html>
|
|
619
|
+
<head><meta name="keywords" content="links, theory"></head>
|
|
620
|
+
<body>
|
|
621
|
+
<nav><a href="/en/feed">Habr</a><a href="/en/search">Search</a></nav>
|
|
622
|
+
<a href="/en/sandbox/start/">Write a publication</a>
|
|
623
|
+
<article>
|
|
624
|
+
<header>
|
|
625
|
+
<h1>The Links Theory 0.0.2</h1>
|
|
626
|
+
<a class="tm-user-info__username" href="/users/links">links</a>
|
|
627
|
+
<time datetime="2026-04-01T00:00:00Z">April 1</time>
|
|
628
|
+
</header>
|
|
629
|
+
<div class="article-formatted-body">
|
|
630
|
+
<p>Last April 1st, as you might have guessed, the project shipped.</p>
|
|
631
|
+
</div>
|
|
632
|
+
</article>
|
|
633
|
+
</body>
|
|
634
|
+
</html>
|
|
635
|
+
`;
|
|
636
|
+
|
|
637
|
+
const result = convertHtmlToMarkdownEnhanced(
|
|
638
|
+
html,
|
|
639
|
+
'https://habr.com/en/articles/895896/',
|
|
640
|
+
{
|
|
641
|
+
contentSelector: 'article',
|
|
642
|
+
bodySelector: '.article-formatted-body',
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
expect(result.markdown.trim()).toMatch(/^# The Links Theory 0\.0\.2/);
|
|
647
|
+
expect(result.markdown).toContain('Last April 1st');
|
|
648
|
+
expect(result.markdown).not.toContain('Habr');
|
|
649
|
+
expect(result.markdown).not.toContain('Search');
|
|
650
|
+
expect(result.markdown).not.toContain('Write a publication');
|
|
651
|
+
expect(result.metadata.author).toBe('links');
|
|
652
|
+
expect(result.metadata.tags).toEqual(['links', 'theory']);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
615
656
|
describe('convertHtmlToMarkdownEnhanced Habr compatibility', () => {
|
|
616
657
|
it('preserves article code line breaks and extracts formulas from a Habr fixture', () => {
|
|
617
658
|
const html = `
|