@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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.7.7",
3
+ "version": "1.7.8",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
- const $ = cheerio.load(html);
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, convertHtmlToMarkdown } from './lib.js';
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 = convertHtmlToMarkdown(html, url);
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;
@@ -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 = `