@link-assistant/web-capture 1.1.2 → 1.4.2
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 +17 -14
- package/README.md +256 -146
- package/bin/web-capture.js +403 -6
- package/package.json +11 -4
- package/src/animation.js +283 -0
- package/src/archive.js +212 -0
- package/src/batch.js +180 -0
- package/src/browser.js +150 -111
- package/src/docx.js +182 -0
- package/src/figures.js +208 -0
- package/src/gdocs.js +333 -0
- package/src/image.js +65 -8
- package/src/index.js +15 -0
- package/src/latex.js +127 -0
- package/src/lib.js +226 -1
- package/src/localize-images.js +202 -0
- package/src/metadata.js +333 -0
- package/src/pdf.js +76 -0
- package/src/popups.js +171 -0
- package/src/postprocess.js +265 -0
- package/src/retry.js +48 -0
- package/src/themed-image.js +167 -0
- package/src/verify.js +312 -0
- package/tests/e2e/docker.test.js +51 -12
- package/tests/e2e/process.test.js +34 -10
- package/tests/fixtures/habr-articles.js +45 -0
- package/tests/integration/api-endpoints.test.js +231 -0
- package/tests/integration/habr-article.test.js +337 -0
- package/tests/mock/index.test.js +5 -3
- package/tests/unit/animation.test.js +33 -0
- package/tests/unit/batch.test.js +114 -0
- package/tests/unit/browser.test.js +38 -2
- package/tests/unit/figures.test.js +74 -0
- package/tests/unit/gdocs.test.js +203 -0
- package/tests/unit/latex.test.js +123 -0
- package/tests/unit/localize-images.test.js +78 -0
- package/tests/unit/metadata.test.js +140 -0
- package/tests/unit/postprocess.test.js +108 -0
- package/tests/unit/retry.test.js +111 -0
- package/tests/unit/verify.test.js +182 -0
- package/.github/workflows/release.yml +0 -294
- package/ARCHITECTURE.md +0 -982
- package/LICENSE +0 -24
- package/apply.sh +0 -166
- package/docs/market-analysis-2025-05-13.md +0 -64
- package/scripts/changeset-version.mjs +0 -38
- package/scripts/create-github-release.mjs +0 -93
- package/scripts/create-manual-changeset.mjs +0 -88
- package/scripts/format-github-release.mjs +0 -83
- package/scripts/format-release-notes.mjs +0 -218
- package/scripts/instant-version-bump.mjs +0 -121
- package/scripts/publish-to-npm.mjs +0 -128
- package/scripts/setup-npm.mjs +0 -37
- package/scripts/validate-changeset.mjs +0 -106
- package/scripts/version-and-commit.mjs +0 -237
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown post-processing pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Applies a series of text transformations to improve markdown quality:
|
|
5
|
+
* - Unicode normalization (non-breaking spaces, curly quotes, dashes)
|
|
6
|
+
* - LaTeX formula spacing fixes for GitHub rendering
|
|
7
|
+
* - Bold formatting cleanup
|
|
8
|
+
* - Percent sign fix for GitHub KaTeX
|
|
9
|
+
*
|
|
10
|
+
* Based on reference implementation from:
|
|
11
|
+
* https://github.com/link-foundation/meta-theory/blob/main/scripts/download-article.mjs
|
|
12
|
+
*
|
|
13
|
+
* @module postprocess
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Apply all post-processing transformations to markdown text.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} markdown - Raw markdown text
|
|
20
|
+
* @param {Object} [options] - Processing options
|
|
21
|
+
* @param {boolean} [options.normalizeUnicode=true] - Normalize unicode characters
|
|
22
|
+
* @param {boolean} [options.fixLatexSpacing=true] - Fix spacing around LaTeX formulas
|
|
23
|
+
* @param {boolean} [options.fixBoldFormatting=true] - Clean up bold formatting artifacts
|
|
24
|
+
* @param {boolean} [options.fixPercentSign=true] - Fix percent signs in formulas
|
|
25
|
+
* @returns {string} Post-processed markdown
|
|
26
|
+
*/
|
|
27
|
+
export function postProcessMarkdown(markdown, options = {}) {
|
|
28
|
+
const {
|
|
29
|
+
normalizeUnicode = true,
|
|
30
|
+
fixLatexSpacing = true,
|
|
31
|
+
fixBoldFormatting = true,
|
|
32
|
+
fixPercentSign = true,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
let result = markdown;
|
|
36
|
+
|
|
37
|
+
if (normalizeUnicode) {
|
|
38
|
+
result = applyUnicodeNormalization(result);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (fixLatexSpacing) {
|
|
42
|
+
result = applyLatexSpacingFixes(result);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (fixPercentSign) {
|
|
46
|
+
result = applyPercentSignFix(result);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (fixBoldFormatting) {
|
|
50
|
+
result = applyBoldFormattingFixes(result);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fix double spaces (but not in code blocks)
|
|
54
|
+
result = result.replace(/([^\n`]) +/g, (match, char) => `${char} `);
|
|
55
|
+
|
|
56
|
+
// Clean up extra spaces around em-dashes
|
|
57
|
+
result = result.replace(/\s+—\s+/g, ' — ');
|
|
58
|
+
|
|
59
|
+
// Fix stray standalone $ signs on their own line
|
|
60
|
+
result = result.replace(/^\$\s*$/gm, '');
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize unicode characters for consistent rendering.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} text - Input text
|
|
69
|
+
* @returns {string} Normalized text
|
|
70
|
+
*/
|
|
71
|
+
export function applyUnicodeNormalization(text) {
|
|
72
|
+
let result = text;
|
|
73
|
+
|
|
74
|
+
// Replace non-breaking spaces (U+00A0) with regular spaces.
|
|
75
|
+
// GitHub's math renderer doesn't recognize \xa0 as a word boundary
|
|
76
|
+
// for inline math $...$ delimiters.
|
|
77
|
+
result = result.replace(/\u00A0/g, ' ');
|
|
78
|
+
|
|
79
|
+
// Normalize curly quotes to straight quotes
|
|
80
|
+
result = result.replace(/[\u2018\u2019]/g, "'");
|
|
81
|
+
result = result.replace(/[\u201C\u201D]/g, '"');
|
|
82
|
+
|
|
83
|
+
// Normalize em-dash and en-dash
|
|
84
|
+
result = result.replace(/\u2014/g, ' \u2014 '); // em-dash with spaces
|
|
85
|
+
result = result.replace(/\u2013/g, '-'); // en-dash to hyphen
|
|
86
|
+
|
|
87
|
+
// Normalize ellipsis
|
|
88
|
+
result = result.replace(/\u2026/g, '...');
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fix spacing around inline LaTeX formulas for GitHub rendering.
|
|
95
|
+
* Uses a line-by-line token-based approach to correctly identify
|
|
96
|
+
* opening/closing $ delimiters.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} text - Input text
|
|
99
|
+
* @returns {string} Text with fixed formula spacing
|
|
100
|
+
*/
|
|
101
|
+
export function applyLatexSpacingFixes(text) {
|
|
102
|
+
return text
|
|
103
|
+
.split('\n')
|
|
104
|
+
.map((line) => {
|
|
105
|
+
// Skip block formula lines ($$...$$) and blockquote block formulas
|
|
106
|
+
const trimmedLine = line.replace(/^>\s*/, '');
|
|
107
|
+
if (trimmedLine.startsWith('$$') && trimmedLine.endsWith('$$')) {
|
|
108
|
+
return line;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find all inline formula spans by tracking $ delimiters
|
|
112
|
+
const formulas = [];
|
|
113
|
+
let i = 0;
|
|
114
|
+
while (i < line.length) {
|
|
115
|
+
if (line[i] === '$' && (i === 0 || line[i - 1] !== '\\')) {
|
|
116
|
+
// Skip $$ block delimiters
|
|
117
|
+
if (line[i + 1] === '$') {
|
|
118
|
+
i += 2;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Found opening $, find closing $
|
|
122
|
+
const start = i;
|
|
123
|
+
i++;
|
|
124
|
+
while (i < line.length && (line[i] !== '$' || line[i - 1] === '\\')) {
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
if (i < line.length) {
|
|
128
|
+
formulas.push({ start, end: i });
|
|
129
|
+
i++;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
i++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (formulas.length === 0) {
|
|
137
|
+
return line;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build the line with fixes applied
|
|
141
|
+
let fixed = '';
|
|
142
|
+
let pos = 0;
|
|
143
|
+
for (const f of formulas) {
|
|
144
|
+
fixed += line.substring(pos, f.start);
|
|
145
|
+
|
|
146
|
+
const rawInner = line.substring(f.start + 1, f.end);
|
|
147
|
+
const inner = rawInner.trim();
|
|
148
|
+
|
|
149
|
+
// Add space before formula if preceded by word char, comma, colon, etc.
|
|
150
|
+
if (
|
|
151
|
+
fixed.length > 0 &&
|
|
152
|
+
/[a-zA-Z\u0430-\u044F\u0410-\u042F\u0451\u0401,:;\u00BB)\]]$/.test(
|
|
153
|
+
fixed
|
|
154
|
+
)
|
|
155
|
+
) {
|
|
156
|
+
fixed += ' ';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fixed += `$${inner}$`;
|
|
160
|
+
|
|
161
|
+
// Add space after formula if followed by word character
|
|
162
|
+
const afterPos = f.end + 1;
|
|
163
|
+
if (
|
|
164
|
+
afterPos < line.length &&
|
|
165
|
+
/^[a-zA-Z\u0430-\u044F\u0410-\u042F\u0451\u0401]/.test(line[afterPos])
|
|
166
|
+
) {
|
|
167
|
+
fixed += ' ';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pos = f.end + 1;
|
|
171
|
+
}
|
|
172
|
+
fixed += line.substring(pos);
|
|
173
|
+
|
|
174
|
+
return fixed;
|
|
175
|
+
})
|
|
176
|
+
.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Fix percent sign in inline formulas for GitHub KaTeX rendering.
|
|
181
|
+
* GitHub's KaTeX treats % as a LaTeX comment character.
|
|
182
|
+
* Workaround: use \\% which GitHub's preprocessor converts to \%.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} text - Input text
|
|
185
|
+
* @returns {string} Text with fixed percent signs
|
|
186
|
+
*/
|
|
187
|
+
export function applyPercentSignFix(text) {
|
|
188
|
+
let result = text;
|
|
189
|
+
result = result.replace(/\$(\d+)\\+%\$/g, '$$$1\\\\%$$');
|
|
190
|
+
result = result.replace(/\$(\d+)\\text\{%\}\$/g, '$$$1\\\\%$$');
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clean up bold formatting artifacts from HTML-to-markdown conversion.
|
|
196
|
+
*
|
|
197
|
+
* @param {string} text - Input text
|
|
198
|
+
* @returns {string} Text with cleaned bold formatting
|
|
199
|
+
*/
|
|
200
|
+
export function applyBoldFormattingFixes(text) {
|
|
201
|
+
let result = text;
|
|
202
|
+
|
|
203
|
+
// Remove empty bold markers
|
|
204
|
+
result = result.replace(/(\S)\*\*[^\S\n]*\*\*(\S)/g, '$1 $2');
|
|
205
|
+
result = result.replace(/\*\*[^\S\n]*\*\*/g, '');
|
|
206
|
+
|
|
207
|
+
// Fix bold marker spacing: trim content inside **...**
|
|
208
|
+
result = result
|
|
209
|
+
.split('\n')
|
|
210
|
+
.map((line) => {
|
|
211
|
+
const parts = [];
|
|
212
|
+
let lastIndex = 0;
|
|
213
|
+
const boldRegex = /\*\*(.+?)\*\*/g;
|
|
214
|
+
let m;
|
|
215
|
+
while ((m = boldRegex.exec(line)) !== null) {
|
|
216
|
+
parts.push({
|
|
217
|
+
type: 'text',
|
|
218
|
+
content: line.substring(lastIndex, m.index),
|
|
219
|
+
});
|
|
220
|
+
parts.push({ type: 'bold', content: m[1].trim() });
|
|
221
|
+
lastIndex = m.index + m[0].length;
|
|
222
|
+
}
|
|
223
|
+
parts.push({
|
|
224
|
+
type: 'text',
|
|
225
|
+
content: line.substring(lastIndex),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (parts.filter((p) => p.type === 'bold').length === 0) {
|
|
229
|
+
return line;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let rebuilt = '';
|
|
233
|
+
for (let i = 0; i < parts.length; i++) {
|
|
234
|
+
const part = parts[i];
|
|
235
|
+
if (part.type === 'bold') {
|
|
236
|
+
if (!part.content) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
rebuilt.length > 0 &&
|
|
241
|
+
/[a-zA-Z\u0430-\u044F\u0410-\u042F\u0451\u04010-9).]$/.test(rebuilt)
|
|
242
|
+
) {
|
|
243
|
+
rebuilt += ' ';
|
|
244
|
+
}
|
|
245
|
+
rebuilt += `**${part.content}**`;
|
|
246
|
+
const nextPart = parts[i + 1];
|
|
247
|
+
if (
|
|
248
|
+
nextPart &&
|
|
249
|
+
nextPart.content &&
|
|
250
|
+
/^[a-zA-Z\u0430-\u044F\u0410-\u042F\u0451\u0401[(]/.test(
|
|
251
|
+
nextPart.content
|
|
252
|
+
)
|
|
253
|
+
) {
|
|
254
|
+
rebuilt += ' ';
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
rebuilt += part.content;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return rebuilt;
|
|
261
|
+
})
|
|
262
|
+
.join('\n');
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
package/src/retry.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry utility with exponential backoff.
|
|
3
|
+
*
|
|
4
|
+
* Used for network-dependent operations (fetching HTML, downloading images, etc.)
|
|
5
|
+
* to handle transient failures gracefully.
|
|
6
|
+
*
|
|
7
|
+
* @param {Function} fn - Async function to retry
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {number} options.retries - Maximum number of retries (default: 3)
|
|
10
|
+
* @param {number} options.baseDelay - Initial delay in ms (default: 1000)
|
|
11
|
+
* @param {number} options.maxDelay - Maximum delay in ms (default: 30000)
|
|
12
|
+
* @param {number} options.factor - Exponential backoff factor (default: 2)
|
|
13
|
+
* @param {Function} options.onRetry - Optional callback(error, attempt) called before each retry
|
|
14
|
+
* @returns {Promise} Result of the function call
|
|
15
|
+
*/
|
|
16
|
+
export async function retry(fn, options = {}) {
|
|
17
|
+
const {
|
|
18
|
+
retries = 3,
|
|
19
|
+
baseDelay = 1000,
|
|
20
|
+
maxDelay = 30000,
|
|
21
|
+
factor = 2,
|
|
22
|
+
onRetry,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
let lastError;
|
|
26
|
+
|
|
27
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
28
|
+
try {
|
|
29
|
+
return await fn(attempt);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
lastError = error;
|
|
32
|
+
|
|
33
|
+
if (attempt >= retries) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const delay = Math.min(baseDelay * Math.pow(factor, attempt), maxDelay);
|
|
38
|
+
|
|
39
|
+
if (onRetry) {
|
|
40
|
+
onRetry(error, attempt + 1, delay);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-themed screenshot capture module (R3).
|
|
3
|
+
*
|
|
4
|
+
* Captures screenshots in both light and dark themes in a single operation.
|
|
5
|
+
* Uses separate browser contexts for reliable colorScheme application.
|
|
6
|
+
*
|
|
7
|
+
* Based on reference implementation from:
|
|
8
|
+
* https://github.com/link-foundation/meta-theory/blob/main/scripts/download.mjs
|
|
9
|
+
*
|
|
10
|
+
* @module themed-image
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createBrowser, getBrowserEngine } from './browser.js';
|
|
14
|
+
import { dismissPopups, scrollToLoadContent } from './popups.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Capture a single themed screenshot.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} browser - Browser instance from createBrowser
|
|
20
|
+
* @param {string} url - URL to capture
|
|
21
|
+
* @param {string} theme - 'light' or 'dark'
|
|
22
|
+
* @param {Object} [options] - Screenshot options
|
|
23
|
+
* @param {number} [options.width=1920] - Viewport width
|
|
24
|
+
* @param {number} [options.height=1080] - Viewport height
|
|
25
|
+
* @param {boolean} [options.fullPage=true] - Full page capture
|
|
26
|
+
* @param {boolean} [options.dismissPopups=true] - Dismiss popups
|
|
27
|
+
* @returns {Promise<Buffer>} Screenshot buffer
|
|
28
|
+
*/
|
|
29
|
+
async function captureThemedScreenshot(browser, url, theme, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
width = 1920,
|
|
32
|
+
height = 1080,
|
|
33
|
+
fullPage = true,
|
|
34
|
+
dismissPopups: shouldDismissPopups = true,
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
// Create browser with theme-specific color scheme
|
|
38
|
+
const themeOpts = { colorScheme: theme };
|
|
39
|
+
const themedBrowser = await createBrowser(
|
|
40
|
+
browser.type || 'puppeteer',
|
|
41
|
+
themeOpts
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const page = await themedBrowser.newPage();
|
|
46
|
+
await page.setViewport({ width, height });
|
|
47
|
+
await page.setUserAgent(
|
|
48
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await page.goto(url, {
|
|
52
|
+
waitUntil: 'networkidle0',
|
|
53
|
+
timeout: 30000,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
57
|
+
|
|
58
|
+
if (fullPage) {
|
|
59
|
+
await scrollToLoadContent(page);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (shouldDismissPopups) {
|
|
63
|
+
await dismissPopups(page);
|
|
64
|
+
// eslint-disable-next-line no-undef
|
|
65
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const buffer = await page.screenshot({
|
|
70
|
+
type: 'png',
|
|
71
|
+
fullPage,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return buffer;
|
|
75
|
+
} finally {
|
|
76
|
+
await themedBrowser.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Capture screenshots in both light and dark themes.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} url - URL to capture
|
|
84
|
+
* @param {Object} [options] - Options
|
|
85
|
+
* @param {string} [options.engine='puppeteer'] - Browser engine
|
|
86
|
+
* @param {number} [options.width=1920] - Viewport width
|
|
87
|
+
* @param {number} [options.height=1080] - Viewport height
|
|
88
|
+
* @param {boolean} [options.fullPage=true] - Full page capture
|
|
89
|
+
* @param {boolean} [options.dismissPopups=true] - Dismiss popups
|
|
90
|
+
* @returns {Promise<Object>} Result with light and dark screenshot buffers
|
|
91
|
+
*/
|
|
92
|
+
export async function captureDualThemeScreenshots(url, options = {}) {
|
|
93
|
+
const absoluteUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
94
|
+
const engine = options.engine || 'puppeteer';
|
|
95
|
+
|
|
96
|
+
// Capture light theme
|
|
97
|
+
const lightBuffer = await captureThemedScreenshot(
|
|
98
|
+
{ type: engine },
|
|
99
|
+
absoluteUrl,
|
|
100
|
+
'light',
|
|
101
|
+
options
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Capture dark theme
|
|
105
|
+
const darkBuffer = await captureThemedScreenshot(
|
|
106
|
+
{ type: engine },
|
|
107
|
+
absoluteUrl,
|
|
108
|
+
'dark',
|
|
109
|
+
options
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
light: lightBuffer,
|
|
114
|
+
dark: darkBuffer,
|
|
115
|
+
url: absoluteUrl,
|
|
116
|
+
width: options.width || 1920,
|
|
117
|
+
height: options.height || 1080,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dual-themed screenshot handler for Express API.
|
|
123
|
+
*
|
|
124
|
+
* Query parameters:
|
|
125
|
+
* url (required) - URL to capture
|
|
126
|
+
* engine - 'puppeteer' or 'playwright'
|
|
127
|
+
* width - viewport width (default 1920)
|
|
128
|
+
* height - viewport height (default 1080)
|
|
129
|
+
* fullPage - 'true' (default) to capture full page
|
|
130
|
+
* dismissPopups - 'true' (default) to dismiss popups
|
|
131
|
+
*/
|
|
132
|
+
export async function themedImageHandler(req, res) {
|
|
133
|
+
const url = req.query.url;
|
|
134
|
+
if (!url) {
|
|
135
|
+
return res.status(400).send('Missing `url` parameter');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const engine = getBrowserEngine(req);
|
|
140
|
+
const width = parseInt(req.query.width, 10) || 1920;
|
|
141
|
+
const height = parseInt(req.query.height, 10) || 1080;
|
|
142
|
+
const fullPage = req.query.fullPage !== 'false';
|
|
143
|
+
const shouldDismissPopups = req.query.dismissPopups !== 'false';
|
|
144
|
+
|
|
145
|
+
const result = await captureDualThemeScreenshots(url, {
|
|
146
|
+
engine,
|
|
147
|
+
width,
|
|
148
|
+
height,
|
|
149
|
+
fullPage,
|
|
150
|
+
dismissPopups: shouldDismissPopups,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Return as JSON with base64-encoded images
|
|
154
|
+
res.json({
|
|
155
|
+
url: result.url,
|
|
156
|
+
width: result.width,
|
|
157
|
+
height: result.height,
|
|
158
|
+
light: result.light.toString('base64'),
|
|
159
|
+
dark: result.dark.toString('base64'),
|
|
160
|
+
lightSize: result.light.length,
|
|
161
|
+
darkSize: result.dark.length,
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error(err);
|
|
165
|
+
res.status(500).send('Error capturing themed screenshots');
|
|
166
|
+
}
|
|
167
|
+
}
|