@link-assistant/web-capture 1.7.18 → 1.7.20
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,17 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.7.20
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5a3e588: Preserve hierarchical heading numbering (e.g. 13, 13.1) in API-path Markdown conversion. Numbered headings wrapped in `<ol><li><hN>` no longer get renumbered to 1, and sub-numbered headings render with their original number on a clean line.
|
|
8
|
+
|
|
9
|
+
## 1.7.19
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3a483aa: Fix Google Docs public-export Markdown for multi-paragraph table cells.
|
|
14
|
+
|
|
3
15
|
## 1.7.18
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/gdocs-preprocess.js
CHANGED
package/src/lib.js
CHANGED
|
@@ -52,6 +52,13 @@ export function convertHtmlToMarkdown(html, baseUrl) {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
// Preserve hierarchical heading numbering in source text (e.g. 13, 13.1).
|
|
56
|
+
// 1) <ol><li><hN>13. Foo</hN></li></ol> → <ol start="13"><li><hN>Foo</hN></li></ol>
|
|
57
|
+
// Then Turndown emits "13. #### Foo" instead of restarting at "1.".
|
|
58
|
+
// 2) <hN>13.1 Bar</hN> (sub-numbering with decimal) → <p><strong>13.1 Bar</strong></p>
|
|
59
|
+
// Demoting avoids ATX heading prefix, leaving the sub-number on a clean line.
|
|
60
|
+
preserveLeadingHeadingNumbering($);
|
|
61
|
+
|
|
55
62
|
// Remove <a> tags with no direct text content (including only whitespace or only child elements)
|
|
56
63
|
$('a').each(function () {
|
|
57
64
|
// Get all text nodes directly under this <a>
|
|
@@ -166,11 +173,128 @@ export function convertHtmlToMarkdown(html, baseUrl) {
|
|
|
166
173
|
style: false,
|
|
167
174
|
});
|
|
168
175
|
turndown.use(turndownPluginGfm.gfm);
|
|
176
|
+
preserveTableCellLineBreaks(turndown);
|
|
169
177
|
// Decode HTML entities to unicode after markdown conversion
|
|
170
178
|
// Preserve non-breaking spaces as entities for clear marking
|
|
171
179
|
return he.decode(turndown.turndown($.html())).replace(/\u00A0/g, ' ');
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
function preserveLeadingHeadingNumbering($) {
|
|
183
|
+
// Hoist the OL counter onto a heading inside its first <li>.
|
|
184
|
+
// Example: <ol><li><h4>13. Foo</h4></li></ol> → <ol start="13"><li><h4>Foo</h4></li></ol>
|
|
185
|
+
$('ol > li').each(function () {
|
|
186
|
+
const $li = $(this);
|
|
187
|
+
const $heading = $li.children('h1, h2, h3, h4, h5, h6').first();
|
|
188
|
+
if (!$heading.length) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Only act when the <li> has just one heading child and no other meaningful text.
|
|
192
|
+
if ($li.children().length !== 1) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const text = $heading.text();
|
|
196
|
+
const m = text.match(/^\s*(\d+)\.\s+(.*)$/s);
|
|
197
|
+
if (!m) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const [, num, rest] = m;
|
|
201
|
+
const $ol = $li.parent();
|
|
202
|
+
if ($ol.children('li').length !== 1) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!$ol.attr('start')) {
|
|
206
|
+
$ol.attr('start', num);
|
|
207
|
+
}
|
|
208
|
+
stripLeadingTextFromHeading($, $heading, m[0].length - rest.length);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Demote <hN> with sub-numbering text (e.g. "13.1 Foo") to a bold paragraph.
|
|
212
|
+
// Avoids emitting `##### 13.1 Foo` (which collides with renderers that strip
|
|
213
|
+
// numbering or blockquote-wrap subsections downstream).
|
|
214
|
+
$('h1, h2, h3, h4, h5, h6').each(function () {
|
|
215
|
+
const $h = $(this);
|
|
216
|
+
const text = $h.text();
|
|
217
|
+
if (!/^\s*\d+\.\d+/.test(text)) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const inner = $h.html() || '';
|
|
221
|
+
const $p = $('<p></p>');
|
|
222
|
+
if (/^\s*<strong[\s>]/i.test(inner)) {
|
|
223
|
+
$p.html(inner);
|
|
224
|
+
} else {
|
|
225
|
+
$p.html(`<strong>${inner}</strong>`);
|
|
226
|
+
}
|
|
227
|
+
$h.replaceWith($p);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function stripLeadingTextFromHeading($, $heading, prefixLen) {
|
|
232
|
+
// Walk text nodes from the start of the heading and remove prefixLen characters.
|
|
233
|
+
let remaining = prefixLen;
|
|
234
|
+
const stack = [$heading.get(0)];
|
|
235
|
+
while (stack.length && remaining > 0) {
|
|
236
|
+
const node = stack.shift();
|
|
237
|
+
const children = node.children || [];
|
|
238
|
+
for (const child of children) {
|
|
239
|
+
if (remaining <= 0) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (child.type === 'text') {
|
|
243
|
+
const data = child.data || '';
|
|
244
|
+
if (data.length <= remaining) {
|
|
245
|
+
remaining -= data.length;
|
|
246
|
+
child.data = '';
|
|
247
|
+
} else {
|
|
248
|
+
child.data = data.slice(remaining);
|
|
249
|
+
remaining = 0;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
stack.push(child);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Trim leading whitespace from the first non-empty text node.
|
|
257
|
+
const walk = (node) => {
|
|
258
|
+
const children = node.children || [];
|
|
259
|
+
for (const child of children) {
|
|
260
|
+
if (child.type === 'text') {
|
|
261
|
+
if (child.data) {
|
|
262
|
+
child.data = child.data.replace(/^\s+/, '');
|
|
263
|
+
if (child.data) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} else if (walk(child)) {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
walk($heading.get(0));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function preserveTableCellLineBreaks(turndown) {
|
|
277
|
+
turndown.addRule('tableCellLineBreak', {
|
|
278
|
+
filter(node) {
|
|
279
|
+
return node.nodeName === 'BR' && hasAncestorNode(node, ['TD', 'TH']);
|
|
280
|
+
},
|
|
281
|
+
replacement() {
|
|
282
|
+
return '<br>';
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasAncestorNode(node, nodeNames) {
|
|
288
|
+
let current = node.parentNode;
|
|
289
|
+
while (current) {
|
|
290
|
+
if (nodeNames.includes(current.nodeName)) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
current = current.parentNode;
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
174
298
|
function selectedHtml($, selector) {
|
|
175
299
|
if (!selector) {
|
|
176
300
|
return null;
|
|
@@ -379,6 +503,9 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
379
503
|
}
|
|
380
504
|
});
|
|
381
505
|
|
|
506
|
+
// Preserve hierarchical heading numbering (see convertHtmlToMarkdown).
|
|
507
|
+
preserveLeadingHeadingNumbering($);
|
|
508
|
+
|
|
382
509
|
// Remove empty links (same logic as convertHtmlToMarkdown)
|
|
383
510
|
$('a').each(function () {
|
|
384
511
|
const directText = $(this)
|
|
@@ -531,6 +658,7 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
|
|
|
531
658
|
style: false,
|
|
532
659
|
});
|
|
533
660
|
turndown.use(turndownPluginGfm.gfm);
|
|
661
|
+
preserveTableCellLineBreaks(turndown);
|
|
534
662
|
|
|
535
663
|
// Decode HTML entities to unicode after markdown conversion
|
|
536
664
|
// Normalize non-breaking spaces to regular spaces in Markdown text
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<body>
|
|
4
|
+
<ol>
|
|
5
|
+
<li>
|
|
6
|
+
<h4><strong>13. Top-level numbered heading in source</strong></h4>
|
|
7
|
+
</li>
|
|
8
|
+
</ol>
|
|
9
|
+
|
|
10
|
+
<h5><strong>13.1 First subsection heading</strong></h5>
|
|
11
|
+
<p>Where it activates: ...</p>
|
|
12
|
+
|
|
13
|
+
<h5><strong>13.2 Second subsection heading</strong></h5>
|
|
14
|
+
<p>Where it activates: ...</p>
|
|
15
|
+
|
|
16
|
+
<h5><strong>13.3 Third subsection heading</strong></h5>
|
|
17
|
+
<p>Where it activates: ...</p>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeGoogleDocsExportMarkdown,
|
|
3
|
+
preprocessGoogleDocsExportHtml,
|
|
4
|
+
} from '../../src/gdocs.js';
|
|
5
|
+
import { convertHtmlToMarkdown } from '../../src/lib.js';
|
|
6
|
+
|
|
7
|
+
describe('preprocessGoogleDocsExportHtml', () => {
|
|
8
|
+
it('keeps multi-paragraph table cells on one GFM row (issue #110)', () => {
|
|
9
|
+
const html = `
|
|
10
|
+
<table>
|
|
11
|
+
<tr><td><p>Feature</p></td><td><p>Notes</p></td></tr>
|
|
12
|
+
<tr>
|
|
13
|
+
<td>
|
|
14
|
+
<p>First paragraph.</p>
|
|
15
|
+
<p>Second paragraph.</p>
|
|
16
|
+
<p>Third paragraph.</p>
|
|
17
|
+
</td>
|
|
18
|
+
<td><p>x</p></td>
|
|
19
|
+
</tr>
|
|
20
|
+
</table>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const preprocessed = preprocessGoogleDocsExportHtml(html);
|
|
24
|
+
const markdown = normalizeGoogleDocsExportMarkdown(
|
|
25
|
+
convertHtmlToMarkdown(preprocessed.html)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(preprocessed.html).toContain(
|
|
29
|
+
'<td>First paragraph.<br><br>Second paragraph.<br><br>Third paragraph.</td>'
|
|
30
|
+
);
|
|
31
|
+
expect(markdown).toContain(
|
|
32
|
+
'| First paragraph.<br><br>Second paragraph.<br><br>Third paragraph. | x |'
|
|
33
|
+
);
|
|
34
|
+
expect(markdown).not.toContain('First paragraph. \n');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
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', 'heading-numbering.html'),
|
|
10
|
+
'utf8'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
it('preserves hierarchical heading numbering (13, 13.1, 13.2, 13.3)', () => {
|
|
14
|
+
const md = convertHtmlToMarkdown(HTML);
|
|
15
|
+
|
|
16
|
+
// Parent number 13 must survive — either as Markdown heading or as numbered-list item.
|
|
17
|
+
expect(md).toMatch(/(^|\n)#{1,6}\s+13\.|(^|\n)13\.\s/);
|
|
18
|
+
|
|
19
|
+
// Sub-numbers 13.1, 13.2, 13.3 must each appear in heading-like context (line start, not buried mid-paragraph).
|
|
20
|
+
for (const sub of ['13.1', '13.2', '13.3']) {
|
|
21
|
+
expect(md).toMatch(
|
|
22
|
+
new RegExp(`(^|\\n)\\s*\\*{0,3}\\s*${sub.replace('.', '\\.')}`, 'm')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Subsections must NOT be wrapped in markdown blockquotes — there is no <blockquote> in the fixture.
|
|
27
|
+
expect(md).not.toMatch(/^>\s+\*{1,3}13\.\d/m);
|
|
28
|
+
});
|