html-get 2.24.8 → 2.24.10

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 CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  ## Features
15
15
 
16
- - Get HTML markup for any URL, including images, video, audio, or pdf.
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.8",
5
+ "version": "2.24.10",
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
- if (mutool && contentType === 'application/pdf') {
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 (prerender === false || isMediaUrl(url) || isPdfUrl(url)) return 'fetch'
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,
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 }