html-get 2.24.7 → 2.24.9
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/README.md +12 -1
- package/package.json +1 -1
- package/src/index.js +49 -4
- package/src/office.js +92 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- Get HTML markup for any URL, including images, video, audio, or
|
|
16
|
+
- Get HTML markup for any URL, including images, video, audio, pdf, or office documents (docx, xlsx, pptx, odt, rtf, epub).
|
|
17
17
|
- Block ads tracker or any non-necessary network subrequest.
|
|
18
18
|
- Handle unreachable or timeout URLs gracefully.
|
|
19
19
|
- Ensure HTML markup is appropriately encoded.
|
|
@@ -130,6 +130,17 @@ It returns a function that receives that executes [mutool](https://mupdf.com/) b
|
|
|
130
130
|
|
|
131
131
|
It can explicitly disabled passing `false`.
|
|
132
132
|
|
|
133
|
+
##### pandoc
|
|
134
|
+
|
|
135
|
+
Type: `function`|`boolean`<br>
|
|
136
|
+
Default: `source code`
|
|
137
|
+
|
|
138
|
+
It returns a function that executes the [pandoc](https://pandoc.org/) binary for turning office documents (docx, xlsx, pptx, odt, rtf, epub) into HTML markup.
|
|
139
|
+
|
|
140
|
+
The format is resolved from the response content-type, falling back to the URL extension.
|
|
141
|
+
|
|
142
|
+
It can be explicitly disabled passing `false`.
|
|
143
|
+
|
|
133
144
|
##### prerender
|
|
134
145
|
|
|
135
146
|
Type: `boolean`|`string`<br>
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "html-get",
|
|
3
3
|
"description": "Get the HTML from any website, fine-tuned for correction & speed",
|
|
4
4
|
"homepage": "https://nicedoc.com/microlinkhq/html-get",
|
|
5
|
-
"version": "2.24.
|
|
5
|
+
"version": "2.24.9",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"main": "src/index.js",
|
|
8
8
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const got = require('got')
|
|
|
15
15
|
const os = require('os')
|
|
16
16
|
|
|
17
17
|
const { getContentLength, getContentType } = require('./util')
|
|
18
|
+
const { getOfficeFormat, isOfficeUrl } = require('./office')
|
|
18
19
|
const autoDomains = require('./auto-domains')
|
|
19
20
|
const addHtml = require('./html')
|
|
20
21
|
|
|
@@ -30,6 +31,7 @@ const fetch = PCancelable.fn(
|
|
|
30
31
|
{
|
|
31
32
|
getTemporalFile,
|
|
32
33
|
mutool,
|
|
34
|
+
pandoc,
|
|
33
35
|
reflect = false,
|
|
34
36
|
timeout = REQ_TIMEOUT,
|
|
35
37
|
toEncode,
|
|
@@ -63,7 +65,12 @@ const fetch = PCancelable.fn(
|
|
|
63
65
|
const html = await (async () => {
|
|
64
66
|
const contentType = getContentType(res.headers)
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
const officeFormat =
|
|
69
|
+
pandoc && getOfficeFormat({ contentType, url: [res.url, url] })
|
|
70
|
+
|
|
71
|
+
// a recognized office file never goes through mutool, even if the
|
|
72
|
+
// response is mislabeled as application/pdf
|
|
73
|
+
if (mutool && !officeFormat && contentType === 'application/pdf') {
|
|
67
74
|
const file = getTemporalFile(url, 'pdf')
|
|
68
75
|
await writeFile(file.path, res.body)
|
|
69
76
|
if (getContentLength(res.headers) > PDF_SIZE_TRESHOLD) {
|
|
@@ -76,6 +83,12 @@ const fetch = PCancelable.fn(
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
if (officeFormat) {
|
|
87
|
+
const file = getTemporalFile(res.url, officeFormat)
|
|
88
|
+
await writeFile(file.path, res.body)
|
|
89
|
+
return pandoc(officeFormat, file.path)
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
return contentType === 'text/html' || !isMediaUrl(url)
|
|
80
93
|
? await toEncode(res.body, res.headers['content-type'])
|
|
81
94
|
: res.body.toString()
|
|
@@ -219,7 +232,14 @@ const isFetchMode = url => {
|
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
const defaultGetMode = (url, { prerender }) => {
|
|
222
|
-
if (
|
|
235
|
+
if (
|
|
236
|
+
prerender === false ||
|
|
237
|
+
isMediaUrl(url) ||
|
|
238
|
+
isPdfUrl(url) ||
|
|
239
|
+
isOfficeUrl(url)
|
|
240
|
+
) {
|
|
241
|
+
return 'fetch'
|
|
242
|
+
}
|
|
223
243
|
if (prerender === true) return 'prerender'
|
|
224
244
|
return isFetchMode(url) ? 'fetch' : 'prerender'
|
|
225
245
|
}
|
|
@@ -245,6 +265,23 @@ const defaultMutool = () =>
|
|
|
245
265
|
} catch (_) {}
|
|
246
266
|
})()
|
|
247
267
|
|
|
268
|
+
const defaultPandoc = () =>
|
|
269
|
+
(() => {
|
|
270
|
+
try {
|
|
271
|
+
const pandocPath = execSync('which pandoc', {
|
|
272
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
273
|
+
})
|
|
274
|
+
.toString()
|
|
275
|
+
.trim()
|
|
276
|
+
return async (format, filepath) => {
|
|
277
|
+
const { stdout } = await $(
|
|
278
|
+
`${pandocPath} --from=${format} --to=html --standalone --embed-resources ${filepath}`
|
|
279
|
+
)
|
|
280
|
+
return stdout
|
|
281
|
+
}
|
|
282
|
+
} catch (_) {}
|
|
283
|
+
})()
|
|
284
|
+
|
|
248
285
|
const getContent = PCancelable.fn(
|
|
249
286
|
(
|
|
250
287
|
url,
|
|
@@ -255,6 +292,7 @@ const getContent = PCancelable.fn(
|
|
|
255
292
|
gotOpts,
|
|
256
293
|
headers,
|
|
257
294
|
mutool,
|
|
295
|
+
pandoc,
|
|
258
296
|
puppeteerOpts,
|
|
259
297
|
rewriteUrls,
|
|
260
298
|
rewriteHtml,
|
|
@@ -265,7 +303,7 @@ const getContent = PCancelable.fn(
|
|
|
265
303
|
const isFetchMode = mode === 'fetch'
|
|
266
304
|
|
|
267
305
|
const fetchOpts = isFetchMode
|
|
268
|
-
? { headers, toEncode, mutool, getTemporalFile, ...gotOpts }
|
|
306
|
+
? { headers, toEncode, mutool, pandoc, getTemporalFile, ...gotOpts }
|
|
269
307
|
: { headers, toEncode, getBrowserless, gotOpts, ...puppeteerOpts }
|
|
270
308
|
|
|
271
309
|
const promise = modes[mode](url, fetchOpts)
|
|
@@ -295,6 +333,7 @@ module.exports = PCancelable.fn(
|
|
|
295
333
|
gotOpts,
|
|
296
334
|
headers,
|
|
297
335
|
mutool = defaultMutool(),
|
|
336
|
+
pandoc = defaultPandoc(),
|
|
298
337
|
prerender = 'auto',
|
|
299
338
|
puppeteerOpts,
|
|
300
339
|
rewriteHtml = false,
|
|
@@ -320,6 +359,7 @@ module.exports = PCancelable.fn(
|
|
|
320
359
|
gotOpts,
|
|
321
360
|
headers,
|
|
322
361
|
mutool,
|
|
362
|
+
pandoc,
|
|
323
363
|
puppeteerOpts,
|
|
324
364
|
rewriteUrls,
|
|
325
365
|
rewriteHtml,
|
|
@@ -332,7 +372,12 @@ module.exports = PCancelable.fn(
|
|
|
332
372
|
|
|
333
373
|
let shadowDOM = hasShadowDOM($)
|
|
334
374
|
|
|
335
|
-
if (
|
|
375
|
+
if (
|
|
376
|
+
mode === 'fetch' &&
|
|
377
|
+
getBrowserless &&
|
|
378
|
+
shadowDOM &&
|
|
379
|
+
prerender !== false
|
|
380
|
+
) {
|
|
336
381
|
debug('shadow DOM detected, retrying with prerender', { url: targetUrl })
|
|
337
382
|
const prerenderPromise = getContent(targetUrl, 'prerender', {
|
|
338
383
|
getBrowserless,
|
package/src/office.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Office document formats Pandoc reads natively (no LibreOffice needed). Each is
|
|
4
|
+
// converted to HTML at fetch time, so the rest of the pipeline (metadata,
|
|
5
|
+
// markdown, screenshot) treats the document like a regular web page.
|
|
6
|
+
//
|
|
7
|
+
// Legacy OLE binaries (.doc/.xls/.ppt) and OpenDocument spreadsheet/presentation
|
|
8
|
+
// (.ods/.odp) are intentionally absent: Pandoc can't read them, they need a
|
|
9
|
+
// headless LibreOffice in the container.
|
|
10
|
+
const OFFICE_FORMATS = {
|
|
11
|
+
docx: {
|
|
12
|
+
extensions: ['docx'],
|
|
13
|
+
contentTypes: [
|
|
14
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
xlsx: {
|
|
18
|
+
extensions: ['xlsx'],
|
|
19
|
+
contentTypes: [
|
|
20
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
pptx: {
|
|
24
|
+
extensions: ['pptx'],
|
|
25
|
+
contentTypes: [
|
|
26
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
odt: {
|
|
30
|
+
extensions: ['odt'],
|
|
31
|
+
contentTypes: ['application/vnd.oasis.opendocument.text']
|
|
32
|
+
},
|
|
33
|
+
rtf: {
|
|
34
|
+
extensions: ['rtf'],
|
|
35
|
+
contentTypes: ['application/rtf', 'text/rtf']
|
|
36
|
+
},
|
|
37
|
+
epub: {
|
|
38
|
+
extensions: ['epub'],
|
|
39
|
+
contentTypes: ['application/epub+zip']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const byContentType = new Map()
|
|
44
|
+
const byExtension = new Map()
|
|
45
|
+
|
|
46
|
+
for (const [format, { contentTypes, extensions }] of Object.entries(
|
|
47
|
+
OFFICE_FORMATS
|
|
48
|
+
)) {
|
|
49
|
+
for (const contentType of contentTypes) byContentType.set(contentType, format)
|
|
50
|
+
for (const extension of extensions) byExtension.set(extension, format)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getUrlExtension = url => {
|
|
54
|
+
const clean = String(url).split('?')[0].split('#')[0]
|
|
55
|
+
const index = clean.lastIndexOf('.')
|
|
56
|
+
if (index === -1) return ''
|
|
57
|
+
const extension = clean.slice(index + 1).toLowerCase()
|
|
58
|
+
return /^[a-z0-9]+$/.test(extension) ? extension : ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getOfficeFormatFromUrl = url => byExtension.get(getUrlExtension(url))
|
|
62
|
+
|
|
63
|
+
// Content-types that positively identify a web page or media. They are
|
|
64
|
+
// authoritative: the URL extension must never override them (e.g. an HTML page
|
|
65
|
+
// served at a `.docx` vanity URL is still a web page, not an office document).
|
|
66
|
+
const isWebContentType = type =>
|
|
67
|
+
/^(text\/html|application\/xhtml\+xml|application\/json|image\/|audio\/|video\/)/.test(
|
|
68
|
+
type || ''
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// Resolve the Pandoc `--from` format for a document:
|
|
72
|
+
// 1. an explicit office content-type wins
|
|
73
|
+
// 2. an explicit web/media content-type means "not an office document"
|
|
74
|
+
// 3. otherwise the content-type is non-committal (octet-stream, zip, missing,
|
|
75
|
+
// or a mislabeled binary type) so the URL extension decides; `url` may be a
|
|
76
|
+
// list of candidates, tried in order (e.g. final URL then original request
|
|
77
|
+
// URL after a redirect)
|
|
78
|
+
const getOfficeFormat = ({ contentType, url } = {}) => {
|
|
79
|
+
const byType = byContentType.get(contentType)
|
|
80
|
+
if (byType) return byType
|
|
81
|
+
if (isWebContentType(contentType)) return undefined
|
|
82
|
+
const candidates = Array.isArray(url) ? url : [url]
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
const format = candidate && getOfficeFormatFromUrl(candidate)
|
|
85
|
+
if (format) return format
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isOfficeUrl = url => byExtension.has(getUrlExtension(url))
|
|
91
|
+
|
|
92
|
+
module.exports = { OFFICE_FORMATS, getOfficeFormat, isOfficeUrl }
|