@link-assistant/web-capture 1.7.5 → 1.7.7

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/src/lib.js CHANGED
@@ -312,6 +312,7 @@ export function convertRelativeUrls(html, baseUrl) {
312
312
  * @param {boolean} [options.extractMetadata=true] - Extract article metadata
313
313
  * @param {boolean} [options.postProcess=true] - Apply post-processing pipeline
314
314
  * @param {boolean} [options.detectCodeLanguage=true] - Detect/correct code languages
315
+ * @param {boolean} [options.preserveCodeWhitespace=false] - Keep original whitespace inside code blocks
315
316
  * @returns {Object} Result with { markdown, metadata }
316
317
  */
317
318
  export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
@@ -320,6 +321,7 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
320
321
  extractMetadata: shouldExtractMetadata = true,
321
322
  postProcess = true,
322
323
  detectCodeLanguage = true,
324
+ preserveCodeWhitespace = false,
323
325
  } = options;
324
326
 
325
327
  // Ensure all URLs are absolute before Markdown conversion
@@ -422,7 +424,7 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
422
424
  // Handle code language detection/correction
423
425
  if (detectCodeLanguage) {
424
426
  $('pre code').each(function () {
425
- const codeText = $(this).text().trim();
427
+ const codeText = $(this).text();
426
428
  const language =
427
429
  $(this)
428
430
  .attr('class')
@@ -444,6 +446,12 @@ export function convertHtmlToMarkdownEnhanced(html, baseUrl, options = {}) {
444
446
  });
445
447
  }
446
448
 
449
+ if (preserveCodeWhitespace) {
450
+ $('pre code').each(function () {
451
+ $(this).text($(this).text().replace(/\r\n?/g, '\n'));
452
+ });
453
+ }
454
+
447
455
  // Preprocess ARIA tables (same as original)
448
456
  $('[role="table"]').each(function () {
449
457
  const $table = $(this);
@@ -24,7 +24,8 @@ import { retry } from './retry.js';
24
24
  * @returns {Object[]} Array of {fullMatch, altText, url}
25
25
  */
26
26
  export function extractImageReferences(markdownText) {
27
- const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g;
27
+ const imageRegex =
28
+ /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)(?:\s+["'][^"']*["'])?\)/g;
28
29
  const images = [];
29
30
  let match;
30
31
 
@@ -73,6 +74,18 @@ export function generateLocalFilename(url, index) {
73
74
  return `image-${String(index + 1).padStart(2, '0')}${ext}`;
74
75
  }
75
76
 
77
+ /**
78
+ * Generate a stable figure-numbered filename.
79
+ *
80
+ * @param {string} url - Image URL
81
+ * @param {number} index - Zero-based index
82
+ * @returns {string} Local filename (e.g., 'figure-1.png')
83
+ */
84
+ export function generateFigureFilename(url, index) {
85
+ const ext = getExtensionFromUrl(url);
86
+ return `figure-${index + 1}${ext}`;
87
+ }
88
+
76
89
  /**
77
90
  * Localize images in markdown text by downloading external images
78
91
  * and replacing URLs with local paths.
@@ -83,6 +96,7 @@ export function generateLocalFilename(url, index) {
83
96
  * @param {boolean} [options.dryRun=false] - Only report what would be done
84
97
  * @param {Function} [options.onProgress] - Callback(index, total, status, url)
85
98
  * @param {string[]} [options.excludeDomains] - Domains to skip (already local)
99
+ * @param {Function} [options.filenameGenerator] - Callback(url, index, image) for local filenames
86
100
  * @returns {Promise<Object>} Result with updated markdown and metadata
87
101
  */
88
102
  export async function localizeImages(markdownText, options = {}) {
@@ -91,6 +105,7 @@ export async function localizeImages(markdownText, options = {}) {
91
105
  dryRun = false,
92
106
  onProgress,
93
107
  excludeDomains = [],
108
+ filenameGenerator = generateLocalFilename,
94
109
  } = options;
95
110
 
96
111
  const allImages = extractImageReferences(markdownText);
@@ -128,7 +143,7 @@ export async function localizeImages(markdownText, options = {}) {
128
143
 
129
144
  for (let i = 0; i < externalImages.length; i++) {
130
145
  const image = externalImages[i];
131
- const localFilename = generateLocalFilename(image.url, i);
146
+ const localFilename = filenameGenerator(image.url, i, image);
132
147
  const relativePath = `${imagesDir}/${localFilename}`;
133
148
 
134
149
  if (onProgress) {
@@ -3,7 +3,13 @@ import {
3
3
  isGoogleDocsUrl,
4
4
  extractDocumentId,
5
5
  buildExportUrl,
6
+ buildEditUrl,
7
+ buildDocsApiUrl,
6
8
  extractBase64Images,
9
+ parseGoogleDocsModelChunks,
10
+ renderGoogleDocsCapture,
11
+ renderDocsApiDocument,
12
+ selectGoogleDocsCaptureMethod,
7
13
  GDOCS_EXPORT_FORMATS,
8
14
  } from '../../src/gdocs.js';
9
15
  import {
@@ -149,6 +155,161 @@ describe('gdocs', () => {
149
155
  });
150
156
  });
151
157
 
158
+ describe('capture method selection (issue #72)', () => {
159
+ it('honors explicit browser capture for Google Docs URLs', () => {
160
+ expect(selectGoogleDocsCaptureMethod('browser')).toBe('browser-model');
161
+ });
162
+
163
+ it('uses public export for API capture without token', () => {
164
+ expect(selectGoogleDocsCaptureMethod('api')).toBe('public-export');
165
+ });
166
+
167
+ it('uses Docs REST API for API capture with token', () => {
168
+ expect(selectGoogleDocsCaptureMethod('api', 'token-123')).toBe(
169
+ 'docs-api'
170
+ );
171
+ });
172
+ });
173
+
174
+ describe('buildEditUrl', () => {
175
+ it('builds the fresh Google Docs editor URL', () => {
176
+ expect(buildEditUrl('abc123')).toBe(
177
+ 'https://docs.google.com/document/d/abc123/edit'
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('buildDocsApiUrl', () => {
183
+ it('builds the Google Docs REST API URL', () => {
184
+ expect(buildDocsApiUrl('abc123')).toBe(
185
+ 'https://docs.googleapis.com/v1/documents/abc123'
186
+ );
187
+ });
188
+ });
189
+
190
+ describe('parseGoogleDocsModelChunks (issue #72)', () => {
191
+ it('includes regular and suggested text from DOCS_modelChunk data', () => {
192
+ const capture = parseGoogleDocsModelChunks([
193
+ {
194
+ chunk: [
195
+ { ty: 'is', s: 'Stable ' },
196
+ { ty: 'iss', s: 'suggested\n' },
197
+ ],
198
+ },
199
+ ]);
200
+
201
+ expect(capture.text).toContain('Stable suggested');
202
+ expect(renderGoogleDocsCapture(capture, 'markdown')).toContain(
203
+ 'Stable suggested'
204
+ );
205
+ });
206
+
207
+ it('extracts table cells and suggested images from DOCS_modelChunk data', () => {
208
+ const chunks = [
209
+ {
210
+ chunk: [
211
+ { ty: 'is', s: String.fromCharCode(0x10) },
212
+ { ty: 'is', s: String.fromCharCode(0x12) },
213
+ { ty: 'is', s: String.fromCharCode(0x1c) },
214
+ { ty: 'is', s: 'Cell A' },
215
+ { ty: 'is', s: String.fromCharCode(0x1c) },
216
+ { ty: 'is', s: '*' },
217
+ { ty: 'is', s: '\n' },
218
+ { ty: 'is', s: String.fromCharCode(0x11) },
219
+ {
220
+ ty: 'ase',
221
+ id: 'suggested-image',
222
+ epm: { ee_eo: { i_cid: 'cid_12345678901234567890' } },
223
+ },
224
+ { ty: 'ste', id: 'suggested-image', spi: 10 },
225
+ ],
226
+ },
227
+ ];
228
+ const capture = parseGoogleDocsModelChunks(chunks, {
229
+ cid_12345678901234567890:
230
+ 'https://docs.google.com/docs-images-rt/image-id',
231
+ });
232
+ const markdown = renderGoogleDocsCapture(capture, 'markdown');
233
+
234
+ expect(capture.tables).toHaveLength(1);
235
+ expect(capture.tables[0].rows[0].cells).toHaveLength(2);
236
+ expect(markdown).toContain('Cell A');
237
+ expect(markdown).toContain(
238
+ '![suggested image](https://docs.google.com/docs-images-rt/image-id)'
239
+ );
240
+ });
241
+ });
242
+
243
+ describe('renderDocsApiDocument (issue #72)', () => {
244
+ it('renders Google Docs REST API paragraphs, tables, and inline images', () => {
245
+ const apiDocument = {
246
+ title: 'API Doc',
247
+ body: {
248
+ content: [
249
+ {
250
+ paragraph: {
251
+ elements: [{ textRun: { content: 'Intro paragraph\n' } }],
252
+ },
253
+ },
254
+ {
255
+ table: {
256
+ tableRows: [
257
+ {
258
+ tableCells: [
259
+ {
260
+ content: [
261
+ {
262
+ paragraph: {
263
+ elements: [{ textRun: { content: 'Name\n' } }],
264
+ },
265
+ },
266
+ ],
267
+ },
268
+ {
269
+ content: [
270
+ {
271
+ paragraph: {
272
+ elements: [
273
+ {
274
+ inlineObjectElement: {
275
+ inlineObjectId: 'image-1',
276
+ },
277
+ },
278
+ ],
279
+ },
280
+ },
281
+ ],
282
+ },
283
+ ],
284
+ },
285
+ ],
286
+ },
287
+ },
288
+ ],
289
+ },
290
+ inlineObjects: {
291
+ 'image-1': {
292
+ inlineObjectProperties: {
293
+ embeddedObject: {
294
+ title: 'Diagram',
295
+ imageProperties: {
296
+ contentUri: 'https://example.com/diagram.png',
297
+ },
298
+ },
299
+ },
300
+ },
301
+ },
302
+ };
303
+
304
+ const rendered = renderDocsApiDocument(apiDocument);
305
+
306
+ expect(rendered.markdown).toContain('Intro paragraph');
307
+ expect(rendered.markdown).toContain('| Name | ![Diagram]');
308
+ expect(rendered.html).toContain('<table>');
309
+ expect(rendered.html).toContain('src="https://example.com/diagram.png"');
310
+ });
311
+ });
312
+
152
313
  describe('GDOCS_EXPORT_FORMATS', () => {
153
314
  it('contains all expected formats', () => {
154
315
  expect(GDOCS_EXPORT_FORMATS).toEqual({
@@ -1,4 +1,7 @@
1
- import { convertHtmlToMarkdown } from '../../src/lib.js';
1
+ import {
2
+ convertHtmlToMarkdown,
3
+ convertHtmlToMarkdownEnhanced,
4
+ } from '../../src/lib.js';
2
5
 
3
6
  describe('convertHtmlToMarkdown', () => {
4
7
  it('removes empty links and anchor headings', () => {
@@ -608,3 +611,44 @@ describe('convertHtmlToMarkdown', () => {
608
611
  );
609
612
  });
610
613
  });
614
+
615
+ describe('convertHtmlToMarkdownEnhanced Habr compatibility', () => {
616
+ it('preserves article code line breaks and extracts formulas from a Habr fixture', () => {
617
+ const html = `
618
+ <article>
619
+ <h1>The Links Theory 0.0.2</h1>
620
+ <div class="article-formatted-body">
621
+ <p>A formula <img class="formula" source="\\mathbf{L}: L \\to L^2" alt="formula"> remains inline.</p>
622
+ <figure>
623
+ <img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/abc.png" alt="Figure 1">
624
+ <figcaption>Figure 1. Link multiplication.</figcaption>
625
+ </figure>
626
+ <pre><code>L = { 1 , 2 }
627
+ L x L = {
628
+ (1, 1),
629
+ (1, 2),
630
+ }</code></pre>
631
+ </div>
632
+ </article>
633
+ `;
634
+
635
+ const { markdown } = convertHtmlToMarkdownEnhanced(
636
+ html,
637
+ 'https://habr.com/en/articles/895896/',
638
+ { preserveCodeWhitespace: true }
639
+ );
640
+
641
+ expect(markdown).toContain('$\\\\mathbf{L}: L \\\\to L^2$');
642
+ expect(markdown).toContain(
643
+ '![Figure 1](https://habrastorage.org/r/w1560/getpro/habr/upload_files/abc.png)'
644
+ );
645
+ expect(markdown).toContain(`\`\`\`
646
+ L = { 1 , 2 }
647
+ L x L = {
648
+ (1, 1),
649
+ (1, 2),
650
+ }
651
+ \`\`\``);
652
+ expect(markdown).not.toContain('L = { 1 , 2 }L x L');
653
+ });
654
+ });
@@ -2,6 +2,8 @@ import {
2
2
  extractImageReferences,
3
3
  getExtensionFromUrl,
4
4
  generateLocalFilename,
5
+ generateFigureFilename,
6
+ localizeImages,
5
7
  } from '../../src/localize-images.js';
6
8
 
7
9
  describe('localize-images module', () => {
@@ -75,4 +77,38 @@ describe('localize-images module', () => {
75
77
  );
76
78
  });
77
79
  });
80
+
81
+ describe('generateFigureFilename', () => {
82
+ it('generates figure-numbered filenames while preserving extensions', () => {
83
+ expect(
84
+ generateFigureFilename('https://habrastorage.org/getpro/habr/a.png', 0)
85
+ ).toBe('figure-1.png');
86
+ expect(
87
+ generateFigureFilename('https://habrastorage.org/getpro/habr/a.jpg', 9)
88
+ ).toBe('figure-10.jpg');
89
+ });
90
+ });
91
+
92
+ describe('localizeImages', () => {
93
+ it('accepts a custom filename generator for archive-compatible paths', async () => {
94
+ const markdown =
95
+ '![Figure 1](https://habrastorage.org/r/w1560/getpro/habr/figure.png "Figure 1")';
96
+
97
+ const result = await localizeImages(markdown, {
98
+ dryRun: true,
99
+ filenameGenerator: generateFigureFilename,
100
+ });
101
+
102
+ expect(result.markdown).toBe('![Figure 1](images/figure-1.png)');
103
+ expect(result.metadata).toEqual([
104
+ {
105
+ index: 1,
106
+ originalUrl:
107
+ 'https://habrastorage.org/r/w1560/getpro/habr/figure.png',
108
+ altText: 'Figure 1',
109
+ localPath: 'images/figure-1.png',
110
+ },
111
+ ]);
112
+ });
113
+ });
78
114
  });