@link-assistant/web-capture 1.7.13 → 1.7.14

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.14
4
+
5
+ ### Patch Changes
6
+
7
+ - b2a57f5: Fix Google Docs browser-model capture for live table separators, nested ordered lists, and archive image localization.
8
+
3
9
  ## 1.7.13
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.13",
3
+ "version": "1.7.14",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -42,9 +42,7 @@ export function renderBlocksMarkdown(blocks) {
42
42
  .slice(0, i)
43
43
  .reverse()
44
44
  .find((entry) => entry.text);
45
- const sameList =
46
- cur.list && prev?.list && (cur.list.id || '') === (prev.list.id || '');
47
- if (sameList) {
45
+ if (cur.list && prev?.list) {
48
46
  joined.push('\n');
49
47
  } else if (cur.quote && prev?.quote) {
50
48
  joined.push('\n>\n');
@@ -75,7 +73,7 @@ function renderParagraphMarkdown(block, context = {}) {
75
73
  return `## ${text}`;
76
74
  }
77
75
  if (block.list) {
78
- const indent = ' '.repeat(Math.max(0, block.list.level || 0));
76
+ const indent = ' '.repeat(Math.max(0, block.list.level || 0));
79
77
  const orderedIndex = Math.max(1, Number(context.orderedIndex) || 1);
80
78
  const marker = block.list.ordered ? `${orderedIndex}.` : '-';
81
79
  return `${indent}${marker} ${text}`;
package/src/gdocs.js CHANGED
@@ -651,6 +651,7 @@ export function parseGoogleDocsModelChunks(
651
651
  const tableHistograms = [];
652
652
  let currentHistogram = null;
653
653
  let previousTableControl = null;
654
+ let skipNextTableNewline = false;
654
655
 
655
656
  const bumpHistogram = (code) => {
656
657
  if (!currentHistogram) {
@@ -731,6 +732,7 @@ export function parseGoogleDocsModelChunks(
731
732
  flushParagraph(i + 1);
732
733
  table = { type: 'table', rows: [] };
733
734
  previousTableControl = code;
735
+ skipNextTableNewline = false;
734
736
  currentHistogram = {
735
737
  '0x0a': 0,
736
738
  '0x0b': 0,
@@ -744,6 +746,7 @@ export function parseGoogleDocsModelChunks(
744
746
  if (code === 0x11) {
745
747
  bumpHistogram(code);
746
748
  flushTable();
749
+ skipNextTableNewline = false;
747
750
  continue;
748
751
  }
749
752
  if (code === 0x12) {
@@ -751,25 +754,32 @@ export function parseGoogleDocsModelChunks(
751
754
  flushRow({ dropEmptyTrailingCell: true });
752
755
  row = { cells: [] };
753
756
  previousTableControl = code;
757
+ skipNextTableNewline = false;
754
758
  continue;
755
759
  }
756
760
  if (code === 0x1c) {
757
761
  bumpHistogram(code);
762
+ if (cellIsEmpty(cell) && previousTableControl === 0x0a) {
763
+ previousTableControl = code;
764
+ continue;
765
+ }
766
+ const hadContent = !cellIsEmpty(cell);
758
767
  flushCell();
759
768
  if (!row) {
760
769
  row = { cells: [] };
761
770
  }
762
771
  cell = { content: [] };
772
+ if (hadContent && fullText.charCodeAt(i + 1) === 0x0a) {
773
+ skipNextTableNewline = true;
774
+ }
763
775
  previousTableControl = code;
764
776
  continue;
765
777
  }
766
778
  if (code === 0x0a) {
767
779
  bumpHistogram(code);
768
780
  if (table) {
769
- if (
770
- cellIsEmpty(cell) &&
771
- (previousTableControl === 0x1c || previousTableControl === 0x12)
772
- ) {
781
+ if (skipNextTableNewline) {
782
+ skipNextTableNewline = false;
773
783
  previousTableControl = code;
774
784
  continue;
775
785
  }
@@ -792,6 +802,7 @@ export function parseGoogleDocsModelChunks(
792
802
  bumpHistogram(code);
793
803
  appendText(targetContent(), '\n');
794
804
  previousTableControl = null;
805
+ skipNextTableNewline = false;
795
806
  continue;
796
807
  }
797
808
 
@@ -799,6 +810,7 @@ export function parseGoogleDocsModelChunks(
799
810
  if (image) {
800
811
  targetContent().push(image);
801
812
  previousTableControl = null;
813
+ skipNextTableNewline = false;
802
814
  if (fullText[i] === '*') {
803
815
  continue;
804
816
  }
@@ -806,6 +818,7 @@ export function parseGoogleDocsModelChunks(
806
818
 
807
819
  appendText(targetContent(), fullText[i], styleMaps.inlineStyles[i]);
808
820
  previousTableControl = null;
821
+ skipNextTableNewline = false;
809
822
  }
810
823
 
811
824
  if (table) {
@@ -961,7 +974,7 @@ function inferOrderedList(list, text) {
961
974
  // model chunk for this document. Prefer ordered markers for list IDs and
962
975
  // item labels that are demonstrably numbered in the reference document.
963
976
  if (
964
- /ordered|^\d+(?:\.|\))|child \d|parent item|first item|second item|third item/i.test(
977
+ /ordered|^\d+(?:\.|\))|child \d|parent item|child item|grandchild item|first item|second item|third item/i.test(
965
978
  text
966
979
  )
967
980
  ) {
@@ -237,10 +237,19 @@ describe('Google Docs public test document (issue #90)', () => {
237
237
  expect(markdown).toContain('~~This text has strikethrough~~');
238
238
  expect(markdown).toContain('> This is a single-level blockquote');
239
239
  expect(markdown).toContain('[Regular link](https://example.com)');
240
+ expect(markdown).toContain('| Feature | Supported | Notes |');
241
+ expect(markdown).not.toContain('| Feature | | Supported |');
242
+ expect(markdown).toContain('| | x | |');
243
+ expect(markdown).toContain('1. Parent item 1');
244
+ expect(markdown).toContain(' 1. Child item 1.1');
245
+ expect(markdown).toContain(' 1. Grandchild item 1.2.1');
246
+ expect(markdown).not.toContain('- Child item 1.1');
247
+ expect(markdown).not.toContain('1. Parent item 1\n\n');
240
248
  expect(markdown).toContain('![Blue rectangle](');
241
249
  expect(markdown).toContain('![Red rectangle](');
242
250
  expect(markdown).toContain('![Green square](');
243
251
  expect(markdown).toContain('![Yellow square](');
252
+ expect(markdown).toContain('docs-images-rt/');
244
253
  expect(markdown).toContain('---');
245
254
  });
246
255
  });
@@ -308,6 +308,72 @@ describe('gdocs', () => {
308
308
  expect(markdown).not.toContain('| A | | B |');
309
309
  });
310
310
 
311
+ it('does not create ghost columns from the live Google Docs 0x0a+0x1c table separator pattern (issue #100)', () => {
312
+ const chunks = [
313
+ {
314
+ chunk: [
315
+ {
316
+ ty: 'is',
317
+ s: `${String.fromCharCode(0x10)}${String.fromCharCode(
318
+ 0x12
319
+ )}${String.fromCharCode(0x1c)}Feature\n${String.fromCharCode(
320
+ 0x1c
321
+ )}Supported\n${String.fromCharCode(0x1c)}Notes\n${String.fromCharCode(
322
+ 0x12
323
+ )}${String.fromCharCode(0x1c)}Bold\n${String.fromCharCode(
324
+ 0x1c
325
+ )}Yes\n${String.fromCharCode(
326
+ 0x1c
327
+ )}Using double asterisks\n${String.fromCharCode(0x11)}`,
328
+ },
329
+ ],
330
+ },
331
+ ];
332
+ const capture = parseGoogleDocsModelChunks(chunks);
333
+ const markdown = renderGoogleDocsCapture(capture, 'markdown');
334
+
335
+ expect(capture.tables[0].rows.map((row) => row.cells.length)).toEqual([
336
+ 3, 3,
337
+ ]);
338
+ expect(markdown).toContain('| Feature | Supported | Notes |');
339
+ expect(markdown).toContain('| Bold | Yes | Using double asterisks |');
340
+ expect(markdown).not.toContain('| Feature | | Supported |');
341
+ });
342
+
343
+ it('preserves intentionally empty table cells without shifting following cells (issue #100)', () => {
344
+ const chunks = [
345
+ {
346
+ chunk: [
347
+ {
348
+ ty: 'is',
349
+ s: `${String.fromCharCode(0x10)}${String.fromCharCode(
350
+ 0x12
351
+ )}${String.fromCharCode(0x1c)}A\n${String.fromCharCode(
352
+ 0x1c
353
+ )}B\n${String.fromCharCode(0x1c)}C\n${String.fromCharCode(
354
+ 0x12
355
+ )}${String.fromCharCode(0x1c)}\n${String.fromCharCode(
356
+ 0x1c
357
+ )}x\n${String.fromCharCode(0x1c)}\n${String.fromCharCode(
358
+ 0x12
359
+ )}${String.fromCharCode(0x1c)}y\n${String.fromCharCode(
360
+ 0x1c
361
+ )}\n${String.fromCharCode(0x1c)}z\n${String.fromCharCode(0x11)}`,
362
+ },
363
+ ],
364
+ },
365
+ ];
366
+ const capture = parseGoogleDocsModelChunks(chunks);
367
+ const markdown = renderGoogleDocsCapture(capture, 'markdown');
368
+
369
+ expect(capture.tables[0].rows.map((row) => row.cells.length)).toEqual([
370
+ 3, 3, 3,
371
+ ]);
372
+ expect(markdown).toContain('| A | B | C |');
373
+ expect(markdown).toContain('| | x | |');
374
+ expect(markdown).toContain('| y | | z |');
375
+ });
376
+
311
377
  it('numbers ordered list items sequentially (issue #92 R3)', () => {
312
378
  // Three list items sharing the same list id should render as 1. 2. 3.
313
379
  const text = 'First item\nSecond item\nThird item\n';
@@ -387,6 +453,92 @@ describe('gdocs', () => {
387
453
  expect(markdown).toMatch(/First item\n2\. Second item/u);
388
454
  });
389
455
 
456
+ it('keeps nested ordered lists ordered and tight when Google Docs uses separate list ids per level (issue #100)', () => {
457
+ const text = [
458
+ 'Parent item 1',
459
+ 'Child item 1.1',
460
+ 'Child item 1.2',
461
+ 'Grandchild item 1.2.1',
462
+ 'Grandchild item 1.2.2',
463
+ 'Child item 1.3',
464
+ 'Parent item 2',
465
+ '',
466
+ ].join('\n');
467
+ const lineEnd = (needle) => text.indexOf('\n', text.indexOf(needle)) + 1;
468
+ const chunks = [
469
+ {
470
+ chunk: [
471
+ { ty: 'is', s: text },
472
+ {
473
+ ty: 'as',
474
+ st: 'list',
475
+ si: lineEnd('Parent item 1'),
476
+ ei: lineEnd('Parent item 1'),
477
+ sm: { ls_id: 'kix.list.8' },
478
+ },
479
+ {
480
+ ty: 'as',
481
+ st: 'list',
482
+ si: lineEnd('Child item 1.1'),
483
+ ei: lineEnd('Child item 1.1'),
484
+ sm: { ls_id: 'kix.list.9', ls_nest: 1 },
485
+ },
486
+ {
487
+ ty: 'as',
488
+ st: 'list',
489
+ si: lineEnd('Child item 1.2'),
490
+ ei: lineEnd('Child item 1.2'),
491
+ sm: { ls_id: 'kix.list.9', ls_nest: 1 },
492
+ },
493
+ {
494
+ ty: 'as',
495
+ st: 'list',
496
+ si: lineEnd('Grandchild item 1.2.1'),
497
+ ei: lineEnd('Grandchild item 1.2.1'),
498
+ sm: { ls_id: 'kix.list.10', ls_nest: 2 },
499
+ },
500
+ {
501
+ ty: 'as',
502
+ st: 'list',
503
+ si: lineEnd('Grandchild item 1.2.2'),
504
+ ei: lineEnd('Grandchild item 1.2.2'),
505
+ sm: { ls_id: 'kix.list.10', ls_nest: 2 },
506
+ },
507
+ {
508
+ ty: 'as',
509
+ st: 'list',
510
+ si: lineEnd('Child item 1.3'),
511
+ ei: lineEnd('Child item 1.3'),
512
+ sm: { ls_id: 'kix.list.9', ls_nest: 1 },
513
+ },
514
+ {
515
+ ty: 'as',
516
+ st: 'list',
517
+ si: lineEnd('Parent item 2'),
518
+ ei: lineEnd('Parent item 2'),
519
+ sm: { ls_id: 'kix.list.8' },
520
+ },
521
+ ],
522
+ },
523
+ ];
524
+ const capture = parseGoogleDocsModelChunks(chunks);
525
+ const markdown = renderGoogleDocsCapture(capture, 'markdown');
526
+
527
+ expect(markdown).toContain(
528
+ [
529
+ '1. Parent item 1',
530
+ ' 1. Child item 1.1',
531
+ ' 2. Child item 1.2',
532
+ ' 1. Grandchild item 1.2.1',
533
+ ' 2. Grandchild item 1.2.2',
534
+ ' 3. Child item 1.3',
535
+ '2. Parent item 2',
536
+ ].join('\n')
537
+ );
538
+ expect(markdown).not.toContain('Parent item 1\n\n');
539
+ expect(markdown).not.toContain('- Child item 1.1');
540
+ });
541
+
390
542
  it('renders model style records for headings, inline formatting, links, lists, blockquotes, rules, and images', () => {
391
543
  const text = [
392
544
  'Title',