@opencraw/core 0.1.3 → 0.1.4
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/dist/access/access-broker.use-case.d.ts +43 -0
- package/dist/access/access-config.error.d.ts +5 -0
- package/dist/access/access-config.repository.d.ts +11 -0
- package/dist/access/access-json-schema.mapper.d.ts +8 -0
- package/dist/access/access-plugin.contract.d.ts +48 -0
- package/dist/access/access-preset.store.d.ts +19 -0
- package/dist/access/access-profile.contract.d.ts +95 -0
- package/dist/access/index.d.ts +11 -0
- package/dist/access/render-access.mapper.d.ts +41 -0
- package/dist/access/session-id.algorithm.d.ts +8 -0
- package/dist/api-steps/extract-from-document.use-case.d.ts +41 -0
- package/dist/api-steps/index.d.ts +4 -0
- package/dist/api-steps/run-api-step.use-case.d.ts +20 -0
- package/dist/api-steps/send-request.use-case.d.ts +23 -0
- package/dist/browser-session/browser-profile.store.d.ts +52 -0
- package/dist/browser-session/browser-session.config.d.ts +21 -0
- package/dist/browser-session/browser.client.d.ts +75 -0
- package/dist/browser-session/index.d.ts +6 -0
- package/dist/captcha/captcha-budget.model.d.ts +21 -0
- package/dist/captcha/captcha-detection.client.d.ts +28 -0
- package/dist/captcha/captcha-guard.use-case.d.ts +64 -0
- package/dist/captcha/captcha-solver-registry.store.d.ts +19 -0
- package/dist/captcha/captcha-solver.contract.d.ts +47 -0
- package/dist/captcha/captcha.error.d.ts +13 -0
- package/dist/captcha/index.d.ts +10 -0
- package/dist/captcha/resolve-captcha.use-case.d.ts +40 -0
- package/dist/crawl-events/crawl-event.contract.d.ts +169 -0
- package/dist/crawl-events/event-bus.store.d.ts +10 -0
- package/dist/crawl-events/index.d.ts +4 -0
- package/dist/crawl-events/trace-line.mapper.d.ts +14 -0
- package/dist/crawl-execution/bootstrap-session.use-case.d.ts +79 -0
- package/dist/crawl-execution/crawl-options.config.d.ts +67 -0
- package/dist/crawl-execution/crawl-report.model.d.ts +33 -0
- package/dist/crawl-execution/create-crawler.use-case.d.ts +20 -0
- package/dist/crawl-execution/index.d.ts +8 -0
- package/dist/crawl-execution/rotating-runner.use-case.d.ts +66 -0
- package/dist/crawl-execution/run-crawl.use-case.d.ts +17 -0
- package/dist/crawl-execution/run-input-recipe.use-case.d.ts +51 -0
- package/dist/deck-document/deck-document.model.d.ts +58 -0
- package/dist/deck-document/deck-table.algorithm.d.ts +35 -0
- package/dist/deck-document/index.d.ts +6 -0
- package/dist/deck-document/read-pptx.client.d.ts +16 -0
- package/dist/docx-document/index.d.ts +2 -0
- package/dist/docx-document/read-docx-html.client.d.ts +23 -0
- package/dist/extraction-scope/extraction-scope.model.d.ts +77 -0
- package/dist/extraction-scope/index.d.ts +5 -0
- package/dist/extraction-scope/live-element.model.d.ts +28 -0
- package/dist/hooks/hook-registry.store.d.ts +16 -0
- package/dist/hooks/hook.contract.d.ts +17 -0
- package/dist/hooks/hook.error.d.ts +7 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/http-session/http-response.contract.d.ts +58 -0
- package/dist/http-session/http.client.d.ts +42 -0
- package/dist/http-session/index.d.ts +5 -0
- package/dist/http-session/text-decoding.algorithm.d.ts +35 -0
- package/dist/index.esm.js +649 -25
- package/dist/markdown-document/index.d.ts +3 -0
- package/dist/markdown-document/read-markdown.client.d.ts +39 -0
- package/dist/output-mapping/coerce-field.mapper.d.ts +19 -0
- package/dist/output-mapping/generated-field.mapper.d.ts +16 -0
- package/dist/output-mapping/index.d.ts +9 -0
- package/dist/output-mapping/map-record.use-case.d.ts +27 -0
- package/dist/output-mapping/mapping.error.d.ts +17 -0
- package/dist/output-mapping/missing-value.policy.d.ts +14 -0
- package/dist/output-mapping/output-field.validator.d.ts +10 -0
- package/dist/output-mapping/output-record.model.d.ts +20 -0
- package/dist/pdf-document/index.d.ts +7 -0
- package/dist/pdf-document/pdf-document.model.d.ts +51 -0
- package/dist/pdf-document/pdf-table.algorithm.d.ts +43 -0
- package/dist/pdf-document/read-pdf.client.d.ts +18 -0
- package/dist/pdf-document/row-assembly.algorithm.d.ts +26 -0
- package/dist/recipe-loading/index.d.ts +9 -0
- package/dist/recipe-loading/load-recipe-set.use-case.d.ts +45 -0
- package/dist/recipe-loading/read-recipe-source.use-case.d.ts +14 -0
- package/dist/recipe-loading/recipe-binding.error.d.ts +14 -0
- package/dist/recipe-loading/recipe-binding.validator.d.ts +34 -0
- package/dist/recipe-loading/recipe-file.repository.d.ts +11 -0
- package/dist/recipe-loading/recipe-set.model.d.ts +12 -0
- package/dist/recipe-loading/recipe-source.contract.d.ts +28 -0
- package/dist/recipe-loading/recipe-text.mapper.d.ts +14 -0
- package/dist/recipe-schema/index.d.ts +15 -0
- package/dist/recipe-schema/input-recipe.contract.d.ts +156 -0
- package/dist/recipe-schema/json-schema.mapper.d.ts +15 -0
- package/dist/recipe-schema/output-recipe.contract.d.ts +47 -0
- package/dist/recipe-schema/recipe-kind.enum.d.ts +35 -0
- package/dist/recipe-schema/recipe-validation.error.d.ts +17 -0
- package/dist/recipe-schema/recipe.validator.d.ts +28 -0
- package/dist/recipe-schema/step.contract.d.ts +206 -0
- package/dist/recipe-schema/transform-rule.contract.d.ts +120 -0
- package/dist/record-diff/index.d.ts +4 -0
- package/dist/record-diff/record-diff.algorithm.d.ts +82 -0
- package/dist/record-diff/records-file.repository.d.ts +10 -0
- package/dist/record-sink/dedupe.policy.d.ts +29 -0
- package/dist/record-sink/index.d.ts +9 -0
- package/dist/record-sink/json-lines-sink.repository.d.ts +19 -0
- package/dist/record-sink/memory-sink.repository.d.ts +9 -0
- package/dist/record-sink/record-sink.contract.d.ts +17 -0
- package/dist/selection/html-selector.algorithm.d.ts +19 -0
- package/dist/selection/index.d.ts +8 -0
- package/dist/selection/json-path.algorithm.d.ts +9 -0
- package/dist/selection/json-text.algorithm.d.ts +58 -0
- package/dist/selection/regex.algorithm.d.ts +12 -0
- package/dist/selection/take-value.mapper.d.ts +27 -0
- package/dist/src/api-steps/extract-from-document.use-case.d.ts +5 -5
- package/dist/src/docx-document/index.d.ts +2 -0
- package/dist/src/docx-document/read-docx-html.client.d.ts +23 -0
- package/dist/src/extraction-scope/extraction-scope.model.d.ts +2 -1
- package/dist/src/http-session/http-response.contract.d.ts +2 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/markdown-document/index.d.ts +1 -1
- package/dist/src/markdown-document/read-markdown.client.d.ts +10 -0
- package/dist/src/recipe-schema/recipe-kind.enum.d.ts +1 -1
- package/dist/src/recipe-schema/step.contract.d.ts +4 -0
- package/dist/src/record-diff/index.d.ts +4 -0
- package/dist/src/record-diff/record-diff.algorithm.d.ts +82 -0
- package/dist/src/record-diff/records-file.repository.d.ts +10 -0
- package/dist/src/selection/html-selector.algorithm.d.ts +2 -1
- package/dist/src/xml-document/index.d.ts +6 -0
- package/dist/src/xml-document/xml-document.model.d.ts +13 -0
- package/dist/src/xml-document/xml-parser.client.d.ts +49 -0
- package/dist/src/xml-document/xpath.algorithm.d.ts +30 -0
- package/dist/step-flow/block-rule.policy.d.ts +21 -0
- package/dist/step-flow/blocked.error.d.ts +9 -0
- package/dist/step-flow/for-each.use-case.d.ts +20 -0
- package/dist/step-flow/host-throttle.policy.d.ts +49 -0
- package/dist/step-flow/index.d.ts +15 -0
- package/dist/step-flow/paginate.use-case.d.ts +17 -0
- package/dist/step-flow/retry.policy.d.ts +21 -0
- package/dist/step-flow/run-gate.policy.d.ts +50 -0
- package/dist/step-flow/run-steps.use-case.d.ts +39 -0
- package/dist/step-flow/step-failure.error.d.ts +14 -0
- package/dist/step-flow/step-runner.contract.d.ts +52 -0
- package/dist/step-flow/transport-retry.policy.d.ts +76 -0
- package/dist/template/expression.algorithm.d.ts +53 -0
- package/dist/template/index.d.ts +7 -0
- package/dist/template/template.algorithm.d.ts +53 -0
- package/dist/template/value-path.algorithm.d.ts +29 -0
- package/dist/template/value-text.algorithm.d.ts +18 -0
- package/dist/transformation/apply-transform-chain.use-case.d.ts +17 -0
- package/dist/transformation/collection.algorithm.d.ts +15 -0
- package/dist/transformation/currency.algorithm.d.ts +15 -0
- package/dist/transformation/date.algorithm.d.ts +13 -0
- package/dist/transformation/index.d.ts +10 -0
- package/dist/transformation/lookup.algorithm.d.ts +24 -0
- package/dist/transformation/number.algorithm.d.ts +30 -0
- package/dist/transformation/string.algorithm.d.ts +18 -0
- package/dist/transformation/transform-registry.store.d.ts +36 -0
- package/dist/transformation/transform.error.d.ts +8 -0
- package/dist/transformation/url.algorithm.d.ts +20 -0
- package/dist/web-steps/evaluate-script.use-case.d.ts +10 -0
- package/dist/web-steps/extract-from-page.use-case.d.ts +10 -0
- package/dist/web-steps/index.d.ts +2 -0
- package/dist/web-steps/interact.use-case.d.ts +36 -0
- package/dist/web-steps/navigate.use-case.d.ts +15 -0
- package/dist/web-steps/run-web-step.use-case.d.ts +37 -0
- package/dist/web-steps/snapshot-elements.use-case.d.ts +13 -0
- package/dist/workbook-document/csv-parser.algorithm.d.ts +26 -0
- package/dist/workbook-document/csv-workbook.mapper.d.ts +24 -0
- package/dist/workbook-document/grid-table.algorithm.d.ts +53 -0
- package/dist/workbook-document/html-tables.mapper.d.ts +14 -0
- package/dist/workbook-document/index.d.ts +9 -0
- package/dist/workbook-document/read-xlsx.client.d.ts +18 -0
- package/dist/workbook-document/workbook-document.model.d.ts +51 -0
- package/dist/xml-document/index.d.ts +6 -0
- package/dist/xml-document/xml-document.model.d.ts +13 -0
- package/dist/xml-document/xml-parser.client.d.ts +49 -0
- package/dist/xml-document/xpath.algorithm.d.ts +30 -0
- package/dist/yaml-document/index.d.ts +3 -0
- package/dist/yaml-document/read-yaml.client.d.ts +25 -0
- package/package.json +4 -2
package/dist/index.esm.js
CHANGED
|
@@ -7,6 +7,9 @@ import { createWriteStream } from 'node:fs';
|
|
|
7
7
|
import { once } from 'node:events';
|
|
8
8
|
import { JSONPath } from 'jsonpath-plus';
|
|
9
9
|
import { load } from 'cheerio';
|
|
10
|
+
import { gunzipSync } from 'node:zlib';
|
|
11
|
+
import { DOMParser, DOMImplementation, XMLSerializer } from '@xmldom/xmldom';
|
|
12
|
+
import { useNamespaces } from 'xpath';
|
|
10
13
|
import { fileURLToPath } from 'node:url';
|
|
11
14
|
|
|
12
15
|
/** Resource types a page may skip loading, to save bandwidth on per-GB proxies. */
|
|
@@ -2988,10 +2991,13 @@ const DOCUMENT = /^\s*(?:<!doctype|<html)/i;
|
|
|
2988
2991
|
*
|
|
2989
2992
|
* @param html - The markup (a whole document or a fragment).
|
|
2990
2993
|
* @param selector - A CSS selector.
|
|
2994
|
+
* @param xml - Read the markup as XML: names keep their case, `take: "html"` gives XML.
|
|
2991
2995
|
* @returns Every match, in document order.
|
|
2992
2996
|
*/
|
|
2993
|
-
function selectHtml(html, selector) {
|
|
2994
|
-
const api =
|
|
2997
|
+
function selectHtml(html, selector, xml = false) {
|
|
2998
|
+
const api = xml ? load(html, {
|
|
2999
|
+
xml: true
|
|
3000
|
+
}) : DOCUMENT.test(html) ? load(html) : load(html, undefined, false);
|
|
2995
3001
|
return api(selector).map((_, element) => ({
|
|
2996
3002
|
api,
|
|
2997
3003
|
element: api(element)
|
|
@@ -4038,8 +4044,8 @@ function filledGrid(sheet) {
|
|
|
4038
4044
|
/** `B10:B13` as 0-based bounds; `undefined` for anything else. */
|
|
4039
4045
|
function rangeOf(reference) {
|
|
4040
4046
|
const [from, to = from] = reference.split(':', 2);
|
|
4041
|
-
const start = cellOf(from);
|
|
4042
|
-
const end = cellOf(to);
|
|
4047
|
+
const start = cellOf$1(from);
|
|
4048
|
+
const end = cellOf$1(to);
|
|
4043
4049
|
if (start === undefined || end === undefined) return undefined;
|
|
4044
4050
|
return {
|
|
4045
4051
|
top: Math.min(start.row, end.row),
|
|
@@ -4048,7 +4054,7 @@ function rangeOf(reference) {
|
|
|
4048
4054
|
right: Math.max(start.column, end.column)
|
|
4049
4055
|
};
|
|
4050
4056
|
}
|
|
4051
|
-
function cellOf(reference) {
|
|
4057
|
+
function cellOf$1(reference) {
|
|
4052
4058
|
const match = /^\$?([A-Z]+)\$?(\d+)$/i.exec(reference.trim());
|
|
4053
4059
|
if (match === null) return undefined;
|
|
4054
4060
|
const letters = match[1].toUpperCase();
|
|
@@ -4181,13 +4187,194 @@ function shapeTables(document, slides, query) {
|
|
|
4181
4187
|
}));
|
|
4182
4188
|
}
|
|
4183
4189
|
|
|
4190
|
+
/**
|
|
4191
|
+
* Whether a value is a read XML document.
|
|
4192
|
+
*
|
|
4193
|
+
* @param value - Anything bound in a scope.
|
|
4194
|
+
* @returns `true` for an `XmlDocument`.
|
|
4195
|
+
*/
|
|
4196
|
+
function isXmlDocument(value) {
|
|
4197
|
+
return typeof value === 'object' && value !== null && value.kind === 'xml' && typeof value.xml === 'string';
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
/** The documents already parsed, by the text they came from, so a recipe's extracts parse a body once. */
|
|
4201
|
+
const parsed = new Map();
|
|
4202
|
+
const PARSED_LIMIT = 16;
|
|
4203
|
+
/**
|
|
4204
|
+
* Parses XML strictly but safely: a malformed document is an error naming
|
|
4205
|
+
* the URL and the line; entities a DOCTYPE declares are never expanded (no
|
|
4206
|
+
* "billion laughs") and nothing external is ever fetched (no XXE), they stay
|
|
4207
|
+
* as they are.
|
|
4208
|
+
*
|
|
4209
|
+
* @param xml - The document text.
|
|
4210
|
+
* @param where - The URL or id, for messages.
|
|
4211
|
+
* @returns The document.
|
|
4212
|
+
* @throws Error when the text is not well-formed XML.
|
|
4213
|
+
*/
|
|
4214
|
+
function parseXml(xml, where) {
|
|
4215
|
+
const cached = parsed.get(xml);
|
|
4216
|
+
if (cached !== undefined) return cached;
|
|
4217
|
+
let document;
|
|
4218
|
+
try {
|
|
4219
|
+
document = new DOMParser({
|
|
4220
|
+
onError: failOnFatal
|
|
4221
|
+
}).parseFromString(xml, 'text/xml');
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
throw new Error(`${where}: not well-formed XML (${firstLine(error)})`, {
|
|
4224
|
+
cause: error
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
if (document.documentElement === null) throw new Error(`${where}: not an XML document (no root element)`);
|
|
4228
|
+
remember(xml, document);
|
|
4229
|
+
return document;
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* HTML as an XML document, for XPath on fetched pages: parsed the way a
|
|
4233
|
+
* browser parses it (a forgiving HTML5 parser that inserts `<tbody>` and
|
|
4234
|
+
* closes what the page left open), without namespaces, so `//table/tbody/tr`
|
|
4235
|
+
* finds what it finds on the live page.
|
|
4236
|
+
*
|
|
4237
|
+
* @param html - A page or a fragment.
|
|
4238
|
+
* @returns The document.
|
|
4239
|
+
*/
|
|
4240
|
+
function htmlAsXml(html) {
|
|
4241
|
+
const cached = parsed.get(html);
|
|
4242
|
+
if (cached !== undefined) return cached;
|
|
4243
|
+
const whole = /^\s*(?:<!doctype|<html)/i.test(html);
|
|
4244
|
+
// A fragment (a row taken with `take: "html"`) keeps its cells, and gets one root to be a document.
|
|
4245
|
+
const xml = whole ? load(html).xml() : `<fragment>${load(html, undefined, false).xml()}</fragment>`;
|
|
4246
|
+
const document = withoutNamespaces(new DOMParser({
|
|
4247
|
+
onError: ignore
|
|
4248
|
+
}).parseFromString(xml, 'text/xml'));
|
|
4249
|
+
remember(html, document);
|
|
4250
|
+
return document;
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* A copy of a document with every namespace dropped: elements and attributes
|
|
4254
|
+
* keep their local names, `xmlns` declarations go. What `ignoreNamespaces`
|
|
4255
|
+
* queries run on: `//entry/title` instead of `//atom:entry/atom:title`.
|
|
4256
|
+
*
|
|
4257
|
+
* @param document - A parsed document.
|
|
4258
|
+
* @returns The copy.
|
|
4259
|
+
*/
|
|
4260
|
+
function withoutNamespaces(document) {
|
|
4261
|
+
const copy = new DOMImplementation().createDocument(null, '', null);
|
|
4262
|
+
const clone = node => {
|
|
4263
|
+
if (node.nodeType === node.ELEMENT_NODE) {
|
|
4264
|
+
const element = node;
|
|
4265
|
+
const target = copy.createElement(element.localName);
|
|
4266
|
+
for (const attribute of element.attributes) {
|
|
4267
|
+
if (attribute.name === 'xmlns' || attribute.prefix === 'xmlns') continue;
|
|
4268
|
+
target.setAttribute(attribute.localName, attribute.value);
|
|
4269
|
+
}
|
|
4270
|
+
for (const child of element.childNodes) {
|
|
4271
|
+
const cloned = clone(child);
|
|
4272
|
+
if (cloned !== undefined) target.insertBefore(cloned, null);
|
|
4273
|
+
}
|
|
4274
|
+
return target;
|
|
4275
|
+
}
|
|
4276
|
+
if (node.nodeType === node.TEXT_NODE || node.nodeType === node.CDATA_SECTION_NODE) return copy.createTextNode(node.nodeValue ?? '');
|
|
4277
|
+
return undefined;
|
|
4278
|
+
};
|
|
4279
|
+
const root = document.documentElement === null ? undefined : clone(document.documentElement);
|
|
4280
|
+
if (root !== undefined) copy.insertBefore(root, null);
|
|
4281
|
+
return copy;
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* A node as markup: the element with its children (`outer`), or its children only.
|
|
4285
|
+
*
|
|
4286
|
+
* @param node - A node.
|
|
4287
|
+
* @param outer - Whether to include the node itself.
|
|
4288
|
+
* @returns The markup.
|
|
4289
|
+
*/
|
|
4290
|
+
function serializeXml(node, outer) {
|
|
4291
|
+
const serializer = new XMLSerializer();
|
|
4292
|
+
if (outer) return serializer.serializeToString(node);
|
|
4293
|
+
return Array.from(node.childNodes, child => serializer.serializeToString(child)).join('');
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* The text of an XML body, gunzipped first when it is gzip (a `sitemap.xml.gz`).
|
|
4297
|
+
*
|
|
4298
|
+
* @param bytes - The body.
|
|
4299
|
+
* @returns The bytes to decode.
|
|
4300
|
+
*/
|
|
4301
|
+
function gunzipIfNeeded(bytes) {
|
|
4302
|
+
return bytes.length > 2 && bytes[0] === 0x1F && bytes[1] === 0x8B ? gunzipSync(bytes) : bytes;
|
|
4303
|
+
}
|
|
4304
|
+
function remember(text, document) {
|
|
4305
|
+
if (parsed.size >= PARSED_LIMIT) parsed.delete(parsed.keys().next().value);
|
|
4306
|
+
parsed.set(text, document);
|
|
4307
|
+
}
|
|
4308
|
+
function failOnFatal(level, message) {
|
|
4309
|
+
if (level === 'fatalError') throw new Error(message);
|
|
4310
|
+
}
|
|
4311
|
+
function ignore() {}
|
|
4312
|
+
function firstLine(error) {
|
|
4313
|
+
return (error instanceof Error ? error.message : String(error)).split('\n', 1)[0];
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
/**
|
|
4317
|
+
* Runs an XPath 1.0 query on a parsed document. Prefixes the root element
|
|
4318
|
+
* declares are known without being listed; a default namespace has no
|
|
4319
|
+
* prefix, so name one in `namespaces` or use `ignoreNamespaces`.
|
|
4320
|
+
*
|
|
4321
|
+
* @param document - The document.
|
|
4322
|
+
* @param expression - The query.
|
|
4323
|
+
* @param options - Namespaces.
|
|
4324
|
+
* @returns Every node selected, or the one value a function returned.
|
|
4325
|
+
* @throws Error naming the query when it does not parse or uses an unknown prefix.
|
|
4326
|
+
*/
|
|
4327
|
+
function selectXpath(document, expression, options = {}) {
|
|
4328
|
+
const target = options.ignoreNamespaces === true ? withoutNamespaces(document) : document;
|
|
4329
|
+
const namespaces = {
|
|
4330
|
+
...declaredPrefixes(target),
|
|
4331
|
+
...options.namespaces
|
|
4332
|
+
};
|
|
4333
|
+
let result;
|
|
4334
|
+
try {
|
|
4335
|
+
result = useNamespaces(namespaces)(expression, target);
|
|
4336
|
+
} catch (error) {
|
|
4337
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4338
|
+
const hint = /prefix|namespace|QName/i.test(message) ? ' (declare it in "namespaces", or set "ignoreNamespaces": true)' : '';
|
|
4339
|
+
throw new Error(`xpath ${expression}: ${message}${hint}`, {
|
|
4340
|
+
cause: error
|
|
4341
|
+
});
|
|
4342
|
+
}
|
|
4343
|
+
if (Array.isArray(result)) return result;
|
|
4344
|
+
if (result === null) return [];
|
|
4345
|
+
return [result];
|
|
4346
|
+
}
|
|
4347
|
+
/**
|
|
4348
|
+
* The value of an XPath result.
|
|
4349
|
+
*
|
|
4350
|
+
* @param value - A node or a function result.
|
|
4351
|
+
* @param take - `text` (whitespace collapsed), `html` (the node's inner markup), `json` (its outer markup), `value` (its text as is), `attr:<name>`.
|
|
4352
|
+
* @returns The value; `undefined` for a missing attribute.
|
|
4353
|
+
*/
|
|
4354
|
+
function takeFromXml(value, take) {
|
|
4355
|
+
if (typeof value !== 'object') return take === 'text' ? collapse(String(value)) : value;
|
|
4356
|
+
if (take === 'text') return collapse(value.textContent ?? '');
|
|
4357
|
+
if (take === 'value') return value.textContent ?? '';
|
|
4358
|
+
if (take === 'html') return serializeXml(value, false);
|
|
4359
|
+
if (take === 'json') return serializeXml(value, true);
|
|
4360
|
+
if (value.nodeType !== value.ELEMENT_NODE) return undefined;
|
|
4361
|
+
const element = value;
|
|
4362
|
+
const name = take.slice('attr:'.length);
|
|
4363
|
+
return element.hasAttribute(name) ? element.getAttribute(name) : undefined;
|
|
4364
|
+
}
|
|
4365
|
+
function declaredPrefixes(document) {
|
|
4366
|
+
const root = document.documentElement;
|
|
4367
|
+
if (root === null) return {};
|
|
4368
|
+
return Object.fromEntries([...root.attributes].filter(attribute => attribute.prefix === 'xmlns').map(attribute => [attribute.localName, attribute.value]));
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4184
4371
|
/**
|
|
4185
4372
|
* Runs an `extract` step against a static document: the value bound under
|
|
4186
|
-
* `from`, else the scope's current document. `css` reads HTML,
|
|
4187
|
-
* reads
|
|
4188
|
-
*
|
|
4189
|
-
*
|
|
4190
|
-
*
|
|
4373
|
+
* `from`, else the scope's current document. `css` reads HTML (or XML),
|
|
4374
|
+
* `xpath` reads XML (a feed, a sitemap) or HTML parsed as a browser parses
|
|
4375
|
+
* it, `jsonpath` reads JSON (or a read PDF, workbook or deck as data), `table`
|
|
4376
|
+
* reads the tables of a PDF, a workbook (a spreadsheet, a CSV), a deck (a
|
|
4377
|
+
* presentation) or HTML (its `<table>`s), `regex` reads any document as text.
|
|
4191
4378
|
*
|
|
4192
4379
|
* A `jsonpath` extract whose `from` is text parses that text as JSON, and a
|
|
4193
4380
|
* list of texts (every `<script type="application/ld+json">` of a page) becomes
|
|
@@ -4206,6 +4393,7 @@ function extractFromDocument(step, scope) {
|
|
|
4206
4393
|
switch (step.kind) {
|
|
4207
4394
|
case 'jsonpath':
|
|
4208
4395
|
{
|
|
4396
|
+
if (document.kind === 'xml') throw new Error('jsonpath needs a JSON document; the current document is xml (read it with kind "xpath")');
|
|
4209
4397
|
if (document.kind === 'html' || document.kind === 'text') throw new Error(`jsonpath needs a JSON document; the current document is ${document.kind}`);
|
|
4210
4398
|
values = selectJson(document.kind === 'json' ? document.data : document, selector).map(node => takeFromJson(node, take));
|
|
4211
4399
|
break;
|
|
@@ -4217,10 +4405,22 @@ function extractFromDocument(step, scope) {
|
|
|
4217
4405
|
}
|
|
4218
4406
|
case 'css':
|
|
4219
4407
|
{
|
|
4408
|
+
if (document.kind === 'xml') {
|
|
4409
|
+
values = selectHtml(document.xml, selector, true).map(match => takeFromHtml(match, take));
|
|
4410
|
+
break;
|
|
4411
|
+
}
|
|
4220
4412
|
if (document.kind !== 'html') throw new Error(`css needs an HTML document; the current document is ${document.kind}${['workbook', 'pdf', 'deck'].includes(document.kind) ? ' (read it with kind "table", "regex" or "jsonpath")' : ''}`);
|
|
4221
4413
|
values = selectHtml(document.html, selector).map(match => takeFromHtml(match, take));
|
|
4222
4414
|
break;
|
|
4223
4415
|
}
|
|
4416
|
+
case 'xpath':
|
|
4417
|
+
{
|
|
4418
|
+
values = selectXpath(xpathDocument(document), selector, {
|
|
4419
|
+
namespaces: step.namespaces,
|
|
4420
|
+
ignoreNamespaces: step.ignoreNamespaces
|
|
4421
|
+
}).map(node => takeFromXml(node, take));
|
|
4422
|
+
break;
|
|
4423
|
+
}
|
|
4224
4424
|
case 'regex':
|
|
4225
4425
|
{
|
|
4226
4426
|
values = selectRegex(textOf$1(document), selector);
|
|
@@ -4228,7 +4428,7 @@ function extractFromDocument(step, scope) {
|
|
|
4228
4428
|
}
|
|
4229
4429
|
default:
|
|
4230
4430
|
{
|
|
4231
|
-
throw new Error(
|
|
4431
|
+
throw new Error(`unknown extract kind ${String(step.kind)}`);
|
|
4232
4432
|
}
|
|
4233
4433
|
}
|
|
4234
4434
|
if (step.many === true) {
|
|
@@ -4334,9 +4534,32 @@ function patternOf(source, where) {
|
|
|
4334
4534
|
});
|
|
4335
4535
|
}
|
|
4336
4536
|
}
|
|
4537
|
+
/** What an xpath extract queries: XML as it is, HTML parsed the way a browser would. */
|
|
4538
|
+
function xpathDocument(document) {
|
|
4539
|
+
if (document.kind === 'xml') return parseXml(document.xml, 'the document');
|
|
4540
|
+
if (document.kind === 'html') return htmlAsXml(document.html);
|
|
4541
|
+
if (document.kind === 'text') return parseXml(document.text, 'the document');
|
|
4542
|
+
throw new Error(`xpath reads XML or HTML; the current document is ${document.kind} (read it with kind ${document.kind === 'json' ? '"jsonpath"' : '"table", "regex" or "jsonpath"'})`);
|
|
4543
|
+
}
|
|
4544
|
+
/** Markup bound to an id: XML when it is well-formed (a feed entry taken with `take: "json"`), else HTML. */
|
|
4545
|
+
function markupDocument(markup) {
|
|
4546
|
+
try {
|
|
4547
|
+
parseXml(markup, 'markup');
|
|
4548
|
+
return {
|
|
4549
|
+
kind: 'xml',
|
|
4550
|
+
xml: markup
|
|
4551
|
+
};
|
|
4552
|
+
} catch {
|
|
4553
|
+
return {
|
|
4554
|
+
kind: 'html',
|
|
4555
|
+
html: markup
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4337
4559
|
/** The text a regex extract reads: markup, text, a PDF's or a workbook's rows, or JSON re-serialised (a list of texts joined by newlines). */
|
|
4338
4560
|
function textOf$1(document) {
|
|
4339
4561
|
if (document.kind === 'html') return document.html;
|
|
4562
|
+
if (document.kind === 'xml') return document.xml;
|
|
4340
4563
|
if (document.kind === 'text') return document.text;
|
|
4341
4564
|
if (document.kind === 'pdf') return pdfText(document);
|
|
4342
4565
|
if (document.kind === 'workbook') return workbookText(document);
|
|
@@ -4352,7 +4575,11 @@ function documentFor(step, scope) {
|
|
|
4352
4575
|
}
|
|
4353
4576
|
const source = scope.get(step.from);
|
|
4354
4577
|
if (source === undefined) throw new Error(`"${step.from}" is not bound`);
|
|
4355
|
-
if (isPdfDocument(source) || isWorkbookDocument(source) || isDeckDocument(source)) return source;
|
|
4578
|
+
if (isPdfDocument(source) || isWorkbookDocument(source) || isDeckDocument(source) || isXmlDocument(source)) return source;
|
|
4579
|
+
if (step.kind === 'xpath') {
|
|
4580
|
+
if (typeof source !== 'string') throw new Error(`"${step.from}" is not markup; xpath reads XML or HTML text`);
|
|
4581
|
+
return markupDocument(source);
|
|
4582
|
+
}
|
|
4356
4583
|
if (step.kind === 'table') throw new Error(`"${step.from}" is not a PDF, a workbook or a deck; request it with "as": "pdf", "csv", "xlsx" or "pptx"`);
|
|
4357
4584
|
if (step.kind === 'regex') {
|
|
4358
4585
|
if (typeof source === 'string') return {
|
|
@@ -4441,6 +4668,12 @@ async function readYaml(text, source, scalars = 'typed') {
|
|
|
4441
4668
|
};
|
|
4442
4669
|
}
|
|
4443
4670
|
|
|
4671
|
+
/**
|
|
4672
|
+
* `<` as the JSON escape `\u003c`, so a front matter value holding `</script>`
|
|
4673
|
+
* cannot close its element. Built from char codes: a formatter would turn a
|
|
4674
|
+
* literal escape back into `<`.
|
|
4675
|
+
*/
|
|
4676
|
+
const ESCAPED_LESS_THAN = String.fromCodePoint(92, 117, 48, 48, 51, 99);
|
|
4444
4677
|
/** A leading `---` block of YAML. */
|
|
4445
4678
|
const FRONT_MATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
4446
4679
|
/**
|
|
@@ -4474,14 +4707,22 @@ async function readMarkdown(text, source) {
|
|
|
4474
4707
|
async: false
|
|
4475
4708
|
});
|
|
4476
4709
|
const sections = sectioned(rendered);
|
|
4477
|
-
const head = front === undefined ? '' : `<script type="application/json" data-front-matter>${JSON.stringify(front.data ?? null).replaceAll('<',
|
|
4710
|
+
const head = front === undefined ? '' : `<script type="application/json" data-front-matter>${JSON.stringify(front.data ?? null).replaceAll('<', () => ESCAPED_LESS_THAN)}</script>`;
|
|
4478
4711
|
return {
|
|
4479
4712
|
html: `<!doctype html><html><head>${head}</head><body>${sections}</body></html>`,
|
|
4480
4713
|
frontMatter: front?.data,
|
|
4481
4714
|
warnings: front?.warnings ?? []
|
|
4482
4715
|
};
|
|
4483
4716
|
}
|
|
4484
|
-
/**
|
|
4717
|
+
/**
|
|
4718
|
+
* Wraps each heading and what follows it, up to the next heading of the same
|
|
4719
|
+
* or a higher level, in `<section data-heading="…" data-level="…">`, sections
|
|
4720
|
+
* nesting; headings get slug ids. Rendered Markdown and Word documents both go
|
|
4721
|
+
* through it, so one selector finds "the table under *Prezzi*" in either.
|
|
4722
|
+
*
|
|
4723
|
+
* @param html - Top-level HTML: headings among paragraphs, lists, tables.
|
|
4724
|
+
* @returns The same content, sectioned.
|
|
4725
|
+
*/
|
|
4485
4726
|
function sectioned(html) {
|
|
4486
4727
|
const $ = load(html, null, false);
|
|
4487
4728
|
const open = [];
|
|
@@ -4502,7 +4743,7 @@ function sectioned(html) {
|
|
|
4502
4743
|
const heading = $(node);
|
|
4503
4744
|
const text = heading.text().replaceAll(/\s+/g, ' ').trim();
|
|
4504
4745
|
heading.attr('id', uniqueSlug(text, used));
|
|
4505
|
-
out += `<section data-heading="${escapeAttribute(text)}" data-level="${depth}">${$.html(node)}`;
|
|
4746
|
+
out += `<section data-heading="${escapeAttribute$1(text)}" data-level="${depth}">${$.html(node)}`;
|
|
4506
4747
|
open.push(depth);
|
|
4507
4748
|
}
|
|
4508
4749
|
return out + '</section>'.repeat(open.length);
|
|
@@ -4514,10 +4755,165 @@ function uniqueSlug(text, used) {
|
|
|
4514
4755
|
used.set(slug, count + 1);
|
|
4515
4756
|
return count === 0 ? slug : `${slug}-${count}`;
|
|
4516
4757
|
}
|
|
4517
|
-
function escapeAttribute(text) {
|
|
4758
|
+
function escapeAttribute$1(text) {
|
|
4518
4759
|
return text.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<');
|
|
4519
4760
|
}
|
|
4520
4761
|
|
|
4762
|
+
/**
|
|
4763
|
+
* Reads a `.docx` Word document into HTML, through `@opencraw/office-reader`,
|
|
4764
|
+
* so every selector that reads a page reads a Word document too:
|
|
4765
|
+
*
|
|
4766
|
+
* - headings become `<h1>`…`<h6>` inside `<section data-heading="…">`, as in
|
|
4767
|
+
* rendered Markdown;
|
|
4768
|
+
* - list items become nested `<ul>` / `<ol>`;
|
|
4769
|
+
* - tables become `<table>`s with their merged cells as `colspan` / `rowspan`,
|
|
4770
|
+
* so a `table` extract reads them like any HTML table;
|
|
4771
|
+
* - a paragraph keeps its style as `data-style` and its links as `<a href>`;
|
|
4772
|
+
* - headers, footers and notes follow the body, in `<header>`, `<footer>`
|
|
4773
|
+
* and `<aside data-part="notes">`; the title goes to `<title>`.
|
|
4774
|
+
*
|
|
4775
|
+
* The reader is imported on first use.
|
|
4776
|
+
*
|
|
4777
|
+
* @param bytes - The file.
|
|
4778
|
+
* @param source - Where it came from, for messages.
|
|
4779
|
+
* @returns The HTML document.
|
|
4780
|
+
* @throws Error naming the source, and saying what to do, for a file that is
|
|
4781
|
+
* not a readable Word document (a legacy `.doc`, a password-protected file, an `.odt`…).
|
|
4782
|
+
*/
|
|
4783
|
+
async function readDocxHtml(bytes, source) {
|
|
4784
|
+
const {
|
|
4785
|
+
readDocx,
|
|
4786
|
+
OfficeReadError
|
|
4787
|
+
} = await import('@opencraw/office-reader/docx');
|
|
4788
|
+
let document;
|
|
4789
|
+
try {
|
|
4790
|
+
document = await readDocx(bytes);
|
|
4791
|
+
} catch (error) {
|
|
4792
|
+
if (error instanceof OfficeReadError) throw new Error(`${source}: ${error.message}`, {
|
|
4793
|
+
cause: error
|
|
4794
|
+
});
|
|
4795
|
+
throw error;
|
|
4796
|
+
}
|
|
4797
|
+
const head = document.title === undefined ? '' : `<title>${escapeText(document.title)}</title>`;
|
|
4798
|
+
const headers = document.headers.map(blocks => `<header data-part="header">${blocksHtml(blocks)}</header>`).join('');
|
|
4799
|
+
const footers = document.footers.map(blocks => `<footer data-part="footer">${blocksHtml(blocks)}</footer>`).join('');
|
|
4800
|
+
const notes = document.notes.length === 0 ? '' : `<aside data-part="notes"><ol>${document.notes.map(note => `<li id="${note.kind}-${escapeAttribute(note.id)}" data-kind="${note.kind}">${textHtml(note.text)}</li>`).join('')}</ol></aside>`;
|
|
4801
|
+
return `<!doctype html><html><head>${head}</head><body>${headers}${sectioned(blocksHtml(document.body))}${footers}${notes}</body></html>`;
|
|
4802
|
+
}
|
|
4803
|
+
/** Blocks as HTML: headings, paragraphs, nested lists, tables. */
|
|
4804
|
+
function blocksHtml(blocks) {
|
|
4805
|
+
let html = '';
|
|
4806
|
+
const lists = [];
|
|
4807
|
+
const closeLists = depth => {
|
|
4808
|
+
while (lists.length > depth) {
|
|
4809
|
+
const list = lists.pop();
|
|
4810
|
+
html += `${list?.itemOpen === true ? '</li>' : ''}</${list?.tag ?? 'ul'}>`;
|
|
4811
|
+
}
|
|
4812
|
+
};
|
|
4813
|
+
for (const block of blocks) {
|
|
4814
|
+
if (block.kind === 'table') {
|
|
4815
|
+
closeLists(0);
|
|
4816
|
+
html += tableHtml(block);
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4819
|
+
if (block.list === undefined) {
|
|
4820
|
+
closeLists(0);
|
|
4821
|
+
html += paragraphHtml(block);
|
|
4822
|
+
continue;
|
|
4823
|
+
}
|
|
4824
|
+
const tag = block.list.ordered ? 'ol' : 'ul';
|
|
4825
|
+
const depth = block.list.level + 1;
|
|
4826
|
+
closeLists(depth);
|
|
4827
|
+
if (lists.length === depth && lists.at(-1)?.tag !== tag) closeLists(depth - 1);
|
|
4828
|
+
while (lists.length < depth) {
|
|
4829
|
+
html += `<${tag}>`;
|
|
4830
|
+
lists.push({
|
|
4831
|
+
tag,
|
|
4832
|
+
itemOpen: false
|
|
4833
|
+
});
|
|
4834
|
+
}
|
|
4835
|
+
const list = lists[depth - 1];
|
|
4836
|
+
html += `${list.itemOpen ? '</li>' : ''}<li${styleAttribute(block)}>${inlineHtml(block)}`;
|
|
4837
|
+
list.itemOpen = true;
|
|
4838
|
+
}
|
|
4839
|
+
closeLists(0);
|
|
4840
|
+
return html;
|
|
4841
|
+
}
|
|
4842
|
+
function paragraphHtml(paragraph) {
|
|
4843
|
+
if (paragraph.heading !== undefined) {
|
|
4844
|
+
const level = Math.min(paragraph.heading, 6);
|
|
4845
|
+
return `<h${level}${styleAttribute(paragraph)}>${inlineHtml(paragraph)}</h${level}>`;
|
|
4846
|
+
}
|
|
4847
|
+
return `<p${styleAttribute(paragraph)}>${inlineHtml(paragraph)}</p>`;
|
|
4848
|
+
}
|
|
4849
|
+
/** A paragraph's text with its links as anchors, in the order they appear. */
|
|
4850
|
+
function inlineHtml(paragraph) {
|
|
4851
|
+
let html = '';
|
|
4852
|
+
let rest = paragraph.text;
|
|
4853
|
+
const links = paragraph.links ?? [];
|
|
4854
|
+
for (const link of links) {
|
|
4855
|
+
const at = rest.indexOf(link.text);
|
|
4856
|
+
if (at === -1) continue;
|
|
4857
|
+
html += `${textHtml(rest.slice(0, at))}<a href="${escapeAttribute(link.href)}">${textHtml(link.text)}</a>`;
|
|
4858
|
+
rest = rest.slice(at + link.text.length);
|
|
4859
|
+
}
|
|
4860
|
+
return html + textHtml(rest);
|
|
4861
|
+
}
|
|
4862
|
+
/** A grid as a table: the top-left cell of a merged range spans it, the cells it covers are left out. */
|
|
4863
|
+
function tableHtml(table) {
|
|
4864
|
+
const spans = new Map();
|
|
4865
|
+
const covered = new Set();
|
|
4866
|
+
for (const range of table.merges) {
|
|
4867
|
+
const [from, to] = range.split(':').map(reference => cellOf(reference));
|
|
4868
|
+
if (from === undefined || to === undefined) continue;
|
|
4869
|
+
spans.set(`${from.row},${from.column}`, {
|
|
4870
|
+
rows: to.row - from.row + 1,
|
|
4871
|
+
columns: to.column - from.column + 1
|
|
4872
|
+
});
|
|
4873
|
+
for (let row = from.row; row <= to.row; row += 1) {
|
|
4874
|
+
for (let column = from.column; column <= to.column; column += 1) {
|
|
4875
|
+
if (row !== from.row || column !== from.column) covered.add(`${row},${column}`);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
const rows = table.rows.map((cells, row) => {
|
|
4880
|
+
const html = cells.map((text, column) => {
|
|
4881
|
+
const key = `${row},${column}`;
|
|
4882
|
+
if (covered.has(key)) return '';
|
|
4883
|
+
const span = spans.get(key);
|
|
4884
|
+
const attributes = span === undefined ? '' : `${span.rows > 1 ? ` rowspan="${span.rows}"` : ''}${span.columns > 1 ? ` colspan="${span.columns}"` : ''}`;
|
|
4885
|
+
return `<td${attributes}>${textHtml(text)}</td>`;
|
|
4886
|
+
}).join('');
|
|
4887
|
+
return `<tr>${html}</tr>`;
|
|
4888
|
+
}).join('');
|
|
4889
|
+
return `<table data-name="${escapeAttribute(table.name)}">${rows}</table>`;
|
|
4890
|
+
}
|
|
4891
|
+
/** An A1 reference as 0-based row and column. */
|
|
4892
|
+
function cellOf(reference) {
|
|
4893
|
+
const match = /^([A-Z]+)(\d+)$/.exec(reference);
|
|
4894
|
+
if (match === null) return undefined;
|
|
4895
|
+
let column = 0;
|
|
4896
|
+
const letters = match[1];
|
|
4897
|
+
for (const letter of letters) column = column * 26 + (letter.codePointAt(0) ?? 64) - 64;
|
|
4898
|
+
return {
|
|
4899
|
+
row: Number(match[2]) - 1,
|
|
4900
|
+
column: column - 1
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
function styleAttribute(paragraph) {
|
|
4904
|
+
return paragraph.style === undefined ? '' : ` data-style="${escapeAttribute(paragraph.style)}"`;
|
|
4905
|
+
}
|
|
4906
|
+
/** Text as HTML: escaped, line breaks as `<br>`. */
|
|
4907
|
+
function textHtml(text) {
|
|
4908
|
+
return escapeText(text).replaceAll('\n', '<br>');
|
|
4909
|
+
}
|
|
4910
|
+
function escapeText(text) {
|
|
4911
|
+
return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
4912
|
+
}
|
|
4913
|
+
function escapeAttribute(text) {
|
|
4914
|
+
return escapeText(text).replaceAll('"', '"');
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4521
4917
|
/** A response with a 4xx or 5xx status. */
|
|
4522
4918
|
class HttpError extends Error {
|
|
4523
4919
|
status;
|
|
@@ -4677,7 +5073,7 @@ class HttpClient {
|
|
|
4677
5073
|
}
|
|
4678
5074
|
async function readBody(response, httpRequest) {
|
|
4679
5075
|
const contentType = response.headers()['content-type'] ?? '';
|
|
4680
|
-
const format = httpRequest.as ?? formatFromContentType(contentType);
|
|
5076
|
+
const format = httpRequest.as ?? formatFromContentType(contentType, response.url());
|
|
4681
5077
|
return parseBody(format, await response.body(), response.url(), {
|
|
4682
5078
|
...httpRequest,
|
|
4683
5079
|
charset: charsetOf(contentType)
|
|
@@ -4695,7 +5091,7 @@ async function readLocalFile(httpRequest) {
|
|
|
4695
5091
|
body,
|
|
4696
5092
|
warnings,
|
|
4697
5093
|
format
|
|
4698
|
-
} = await parseBody(httpRequest.as ?? formatFromExtension(
|
|
5094
|
+
} = await parseBody(httpRequest.as ?? formatFromExtension(path), bytes, httpRequest.url, httpRequest);
|
|
4699
5095
|
return {
|
|
4700
5096
|
status: 200,
|
|
4701
5097
|
url: httpRequest.url,
|
|
@@ -4725,6 +5121,35 @@ async function parseBody(format, bytes, url, reading) {
|
|
|
4725
5121
|
format
|
|
4726
5122
|
};
|
|
4727
5123
|
}
|
|
5124
|
+
if (format === 'xml') {
|
|
5125
|
+
// A sitemap is often served gzipped as a file (`sitemap.xml.gz`), not as a compressed response.
|
|
5126
|
+
const {
|
|
5127
|
+
text
|
|
5128
|
+
} = decodeText(gunzipIfNeeded(bytes), reading);
|
|
5129
|
+
try {
|
|
5130
|
+
parseXml(text, url);
|
|
5131
|
+
} catch (error) {
|
|
5132
|
+
throw new Error(`${error.message}${/^\s*(?:<!doctype html|<html)/i.test(text) ? '; the body is HTML: read it with "as": "html"' : ''}`, {
|
|
5133
|
+
cause: error
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
return {
|
|
5137
|
+
body: {
|
|
5138
|
+
kind: 'xml',
|
|
5139
|
+
xml: text
|
|
5140
|
+
},
|
|
5141
|
+
warnings: [],
|
|
5142
|
+
format
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
if (format === 'docx') return {
|
|
5146
|
+
body: {
|
|
5147
|
+
kind: 'html',
|
|
5148
|
+
html: await readDocxHtml(bytes, url)
|
|
5149
|
+
},
|
|
5150
|
+
warnings: [],
|
|
5151
|
+
format
|
|
5152
|
+
};
|
|
4728
5153
|
if (format === 'markdown') {
|
|
4729
5154
|
const {
|
|
4730
5155
|
text
|
|
@@ -4790,24 +5215,31 @@ function looksLikeJsonLines(text) {
|
|
|
4790
5215
|
const first = parseJsonLike(lines[0]);
|
|
4791
5216
|
return 'value' in first;
|
|
4792
5217
|
}
|
|
4793
|
-
function formatFromContentType(contentType) {
|
|
5218
|
+
function formatFromContentType(contentType, url) {
|
|
4794
5219
|
const type = contentType.toLowerCase().split(';', 1)[0].trim();
|
|
5220
|
+
// A gzipped sitemap is served as an archive; its name says what is inside.
|
|
5221
|
+
if (GZIP_TYPES.has(type) && /\.xml\.gz$/i.test(new URL(url).pathname)) return 'xml';
|
|
4795
5222
|
if (CSV_TYPES.has(type)) return 'csv';
|
|
4796
5223
|
if (JSON_LINES_TYPES.has(type)) return 'jsonl';
|
|
4797
5224
|
// A legacy .xls or .ppt goes to the Office reader too, which says what to do with it.
|
|
4798
5225
|
if (type.includes('spreadsheetml') || type.startsWith('application/vnd.ms-excel')) return 'xlsx';
|
|
4799
5226
|
if (type.includes('presentationml') || type.startsWith('application/vnd.ms-powerpoint')) return 'pptx';
|
|
5227
|
+
if (type === 'application/msword' || type.includes('wordprocessingml') || type.startsWith('application/vnd.ms-word')) return 'docx';
|
|
4800
5228
|
if (YAML_TYPES.has(type)) return 'yaml';
|
|
4801
5229
|
if (type === 'text/markdown' || type === 'text/x-markdown') return 'markdown';
|
|
4802
5230
|
if (type.includes('json')) return 'json';
|
|
4803
5231
|
if (type.includes('pdf')) return 'pdf';
|
|
4804
|
-
if (type.includes('html')
|
|
5232
|
+
if (type.includes('html')) return 'html';
|
|
5233
|
+
if (type.includes('xml')) return 'xml';
|
|
4805
5234
|
return 'text';
|
|
4806
5235
|
}
|
|
4807
5236
|
const JSON_LINES_TYPES = new Set(['application/x-ndjson', 'application/ndjson', 'application/jsonl', 'application/x-jsonlines', 'application/jsonlines']);
|
|
4808
5237
|
const YAML_TYPES = new Set(['application/yaml', 'application/x-yaml', 'text/yaml', 'text/x-yaml']);
|
|
5238
|
+
const GZIP_TYPES = new Set(['application/gzip', 'application/x-gzip', 'application/octet-stream']);
|
|
4809
5239
|
const CSV_TYPES = new Set(['text/csv', 'application/csv', 'text/x-csv', 'application/x-csv', 'text/comma-separated-values', 'text/tab-separated-values']);
|
|
4810
|
-
function formatFromExtension(
|
|
5240
|
+
function formatFromExtension(path) {
|
|
5241
|
+
if (/\.xml\.gz$/i.test(path)) return 'xml';
|
|
5242
|
+
const extension = extname(path);
|
|
4811
5243
|
const formats = {
|
|
4812
5244
|
'.json': 'json',
|
|
4813
5245
|
'.jsonl': 'jsonl',
|
|
@@ -4822,13 +5254,21 @@ function formatFromExtension(extension) {
|
|
|
4822
5254
|
'.pptm': 'pptx',
|
|
4823
5255
|
'.ppsx': 'pptx',
|
|
4824
5256
|
'.ppt': 'pptx',
|
|
5257
|
+
'.docx': 'docx',
|
|
5258
|
+
'.docm': 'docx',
|
|
5259
|
+
'.dotx': 'docx',
|
|
5260
|
+
'.doc': 'docx',
|
|
4825
5261
|
'.yaml': 'yaml',
|
|
4826
5262
|
'.yml': 'yaml',
|
|
4827
5263
|
'.md': 'markdown',
|
|
4828
5264
|
'.markdown': 'markdown',
|
|
4829
5265
|
'.html': 'html',
|
|
4830
5266
|
'.htm': 'html',
|
|
4831
|
-
'.xml': '
|
|
5267
|
+
'.xml': 'xml',
|
|
5268
|
+
'.rss': 'xml',
|
|
5269
|
+
'.atom': 'xml',
|
|
5270
|
+
'.kml': 'xml',
|
|
5271
|
+
'.gpx': 'xml'
|
|
4832
5272
|
};
|
|
4833
5273
|
return formats[extension.toLowerCase()] ?? 'text';
|
|
4834
5274
|
}
|
|
@@ -4937,6 +5377,7 @@ function bodyText(body) {
|
|
|
4937
5377
|
if (body.kind === 'pdf') return pdfText(body);
|
|
4938
5378
|
if (body.kind === 'workbook') return workbookText(body);
|
|
4939
5379
|
if (body.kind === 'deck') return deckText(body);
|
|
5380
|
+
if (body.kind === 'xml') return body.xml;
|
|
4940
5381
|
return body.kind === 'html' ? body.html : body.text;
|
|
4941
5382
|
}
|
|
4942
5383
|
function renderMap(map, lookup) {
|
|
@@ -7189,7 +7630,7 @@ const CRAWL_MODES = ['web', 'api'];
|
|
|
7189
7630
|
const SELECTOR_KINDS = ['css', 'xpath', 'jsonpath', 'regex', 'table'];
|
|
7190
7631
|
/** `take` also accepts `attr:<name>`, which is validated by pattern rather than listed. */
|
|
7191
7632
|
const TAKE_KINDS = ['text', 'html', 'value', 'json'];
|
|
7192
|
-
const BODY_KINDS = ['json', 'jsonl', 'html', 'text', 'pdf', 'csv', 'xlsx', 'pptx', 'yaml', 'markdown'];
|
|
7633
|
+
const BODY_KINDS = ['json', 'jsonl', 'html', 'text', 'pdf', 'csv', 'xlsx', 'pptx', 'yaml', 'markdown', 'xml', 'docx'];
|
|
7193
7634
|
const YAML_SCALARS = ['typed', 'text'];
|
|
7194
7635
|
/** How a PDF table aligns a row's values against a cell wrapped over several lines. */
|
|
7195
7636
|
const TABLE_ALIGNS = ['auto', 'top', 'center', 'bottom'];
|
|
@@ -7395,8 +7836,20 @@ const extractStep = z.strictObject({
|
|
|
7395
7836
|
fillDown: z.array(z.string().min(1)).min(1).optional(),
|
|
7396
7837
|
includeHidden: z.boolean().optional(),
|
|
7397
7838
|
slide: z.string().min(1).optional(),
|
|
7398
|
-
shapes: z.boolean().optional()
|
|
7839
|
+
shapes: z.boolean().optional(),
|
|
7840
|
+
namespaces: z.record(z.string().regex(/^[A-Z_][\w.-]*$/i, 'a namespace prefix such as atom'), z.string().min(1)).optional(),
|
|
7841
|
+
ignoreNamespaces: z.boolean().optional()
|
|
7399
7842
|
}).check(context => {
|
|
7843
|
+
if (context.value.kind !== 'xpath') {
|
|
7844
|
+
for (const key of ['namespaces', 'ignoreNamespaces']) {
|
|
7845
|
+
if (context.value[key] !== undefined) context.issues.push({
|
|
7846
|
+
code: 'custom',
|
|
7847
|
+
input: context.value,
|
|
7848
|
+
path: [key],
|
|
7849
|
+
message: `"${key}" belongs to kind "xpath"`
|
|
7850
|
+
});
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7400
7853
|
if (context.value.kind === 'table') return;
|
|
7401
7854
|
for (const key of tableOnly) {
|
|
7402
7855
|
if (context.value[key] !== undefined) context.issues.push({
|
|
@@ -8190,5 +8643,176 @@ function isSameOutput(document, output) {
|
|
|
8190
8643
|
return recipeKindOf(document.content) === 'output' && document.content.id === output.id;
|
|
8191
8644
|
}
|
|
8192
8645
|
|
|
8193
|
-
|
|
8646
|
+
const BOOKKEEPING = new Set(['_source', '_key']);
|
|
8647
|
+
/**
|
|
8648
|
+
* Compares two runs' records by key: what was added, what was removed, and
|
|
8649
|
+
* for the records in both, which fields changed, before and after. Like a
|
|
8650
|
+
* `git diff` of a price list, but row by row instead of line by line, so a
|
|
8651
|
+
* reordered file is not a change.
|
|
8652
|
+
*
|
|
8653
|
+
* @param previous - The earlier run's records.
|
|
8654
|
+
* @param current - The later run's records.
|
|
8655
|
+
* @param options - The key fields, the fields to ignore, the shrink threshold.
|
|
8656
|
+
* @returns The differences.
|
|
8657
|
+
* @throws Error when records carry no key: without `key`, every line needs `_key`.
|
|
8658
|
+
*/
|
|
8659
|
+
function diffRecords(previous, current, options = {}) {
|
|
8660
|
+
const ignore = new Set([...BOOKKEEPING, ...(options.ignore ?? [])]);
|
|
8661
|
+
const keyOf = keyReader(options.key);
|
|
8662
|
+
let repeated = 0;
|
|
8663
|
+
const index = (records, side) => {
|
|
8664
|
+
const byKey = new Map();
|
|
8665
|
+
for (const [line, record] of records.entries()) {
|
|
8666
|
+
const key = keyOf(record, `${side} record ${line + 1}`);
|
|
8667
|
+
if (byKey.has(key)) repeated += 1;else byKey.set(key, record);
|
|
8668
|
+
}
|
|
8669
|
+
return byKey;
|
|
8670
|
+
};
|
|
8671
|
+
const before = index(previous, 'previous');
|
|
8672
|
+
const after = index(current, 'current');
|
|
8673
|
+
const removed = [];
|
|
8674
|
+
const changed = [];
|
|
8675
|
+
const added = [];
|
|
8676
|
+
let unchanged = 0;
|
|
8677
|
+
for (const [key, record] of before) {
|
|
8678
|
+
const now = after.get(key);
|
|
8679
|
+
if (now === undefined) {
|
|
8680
|
+
removed.push({
|
|
8681
|
+
change: 'removed',
|
|
8682
|
+
key,
|
|
8683
|
+
before: record
|
|
8684
|
+
});
|
|
8685
|
+
continue;
|
|
8686
|
+
}
|
|
8687
|
+
const fields = fieldChanges(record, now, ignore, '');
|
|
8688
|
+
if (fields.length === 0) unchanged += 1;else changed.push({
|
|
8689
|
+
change: 'changed',
|
|
8690
|
+
key,
|
|
8691
|
+
before: record,
|
|
8692
|
+
after: now,
|
|
8693
|
+
fields
|
|
8694
|
+
});
|
|
8695
|
+
}
|
|
8696
|
+
for (const [key, record] of after) {
|
|
8697
|
+
if (!before.has(key)) added.push({
|
|
8698
|
+
change: 'added',
|
|
8699
|
+
key,
|
|
8700
|
+
after: record
|
|
8701
|
+
});
|
|
8702
|
+
}
|
|
8703
|
+
const threshold = options.shrink ?? 0.5;
|
|
8704
|
+
const shrunk = previous.length > 0 && current.length < previous.length * (1 - threshold);
|
|
8705
|
+
return {
|
|
8706
|
+
added: added.length,
|
|
8707
|
+
removed: removed.length,
|
|
8708
|
+
changed: changed.length,
|
|
8709
|
+
unchanged,
|
|
8710
|
+
changes: [...removed, ...changed, ...added],
|
|
8711
|
+
repeated,
|
|
8712
|
+
counts: {
|
|
8713
|
+
previous: previous.length,
|
|
8714
|
+
current: current.length
|
|
8715
|
+
},
|
|
8716
|
+
...(shrunk && {
|
|
8717
|
+
shrunk: {
|
|
8718
|
+
previous: previous.length,
|
|
8719
|
+
current: current.length
|
|
8720
|
+
}
|
|
8721
|
+
})
|
|
8722
|
+
};
|
|
8723
|
+
}
|
|
8724
|
+
/**
|
|
8725
|
+
* The diff options an output recipe implies: its key fields, and its fields
|
|
8726
|
+
* the engine fills differently every run (`generated: now`, `uuid`).
|
|
8727
|
+
*
|
|
8728
|
+
* @param output - The output recipe.
|
|
8729
|
+
* @returns The key and the fields to ignore.
|
|
8730
|
+
*/
|
|
8731
|
+
function diffOptionsFor(output) {
|
|
8732
|
+
const fields = Object.entries(output.fields);
|
|
8733
|
+
return {
|
|
8734
|
+
key: fields.filter(([, field]) => field.key === true).map(([name]) => name),
|
|
8735
|
+
ignore: fields.filter(([, field]) => field.generated === 'now' || field.generated === 'uuid').map(([name]) => name)
|
|
8736
|
+
};
|
|
8737
|
+
}
|
|
8738
|
+
/**
|
|
8739
|
+
* A key as people read it: the key fields' values joined (`Pandina · 1.0 Hybrid`).
|
|
8740
|
+
*
|
|
8741
|
+
* @param key - A record key (a JSON array of the key values).
|
|
8742
|
+
* @returns The readable form.
|
|
8743
|
+
*/
|
|
8744
|
+
function readableKey(key) {
|
|
8745
|
+
try {
|
|
8746
|
+
const values = JSON.parse(key);
|
|
8747
|
+
return Array.isArray(values) ? values.map(value => typeof value === 'string' ? value : JSON.stringify(value)).join(' · ') : key;
|
|
8748
|
+
} catch {
|
|
8749
|
+
return key;
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
function keyReader(fields) {
|
|
8753
|
+
if (fields !== undefined && fields.length > 0) return record => JSON.stringify(fields.map(field => record[field] ?? null));
|
|
8754
|
+
return storedKey;
|
|
8755
|
+
}
|
|
8756
|
+
/** The `_key` a sink in append mode wrote on the line. */
|
|
8757
|
+
function storedKey(record, where) {
|
|
8758
|
+
const key = record._key;
|
|
8759
|
+
if (typeof key !== 'string') throw new Error(`${where} has no _key: name the fields that identify a record (--key model,version, or the output recipe's key fields)`);
|
|
8760
|
+
return key;
|
|
8761
|
+
}
|
|
8762
|
+
function fieldChanges(before, after, ignore, prefix) {
|
|
8763
|
+
const names = [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
|
8764
|
+
const changes = [];
|
|
8765
|
+
for (const name of names) {
|
|
8766
|
+
const field = `${prefix}${name}`;
|
|
8767
|
+
if (ignore.has(field) || prefix === '' && ignore.has(name)) continue;
|
|
8768
|
+
const was = before[name];
|
|
8769
|
+
const is = after[name];
|
|
8770
|
+
if (isPlainObject(was) && isPlainObject(is)) changes.push(...fieldChanges(was, is, ignore, `${field}.`));else if (!sameValue(was, is)) changes.push({
|
|
8771
|
+
field,
|
|
8772
|
+
before: was ?? null,
|
|
8773
|
+
after: is ?? null
|
|
8774
|
+
});
|
|
8775
|
+
}
|
|
8776
|
+
return changes;
|
|
8777
|
+
}
|
|
8778
|
+
function isPlainObject(value) {
|
|
8779
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8780
|
+
}
|
|
8781
|
+
/** Equal as JSON, whatever the order of object keys. */
|
|
8782
|
+
function sameValue(first, second) {
|
|
8783
|
+
return stableJson(first ?? null) === stableJson(second ?? null);
|
|
8784
|
+
}
|
|
8785
|
+
function stableJson(value) {
|
|
8786
|
+
if (Array.isArray(value)) return `[${value.map(entry => stableJson(entry)).join(',')}]`;
|
|
8787
|
+
if (isPlainObject(value)) return `{${Object.keys(value).sort((first, second) => first.localeCompare(second)).map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
8788
|
+
return JSON.stringify(value) ?? 'null';
|
|
8789
|
+
}
|
|
8790
|
+
|
|
8791
|
+
/**
|
|
8792
|
+
* The records of a JSON Lines file, as a sink wrote them.
|
|
8793
|
+
*
|
|
8794
|
+
* @param path - The file.
|
|
8795
|
+
* @returns One record per non-empty line.
|
|
8796
|
+
* @throws Error naming the file and the line that is not a JSON object.
|
|
8797
|
+
*/
|
|
8798
|
+
async function readRecordsFile(path) {
|
|
8799
|
+
const text = await readFile(path, 'utf8');
|
|
8800
|
+
const records = [];
|
|
8801
|
+
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
8802
|
+
if (line.trim() === '') continue;
|
|
8803
|
+
let value;
|
|
8804
|
+
try {
|
|
8805
|
+
value = JSON.parse(line);
|
|
8806
|
+
} catch (error) {
|
|
8807
|
+
throw new Error(`${path}:${index + 1}: not JSON (${error.message})`, {
|
|
8808
|
+
cause: error
|
|
8809
|
+
});
|
|
8810
|
+
}
|
|
8811
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${path}:${index + 1}: not a record (a JSON object)`);
|
|
8812
|
+
records.push(value);
|
|
8813
|
+
}
|
|
8814
|
+
return records;
|
|
8815
|
+
}
|
|
8816
|
+
|
|
8817
|
+
export { ACCESS_PRESETS, AccessBroker, AccessConfigError, BrowserClient, BrowserSession, CaptchaError, DEFAULT_CAPTCHA_SELECTOR, DEFAULT_RETRY_RULE, HostThrottle, HttpClient, HttpError, MappingFailedError, PdfReadError, RecipeBindingError, RecipeSet, RecipeValidationError, RecordRejectedError, StepFailure, TransformError, UnknownHookError, accessConfigJsonSchema, accessConfigSchema, bindRecipeSet, createCrawler, csvWorkbook, deckText, detectChallenge, detectDelimiter, diffOptionsFor, diffRecords, fillDown, findDeckTables, findGridTables, findTables, htmlTableSheets, inputRecipeJsonSchema, inputRecipeSchema, isDeckDocument, isWorkbookDocument, isXmlDocument, jsonLinesSink, loadAccessConfig, loadRecipeSet, loadRecipes, memorySink, outputRecipeJsonSchema, outputRecipeSchema, parseCsv, parseInputRecipe, parseOutputRecipe, parseXml, pdfText, readMarkdown, readPdf, readRecipeSource, readRecordsFile, readYaml, readableKey, retryRuleSchema, selectXpath, takeFromXml, throttleConfigSchema, traceLine, tryParseJson, validateBinding, workbookText };
|
|
8194
8818
|
//# sourceMappingURL=index.esm.js.map
|