@link-assistant/web-capture 1.10.4 → 1.10.6

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/markdown.js CHANGED
@@ -15,10 +15,19 @@ import {
15
15
  } from './github.js';
16
16
  import { convertWithKreuzberg, isKreuzbergAvailable } from './kreuzberg.js';
17
17
  import { applyImageMode } from './extract-images.js';
18
+ import { createBrowser, getBrowserEngine } from './browser.js';
19
+ import { retry } from './retry.js';
18
20
  import archiver from 'archiver';
19
21
  import fetch from 'node-fetch';
22
+ import * as cheerio from 'cheerio';
20
23
 
21
24
  const INLINE_MARKDOWN_LINE_LIMIT = 1500;
25
+ const RENDERED_PAGE_SETTLE_TIMEOUT_MS = 5000;
26
+ const RENDERED_PAGE_STABLE_FOR_MS = 1000;
27
+ const RENDERED_PAGE_POLL_MS = 250;
28
+ const RENDERED_PAGE_MIN_TEXT_LENGTH = 200;
29
+ const DESKTOP_USER_AGENT =
30
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
22
31
 
23
32
  export async function markdownHandler(req, res) {
24
33
  const url = req.query.url;
@@ -43,7 +52,7 @@ export async function markdownHandler(req, res) {
43
52
  }
44
53
 
45
54
  try {
46
- const pageUrl = normalizeUrlForTextPage(url);
55
+ const pageUrl = ensureAbsoluteUrl(normalizeUrlForTextPage(url));
47
56
  if (
48
57
  format === 'text' &&
49
58
  !req.query.contentSelector &&
@@ -56,10 +65,10 @@ export async function markdownHandler(req, res) {
56
65
  mode: embedImages ? 'embed' : 'default',
57
66
  });
58
67
  markdown = result.markdown;
59
- return await sendMarkdownResponse(res, url, markdown);
68
+ return await sendMarkdownResponse(res, pageUrl, markdown);
60
69
  }
61
70
 
62
- const html = await fetchHtml(pageUrl);
71
+ const html = await fetchMarkdownHtml(req, pageUrl);
63
72
 
64
73
  if (converter === 'kreuzberg') {
65
74
  const available = await isKreuzbergAvailable();
@@ -86,7 +95,7 @@ export async function markdownHandler(req, res) {
86
95
  if (format === 'json') {
87
96
  return res.json(result);
88
97
  }
89
- return await sendMarkdownResponse(res, url, result.content);
98
+ return await sendMarkdownResponse(res, pageUrl, result.content);
90
99
  }
91
100
 
92
101
  let { markdown } = convertHtmlToMarkdownEnhanced(html, pageUrl, {
@@ -100,13 +109,170 @@ export async function markdownHandler(req, res) {
100
109
  mode: embedImages ? 'embed' : 'default',
101
110
  });
102
111
  markdown = result.markdown;
103
- return await sendMarkdownResponse(res, url, markdown);
112
+ return await sendMarkdownResponse(res, pageUrl, markdown);
104
113
  } catch (err) {
105
114
  console.error(err);
106
115
  res.status(500).send('Error converting to Markdown');
107
116
  }
108
117
  }
109
118
 
119
+ function ensureAbsoluteUrl(url) {
120
+ return /^https?:\/\//i.test(url) ? url : `https://${url}`;
121
+ }
122
+
123
+ async function fetchMarkdownHtml(req, pageUrl) {
124
+ const html = await fetchHtml(pageUrl);
125
+ if (!shouldRenderHtmlWithBrowser(html)) {
126
+ return html;
127
+ }
128
+ return await renderHtmlWithBrowser(req, pageUrl);
129
+ }
130
+
131
+ export function shouldRenderHtmlWithBrowser(html) {
132
+ const isCompleteHtml = /<html[\s>][\s\S]*<\/html>/i.test(html);
133
+ if (!isCompleteHtml) {
134
+ return true;
135
+ }
136
+
137
+ if (!hasClientRenderedAppMarkers(html)) {
138
+ return false;
139
+ }
140
+
141
+ return getStaticBodyTextLength(html) < RENDERED_PAGE_MIN_TEXT_LENGTH;
142
+ }
143
+
144
+ function hasClientRenderedAppMarkers(html) {
145
+ return /(?:__NEXT_DATA__|self\.__next_f|\/_next\/static|id=["']__(?:next|nuxt)["']|id=["']root["']|window\.__NUXT__|ng-version=|\/@vite\/client)/i.test(
146
+ html
147
+ );
148
+ }
149
+
150
+ function getStaticBodyTextLength(html) {
151
+ const $ = cheerio.load(html);
152
+ $('script, style, noscript, template').remove();
153
+ return $('body').text().replace(/\s+/g, ' ').trim().length;
154
+ }
155
+
156
+ async function renderHtmlWithBrowser(req, pageUrl) {
157
+ const engine = getBrowserEngine(req);
158
+ const browser = await createBrowser(engine, {
159
+ args: [
160
+ `--user-agent=${DESKTOP_USER_AGENT}`,
161
+ '--window-size=1280,800',
162
+ '--disable-blink-features=AutomationControlled',
163
+ ],
164
+ });
165
+ try {
166
+ const page = await browser.newPage();
167
+ await prepareRenderedPage(page);
168
+ await navigateRenderedPage(page, pageUrl);
169
+ await waitForRenderedPageToSettle(page);
170
+ return await page.content();
171
+ } finally {
172
+ await browser.close();
173
+ }
174
+ }
175
+
176
+ async function prepareRenderedPage(page) {
177
+ await page.setViewport({ width: 1280, height: 800 });
178
+ if (page.type === 'playwright') {
179
+ await page.setExtraHTTPHeaders({
180
+ 'Accept-Language': 'en-US,en;q=0.9',
181
+ 'Accept-Charset': 'utf-8',
182
+ 'User-Agent': DESKTOP_USER_AGENT,
183
+ });
184
+ return;
185
+ }
186
+
187
+ await page.setExtraHTTPHeaders({
188
+ 'Accept-Language': 'en-US,en;q=0.9',
189
+ 'Accept-Charset': 'utf-8',
190
+ });
191
+ await page.setUserAgent(DESKTOP_USER_AGENT);
192
+ }
193
+
194
+ async function navigateRenderedPage(page, pageUrl) {
195
+ await retry(
196
+ async () => {
197
+ try {
198
+ await page.goto(pageUrl, {
199
+ waitUntil: 'networkidle0',
200
+ timeout: 60000,
201
+ });
202
+ } catch (err) {
203
+ if (!/timeout/i.test(err.message || '')) {
204
+ throw err;
205
+ }
206
+ await page.goto(pageUrl, {
207
+ waitUntil: 'domcontentloaded',
208
+ timeout: 60000,
209
+ });
210
+ }
211
+ },
212
+ {
213
+ retries: 2,
214
+ baseDelay: 2000,
215
+ onRetry: (err, attempt, delay) => {
216
+ console.log(
217
+ `Rendered markdown navigation retry ${attempt} for ${pageUrl} after ${delay}ms: ${err.message}`
218
+ );
219
+ },
220
+ }
221
+ );
222
+ }
223
+
224
+ async function waitForRenderedPageToSettle(page) {
225
+ const deadline = Date.now() + RENDERED_PAGE_SETTLE_TIMEOUT_MS;
226
+ let lastTextLength = -1;
227
+ let stableSince = 0;
228
+
229
+ while (Date.now() < deadline) {
230
+ let textLength;
231
+ try {
232
+ textLength = await getBodyTextLength(page);
233
+ } catch (err) {
234
+ if (!isTransientPageEvaluationError(err)) {
235
+ throw err;
236
+ }
237
+ lastTextLength = -1;
238
+ stableSince = 0;
239
+ await page.waitForTimeout(RENDERED_PAGE_POLL_MS);
240
+ continue;
241
+ }
242
+ const now = Date.now();
243
+ if (
244
+ textLength === lastTextLength &&
245
+ textLength >= RENDERED_PAGE_MIN_TEXT_LENGTH
246
+ ) {
247
+ if (!stableSince) {
248
+ stableSince = now;
249
+ }
250
+ if (now - stableSince >= RENDERED_PAGE_STABLE_FOR_MS) {
251
+ return;
252
+ }
253
+ } else {
254
+ lastTextLength = textLength;
255
+ stableSince = now;
256
+ }
257
+ await page.waitForTimeout(RENDERED_PAGE_POLL_MS);
258
+ }
259
+ }
260
+
261
+ async function getBodyTextLength(page) {
262
+ const rawPage = page.rawPage || page;
263
+ return await rawPage.evaluate(() => {
264
+ // eslint-disable-next-line no-undef
265
+ const text = document.body?.innerText || '';
266
+ return text.trim().length;
267
+ });
268
+ }
269
+
270
+ function isTransientPageEvaluationError(err) {
271
+ return /execution context was destroyed|context was destroyed|cannot find context/i.test(
272
+ err.message || ''
273
+ );
274
+ }
275
+
110
276
  async function sendMarkdownResponse(res, url, markdown) {
111
277
  if (!isTextPasteUrl(url)) {
112
278
  return res.type('text/markdown').send(markdown);
package/src/pdf.js CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { createBrowser, getBrowserEngine } from './browser.js';
17
17
  import { dismissPopups, scrollToLoadContent } from './popups.js';
18
+ import { fetchGoogleDriveImage, googleDriveImageHtmlForUrl } from './lib.js';
18
19
 
19
20
  export async function pdfHandler(req, res) {
20
21
  const url = req.query.url;
@@ -29,6 +30,12 @@ export async function pdfHandler(req, res) {
29
30
 
30
31
  try {
31
32
  const absoluteUrl = url.startsWith('http') ? url : `https://${url}`;
33
+ const googleDriveImage = await fetchGoogleDriveImage(absoluteUrl);
34
+ const googleDriveHtml = googleDriveImage
35
+ ? googleDriveImageHtmlForUrl(absoluteUrl, {
36
+ imageSrc: `data:${googleDriveImage.contentType};base64,${googleDriveImage.buffer.toString('base64')}`,
37
+ })
38
+ : null;
32
39
  const engine = getBrowserEngine(req);
33
40
  const browserOpts = theme ? { colorScheme: theme } : {};
34
41
  const browser = await createBrowser(engine, browserOpts);
@@ -42,16 +49,23 @@ export async function pdfHandler(req, res) {
42
49
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
43
50
  );
44
51
  await page.setViewport({ width, height });
45
- await page.goto(absoluteUrl, {
46
- waitUntil: 'networkidle0',
47
- timeout: 30000,
48
- });
52
+ if (googleDriveHtml) {
53
+ await page.setContent(googleDriveHtml, {
54
+ waitUntil: 'networkidle0',
55
+ timeout: 30000,
56
+ });
57
+ } else {
58
+ await page.goto(absoluteUrl, {
59
+ waitUntil: 'networkidle0',
60
+ timeout: 30000,
61
+ });
49
62
 
50
- await new Promise((resolve) => setTimeout(resolve, 5000));
51
- await scrollToLoadContent(page);
63
+ await new Promise((resolve) => setTimeout(resolve, 5000));
64
+ await scrollToLoadContent(page);
52
65
 
53
- if (shouldDismissPopups) {
54
- await dismissPopups(page);
66
+ if (shouldDismissPopups) {
67
+ await dismissPopups(page);
68
+ }
55
69
  }
56
70
 
57
71
  // Generate PDF using browser-commander's unified pdf() API (v0.8.0+)
package/src/stream.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { pipeline } from 'stream';
3
+ import { convertGoogleDriveUrl } from './lib.js';
3
4
 
4
5
  export async function streamHandler(req, res) {
5
6
  const url = req.query.url;
@@ -7,7 +8,7 @@ export async function streamHandler(req, res) {
7
8
  return res.status(400).send('Missing `url` parameter');
8
9
  }
9
10
  try {
10
- const response = await fetch(url);
11
+ const response = await fetch(convertGoogleDriveUrl(url));
11
12
  // Copy status and headers
12
13
  res.status(response.status);
13
14
 
package/src/txt.js CHANGED
@@ -2,7 +2,10 @@ import fetch from 'node-fetch';
2
2
  import { URL } from 'node:url';
3
3
  import {
4
4
  fetchHtml,
5
+ fetchGoogleDriveImageHtml,
6
+ googleDriveImageTextForUrl,
5
7
  getTextPasteFilename,
8
+ isGoogleDriveFileUrl,
6
9
  isStackOverflowQuestionUrl,
7
10
  normalizeUrlForTextContent,
8
11
  } from './lib.js';
@@ -31,6 +34,13 @@ export async function txtHandler(req, res) {
31
34
  return sendTextResponse(res, url, text);
32
35
  }
33
36
 
37
+ if (isGoogleDriveFileUrl(url)) {
38
+ const googleDriveImageHtml = await fetchGoogleDriveImageHtml(url);
39
+ if (googleDriveImageHtml) {
40
+ return sendTextResponse(res, url, googleDriveImageTextForUrl(url));
41
+ }
42
+ }
43
+
34
44
  // Normalize URL to get text content (e.g., xpaste.pro -> xpaste.pro/raw)
35
45
  const textUrl = normalizeUrlForTextContent(url);
36
46