@link-assistant/web-capture 1.7.21 → 1.7.22

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,11 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 1.7.22
4
+
5
+ ### Patch Changes
6
+
7
+ - a339ca2: Fix `--capture api` collapsing `<br>`-separated lines inside list items into one run. The Google Docs export-html path lost line breaks when a `<br>` was the leading or trailing child of an inline element (e.g. a `<span>` between bold runs), because Turndown trims inner content of inline elements with edge whitespace. The HTML pre-processing now hoists those edge `<br>`s out of their inline parents before Turndown sees them, restoring CommonMark hard breaks. Additionally, the post-processor's double-space collapse no longer eats the two trailing spaces that mark a hard break.
8
+
3
9
  ## 1.7.21
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.7.21",
3
+ "version": "1.7.22",
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
@@ -164,6 +164,10 @@ export function convertHtmlToMarkdown(html, baseUrl) {
164
164
  $table.replaceWith($newTable);
165
165
  });
166
166
 
167
+ // Hoist <br>s out of inline edges so Turndown's flanking-whitespace trim
168
+ // does not eat the hard break. See liftBrFromInlineEdges for details.
169
+ liftBrFromInlineEdges($);
170
+
167
171
  // Convert cleaned HTML to Markdown
168
172
  const turndown = new TurndownService({
169
173
  headingStyle: 'atx',
@@ -183,6 +187,72 @@ export function convertHtmlToMarkdown(html, baseUrl) {
183
187
  return he.decode(turndown.turndown($.html())).replace(/\u00A0/g, '&nbsp;');
184
188
  }
185
189
 
190
+ // Lift <br> nodes out of inline parents when they sit at the leading or
191
+ // trailing edge.
192
+ //
193
+ // Turndown calls `content.trim()` on the inner markdown of any inline element
194
+ // whose `flankingWhitespace` is non-empty (turndown.cjs.js:902), which strips
195
+ // the ` \n` hard break that the <br> rule emits. Google Docs export-html
196
+ // commonly produces `<strong>X</strong><span> Y.<br></span><strong>Z</strong>`
197
+ // — the trailing <br> inside a leading-space <span> gets `.trim()`-ed away,
198
+ // so X/Y/Z collapse onto one line.
199
+ //
200
+ // Hoisting trailing/leading <br>s out of their inline wrapper sidesteps the
201
+ // trim entirely. The hard break then attaches at the parent level, which
202
+ // Turndown handles correctly.
203
+ function liftBrFromInlineEdges($) {
204
+ const inlineParents = new Set([
205
+ 'span',
206
+ 'a',
207
+ 'strong',
208
+ 'b',
209
+ 'em',
210
+ 'i',
211
+ 'u',
212
+ 's',
213
+ 'del',
214
+ 'ins',
215
+ 'sub',
216
+ 'sup',
217
+ 'mark',
218
+ 'small',
219
+ 'q',
220
+ 'cite',
221
+ 'code',
222
+ 'abbr',
223
+ 'time',
224
+ 'kbd',
225
+ 'samp',
226
+ 'var',
227
+ 'font',
228
+ ]);
229
+
230
+ // Hoist <br>s repeatedly until none sit at an inline edge — this handles
231
+ // nested wrappers like `<span><em>foo<br></em></span>`.
232
+ let changed = true;
233
+ while (changed) {
234
+ changed = false;
235
+ $('br').each(function () {
236
+ const el = this;
237
+ const parent = el.parent;
238
+ if (!parent || !inlineParents.has(parent.tagName)) {
239
+ return;
240
+ }
241
+ const isFirst = parent.children[0] === el;
242
+ const isLast = parent.children[parent.children.length - 1] === el;
243
+ if (!isFirst && !isLast) {
244
+ return;
245
+ }
246
+ if (isLast) {
247
+ $(parent).after(el);
248
+ } else {
249
+ $(parent).before(el);
250
+ }
251
+ changed = true;
252
+ });
253
+ }
254
+ }
255
+
186
256
  function preserveLeadingHeadingNumbering($) {
187
257
  // Hoist the OL counter onto a heading inside its first <li>.
188
258
  // Example: <ol><li><h4>13. Foo</h4></li></ol> → <ol start="13"><li><h4>Foo</h4></li></ol>
@@ -674,6 +744,9 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
674
744
  $table.replaceWith($newTable);
675
745
  });
676
746
 
747
+ // Hoist <br>s out of inline edges (see liftBrFromInlineEdges).
748
+ liftBrFromInlineEdges($);
749
+
677
750
  // Convert to Markdown using Turndown
678
751
  const turndown = new TurndownService({
679
752
  headingStyle: 'atx',
@@ -50,8 +50,9 @@ export function postProcessMarkdown(markdown, options = {}) {
50
50
  result = applyBoldFormattingFixes(result);
51
51
  }
52
52
 
53
- // Fix double spaces (but not in code blocks)
54
- result = result.replace(/([^\n`]) +/g, (match, char) => `${char} `);
53
+ // Fix double spaces (but not in code blocks, and preserve CommonMark hard
54
+ // breaks which require two trailing spaces before a newline).
55
+ result = result.replace(/([^\n`]) +(?!\n)/g, (match, char) => `${char} `);
55
56
 
56
57
  // Clean up extra spaces around em-dashes
57
58
  result = result.replace(/\s+—\s+/g, ' — ');
@@ -0,0 +1,8 @@
1
+ <ol>
2
+ <li>
3
+ <strong>Glossary entry.</strong><span> Some lead text.<br /></span
4
+ ><strong>TAG1</strong><span> – Definition one.<br /></span
5
+ ><strong>TAG2</strong><span> – Definition two.<br /></span
6
+ ><strong>TAG3</strong><span> – Definition three.</span>
7
+ </li>
8
+ </ol>
@@ -0,0 +1,23 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { convertHtmlToMarkdown } from '../../src/lib.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const HTML = fs.readFileSync(
9
+ path.join(__dirname, '..', 'fixtures', 'list-item-with-br.html'),
10
+ 'utf8'
11
+ );
12
+
13
+ it('preserves <br> as a line separator inside list items', () => {
14
+ const md = convertHtmlToMarkdown(HTML);
15
+ for (const tag of ['TAG1', 'TAG2', 'TAG3']) {
16
+ expect(md).toMatch(
17
+ new RegExp(`(^|\\n)\\s*\\*{0,2}${tag}\\*{0,2}\\s*–`, 'm')
18
+ );
19
+ }
20
+ // Definitions must not be glued on the same line (no <br> dropped).
21
+ expect(md).not.toMatch(/Definition one\.[ \t]*\*{0,2}TAG2/);
22
+ expect(md).not.toMatch(/Definition two\.[ \t]*\*{0,2}TAG3/);
23
+ });