@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
package/src/browser.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
// Browser abstraction layer
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Browser abstraction layer using browser-commander for all browser operations
|
|
2
|
+
// See: https://github.com/link-foundation/browser-commander
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
launchBrowser,
|
|
6
|
+
makeBrowserCommander,
|
|
7
|
+
emulateMedia,
|
|
8
|
+
} from 'browser-commander';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @property {Function} newPage - Create a new page
|
|
9
|
-
* @property {Function} close - Close the browser
|
|
10
|
-
* @property {string} type - Browser type ('puppeteer' or 'playwright')
|
|
13
|
+
* Additional Chrome args needed for headless server environments
|
|
14
|
+
* These are appended to browser-commander's default CHROME_ARGS
|
|
11
15
|
*/
|
|
16
|
+
const SERVER_CHROME_ARGS = [
|
|
17
|
+
'--no-sandbox',
|
|
18
|
+
'--disable-setuid-sandbox',
|
|
19
|
+
'--disable-dev-shm-usage',
|
|
20
|
+
];
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
|
-
* Unified page interface
|
|
23
|
+
* Unified page interface wrapping browser-commander's makeBrowserCommander.
|
|
24
|
+
*
|
|
25
|
+
* Exposes:
|
|
26
|
+
* - setExtraHTTPHeaders / setUserAgent / setViewport / goto / content / screenshot / close
|
|
27
|
+
* (via the underlying raw page for cross-engine compatibility)
|
|
28
|
+
* - commander.pdf() – PDF generation (browser-commander v0.8.0)
|
|
29
|
+
* - commander.emulateMedia() – Color scheme emulation (browser-commander v0.7.0)
|
|
30
|
+
* - commander.keyboard – Keyboard interaction (browser-commander v0.7.0)
|
|
31
|
+
* - commander.onDialog() – Dialog event handling (browser-commander v0.7.0)
|
|
32
|
+
* - commander.evaluate() – Page evaluation (browser-commander)
|
|
33
|
+
* All other browser-commander APIs are available on the returned commander object.
|
|
34
|
+
*
|
|
15
35
|
* @typedef {Object} PageAdapter
|
|
16
36
|
* @property {Function} setExtraHTTPHeaders - Set HTTP headers
|
|
17
37
|
* @property {Function} setUserAgent - Set user agent
|
|
@@ -20,158 +40,177 @@ import playwright from 'playwright';
|
|
|
20
40
|
* @property {Function} content - Get page HTML content
|
|
21
41
|
* @property {Function} screenshot - Take screenshot
|
|
22
42
|
* @property {Function} close - Close the page
|
|
23
|
-
* @property {
|
|
24
|
-
* @property {
|
|
43
|
+
* @property {Function} pdf - Generate PDF (browser-commander v0.8.0)
|
|
44
|
+
* @property {Function} emulateMedia - Color scheme emulation (browser-commander v0.7.0)
|
|
45
|
+
* @property {Object} keyboard - Keyboard interaction (browser-commander v0.7.0)
|
|
46
|
+
* @property {Function} onDialog - Dialog event handling (browser-commander v0.7.0)
|
|
47
|
+
* @property {Function} evaluate - Page evaluation
|
|
48
|
+
* @property {string} type - Browser type ('puppeteer' or 'playwright')
|
|
25
49
|
*/
|
|
26
50
|
|
|
27
51
|
/**
|
|
28
|
-
* Create a browser instance using the specified engine
|
|
52
|
+
* Create a browser instance using the specified engine.
|
|
53
|
+
* Uses browser-commander's launchBrowser + makeBrowserCommander facade.
|
|
54
|
+
*
|
|
29
55
|
* @param {string} engine - 'puppeteer' or 'playwright' (defaults to puppeteer)
|
|
30
56
|
* @param {Object} options - Browser launch options
|
|
31
|
-
* @
|
|
57
|
+
* @param {string} [options.colorScheme] - Color scheme: 'light', 'dark', or 'no-preference'
|
|
58
|
+
* @returns {Promise<Object>} - Browser handle with newPage() and close()
|
|
32
59
|
*/
|
|
33
60
|
export async function createBrowser(engine = 'puppeteer', options = {}) {
|
|
34
61
|
const normalizedEngine = engine.toLowerCase();
|
|
62
|
+
const engineType =
|
|
63
|
+
normalizedEngine === 'playwright' || normalizedEngine === 'pw'
|
|
64
|
+
? 'playwright'
|
|
65
|
+
: 'puppeteer';
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
return createPlaywrightBrowser(options);
|
|
38
|
-
} else {
|
|
39
|
-
return createPuppeteerBrowser(options);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
67
|
+
const { colorScheme, ...launchOpts } = options;
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
// Generate unique userDataDir for this session to avoid conflicts
|
|
70
|
+
const userDataDir = path.join(
|
|
71
|
+
os.tmpdir(),
|
|
72
|
+
`web-capture-${engineType}-${Date.now()}`
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Use browser-commander's launchBrowser with server-specific args
|
|
76
|
+
const { browser, page: initialPage } = await launchBrowser({
|
|
77
|
+
engine: engineType,
|
|
78
|
+
args: SERVER_CHROME_ARGS,
|
|
79
|
+
headless: true,
|
|
80
|
+
userDataDir,
|
|
81
|
+
slowMo: 0,
|
|
82
|
+
...launchOpts,
|
|
83
|
+
});
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
// Close the initial page since we'll create new ones via newPage()
|
|
86
|
+
await initialPage.close();
|
|
58
87
|
|
|
59
88
|
return {
|
|
60
89
|
async newPage() {
|
|
61
|
-
const
|
|
62
|
-
return createPuppeteerPageAdapter(page);
|
|
63
|
-
},
|
|
64
|
-
async close() {
|
|
65
|
-
await browser.close();
|
|
66
|
-
},
|
|
67
|
-
type: 'puppeteer',
|
|
68
|
-
_browser: browser,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
90
|
+
const rawPage = await browser.newPage();
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
92
|
+
// Apply color scheme emulation using browser-commander's unified API (v0.7.0+)
|
|
93
|
+
if (colorScheme) {
|
|
94
|
+
try {
|
|
95
|
+
await emulateMedia({
|
|
96
|
+
page: rawPage,
|
|
97
|
+
engine: engineType,
|
|
98
|
+
colorScheme,
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
/* emulateMedia not available in all environments */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
85
104
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
...defaultOptions,
|
|
89
|
-
...options,
|
|
90
|
-
});
|
|
105
|
+
// Wrap with the full browser-commander facade for unified API access
|
|
106
|
+
const commander = makeBrowserCommander({ page: rawPage });
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
108
|
+
// Build a unified page adapter that exposes both:
|
|
109
|
+
// 1. The familiar cross-engine adapter interface (setExtraHTTPHeaders, goto, etc.)
|
|
110
|
+
// 2. All browser-commander v0.7.0+ APIs (pdf, emulateMedia, keyboard, onDialog, evaluate)
|
|
111
|
+
return createPageAdapter(rawPage, commander, engineType);
|
|
96
112
|
},
|
|
97
113
|
async close() {
|
|
98
114
|
await browser.close();
|
|
99
115
|
},
|
|
100
|
-
type:
|
|
101
|
-
|
|
116
|
+
type: engineType,
|
|
117
|
+
rawBrowser: browser,
|
|
102
118
|
};
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
/**
|
|
106
|
-
* Create a page adapter
|
|
107
|
-
*
|
|
122
|
+
* Create a unified page adapter from a raw page and its browser-commander facade.
|
|
123
|
+
* Provides a stable cross-engine interface plus all new browser-commander APIs.
|
|
124
|
+
*
|
|
125
|
+
* @param {Object} rawPage - Raw Playwright or Puppeteer page
|
|
126
|
+
* @param {Object} commander - browser-commander facade from makeBrowserCommander
|
|
127
|
+
* @param {string} engineType - 'puppeteer' or 'playwright'
|
|
108
128
|
* @returns {PageAdapter}
|
|
109
129
|
*/
|
|
110
|
-
function
|
|
130
|
+
function createPageAdapter(rawPage, commander, engineType) {
|
|
111
131
|
return {
|
|
132
|
+
// --- Cross-engine adapter methods (stable interface) ---
|
|
133
|
+
|
|
112
134
|
async setExtraHTTPHeaders(headers) {
|
|
113
|
-
await
|
|
135
|
+
await rawPage.setExtraHTTPHeaders(headers);
|
|
114
136
|
},
|
|
115
137
|
async setUserAgent(userAgent) {
|
|
116
|
-
|
|
138
|
+
if (engineType === 'playwright') {
|
|
139
|
+
// Playwright doesn't have page.setUserAgent, use extraHTTPHeaders instead
|
|
140
|
+
await rawPage.setExtraHTTPHeaders({ 'User-Agent': userAgent });
|
|
141
|
+
} else {
|
|
142
|
+
await rawPage.setUserAgent(userAgent);
|
|
143
|
+
}
|
|
117
144
|
},
|
|
118
145
|
async setViewport(viewport) {
|
|
119
|
-
|
|
146
|
+
if (engineType === 'playwright') {
|
|
147
|
+
// Playwright uses setViewportSize instead of setViewport
|
|
148
|
+
await rawPage.setViewportSize(viewport);
|
|
149
|
+
} else {
|
|
150
|
+
await rawPage.setViewport(viewport);
|
|
151
|
+
}
|
|
120
152
|
},
|
|
121
153
|
async goto(url, options = {}) {
|
|
122
|
-
|
|
154
|
+
if (engineType === 'playwright') {
|
|
155
|
+
// Convert Puppeteer waitUntil options to Playwright equivalents
|
|
156
|
+
const playwrightOptions = { ...options };
|
|
157
|
+
if (playwrightOptions.waitUntil === 'networkidle0') {
|
|
158
|
+
playwrightOptions.waitUntil = 'networkidle';
|
|
159
|
+
}
|
|
160
|
+
await rawPage.goto(url, playwrightOptions);
|
|
161
|
+
} else {
|
|
162
|
+
await rawPage.goto(url, options);
|
|
163
|
+
}
|
|
123
164
|
},
|
|
124
165
|
async content() {
|
|
125
|
-
return await
|
|
166
|
+
return await rawPage.content();
|
|
126
167
|
},
|
|
127
168
|
async screenshot(options = {}) {
|
|
128
|
-
return await
|
|
169
|
+
return await rawPage.screenshot(options);
|
|
129
170
|
},
|
|
130
171
|
async close() {
|
|
131
|
-
await
|
|
172
|
+
await commander.destroy();
|
|
173
|
+
await rawPage.close();
|
|
132
174
|
},
|
|
133
|
-
_page: page,
|
|
134
|
-
_type: 'puppeteer',
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
175
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*/
|
|
143
|
-
function createPlaywrightPageAdapter(page) {
|
|
144
|
-
return {
|
|
145
|
-
async setExtraHTTPHeaders(headers) {
|
|
146
|
-
await page.setExtraHTTPHeaders(headers);
|
|
176
|
+
// --- browser-commander v0.8.0+: PDF generation ---
|
|
177
|
+
// Usage: await page.pdf({ pdfOptions: { format: 'A4', printBackground: true } })
|
|
178
|
+
async pdf(opts = {}) {
|
|
179
|
+
return await commander.pdf(opts);
|
|
147
180
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
|
|
182
|
+
// --- browser-commander v0.7.0+: Color scheme emulation ---
|
|
183
|
+
// Usage: await page.emulateMedia({ colorScheme: 'dark' })
|
|
184
|
+
async emulateMedia(opts = {}) {
|
|
185
|
+
return await commander.emulateMedia(opts);
|
|
151
186
|
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
187
|
+
|
|
188
|
+
// --- browser-commander v0.7.0+: Keyboard interaction ---
|
|
189
|
+
// Usage: await page.keyboard.press('Escape')
|
|
190
|
+
get keyboard() {
|
|
191
|
+
return commander.keyboard;
|
|
155
192
|
},
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
await page.goto(url, playwrightOptions);
|
|
193
|
+
|
|
194
|
+
// --- browser-commander v0.7.0+: Dialog event handling ---
|
|
195
|
+
// Usage: page.onDialog(async (dialog) => { await dialog.dismiss(); })
|
|
196
|
+
onDialog(handler) {
|
|
197
|
+
return commander.onDialog(handler);
|
|
163
198
|
},
|
|
164
|
-
|
|
165
|
-
return
|
|
199
|
+
offDialog(handler) {
|
|
200
|
+
return commander.offDialog(handler);
|
|
166
201
|
},
|
|
167
|
-
|
|
168
|
-
return
|
|
202
|
+
clearDialogHandlers() {
|
|
203
|
+
return commander.clearDialogHandlers();
|
|
169
204
|
},
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
|
|
206
|
+
// --- browser-commander: Page evaluation ---
|
|
207
|
+
// Usage: await page.evaluate(() => window.scrollTo(0, 0))
|
|
208
|
+
async evaluate(fn, ...args) {
|
|
209
|
+
return await commander.evaluate({ fn, args });
|
|
172
210
|
},
|
|
173
|
-
|
|
174
|
-
|
|
211
|
+
|
|
212
|
+
// Engine type identifier
|
|
213
|
+
type: engineType,
|
|
175
214
|
};
|
|
176
215
|
}
|
|
177
216
|
|
package/src/docx.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOCX export handler.
|
|
3
|
+
*
|
|
4
|
+
* Converts a web page to a DOCX document with embedded images.
|
|
5
|
+
* Uses the page's markdown conversion and the `docx` library to build the document.
|
|
6
|
+
*
|
|
7
|
+
* Query parameters:
|
|
8
|
+
* url (required) - URL to convert
|
|
9
|
+
* engine - 'puppeteer' or 'playwright' (only used for browser rendering fallback)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fetch from 'node-fetch';
|
|
13
|
+
import * as cheerio from 'cheerio';
|
|
14
|
+
import { URL } from 'url';
|
|
15
|
+
import {
|
|
16
|
+
Document,
|
|
17
|
+
Packer,
|
|
18
|
+
Paragraph,
|
|
19
|
+
TextRun,
|
|
20
|
+
HeadingLevel,
|
|
21
|
+
ImageRun,
|
|
22
|
+
ExternalHyperlink,
|
|
23
|
+
} from 'docx';
|
|
24
|
+
import { fetchHtml, convertRelativeUrls } from './lib.js';
|
|
25
|
+
|
|
26
|
+
export async function docxHandler(req, res) {
|
|
27
|
+
const url = req.query.url;
|
|
28
|
+
if (!url) {
|
|
29
|
+
return res.status(400).send('Missing `url` parameter');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const absoluteUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
34
|
+
const html = await fetchHtml(absoluteUrl);
|
|
35
|
+
const absHtml = convertRelativeUrls(html, absoluteUrl);
|
|
36
|
+
const $ = cheerio.load(absHtml);
|
|
37
|
+
|
|
38
|
+
// Remove unwanted elements
|
|
39
|
+
$('style, script, noscript, nav, footer, header').remove();
|
|
40
|
+
|
|
41
|
+
// Build DOCX sections from HTML structure
|
|
42
|
+
const children = [];
|
|
43
|
+
|
|
44
|
+
// Extract title
|
|
45
|
+
const title = $('h1').first().text().trim() || $('title').text().trim();
|
|
46
|
+
if (title) {
|
|
47
|
+
children.push(
|
|
48
|
+
new Paragraph({ text: title, heading: HeadingLevel.TITLE })
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Process body content
|
|
53
|
+
const body = $('article').length ? $('article') : $('body');
|
|
54
|
+
for (const el of body.children().toArray()) {
|
|
55
|
+
const tagName = (el.tagName || '').toLowerCase();
|
|
56
|
+
const text = $(el).text().trim();
|
|
57
|
+
|
|
58
|
+
if (!text && tagName !== 'img' && tagName !== 'figure') {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (tagName === 'h1') {
|
|
63
|
+
children.push(new Paragraph({ text, heading: HeadingLevel.HEADING_1 }));
|
|
64
|
+
} else if (tagName === 'h2') {
|
|
65
|
+
children.push(new Paragraph({ text, heading: HeadingLevel.HEADING_2 }));
|
|
66
|
+
} else if (tagName === 'h3') {
|
|
67
|
+
children.push(new Paragraph({ text, heading: HeadingLevel.HEADING_3 }));
|
|
68
|
+
} else if (tagName === 'h4' || tagName === 'h5' || tagName === 'h6') {
|
|
69
|
+
children.push(new Paragraph({ text, heading: HeadingLevel.HEADING_4 }));
|
|
70
|
+
} else if (tagName === 'figure' || tagName === 'img') {
|
|
71
|
+
const img = tagName === 'img' ? $(el) : $(el).find('img').first();
|
|
72
|
+
const src = img.attr('src');
|
|
73
|
+
if (src && !src.startsWith('data:')) {
|
|
74
|
+
try {
|
|
75
|
+
const imgUrl = new URL(src, absoluteUrl).href;
|
|
76
|
+
const imgResp = await fetch(imgUrl);
|
|
77
|
+
if (imgResp.ok) {
|
|
78
|
+
const buffer = await imgResp.buffer();
|
|
79
|
+
children.push(
|
|
80
|
+
new Paragraph({
|
|
81
|
+
children: [
|
|
82
|
+
new ImageRun({
|
|
83
|
+
data: buffer,
|
|
84
|
+
transformation: { width: 600, height: 400 },
|
|
85
|
+
type: guessImageType(imgUrl),
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
/* skip failed image embeds */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Add caption if present
|
|
96
|
+
const caption = $(el).find('figcaption').text().trim();
|
|
97
|
+
if (caption) {
|
|
98
|
+
children.push(
|
|
99
|
+
new Paragraph({
|
|
100
|
+
children: [new TextRun({ text: caption, italics: true })],
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} else if (tagName === 'ul' || tagName === 'ol') {
|
|
105
|
+
$(el)
|
|
106
|
+
.find('li')
|
|
107
|
+
.each(function () {
|
|
108
|
+
const liText = $(this).text().trim();
|
|
109
|
+
if (liText) {
|
|
110
|
+
children.push(
|
|
111
|
+
new Paragraph({
|
|
112
|
+
text: liText,
|
|
113
|
+
bullet: { level: 0 },
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} else if (tagName === 'pre' || tagName === 'code') {
|
|
119
|
+
children.push(
|
|
120
|
+
new Paragraph({
|
|
121
|
+
children: [new TextRun({ text, font: 'Courier New', size: 20 })],
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
} else if (tagName === 'a') {
|
|
125
|
+
const href = $(el).attr('href');
|
|
126
|
+
if (href && text) {
|
|
127
|
+
children.push(
|
|
128
|
+
new Paragraph({
|
|
129
|
+
children: [
|
|
130
|
+
new ExternalHyperlink({
|
|
131
|
+
children: [
|
|
132
|
+
new TextRun({
|
|
133
|
+
text,
|
|
134
|
+
style: 'Hyperlink',
|
|
135
|
+
}),
|
|
136
|
+
],
|
|
137
|
+
link: href,
|
|
138
|
+
}),
|
|
139
|
+
],
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Default: treat as paragraph
|
|
145
|
+
if (text) {
|
|
146
|
+
children.push(new Paragraph({ text }));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (children.length === 0) {
|
|
152
|
+
children.push(new Paragraph({ text: 'No content extracted.' }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const doc = new Document({
|
|
156
|
+
sections: [{ children }],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const buffer = await Packer.toBuffer(doc);
|
|
160
|
+
|
|
161
|
+
res.set(
|
|
162
|
+
'Content-Type',
|
|
163
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
164
|
+
);
|
|
165
|
+
res.set('Content-Disposition', 'attachment; filename="page.docx"');
|
|
166
|
+
res.end(buffer);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(err);
|
|
169
|
+
res.status(500).send('Error generating DOCX');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function guessImageType(url) {
|
|
174
|
+
const pathname = new URL(url).pathname.toLowerCase();
|
|
175
|
+
if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) {
|
|
176
|
+
return 'jpg';
|
|
177
|
+
}
|
|
178
|
+
if (pathname.endsWith('.gif')) {
|
|
179
|
+
return 'gif';
|
|
180
|
+
}
|
|
181
|
+
return 'png';
|
|
182
|
+
}
|