@link-assistant/web-capture 1.1.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.
Files changed (56) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +294 -0
  4. package/.jscpd.json +20 -0
  5. package/.lenv +8 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/ARCHITECTURE.md +982 -0
  9. package/CHANGELOG.md +22 -0
  10. package/Dockerfile +45 -0
  11. package/LICENSE +24 -0
  12. package/README.md +268 -0
  13. package/apply.sh +166 -0
  14. package/babel.config.cjs +13 -0
  15. package/bin/web-capture.js +285 -0
  16. package/docker-compose.yml +9 -0
  17. package/docs/market-analysis-2025-05-13.md +64 -0
  18. package/eslint.config.js +112 -0
  19. package/examples/js/engine_comparison.js +39 -0
  20. package/examples/js/html_download.js +23 -0
  21. package/examples/js/image_download.js +36 -0
  22. package/examples/js/markdown_download.js +23 -0
  23. package/examples/js/playwright_example.js +23 -0
  24. package/examples/python/html_download.py +15 -0
  25. package/examples/python/image_download.py +20 -0
  26. package/examples/python/markdown_download.py +15 -0
  27. package/examples/python/playwright_example.py +22 -0
  28. package/jest.config.mjs +18 -0
  29. package/package.json +83 -0
  30. package/scripts/changeset-version.mjs +38 -0
  31. package/scripts/create-github-release.mjs +93 -0
  32. package/scripts/create-manual-changeset.mjs +88 -0
  33. package/scripts/format-github-release.mjs +83 -0
  34. package/scripts/format-release-notes.mjs +218 -0
  35. package/scripts/instant-version-bump.mjs +121 -0
  36. package/scripts/publish-to-npm.mjs +128 -0
  37. package/scripts/setup-npm.mjs +37 -0
  38. package/scripts/validate-changeset.mjs +106 -0
  39. package/scripts/version-and-commit.mjs +237 -0
  40. package/src/browser.js +207 -0
  41. package/src/fetch.js +38 -0
  42. package/src/html.js +64 -0
  43. package/src/image.js +41 -0
  44. package/src/index.js +55 -0
  45. package/src/lib.js +330 -0
  46. package/src/markdown.js +17 -0
  47. package/src/stream.js +50 -0
  48. package/tests/e2e/docker.test.js +148 -0
  49. package/tests/e2e/process.test.js +117 -0
  50. package/tests/integration/browser-engines.test.js +128 -0
  51. package/tests/jest.setup.mjs +7 -0
  52. package/tests/mock/index.test.js +223 -0
  53. package/tests/unit/browser.test.js +136 -0
  54. package/tests/unit/cli.test.js +151 -0
  55. package/tests/unit/convertRelativeUrls.test.js +34 -0
  56. package/tests/unit/html2md.test.js +610 -0
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // CLI entry point for web-capture
3
+ // Supports two modes:
4
+ // 1. Server mode: web-capture --serve [--port 3000]
5
+ // 2. Capture mode: web-capture <url> [options]
6
+
7
+ import fs from 'fs';
8
+ import { URL } from 'node:url';
9
+ import { makeConfig } from 'lino-arguments';
10
+
11
+ // Create configuration using lino-arguments pattern
12
+ const config = makeConfig({
13
+ yargs: ({ yargs, getenv }) =>
14
+ yargs
15
+ .usage(
16
+ 'web-capture - Capture web pages as HTML, Markdown, or PNG\n\nUsage:\n web-capture --serve [--port <port>] Start as API server\n web-capture <url> [options] Capture a URL to file/stdout'
17
+ )
18
+ .option('serve', {
19
+ alias: 's',
20
+ type: 'boolean',
21
+ description: 'Start as HTTP API server',
22
+ default: false,
23
+ })
24
+ .option('port', {
25
+ alias: 'p',
26
+ type: 'number',
27
+ description: 'Port to listen on (default: 3000, or PORT env)',
28
+ // Don't use getenv here - let yargs read from process.env.PORT directly
29
+ // which will be set by the test or by lino-arguments from .lenv
30
+ default: 3000,
31
+ })
32
+ .option('format', {
33
+ alias: 'f',
34
+ type: 'string',
35
+ description: 'Output format: html, markdown, md, image, png',
36
+ default: 'html',
37
+ })
38
+ .option('output', {
39
+ alias: 'o',
40
+ type: 'string',
41
+ description:
42
+ 'Output file path (default: stdout for text, auto-generated for images)',
43
+ })
44
+ .option('engine', {
45
+ alias: 'e',
46
+ type: 'string',
47
+ description: 'Browser engine: puppeteer, playwright',
48
+ default: getenv('BROWSER_ENGINE', 'puppeteer'),
49
+ })
50
+ .option('configuration', {
51
+ type: 'string',
52
+ description: 'Path to .lenv configuration file',
53
+ })
54
+ .help('help')
55
+ .alias('help', 'h')
56
+ .version()
57
+ .alias('version', 'v')
58
+ .example('web-capture --serve', 'Start API server on port 3000')
59
+ .example(
60
+ 'web-capture --serve --port 8080',
61
+ 'Start API server on custom port'
62
+ )
63
+ .example(
64
+ 'web-capture https://example.com',
65
+ 'Capture URL as HTML to stdout'
66
+ )
67
+ .example(
68
+ 'web-capture https://example.com --format markdown --output page.md',
69
+ 'Capture URL as Markdown to file'
70
+ )
71
+ .example(
72
+ 'web-capture https://example.com --format png --engine playwright -o screenshot.png',
73
+ 'Capture screenshot using Playwright'
74
+ )
75
+ .epilogue(
76
+ 'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&engine=<ENGINE> Get PNG screenshot\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy'
77
+ )
78
+ .strict(),
79
+ lenv: {
80
+ enabled: true,
81
+ path: '.lenv',
82
+ },
83
+ });
84
+
85
+ async function startServer(port) {
86
+ // Import the Express app
87
+ const { app } = await import('../src/index.js');
88
+
89
+ return new Promise((resolve, reject) => {
90
+ const server = app.listen(port, () => {
91
+ console.log(`web-capture server listening on http://localhost:${port}`);
92
+ console.log('');
93
+ console.log('Available endpoints:');
94
+ console.log(` GET /html?url=<URL> - Render page as HTML`);
95
+ console.log(` GET /markdown?url=<URL> - Convert page to Markdown`);
96
+ console.log(` GET /image?url=<URL> - Screenshot page as PNG`);
97
+ console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
98
+ console.log(` GET /stream?url=<URL> - Stream content`);
99
+ console.log('');
100
+ console.log('Press Ctrl+C to stop the server');
101
+ resolve(server);
102
+ });
103
+
104
+ server.on('error', reject);
105
+
106
+ // Handle graceful shutdown
107
+ function shutdown(signal) {
108
+ console.log(`\nReceived ${signal}, shutting down...`);
109
+ server.close(() => {
110
+ console.log('Server closed');
111
+ process.exit(0);
112
+ });
113
+ // Force exit if not closed in 2 seconds
114
+ setTimeout(() => {
115
+ console.error('Force exiting after 2s');
116
+ process.exit(1);
117
+ }, 2000);
118
+ }
119
+
120
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
121
+ process.on('SIGINT', () => shutdown('SIGINT'));
122
+ });
123
+ }
124
+
125
+ async function captureUrl(url, options) {
126
+ const { format, output, engine } = options;
127
+
128
+ // Ensure URL is absolute
129
+ let absoluteUrl = url;
130
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
131
+ absoluteUrl = `https://${url}`;
132
+ }
133
+
134
+ // Validate URL
135
+ try {
136
+ new URL(absoluteUrl);
137
+ } catch {
138
+ console.error(`Error: Invalid URL "${url}"`);
139
+ process.exit(1);
140
+ }
141
+
142
+ // Import required modules
143
+ const {
144
+ fetchHtml,
145
+ convertHtmlToMarkdown,
146
+ convertToUtf8,
147
+ convertRelativeUrls,
148
+ } = await import('../src/lib.js');
149
+ const { createBrowser } = await import('../src/browser.js');
150
+
151
+ const normalizedFormat = format.toLowerCase();
152
+
153
+ try {
154
+ if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
155
+ // Markdown format
156
+ const html = await fetchHtml(absoluteUrl);
157
+ const markdown = convertHtmlToMarkdown(html, absoluteUrl);
158
+
159
+ if (output) {
160
+ fs.writeFileSync(output, markdown, 'utf-8');
161
+ console.error(`Markdown saved to: ${output}`);
162
+ } else {
163
+ process.stdout.write(markdown);
164
+ }
165
+ } else if (
166
+ normalizedFormat === 'image' ||
167
+ normalizedFormat === 'png' ||
168
+ normalizedFormat === 'screenshot'
169
+ ) {
170
+ // Image/screenshot format
171
+ const browser = await createBrowser(engine);
172
+ try {
173
+ const page = await browser.newPage();
174
+ await page.setExtraHTTPHeaders({
175
+ 'Accept-Language': 'en-US,en;q=0.9',
176
+ 'Accept-Charset': 'utf-8',
177
+ });
178
+ await page.setUserAgent(
179
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
180
+ );
181
+ await page.setViewport({ width: 1280, height: 800 });
182
+ await page.goto(absoluteUrl, {
183
+ waitUntil: 'networkidle0',
184
+ timeout: 30000,
185
+ });
186
+ // Wait for 5 seconds after page load
187
+ await new Promise((resolve) => setTimeout(resolve, 5000));
188
+
189
+ const buffer = await page.screenshot({ type: 'png' });
190
+
191
+ if (output) {
192
+ fs.writeFileSync(output, buffer);
193
+ console.error(`Screenshot saved to: ${output}`);
194
+ } else {
195
+ // Generate default filename based on URL
196
+ const urlObj = new URL(absoluteUrl);
197
+ const defaultFilename = `${urlObj.hostname.replace(/\./g, '_')}_${Date.now()}.png`;
198
+ fs.writeFileSync(defaultFilename, buffer);
199
+ console.error(`Screenshot saved to: ${defaultFilename}`);
200
+ }
201
+ } finally {
202
+ await browser.close();
203
+ }
204
+ } else {
205
+ // HTML format (default)
206
+ const html = await fetchHtml(absoluteUrl);
207
+ const hasJavaScript =
208
+ /<script[^>]*>[\s\S]*?<\/script>|<script[^>]*\/>|javascript:/i.test(
209
+ html
210
+ );
211
+ const isHtml = /<html[^>]*>[\s\S]*?<\/html>/i.test(html);
212
+
213
+ let resultHtml;
214
+
215
+ if (!isHtml || hasJavaScript) {
216
+ // Use browser to get rendered HTML
217
+ const browser = await createBrowser(engine);
218
+ try {
219
+ const page = await browser.newPage();
220
+ await page.setExtraHTTPHeaders({
221
+ 'Accept-Language': 'en-US,en;q=0.9',
222
+ 'Accept-Charset': 'utf-8',
223
+ });
224
+ await page.setUserAgent(
225
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
226
+ );
227
+ await page.setViewport({ width: 1280, height: 800 });
228
+ await page.goto(absoluteUrl, {
229
+ waitUntil: 'networkidle0',
230
+ timeout: 30000,
231
+ });
232
+ // Wait for 5 seconds after page load
233
+ await new Promise((resolve) => setTimeout(resolve, 5000));
234
+
235
+ const renderedHtml = await page.content();
236
+ const utf8Html = convertToUtf8(renderedHtml);
237
+ resultHtml = convertRelativeUrls(utf8Html, absoluteUrl);
238
+ } finally {
239
+ await browser.close();
240
+ }
241
+ } else {
242
+ // Plain HTML without JavaScript
243
+ const utf8Html = convertToUtf8(html);
244
+ resultHtml = convertRelativeUrls(utf8Html, absoluteUrl);
245
+ }
246
+
247
+ if (output) {
248
+ fs.writeFileSync(output, resultHtml, 'utf-8');
249
+ console.error(`HTML saved to: ${output}`);
250
+ } else {
251
+ process.stdout.write(resultHtml);
252
+ }
253
+ }
254
+ } catch (err) {
255
+ console.error(`Error capturing ${absoluteUrl}: ${err.message}`);
256
+ process.exit(1);
257
+ }
258
+ }
259
+
260
+ async function main() {
261
+ // Get positional arguments (non-option arguments)
262
+ const url = config._ && config._.length > 0 ? config._[0] : null;
263
+
264
+ if (config.serve) {
265
+ // Server mode
266
+ await startServer(config.port);
267
+ } else if (url) {
268
+ // Capture mode
269
+ await captureUrl(url, {
270
+ format: config.format,
271
+ output: config.output,
272
+ engine: config.engine,
273
+ });
274
+ } else {
275
+ // No arguments - show error
276
+ console.error('Error: Missing URL or --serve flag');
277
+ console.error('Run with --help for usage information');
278
+ process.exit(1);
279
+ }
280
+ }
281
+
282
+ main().catch((err) => {
283
+ console.error('Fatal error:', err.message);
284
+ process.exit(1);
285
+ });
@@ -0,0 +1,9 @@
1
+ services:
2
+ web-capture:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ ports:
7
+ - '3000:3000'
8
+ environment:
9
+ - PORT=3000
@@ -0,0 +1,64 @@
1
+ # Market Analysis Report: 13 May 2025
2
+
3
+ This report analyzes open-source projects similar to "web-capture," a microservice for capturing web content in HTML, Markdown, and PNG formats. The projects are grouped into **microservices** and **libraries**, with each evaluated for its features and relevance to web content capture. Below are two tables summarizing the findings, followed by detailed feature lists for each project.
4
+
5
+ ---
6
+
7
+ ## Microservices for Web Capture
8
+
9
+ | **Project** | **License** | **GitHub Link** |
10
+ | ----------------------- | ----------- | ----------------------------------------------------------------------------------------- |
11
+ | **screeenly** | MIT | [stefanzweifel/screeenly](https://github.com/stefanzweifel/screeenly) |
12
+ | **capture-website-api** | ISC | [robvanderleek/capture-website-api](https://github.com/robvanderleek/capture-website-api) |
13
+ | **captino** | MIT | [pistatium/captino](https://github.com/pistatium/captino) |
14
+ | **gowitness** | MIT | [gowitness/gowitness](https://github.com/gowitness/gowitness) |
15
+
16
+ ### Features of Microservices
17
+
18
+ - **screeenly**
19
+ - API for screenshots (PNG), PDFs, and HTML rendering
20
+ - Customizable via pixel density and image loading options
21
+ - Self-hosting support via Heroku, Zeet, or Docker
22
+
23
+ - **capture-website-api**
24
+ - API for website screenshots (PNG)
25
+ - Customizable via query parameters (e.g., width, height, scale)
26
+ - Uses Puppeteer for rendering complex sites
27
+ - Configurable via environment variables
28
+
29
+ - **captino**
30
+ - API for webpage screenshots (PNG)
31
+ - Supports full-page captures
32
+ - Easy deployment with Docker
33
+
34
+ - **gowitness**
35
+ - Command-line tool for website screenshots (PNG)
36
+ - Batch processing and report generation
37
+ - Built with Golang and Chrome Headless
38
+
39
+ ---
40
+
41
+ ## Libraries for Web Capture
42
+
43
+ | **Project** | **License** | **GitHub Link** |
44
+ | -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
45
+ | **webpage-capture** | MIT | [b4dnewz/webpage-capture](https://github.com/b4dnewz/webpage-capture) |
46
+ | **html-screen-capture-js** | MIT | [html-screen-capture-js/html-screen-capture-js](https://github.com/html-screen-capture-js/html-screen-capture-js) |
47
+
48
+ ### Features of Libraries
49
+
50
+ - **webpage-capture**
51
+ - Captures web pages in PNG, JPEG, PDF, HTML, and other formats
52
+ - Customizable via options like timeout and headers
53
+ - Supports single and batch captures
54
+
55
+ - **html-screen-capture-js**
56
+ - Captures webpages as self-contained HTML
57
+ - Removes external dependencies while preserving appearance
58
+ - Lightweight (12KB)
59
+
60
+ ---
61
+
62
+ ## Summary
63
+
64
+ The report identifies four microservices (**screeenly**, **capture-website-api**, **captino**, **gowitness**) and two libraries (**webpage-capture**, **html-screen-capture-js**) that align with the functionality of "web-capture." Microservices provide API-driven solutions, while libraries offer flexibility for custom implementations. All projects are open-source, licensed under permissive terms (MIT or ISC), and actively maintained as of May 13, 2025.
@@ -0,0 +1,112 @@
1
+ import js from '@eslint/js';
2
+ import prettierConfig from 'eslint-config-prettier';
3
+ import prettierPlugin from 'eslint-plugin-prettier';
4
+
5
+ export default [
6
+ js.configs.recommended,
7
+ prettierConfig,
8
+ {
9
+ files: ['**/*.js', '**/*.mjs'],
10
+ plugins: {
11
+ prettier: prettierPlugin,
12
+ },
13
+ languageOptions: {
14
+ ecmaVersion: 'latest',
15
+ sourceType: 'module',
16
+ globals: {
17
+ // Node.js globals
18
+ console: 'readonly',
19
+ process: 'readonly',
20
+ Buffer: 'readonly',
21
+ __dirname: 'readonly',
22
+ __filename: 'readonly',
23
+ global: 'readonly',
24
+ setTimeout: 'readonly',
25
+ clearTimeout: 'readonly',
26
+ setInterval: 'readonly',
27
+ clearInterval: 'readonly',
28
+ // Node.js 18+ globals
29
+ fetch: 'readonly',
30
+ // Runtime-specific globals
31
+ Bun: 'readonly',
32
+ Deno: 'readonly',
33
+ },
34
+ },
35
+ rules: {
36
+ // Prettier integration
37
+ 'prettier/prettier': 'error',
38
+
39
+ // Code quality rules
40
+ 'no-unused-vars': 'error',
41
+ 'no-console': 'off', // Allow console in this project
42
+ 'no-debugger': 'error',
43
+
44
+ // Best practices
45
+ eqeqeq: ['error', 'always'],
46
+ curly: ['error', 'all'],
47
+ 'no-var': 'error',
48
+ 'prefer-const': 'error',
49
+ 'prefer-arrow-callback': 'error',
50
+ 'no-duplicate-imports': 'error',
51
+
52
+ // ES6+ features
53
+ 'arrow-body-style': ['error', 'as-needed'],
54
+ 'object-shorthand': ['error', 'always'],
55
+ 'prefer-template': 'error',
56
+
57
+ // Async/await
58
+ 'no-async-promise-executor': 'error',
59
+ 'require-await': 'warn',
60
+
61
+ // Comments and documentation
62
+ 'spaced-comment': ['error', 'always', { markers: ['/'] }],
63
+
64
+ // Complexity rules - reasonable thresholds for maintainability
65
+ complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8
66
+ 'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4
67
+ 'max-lines-per-function': [
68
+ 'warn',
69
+ {
70
+ max: 150, // More reasonable than strict 50 lines per function
71
+ skipBlankLines: true,
72
+ skipComments: true,
73
+ },
74
+ ],
75
+ 'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5
76
+ 'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions
77
+ 'max-lines': ['error', 1500], // Maximum lines per file - counts all lines including blank lines and comments
78
+ },
79
+ },
80
+ {
81
+ // Test files have different requirements
82
+ files: ['tests/**/*.js', '**/*.test.js', '**/*.test.mjs'],
83
+ languageOptions: {
84
+ globals: {
85
+ // Jest globals
86
+ describe: 'readonly',
87
+ it: 'readonly',
88
+ test: 'readonly',
89
+ expect: 'readonly',
90
+ beforeAll: 'readonly',
91
+ afterAll: 'readonly',
92
+ beforeEach: 'readonly',
93
+ afterEach: 'readonly',
94
+ jest: 'readonly',
95
+ },
96
+ },
97
+ rules: {
98
+ 'require-await': 'off', // Async functions without await are common in tests
99
+ 'max-lines-per-function': 'off', // Test files can have long test functions
100
+ 'max-statements': 'off', // Test files can have many statements
101
+ },
102
+ },
103
+ {
104
+ ignores: [
105
+ 'node_modules/**',
106
+ 'coverage/**',
107
+ 'dist/**',
108
+ '*.min.js',
109
+ '.eslintcache',
110
+ ],
111
+ },
112
+ ];
@@ -0,0 +1,39 @@
1
+ import fetch from 'node-fetch';
2
+ import fs from 'fs';
3
+
4
+ const baseUrl = 'http://localhost:3000';
5
+ const targetUrl = 'https://example.com';
6
+
7
+ console.log('Comparing Puppeteer and Playwright engines...\n');
8
+
9
+ // Fetch using Puppeteer
10
+ console.log('1. Fetching HTML using Puppeteer (default)...');
11
+ const startPuppeteer = Date.now();
12
+ const puppeteerResponse = await fetch(
13
+ `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}`
14
+ );
15
+ const puppeteerHtml = await puppeteerResponse.text();
16
+ const puppeteerTime = Date.now() - startPuppeteer;
17
+ fs.writeFileSync('output/puppeteer_comparison.html', puppeteerHtml);
18
+ console.log(` Time: ${puppeteerTime}ms`);
19
+ console.log(` Size: ${puppeteerHtml.length} bytes`);
20
+
21
+ // Fetch using Playwright
22
+ console.log('\n2. Fetching HTML using Playwright...');
23
+ const startPlaywright = Date.now();
24
+ const playwrightResponse = await fetch(
25
+ `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
26
+ );
27
+ const playwrightHtml = await playwrightResponse.text();
28
+ const playwrightTime = Date.now() - startPlaywright;
29
+ fs.writeFileSync('output/playwright_comparison.html', playwrightHtml);
30
+ console.log(` Time: ${playwrightTime}ms`);
31
+ console.log(` Size: ${playwrightHtml.length} bytes`);
32
+
33
+ // Summary
34
+ console.log('\n=== Summary ===');
35
+ console.log(`Puppeteer: ${puppeteerTime}ms, ${puppeteerHtml.length} bytes`);
36
+ console.log(`Playwright: ${playwrightTime}ms, ${playwrightHtml.length} bytes`);
37
+ console.log(`Difference: ${Math.abs(puppeteerTime - playwrightTime)}ms`);
38
+
39
+ console.log('\nResults saved to output/ directory.');
@@ -0,0 +1,23 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const url = process.argv[2] || 'https://example.com';
7
+ const endpoint = `http://localhost:3000/html?url=${encodeURIComponent(url)}`;
8
+
9
+ fetch(endpoint)
10
+ .then((res) => {
11
+ if (!res.ok) {
12
+ throw new Error(`HTTP error! status: ${res.status}`);
13
+ }
14
+ return res.text();
15
+ })
16
+ .then((text) => {
17
+ const outputPath = path.join(__dirname, 'downloaded.html');
18
+ fs.writeFileSync(outputPath, text, 'utf-8');
19
+ console.log(`HTML saved to ${outputPath}`);
20
+ })
21
+ .catch((err) => {
22
+ console.error('Error:', err);
23
+ });
@@ -0,0 +1,36 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const url = process.argv[2] || 'https://example.com';
7
+ const endpoint = `http://localhost:3000/image?url=${encodeURIComponent(url)}`;
8
+
9
+ fetch(endpoint)
10
+ .then((res) => {
11
+ console.log('Status:', res.status);
12
+ console.log('Content-Type:', res.headers.get('content-type'));
13
+ if (!res.ok) {
14
+ throw new Error(`HTTP error! status: ${res.status}`);
15
+ }
16
+ return res.arrayBuffer();
17
+ })
18
+ .then((buffer) => {
19
+ const buf = Buffer.from(buffer);
20
+ const pngSignature = Buffer.from([
21
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
22
+ ]);
23
+ if (buf.slice(0, 8).equals(pngSignature)) {
24
+ const outputPath = path.join(__dirname, 'downloaded.png');
25
+ fs.writeFileSync(outputPath, buf);
26
+ console.log(`Image saved to ${outputPath} (valid PNG)`);
27
+ } else {
28
+ console.log(
29
+ 'Response is not a valid PNG! First bytes:',
30
+ buf.slice(0, 16)
31
+ );
32
+ }
33
+ })
34
+ .catch((err) => {
35
+ console.error('Error:', err);
36
+ });
@@ -0,0 +1,23 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const url = process.argv[2] || 'https://example.com';
7
+ const endpoint = `http://localhost:3000/markdown?url=${encodeURIComponent(url)}`;
8
+
9
+ fetch(endpoint)
10
+ .then((res) => {
11
+ if (!res.ok) {
12
+ throw new Error(`HTTP error! status: ${res.status}`);
13
+ }
14
+ return res.text();
15
+ })
16
+ .then((text) => {
17
+ const outputPath = path.join(__dirname, 'downloaded.md');
18
+ fs.writeFileSync(outputPath, text, 'utf-8');
19
+ console.log(`Markdown saved to ${outputPath}`);
20
+ })
21
+ .catch((err) => {
22
+ console.error('Error:', err);
23
+ });
@@ -0,0 +1,23 @@
1
+ import fetch from 'node-fetch';
2
+ import fs from 'fs';
3
+
4
+ const baseUrl = 'http://localhost:3000';
5
+ const targetUrl = 'https://example.com';
6
+
7
+ console.log('Fetching HTML using Playwright engine...');
8
+ const htmlResponse = await fetch(
9
+ `${baseUrl}/html?url=${encodeURIComponent(targetUrl)}&engine=playwright`
10
+ );
11
+ const html = await htmlResponse.text();
12
+ fs.writeFileSync('output/playwright_html.html', html);
13
+ console.log('HTML saved to output/playwright_html.html');
14
+
15
+ console.log('Fetching screenshot using Playwright engine...');
16
+ const imageResponse = await fetch(
17
+ `${baseUrl}/image?url=${encodeURIComponent(targetUrl)}&engine=playwright`
18
+ );
19
+ const imageBuffer = await imageResponse.buffer();
20
+ fs.writeFileSync('output/playwright_screenshot.png', imageBuffer);
21
+ console.log('Screenshot saved to output/playwright_screenshot.png');
22
+
23
+ console.log('Done! Both requests used Playwright engine.');
@@ -0,0 +1,15 @@
1
+ import sys
2
+ import requests
3
+ import os
4
+
5
+ url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
+ endpoint = f'http://localhost:3000/html?url={url}'
7
+
8
+ response = requests.get(endpoint)
9
+ response.raise_for_status()
10
+
11
+ output_path = os.path.join(os.path.dirname(__file__), 'downloaded.html')
12
+ with open(output_path, 'w', encoding='utf-8') as f:
13
+ f.write(response.text)
14
+
15
+ print(f'HTML saved to {output_path}')
@@ -0,0 +1,20 @@
1
+ import sys
2
+ import requests
3
+ import os
4
+
5
+ url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
+ endpoint = f'http://localhost:3000/image?url={url}'
7
+
8
+ response = requests.get(endpoint)
9
+ print('Status:', response.status_code)
10
+ print('Content-Type:', response.headers.get('content-type'))
11
+
12
+ # PNG signature: first 8 bytes
13
+ png_signature = b'\x89PNG\r\n\x1a\n'
14
+ if response.content[:8] == png_signature:
15
+ output_path = os.path.join(os.path.dirname(__file__), 'downloaded.png')
16
+ with open(output_path, 'wb') as f:
17
+ f.write(response.content)
18
+ print(f'Image saved to {output_path} (valid PNG)')
19
+ else:
20
+ print('Response is not a valid PNG! First bytes:', response.content[:16])
@@ -0,0 +1,15 @@
1
+ import sys
2
+ import requests
3
+ import os
4
+
5
+ url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
+ endpoint = f'http://localhost:3000/markdown?url={url}'
7
+
8
+ response = requests.get(endpoint)
9
+ response.raise_for_status()
10
+
11
+ output_path = os.path.join(os.path.dirname(__file__), 'downloaded.md')
12
+ with open(output_path, 'w', encoding='utf-8') as f:
13
+ f.write(response.text)
14
+
15
+ print(f'Markdown saved to {output_path}')