polite-crawler 0.2.3 → 0.3.0
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/build/{cli-daemon-DAq4B_u-.mjs → cli-daemon-C2_Pd4v_.mjs} +2 -2
- package/build/{cli-daemon-DAq4B_u-.mjs.map → cli-daemon-C2_Pd4v_.mjs.map} +1 -1
- package/build/{cli-recipes-BXTuQHc3.mjs → cli-recipes-XlxoNo75.mjs} +2 -2
- package/build/{cli-recipes-BXTuQHc3.mjs.map → cli-recipes-XlxoNo75.mjs.map} +1 -1
- package/build/cli.mjs +7 -7
- package/build/{daemon-CSge_vif.mjs → daemon-B0Cp8xeB.mjs} +3 -3
- package/build/{daemon-CSge_vif.mjs.map → daemon-B0Cp8xeB.mjs.map} +1 -1
- package/build/{diff-DxhmN3bo.mjs → diff-DHpF9lYY.mjs} +2 -2
- package/build/{diff-DxhmN3bo.mjs.map → diff-DHpF9lYY.mjs.map} +1 -1
- package/build/{extract-DmNeGMHX.mjs → extract-BgcEkiQr.mjs} +3 -3
- package/build/{extract-DmNeGMHX.mjs.map → extract-BgcEkiQr.mjs.map} +1 -1
- package/build/{html-DkQUAE_d.mjs → html-DzobO0fy.mjs} +17 -2
- package/build/html-DzobO0fy.mjs.map +1 -0
- package/build/html.mjs +1 -1
- package/build/index.d.mts +1 -1
- package/build/index.mjs +11 -11
- package/build/{open-DWzixXcq.mjs → open-YGJPjbDc.mjs} +7 -7
- package/build/{open-DWzixXcq.mjs.map → open-YGJPjbDc.mjs.map} +1 -1
- package/build/{recipe-BQX7GI1z.mjs → recipe-CPmYb91t.mjs} +4 -4
- package/build/{recipe-BQX7GI1z.mjs.map → recipe-CPmYb91t.mjs.map} +1 -1
- package/build/{recipe-product-DVj9oHdJ.mjs → recipe-product-B6XmkpB3.mjs} +5 -5
- package/build/{recipe-product-DVj9oHdJ.mjs.map → recipe-product-B6XmkpB3.mjs.map} +1 -1
- package/build/robots.mjs +11 -6
- package/build/robots.mjs.map +1 -1
- package/build/{runtime-CmAOsSPn.mjs → runtime-j2NpXEiN.mjs} +10 -4
- package/build/runtime-j2NpXEiN.mjs.map +1 -0
- package/build/{version-BBVugNVm.mjs → version-Dw3pZOVT.mjs} +2 -2
- package/build/{version-BBVugNVm.mjs.map → version-Dw3pZOVT.mjs.map} +1 -1
- package/build/{wacz-t-JDRIMf.mjs → wacz-ixsUXYCs.mjs} +3 -3
- package/build/{wacz-t-JDRIMf.mjs.map → wacz-ixsUXYCs.mjs.map} +1 -1
- package/build/{warc-Ci4ygka2.mjs → warc-B47Bdjzk.mjs} +2 -2
- package/build/{warc-Ci4ygka2.mjs.map → warc-B47Bdjzk.mjs.map} +1 -1
- package/package.json +5 -4
- package/build/html-DkQUAE_d.mjs.map +0 -1
- package/build/runtime-CmAOsSPn.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { i as durationMs } from "./version-
|
|
2
|
-
import { b as fallbackReason, c as isHeldBack, d as recordOverride, g as archivedCopy, i as fetchAndIndex, n as HELD_BACK, s as heldBackMeta, y as archivedOf } from "./runtime-
|
|
3
|
-
import { o as decodeBody } from "./html-
|
|
1
|
+
import { i as durationMs } from "./version-Dw3pZOVT.mjs";
|
|
2
|
+
import { b as fallbackReason, c as isHeldBack, d as recordOverride, g as archivedCopy, i as fetchAndIndex, n as HELD_BACK, s as heldBackMeta, y as archivedOf } from "./runtime-j2NpXEiN.mjs";
|
|
3
|
+
import { o as decodeBody } from "./html-DzobO0fy.mjs";
|
|
4
4
|
//#region src/recipe.ts
|
|
5
5
|
/**
|
|
6
6
|
* This function reads a body as JSON, in the encoding that the response declares. A body that is
|
|
@@ -282,4 +282,4 @@ const runRecipe = async (target, recipe, input, options = {}) => {
|
|
|
282
282
|
//#endregion
|
|
283
283
|
export { readJson as a, runtimeOf as c, defineRecipe as i, RESERVED_RECIPES as n, registerRecipe as o, deadlineOf as r, runRecipe as s, RECIPE_LIMITS as t };
|
|
284
284
|
|
|
285
|
-
//# sourceMappingURL=recipe-
|
|
285
|
+
//# sourceMappingURL=recipe-CPmYb91t.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipe-BQX7GI1z.mjs","names":[],"sources":["../src/recipe.ts"],"sourcesContent":["/*\n * The recipe SDK. A recipe tells the crawler what to fetch and how to read it into documents.\n *\n * - `defineRecipe` declares a recipe: its name, the request for an input, and its extractor.\n * - `runRecipe` fetches one input through the crawler and gives the documents of the capture.\n * - `registerRecipe` gives the extractor to the extractors of a runtime. `openCrawler` and\n * `createRuntime` take `recipes` and register them at open, so `reindex` reads their captures.\n *\n * Each fetch of a recipe is a fetch of the crawler. Thus robots.txt (for a page), the pace of each\n * host, the stops, the egress policy and the capture log apply. The documents are versions in the\n * index, so `find`, `read`, `verify`, `extract`, `diff` and the time travel use them.\n */\nimport type { FetchRequest } from \"./crawler.ts\";\nimport type { Doc, ExtractedDoc, VersionText } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedCopy, archivedOf, fallbackReason } from \"./fallback.ts\";\nimport type { CrawlRuntime, Extractor, Extractors, Indexed, PageOptions } from \"./runtime.ts\";\nimport { HELD_BACK, fetchAndIndex, heldBackMeta, isHeldBack, recordOverride } from \"./runtime.ts\";\nimport type { AiInput } from \"./signals.ts\";\nimport { decodeBody } from \"./text.ts\";\n\n/** The capture that an extractor reads: the final URL of the response and its content type. */\nexport interface RecipeCapture {\n readonly url: string;\n readonly contentType: string | null;\n}\n\n/**\n * This function reads one captured body into the documents that it holds. It is pure: the same\n * body and capture always give the same documents. It sends no request. It can answer later.\n */\nexport type RecipeExtract = (\n body: Uint8Array,\n capture: RecipeCapture,\n) => ReadonlyArray<ExtractedDoc> | Promise<ReadonlyArray<ExtractedDoc>>;\n\n/** The request of a recipe for one input: the URL, and the parts that change with the input. */\nexport interface RecipeRequest {\n readonly url: string;\n /** This value replaces the `accept` of the recipe for this request. */\n readonly accept?: string | undefined;\n /** These headers go with the request. The crawler does not write them into the capture. */\n readonly headers?: Readonly<Record<string, string>> | undefined;\n /** These query parameters hold secrets. The capture shows `REDACTED` in their place. */\n readonly secretParams?: readonly string[] | undefined;\n}\n\n/** These values declare a recipe. Only `name` and `extract` are necessary. */\nexport interface RecipeDefinition<Input = string> {\n /**\n * This name goes into each capture record of the recipe, so it is part of each capture id.\n * `reindex` finds the extractor by it. Do not change it after the first capture.\n */\n readonly name: string;\n /** This text tells what the recipe reads. */\n readonly description?: string | undefined;\n /**\n * The value `page` (the default) is a web page: robots.txt applies. The value `api` is a\n * documented API with terms of its own: robots.txt does not apply, and the pace still does.\n */\n readonly mode?: \"api\" | \"page\" | undefined;\n /** This value is the `Accept` header. The defaults are HTML for a page and JSON for an API. */\n readonly accept?: string | undefined;\n /** This value is the documented pace of the source: the shortest time between two of its requests. */\n readonly minInterval?: Duration | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** This value is the most bytes of a body that the crawler keeps. */\n readonly maxBytes?: number | undefined;\n /** This function gives the request for an input. With no function, the input is the URL. */\n readonly fetch?: ((input: Input) => string | RecipeRequest) | undefined;\n /** This function reads a captured body into documents. It is pure, and it sends no request. */\n readonly extract: RecipeExtract;\n}\n\n/** These options build the request of a recipe for one input (`Recipe.request`). */\nexport interface RecipeRequestOptions {\n /** This value replaces the `maxAge` of the recipe. The value `0` asks the source again. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for a recipe of the mode `page`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n /** This name connects the captures to the job that asked for them. */\n readonly job?: string | undefined;\n /** When this signal aborts, the crawler stops the fetch. */\n readonly signal?: AbortSignal | undefined;\n /** This label goes into each capture record of the request. */\n readonly scope?: string | undefined;\n /** These labels go into each capture record of the request. They are not part of the capture id. */\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** No request starts at or after this time (milliseconds since the epoch, on the clock of the crawler). */\n readonly deadline?: number | undefined;\n}\n\n/** A recipe that `defineRecipe` made. Give it to `runRecipe`, to `registerRecipe` or to `openCrawler`. */\nexport interface Recipe<Input = string> {\n readonly name: string;\n readonly description: string | null;\n readonly mode: \"api\" | \"page\";\n readonly accept: string;\n /**\n * This extractor reads the captures of the recipe. It checks the documents of the definition.\n *\n * @beta\n */\n readonly extractor: Extractor;\n /**\n * This method gives the request of the crawler for one input. A value that is not a URL throws a `TypeError`.\n *\n * @beta\n */\n request(input: Input, options?: RecipeRequestOptions): FetchRequest;\n}\n\n/**\n * This function reads a body as JSON, in the encoding that the response declares. A body that is\n * not JSON (a page about maintenance, or a login page) throws an error that names the URL and the\n * content type. In an extractor, the runtime then reports that error.\n */\nexport const readJson = (body: Uint8Array, capture: RecipeCapture): unknown => {\n const text = decodeBody(body, capture.contentType);\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new Error(\n `${capture.url} answered ${capture.contentType ?? \"a body of no stated type\"}, not JSON`,\n );\n }\n};\n\n/**\n * These recipe names belong to the crawler: the page reader, the archived fallback, robots.txt,\n * and the sitemaps and feeds of `discover`. A recipe cannot use them.\n */\nexport const RESERVED_RECIPES: readonly string[] = [\n \"page\",\n \"archive-copy\",\n \"archive-index\",\n \"robots.txt\",\n \"sitemap\",\n \"feed\",\n];\n\n/**\n * These are the limits of a recipe.\n *\n * @beta\n */\nexport const RECIPE_LIMITS: {\n /** The longest recipe name. */\n readonly nameChars: number;\n /** The longest document id. */\n readonly docIdChars: number;\n} = { nameChars: 64, docIdChars: 2048 };\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;\nconst PAGE_ACCEPT = \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.5\";\nconst API_ACCEPT = \"application/json\";\nconst AI_INPUTS: readonly AiInput[] = [\"yes\", \"no\", \"unset\"];\n\n/** This function tells what is wrong with one document of an extractor, or gives undefined. */\nconst faultOf = (value: unknown, seen: Set<string>): string | undefined => {\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return \"a document must be an object\";\n const doc = value as Record<string, unknown>;\n const id = doc[\"doc\"];\n if (typeof id !== \"string\" || id.length === 0 || id.length > RECIPE_LIMITS.docIdChars)\n return `doc must be a text of 1 to ${String(RECIPE_LIMITS.docIdChars)} characters`;\n // the archived fallback labels the copies of third parties, and only it makes these\n if (id.startsWith(\"archive:\"))\n return `the id ${id} starts with archive:, which is for the archived copies`;\n if (seen.has(id)) return `two documents have the id ${id}`;\n seen.add(id);\n const kind = doc[\"kind\"];\n if (typeof kind !== \"string\" || kind.length === 0) return \"kind must be a text that is not empty\";\n if (kind === \"archived\") return \"the kind archived is for the archived copies\";\n if (typeof doc[\"title\"] !== \"string\") return \"title must be a text\";\n if (typeof doc[\"text\"] !== \"string\") return \"text must be a text\";\n if (doc[\"url\"] !== null && typeof doc[\"url\"] !== \"string\") return \"url must be a text or null\";\n const meta = doc[\"meta\"];\n if (meta === null || typeof meta !== \"object\" || Array.isArray(meta))\n return \"meta must be an object\";\n try {\n JSON.stringify(meta);\n } catch {\n return \"meta must be JSON: no cycle and no BigInt\";\n }\n const merge = doc[\"merge\"];\n if (merge !== undefined && merge !== \"latest\" && merge !== \"richest\")\n return 'merge must be \"latest\" or \"richest\"';\n if (doc[\"full_text\"] !== undefined && typeof doc[\"full_text\"] !== \"string\")\n return \"full_text must be a text\";\n const aiInput = doc[\"ai_input\"];\n if (aiInput !== undefined && !AI_INPUTS.includes(aiInput as AiInput))\n return 'ai_input must be \"yes\", \"no\" or \"unset\"';\n return undefined;\n};\n\n/** This function gives the documents of an extractor, or throws an error that names the first fault. */\nconst checkedDocs = (name: string, docs: unknown): ReadonlyArray<ExtractedDoc> => {\n if (!Array.isArray(docs))\n throw new TypeError(`the recipe ${name} gave ${typeof docs}, not an array of documents`);\n const seen = new Set<string>();\n for (const [index, doc] of docs.entries()) {\n const fault = faultOf(doc, seen);\n if (fault !== undefined)\n throw new TypeError(\n `the recipe ${name} gave a document that is not valid (${String(index)}): ${fault}`,\n );\n }\n return docs as ReadonlyArray<ExtractedDoc>;\n};\n\n/**\n * This function declares a recipe. It checks the definition at once: a name that is not valid, a\n * reserved name or a duration that it cannot read throws an error. The extractor of the recipe\n * checks each document of `extract`. A document that is not valid makes the extractor throw, and\n * the runtime reports it as an error of the extractor.\n */\nexport const defineRecipe = <Input = string>(\n definition: RecipeDefinition<Input>,\n): Recipe<Input> => {\n const name = definition.name;\n if (typeof name !== \"string\" || name.length > RECIPE_LIMITS.nameChars || !NAME.test(name)) {\n throw new RangeError(\n `a recipe name is 1 to ${String(RECIPE_LIMITS.nameChars)} letters, digits and . _ : -, and it starts with a letter or a digit, not ${JSON.stringify(name) ?? \"undefined\"}`,\n );\n }\n if (RESERVED_RECIPES.includes(name)) {\n throw new RangeError(\n `the recipe name ${name} belongs to the crawler: give the recipe a name of its own`,\n );\n }\n if (typeof definition.extract !== \"function\") {\n throw new TypeError(\n `the recipe ${name} needs extract, a function that reads a body into documents`,\n );\n }\n const mode = definition.mode ?? \"page\";\n if (mode !== \"page\" && mode !== \"api\") {\n throw new RangeError(\n `the mode of the recipe ${name} must be \"page\" or \"api\", not ${String(mode)}`,\n );\n }\n const accept = definition.accept ?? (mode === \"api\" ? API_ACCEPT : PAGE_ACCEPT);\n const minIntervalMs =\n definition.minInterval === undefined ? undefined : durationMs(definition.minInterval);\n const maxAgeOf = (value: Duration): number | \"auto\" =>\n value === \"auto\" ? \"auto\" : durationMs(value);\n const maxAge = maxAgeOf(definition.maxAge ?? \"24h\");\n const maxBytes = definition.maxBytes;\n if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 1)) {\n throw new RangeError(`maxBytes of the recipe ${name} must be a whole number of 1 or more`);\n }\n const extract = definition.extract;\n const extractor: Extractor = async (body, capture) =>\n checkedDocs(name, await extract(body, { url: capture.url, contentType: capture.contentType }));\n const fetch = definition.fetch;\n return {\n name,\n description: definition.description ?? null,\n mode,\n accept,\n extractor,\n request(input, options = {}) {\n const asked: unknown = fetch === undefined ? input : fetch(input);\n const target = typeof asked === \"string\" ? { url: asked } : (asked as RecipeRequest | null);\n const url = target?.url;\n let parsed: URL | undefined;\n try {\n parsed = typeof url === \"string\" ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\")) {\n throw new TypeError(\n `the recipe ${name} needs an http or https URL for its input, not ${JSON.stringify(url) ?? String(url)}`,\n );\n }\n return {\n url: parsed.href,\n mode,\n recipe: name,\n accept: target?.accept ?? accept,\n headers: target?.headers,\n secretParams: target?.secretParams,\n minIntervalMs,\n maxAgeMs: options.maxAge === undefined ? maxAge : maxAgeOf(options.maxAge),\n maxBytes,\n onBehalfOf: options.onBehalfOf,\n job: options.job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline: options.deadline,\n };\n },\n };\n};\n\n/**\n * This function gives the extractor of a recipe to the extractors of a runtime. A second\n * registration of the same recipe does nothing. A different extractor with the same name throws\n * an error, because `reindex` must read each capture with one extractor.\n *\n * @beta\n */\nexport const registerRecipe = <Input>(extractors: Extractors, recipe: Recipe<Input>): void => {\n const known = extractors.get(recipe.name);\n if (known === recipe.extractor) return;\n if (known !== undefined) {\n throw new Error(\n `the recipe ${recipe.name} has a different extractor in this runtime: give each recipe a name of its own`,\n );\n }\n extractors.register(recipe.name, recipe.extractor);\n};\n\n/** These options control one run of a recipe. */\nexport interface RecipeRunOptions extends Omit<RecipeRequestOptions, \"deadline\"> {\n /** No request starts after this time from the start of the run, for example `\"30s\"`. */\n readonly budget?: Duration | undefined;\n /** This value is true when a person gave this input directly. It is the only way to see a held-back document. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /**\n * The value `archive` gives a copy from a public web archive when the site refuses the page or is\n * down. Only a recipe of the mode `page` takes it. The page reader reads the copy.\n */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n /** The value true also gives an archived copy of a page that the current robots.txt disallows. */\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** One document of a run of a recipe: the version of the capture of the run. */\nexport interface RecipeDoc {\n readonly doc: string;\n readonly kind: string;\n /** This value is the title. A held-back document shows no title. */\n readonly title: string;\n readonly url: string | null;\n /** This value is the metadata. A held-back document shows only the structural fields. */\n readonly meta: Readonly<Record<string, unknown>>;\n /** This value is the length of the text. `read` gives the text. */\n readonly chars: number;\n /** This id is the capture of this version. A citation names it. */\n readonly capture: string;\n readonly fetched_at: string;\n readonly held_back: boolean;\n /** This value tells that the document is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n}\n\n/** The result of `runRecipe`. */\nexport interface RecipeResult {\n readonly recipe: string;\n /** This URL is the URL of the capture. Its secrets show as `REDACTED`. */\n readonly url: string;\n readonly outcome: string;\n readonly status: number;\n /** These documents came from the capture of the run, in the order of the extractor. */\n readonly docs: readonly RecipeDoc[];\n /** This id is the capture of the documents. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n /** This value is true when the stored copy answered, and the crawler sent no request. */\n readonly from_cache: boolean;\n /** This value is true when a document is held back. */\n readonly held_back: boolean;\n /** This text is the error of the extractor, when it threw for the capture. */\n readonly error: string | null;\n /** This value tells that the documents are an archived copy, not a fresh fetch. */\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\n/** A recipe operates on a runtime, or on a crawler that has one (`crawler.runtime`). */\nexport type RecipeTarget = CrawlRuntime | { readonly runtime: CrawlRuntime };\n\n/** This function gives the runtime of a target. */\nexport const runtimeOf = (target: RecipeTarget): CrawlRuntime =>\n \"crawler\" in target ? target : target.runtime;\n\n/** This function gives the deadline of a budget, on the clock of the runtime. */\nexport const deadlineOf = (\n runtime: CrawlRuntime,\n budget: Duration | undefined,\n): number | undefined =>\n budget === undefined ? undefined : Date.parse(runtime.now()) + durationMs(budget);\n\n/** This function gives one document of a run, with the rule for a held-back document. */\nconst recipeDoc = async (\n runtime: CrawlRuntime,\n doc: Doc,\n capture: string | undefined,\n userProvided: boolean,\n action: string,\n): Promise<RecipeDoc> => {\n const current = runtime.index.get(doc.doc) ?? doc;\n const version: VersionText | Doc =\n (capture === undefined ? undefined : runtime.index.versionText(doc.doc, capture)) ?? current;\n const aiInput: AiInput =\n current.ai_input === \"no\" || version.ai_input === \"no\" ? \"no\" : version.ai_input;\n const held = isHeldBack({ ai_input: aiInput }, userProvided);\n if (aiInput === \"no\" && userProvided) await recordOverride(runtime, current, action);\n const archived = archivedOf(version.meta);\n return {\n doc: doc.doc,\n kind: version.kind,\n title: held ? \"\" : version.title,\n url: version.url,\n meta: held ? heldBackMeta(version.meta) : version.meta,\n chars: version.text.length,\n capture: version.capture,\n fetched_at: version.fetched_at,\n held_back: held,\n ...(archived === undefined ? {} : { archived }),\n };\n};\n\nconst noteOf = (parts: ReadonlyArray<string | null | undefined>): string | null => {\n const kept = parts.filter((part): part is string => typeof part === \"string\" && part.length > 0);\n return kept.length > 0 ? kept.join(\"; \") : null;\n};\n\n/**\n * This function starts a recipe for one input. It fetches the request through the crawler, and it\n * reads the capture with the extractor of the recipe. It gives the documents of that capture.\n * These are its rules:\n *\n * - The runtime registers the recipe the first time that it gets it. Give the recipe to\n * `openCrawler` too, so that `reindex` reads its captures before the first fetch.\n * - A stored copy younger than `maxAge` answers with no request. When the extractor cannot read a\n * stored copy, the crawler asks the source one more time.\n * - An extractor that throws gives no document, and `error` tells why. The fetch still succeeds.\n * - A document that asked not to be AI input shows no title and only its structural metadata,\n * unless `userProvided`. The crawler logs each `userProvided` read of a held-back document.\n * - With `fallback: \"archive\"`, a page that the site refuses gets an archived copy, with\n * `archived`. The page reader reads the copy. The extractor of the recipe reads only live\n * captures. Thus its documents do not mix a live answer and an old copy of a third party.\n */\nexport const runRecipe = async <Input>(\n target: RecipeTarget,\n recipe: Recipe<Input>,\n input: Input,\n options: RecipeRunOptions = {},\n): Promise<RecipeResult> => {\n const runtime = runtimeOf(target);\n if (options.fallback === \"archive\" && recipe.mode !== \"page\") {\n throw new RangeError(\n `the recipe ${recipe.name} reads an API: an archived copy is for a page, so give fallback only to a recipe of the mode page`,\n );\n }\n registerRecipe(runtime.extractors, recipe);\n const deadline = deadlineOf(runtime, options.budget);\n const request = recipe.request(input, { ...options, deadline });\n let indexed: Indexed = await fetchAndIndex(runtime, request);\n // a stored answer that the extractor cannot read is no answer: it would give the same error until maxAge\n if (indexed.error !== undefined && indexed.blocked === undefined && indexed.fetched.fromCache) {\n indexed = await fetchAndIndex(runtime, { ...request, maxAgeMs: 0 });\n }\n const userProvided = options.userProvided ?? false;\n const action = options.job ?? `recipe ${recipe.name}`;\n const fetched = indexed.fetched;\n const notes: Array<string | undefined> = [fetched.note, indexed.error];\n if (indexed.docs.length === 0 && options.fallback === \"archive\") {\n const reason = fallbackReason(indexed);\n if (reason !== undefined) {\n const page: PageOptions = {\n render: \"never\",\n maxAgeMs: request.maxAgeMs,\n onBehalfOf: request.onBehalfOf,\n job: request.job,\n signal: request.signal,\n scope: request.scope,\n labels: request.labels,\n deadline,\n fallback: \"archive\",\n archiveDespiteRobots: options.archiveDespiteRobots,\n };\n const answer = await archivedCopy(runtime, request.url, reason, page);\n if (answer.doc !== undefined && answer.archived !== undefined) {\n const doc = await recipeDoc(runtime, answer.doc, answer.doc.capture, userProvided, action);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs: [doc],\n capture: doc.capture,\n fetched_at: doc.fetched_at,\n from_cache: answer.fromCache,\n held_back: doc.held_back,\n error: indexed.error ?? null,\n archived: answer.archived,\n note: noteOf([\n ...notes,\n ...answer.notes,\n \"the page reader read the archived copy, not the extractor of the recipe\",\n doc.held_back ? HELD_BACK : undefined,\n ]),\n };\n }\n notes.push(...answer.notes);\n }\n }\n const capture = fetched.capture;\n const docs: RecipeDoc[] = [];\n for (const doc of indexed.docs)\n docs.push(await recipeDoc(runtime, doc, capture, userProvided, action));\n const held = docs.some((doc) => doc.held_back);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs,\n capture: docs[0]?.capture ?? capture ?? null,\n fetched_at: docs[0]?.fetched_at ?? fetched.fetchedAt ?? null,\n from_cache: fetched.fromCache,\n held_back: held,\n error: indexed.error ?? null,\n note: noteOf([...notes, held ? HELD_BACK : undefined]),\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"recipe-CPmYb91t.mjs","names":[],"sources":["../src/recipe.ts"],"sourcesContent":["/*\n * The recipe SDK. A recipe tells the crawler what to fetch and how to read it into documents.\n *\n * - `defineRecipe` declares a recipe: its name, the request for an input, and its extractor.\n * - `runRecipe` fetches one input through the crawler and gives the documents of the capture.\n * - `registerRecipe` gives the extractor to the extractors of a runtime. `openCrawler` and\n * `createRuntime` take `recipes` and register them at open, so `reindex` reads their captures.\n *\n * Each fetch of a recipe is a fetch of the crawler. Thus robots.txt (for a page), the pace of each\n * host, the stops, the egress policy and the capture log apply. The documents are versions in the\n * index, so `find`, `read`, `verify`, `extract`, `diff` and the time travel use them.\n */\nimport type { FetchRequest } from \"./crawler.ts\";\nimport type { Doc, ExtractedDoc, VersionText } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedCopy, archivedOf, fallbackReason } from \"./fallback.ts\";\nimport type { CrawlRuntime, Extractor, Extractors, Indexed, PageOptions } from \"./runtime.ts\";\nimport { HELD_BACK, fetchAndIndex, heldBackMeta, isHeldBack, recordOverride } from \"./runtime.ts\";\nimport type { AiInput } from \"./signals.ts\";\nimport { decodeBody } from \"./text.ts\";\n\n/** The capture that an extractor reads: the final URL of the response and its content type. */\nexport interface RecipeCapture {\n readonly url: string;\n readonly contentType: string | null;\n}\n\n/**\n * This function reads one captured body into the documents that it holds. It is pure: the same\n * body and capture always give the same documents. It sends no request. It can answer later.\n */\nexport type RecipeExtract = (\n body: Uint8Array,\n capture: RecipeCapture,\n) => ReadonlyArray<ExtractedDoc> | Promise<ReadonlyArray<ExtractedDoc>>;\n\n/** The request of a recipe for one input: the URL, and the parts that change with the input. */\nexport interface RecipeRequest {\n readonly url: string;\n /** This value replaces the `accept` of the recipe for this request. */\n readonly accept?: string | undefined;\n /** These headers go with the request. The crawler does not write them into the capture. */\n readonly headers?: Readonly<Record<string, string>> | undefined;\n /** These query parameters hold secrets. The capture shows `REDACTED` in their place. */\n readonly secretParams?: readonly string[] | undefined;\n}\n\n/** These values declare a recipe. Only `name` and `extract` are necessary. */\nexport interface RecipeDefinition<Input = string> {\n /**\n * This name goes into each capture record of the recipe, so it is part of each capture id.\n * `reindex` finds the extractor by it. Do not change it after the first capture.\n */\n readonly name: string;\n /** This text tells what the recipe reads. */\n readonly description?: string | undefined;\n /**\n * The value `page` (the default) is a web page: robots.txt applies. The value `api` is a\n * documented API with terms of its own: robots.txt does not apply, and the pace still does.\n */\n readonly mode?: \"api\" | \"page\" | undefined;\n /** This value is the `Accept` header. The defaults are HTML for a page and JSON for an API. */\n readonly accept?: string | undefined;\n /** This value is the documented pace of the source: the shortest time between two of its requests. */\n readonly minInterval?: Duration | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** This value is the most bytes of a body that the crawler keeps. */\n readonly maxBytes?: number | undefined;\n /** This function gives the request for an input. With no function, the input is the URL. */\n readonly fetch?: ((input: Input) => string | RecipeRequest) | undefined;\n /** This function reads a captured body into documents. It is pure, and it sends no request. */\n readonly extract: RecipeExtract;\n}\n\n/** These options build the request of a recipe for one input (`Recipe.request`). */\nexport interface RecipeRequestOptions {\n /** This value replaces the `maxAge` of the recipe. The value `0` asks the source again. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for a recipe of the mode `page`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n /** This name connects the captures to the job that asked for them. */\n readonly job?: string | undefined;\n /** When this signal aborts, the crawler stops the fetch. */\n readonly signal?: AbortSignal | undefined;\n /** This label goes into each capture record of the request. */\n readonly scope?: string | undefined;\n /** These labels go into each capture record of the request. They are not part of the capture id. */\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** No request starts at or after this time (milliseconds since the epoch, on the clock of the crawler). */\n readonly deadline?: number | undefined;\n}\n\n/** A recipe that `defineRecipe` made. Give it to `runRecipe`, to `registerRecipe` or to `openCrawler`. */\nexport interface Recipe<Input = string> {\n readonly name: string;\n readonly description: string | null;\n readonly mode: \"api\" | \"page\";\n readonly accept: string;\n /**\n * This extractor reads the captures of the recipe. It checks the documents of the definition.\n *\n * @beta\n */\n readonly extractor: Extractor;\n /**\n * This method gives the request of the crawler for one input. A value that is not a URL throws a `TypeError`.\n *\n * @beta\n */\n request(input: Input, options?: RecipeRequestOptions): FetchRequest;\n}\n\n/**\n * This function reads a body as JSON, in the encoding that the response declares. A body that is\n * not JSON (a page about maintenance, or a login page) throws an error that names the URL and the\n * content type. In an extractor, the runtime then reports that error.\n */\nexport const readJson = (body: Uint8Array, capture: RecipeCapture): unknown => {\n const text = decodeBody(body, capture.contentType);\n try {\n return JSON.parse(text) as unknown;\n } catch {\n throw new Error(\n `${capture.url} answered ${capture.contentType ?? \"a body of no stated type\"}, not JSON`,\n );\n }\n};\n\n/**\n * These recipe names belong to the crawler: the page reader, the archived fallback, robots.txt,\n * and the sitemaps and feeds of `discover`. A recipe cannot use them.\n */\nexport const RESERVED_RECIPES: readonly string[] = [\n \"page\",\n \"archive-copy\",\n \"archive-index\",\n \"robots.txt\",\n \"sitemap\",\n \"feed\",\n];\n\n/**\n * These are the limits of a recipe.\n *\n * @beta\n */\nexport const RECIPE_LIMITS: {\n /** The longest recipe name. */\n readonly nameChars: number;\n /** The longest document id. */\n readonly docIdChars: number;\n} = { nameChars: 64, docIdChars: 2048 };\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;\nconst PAGE_ACCEPT = \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.5\";\nconst API_ACCEPT = \"application/json\";\nconst AI_INPUTS: readonly AiInput[] = [\"yes\", \"no\", \"unset\"];\n\n/** This function tells what is wrong with one document of an extractor, or gives undefined. */\nconst faultOf = (value: unknown, seen: Set<string>): string | undefined => {\n if (value === null || typeof value !== \"object\" || Array.isArray(value))\n return \"a document must be an object\";\n const doc = value as Record<string, unknown>;\n const id = doc[\"doc\"];\n if (typeof id !== \"string\" || id.length === 0 || id.length > RECIPE_LIMITS.docIdChars)\n return `doc must be a text of 1 to ${String(RECIPE_LIMITS.docIdChars)} characters`;\n // the archived fallback labels the copies of third parties, and only it makes these\n if (id.startsWith(\"archive:\"))\n return `the id ${id} starts with archive:, which is for the archived copies`;\n if (seen.has(id)) return `two documents have the id ${id}`;\n seen.add(id);\n const kind = doc[\"kind\"];\n if (typeof kind !== \"string\" || kind.length === 0) return \"kind must be a text that is not empty\";\n if (kind === \"archived\") return \"the kind archived is for the archived copies\";\n if (typeof doc[\"title\"] !== \"string\") return \"title must be a text\";\n if (typeof doc[\"text\"] !== \"string\") return \"text must be a text\";\n if (doc[\"url\"] !== null && typeof doc[\"url\"] !== \"string\") return \"url must be a text or null\";\n const meta = doc[\"meta\"];\n if (meta === null || typeof meta !== \"object\" || Array.isArray(meta))\n return \"meta must be an object\";\n try {\n JSON.stringify(meta);\n } catch {\n return \"meta must be JSON: no cycle and no BigInt\";\n }\n const merge = doc[\"merge\"];\n if (merge !== undefined && merge !== \"latest\" && merge !== \"richest\")\n return 'merge must be \"latest\" or \"richest\"';\n if (doc[\"full_text\"] !== undefined && typeof doc[\"full_text\"] !== \"string\")\n return \"full_text must be a text\";\n const aiInput = doc[\"ai_input\"];\n if (aiInput !== undefined && !AI_INPUTS.includes(aiInput as AiInput))\n return 'ai_input must be \"yes\", \"no\" or \"unset\"';\n return undefined;\n};\n\n/** This function gives the documents of an extractor, or throws an error that names the first fault. */\nconst checkedDocs = (name: string, docs: unknown): ReadonlyArray<ExtractedDoc> => {\n if (!Array.isArray(docs))\n throw new TypeError(`the recipe ${name} gave ${typeof docs}, not an array of documents`);\n const seen = new Set<string>();\n for (const [index, doc] of docs.entries()) {\n const fault = faultOf(doc, seen);\n if (fault !== undefined)\n throw new TypeError(\n `the recipe ${name} gave a document that is not valid (${String(index)}): ${fault}`,\n );\n }\n return docs as ReadonlyArray<ExtractedDoc>;\n};\n\n/**\n * This function declares a recipe. It checks the definition at once: a name that is not valid, a\n * reserved name or a duration that it cannot read throws an error. The extractor of the recipe\n * checks each document of `extract`. A document that is not valid makes the extractor throw, and\n * the runtime reports it as an error of the extractor.\n */\nexport const defineRecipe = <Input = string>(\n definition: RecipeDefinition<Input>,\n): Recipe<Input> => {\n const name = definition.name;\n if (typeof name !== \"string\" || name.length > RECIPE_LIMITS.nameChars || !NAME.test(name)) {\n throw new RangeError(\n `a recipe name is 1 to ${String(RECIPE_LIMITS.nameChars)} letters, digits and . _ : -, and it starts with a letter or a digit, not ${JSON.stringify(name) ?? \"undefined\"}`,\n );\n }\n if (RESERVED_RECIPES.includes(name)) {\n throw new RangeError(\n `the recipe name ${name} belongs to the crawler: give the recipe a name of its own`,\n );\n }\n if (typeof definition.extract !== \"function\") {\n throw new TypeError(\n `the recipe ${name} needs extract, a function that reads a body into documents`,\n );\n }\n const mode = definition.mode ?? \"page\";\n if (mode !== \"page\" && mode !== \"api\") {\n throw new RangeError(\n `the mode of the recipe ${name} must be \"page\" or \"api\", not ${String(mode)}`,\n );\n }\n const accept = definition.accept ?? (mode === \"api\" ? API_ACCEPT : PAGE_ACCEPT);\n const minIntervalMs =\n definition.minInterval === undefined ? undefined : durationMs(definition.minInterval);\n const maxAgeOf = (value: Duration): number | \"auto\" =>\n value === \"auto\" ? \"auto\" : durationMs(value);\n const maxAge = maxAgeOf(definition.maxAge ?? \"24h\");\n const maxBytes = definition.maxBytes;\n if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes < 1)) {\n throw new RangeError(`maxBytes of the recipe ${name} must be a whole number of 1 or more`);\n }\n const extract = definition.extract;\n const extractor: Extractor = async (body, capture) =>\n checkedDocs(name, await extract(body, { url: capture.url, contentType: capture.contentType }));\n const fetch = definition.fetch;\n return {\n name,\n description: definition.description ?? null,\n mode,\n accept,\n extractor,\n request(input, options = {}) {\n const asked: unknown = fetch === undefined ? input : fetch(input);\n const target = typeof asked === \"string\" ? { url: asked } : (asked as RecipeRequest | null);\n const url = target?.url;\n let parsed: URL | undefined;\n try {\n parsed = typeof url === \"string\" ? new URL(url) : undefined;\n } catch {\n parsed = undefined;\n }\n if (parsed === undefined || (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\")) {\n throw new TypeError(\n `the recipe ${name} needs an http or https URL for its input, not ${JSON.stringify(url) ?? String(url)}`,\n );\n }\n return {\n url: parsed.href,\n mode,\n recipe: name,\n accept: target?.accept ?? accept,\n headers: target?.headers,\n secretParams: target?.secretParams,\n minIntervalMs,\n maxAgeMs: options.maxAge === undefined ? maxAge : maxAgeOf(options.maxAge),\n maxBytes,\n onBehalfOf: options.onBehalfOf,\n job: options.job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline: options.deadline,\n };\n },\n };\n};\n\n/**\n * This function gives the extractor of a recipe to the extractors of a runtime. A second\n * registration of the same recipe does nothing. A different extractor with the same name throws\n * an error, because `reindex` must read each capture with one extractor.\n *\n * @beta\n */\nexport const registerRecipe = <Input>(extractors: Extractors, recipe: Recipe<Input>): void => {\n const known = extractors.get(recipe.name);\n if (known === recipe.extractor) return;\n if (known !== undefined) {\n throw new Error(\n `the recipe ${recipe.name} has a different extractor in this runtime: give each recipe a name of its own`,\n );\n }\n extractors.register(recipe.name, recipe.extractor);\n};\n\n/** These options control one run of a recipe. */\nexport interface RecipeRunOptions extends Omit<RecipeRequestOptions, \"deadline\"> {\n /** No request starts after this time from the start of the run, for example `\"30s\"`. */\n readonly budget?: Duration | undefined;\n /** This value is true when a person gave this input directly. It is the only way to see a held-back document. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /**\n * The value `archive` gives a copy from a public web archive when the site refuses the page or is\n * down. Only a recipe of the mode `page` takes it. The page reader reads the copy.\n */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n /** The value true also gives an archived copy of a page that the current robots.txt disallows. */\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** One document of a run of a recipe: the version of the capture of the run. */\nexport interface RecipeDoc {\n readonly doc: string;\n readonly kind: string;\n /** This value is the title. A held-back document shows no title. */\n readonly title: string;\n readonly url: string | null;\n /** This value is the metadata. A held-back document shows only the structural fields. */\n readonly meta: Readonly<Record<string, unknown>>;\n /** This value is the length of the text. `read` gives the text. */\n readonly chars: number;\n /** This id is the capture of this version. A citation names it. */\n readonly capture: string;\n readonly fetched_at: string;\n readonly held_back: boolean;\n /** This value tells that the document is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n}\n\n/** The result of `runRecipe`. */\nexport interface RecipeResult {\n readonly recipe: string;\n /** This URL is the URL of the capture. Its secrets show as `REDACTED`. */\n readonly url: string;\n readonly outcome: string;\n readonly status: number;\n /** These documents came from the capture of the run, in the order of the extractor. */\n readonly docs: readonly RecipeDoc[];\n /** This id is the capture of the documents. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n /** This value is true when the stored copy answered, and the crawler sent no request. */\n readonly from_cache: boolean;\n /** This value is true when a document is held back. */\n readonly held_back: boolean;\n /** This text is the error of the extractor, when it threw for the capture. */\n readonly error: string | null;\n /** This value tells that the documents are an archived copy, not a fresh fetch. */\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\n/** A recipe operates on a runtime, or on a crawler that has one (`crawler.runtime`). */\nexport type RecipeTarget = CrawlRuntime | { readonly runtime: CrawlRuntime };\n\n/** This function gives the runtime of a target. */\nexport const runtimeOf = (target: RecipeTarget): CrawlRuntime =>\n \"crawler\" in target ? target : target.runtime;\n\n/** This function gives the deadline of a budget, on the clock of the runtime. */\nexport const deadlineOf = (\n runtime: CrawlRuntime,\n budget: Duration | undefined,\n): number | undefined =>\n budget === undefined ? undefined : Date.parse(runtime.now()) + durationMs(budget);\n\n/** This function gives one document of a run, with the rule for a held-back document. */\nconst recipeDoc = async (\n runtime: CrawlRuntime,\n doc: Doc,\n capture: string | undefined,\n userProvided: boolean,\n action: string,\n): Promise<RecipeDoc> => {\n const current = runtime.index.get(doc.doc) ?? doc;\n const version: VersionText | Doc =\n (capture === undefined ? undefined : runtime.index.versionText(doc.doc, capture)) ?? current;\n const aiInput: AiInput =\n current.ai_input === \"no\" || version.ai_input === \"no\" ? \"no\" : version.ai_input;\n const held = isHeldBack({ ai_input: aiInput }, userProvided);\n if (aiInput === \"no\" && userProvided) await recordOverride(runtime, current, action);\n const archived = archivedOf(version.meta);\n return {\n doc: doc.doc,\n kind: version.kind,\n title: held ? \"\" : version.title,\n url: version.url,\n meta: held ? heldBackMeta(version.meta) : version.meta,\n chars: version.text.length,\n capture: version.capture,\n fetched_at: version.fetched_at,\n held_back: held,\n ...(archived === undefined ? {} : { archived }),\n };\n};\n\nconst noteOf = (parts: ReadonlyArray<string | null | undefined>): string | null => {\n const kept = parts.filter((part): part is string => typeof part === \"string\" && part.length > 0);\n return kept.length > 0 ? kept.join(\"; \") : null;\n};\n\n/**\n * This function starts a recipe for one input. It fetches the request through the crawler, and it\n * reads the capture with the extractor of the recipe. It gives the documents of that capture.\n * These are its rules:\n *\n * - The runtime registers the recipe the first time that it gets it. Give the recipe to\n * `openCrawler` too, so that `reindex` reads its captures before the first fetch.\n * - A stored copy younger than `maxAge` answers with no request. When the extractor cannot read a\n * stored copy, the crawler asks the source one more time.\n * - An extractor that throws gives no document, and `error` tells why. The fetch still succeeds.\n * - A document that asked not to be AI input shows no title and only its structural metadata,\n * unless `userProvided`. The crawler logs each `userProvided` read of a held-back document.\n * - With `fallback: \"archive\"`, a page that the site refuses gets an archived copy, with\n * `archived`. The page reader reads the copy. The extractor of the recipe reads only live\n * captures. Thus its documents do not mix a live answer and an old copy of a third party.\n */\nexport const runRecipe = async <Input>(\n target: RecipeTarget,\n recipe: Recipe<Input>,\n input: Input,\n options: RecipeRunOptions = {},\n): Promise<RecipeResult> => {\n const runtime = runtimeOf(target);\n if (options.fallback === \"archive\" && recipe.mode !== \"page\") {\n throw new RangeError(\n `the recipe ${recipe.name} reads an API: an archived copy is for a page, so give fallback only to a recipe of the mode page`,\n );\n }\n registerRecipe(runtime.extractors, recipe);\n const deadline = deadlineOf(runtime, options.budget);\n const request = recipe.request(input, { ...options, deadline });\n let indexed: Indexed = await fetchAndIndex(runtime, request);\n // a stored answer that the extractor cannot read is no answer: it would give the same error until maxAge\n if (indexed.error !== undefined && indexed.blocked === undefined && indexed.fetched.fromCache) {\n indexed = await fetchAndIndex(runtime, { ...request, maxAgeMs: 0 });\n }\n const userProvided = options.userProvided ?? false;\n const action = options.job ?? `recipe ${recipe.name}`;\n const fetched = indexed.fetched;\n const notes: Array<string | undefined> = [fetched.note, indexed.error];\n if (indexed.docs.length === 0 && options.fallback === \"archive\") {\n const reason = fallbackReason(indexed);\n if (reason !== undefined) {\n const page: PageOptions = {\n render: \"never\",\n maxAgeMs: request.maxAgeMs,\n onBehalfOf: request.onBehalfOf,\n job: request.job,\n signal: request.signal,\n scope: request.scope,\n labels: request.labels,\n deadline,\n fallback: \"archive\",\n archiveDespiteRobots: options.archiveDespiteRobots,\n };\n const answer = await archivedCopy(runtime, request.url, reason, page);\n if (answer.doc !== undefined && answer.archived !== undefined) {\n const doc = await recipeDoc(runtime, answer.doc, answer.doc.capture, userProvided, action);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs: [doc],\n capture: doc.capture,\n fetched_at: doc.fetched_at,\n from_cache: answer.fromCache,\n held_back: doc.held_back,\n error: indexed.error ?? null,\n archived: answer.archived,\n note: noteOf([\n ...notes,\n ...answer.notes,\n \"the page reader read the archived copy, not the extractor of the recipe\",\n doc.held_back ? HELD_BACK : undefined,\n ]),\n };\n }\n notes.push(...answer.notes);\n }\n }\n const capture = fetched.capture;\n const docs: RecipeDoc[] = [];\n for (const doc of indexed.docs)\n docs.push(await recipeDoc(runtime, doc, capture, userProvided, action));\n const held = docs.some((doc) => doc.held_back);\n return {\n recipe: recipe.name,\n url: fetched.url,\n outcome: fetched.outcome,\n status: fetched.status,\n docs,\n capture: docs[0]?.capture ?? capture ?? null,\n fetched_at: docs[0]?.fetched_at ?? fetched.fetchedAt ?? null,\n from_cache: fetched.fromCache,\n held_back: held,\n error: indexed.error ?? null,\n note: noteOf([...notes, held ? HELD_BACK : undefined]),\n };\n};\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as durationMs } from "./version-
|
|
2
|
-
import { a as fetchPage, c as isHeldBack, d as recordOverride, n as HELD_BACK, y as archivedOf } from "./runtime-
|
|
3
|
-
import { o as decodeBody, r as typesOf, t as extractPage } from "./html-
|
|
4
|
-
import { c as runtimeOf, r as deadlineOf } from "./recipe-
|
|
1
|
+
import { i as durationMs } from "./version-Dw3pZOVT.mjs";
|
|
2
|
+
import { a as fetchPage, c as isHeldBack, d as recordOverride, n as HELD_BACK, y as archivedOf } from "./runtime-j2NpXEiN.mjs";
|
|
3
|
+
import { o as decodeBody, r as typesOf, t as extractPage } from "./html-DzobO0fy.mjs";
|
|
4
|
+
import { c as runtimeOf, r as deadlineOf } from "./recipe-CPmYb91t.mjs";
|
|
5
5
|
import { DAYS, describeWeek, disagreements, fromCompact, fromSchemaOrg, fromText } from "./hours.mjs";
|
|
6
6
|
//#region src/recipe-venue.ts
|
|
7
7
|
/**
|
|
@@ -911,4 +911,4 @@ const readProduct = async (target, url, options = {}) => {
|
|
|
911
911
|
//#endregion
|
|
912
912
|
export { readAmount as a, locationMatches as c, readVenue as d, venueReadingsOf as f, productOf as i, namesVenue as l, pickProduct as n, readProduct as o, pickVariant as r, VENUE_LIMITS as s, PRODUCT_LIMITS as t, rankHoursLinks as u };
|
|
913
913
|
|
|
914
|
-
//# sourceMappingURL=recipe-product-
|
|
914
|
+
//# sourceMappingURL=recipe-product-B6XmkpB3.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recipe-product-DVj9oHdJ.mjs","names":["answered"],"sources":["../src/recipe-venue.ts","../src/recipe-product.ts"],"sourcesContent":["/*\n * The venue recipe reads the opening hours of a venue (a restaurant, a shop, a museum) from its own\n * site. It reads the cheap sources first:\n *\n * 1. The home page: its JSON-LD (`openingHoursSpecification` and `openingHours`) and its text.\n * 2. The few pages of the site that most likely hold hours or a menu, by the words of their links.\n * 3. An OpenStreetMap `opening_hours` value, when the caller gives one. The recipe sends no\n * request to OpenStreetMap.\n *\n * Each reading names the page, the document and the capture that it came from. The recipe gives\n * readings only. To compare them with the hours that an application keeps is the work of the\n * application, and `compareHours` is the pure helper for it.\n */\nimport type { Doc } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedOf } from \"./fallback.ts\";\nimport type { Day, Hours, Week } from \"./hours.ts\";\nimport {\n DAYS,\n describeWeek,\n disagreements,\n fromCompact,\n fromSchemaOrg,\n fromText,\n} from \"./hours.ts\";\nimport type { PageLink } from \"./html.ts\";\nimport { typesOf } from \"./html.ts\";\nimport type { RecipeTarget } from \"./recipe.ts\";\nimport { deadlineOf, runtimeOf } from \"./recipe.ts\";\nimport type { PageOptions, PageResult } from \"./runtime.ts\";\nimport { HELD_BACK, fetchPage, isHeldBack, recordOverride } from \"./runtime.ts\";\n\n/**\n * These are the limits of the venue recipe.\n *\n * @beta\n */\nexport const VENUE_LIMITS: {\n /** The pages of the site that a read fetches after the home page, by default. */\n readonly pages: number;\n /** The most pages after the home page. */\n readonly maxPages: number;\n /** The most menu links of a result. */\n readonly menus: number;\n /** The most readings of one page. A page with more is a list of something else. */\n readonly readingsPerPage: number;\n /** The most links of one page that the recipe ranks. */\n readonly links: number;\n /** The distance in metres within which a JSON-LD location is the venue. */\n readonly nearMetres: number;\n} = { pages: 3, maxPages: 8, menus: 8, readingsPerPage: 20, links: 200, nearMetres: 150 };\n\n// ── words ───────────────────────────────────────────────────────────────────\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\"the\", \"a\", \"an\", \"and\", \"of\", \"at\"]);\n\n/** This function gives the words of a text in one form: lower case, no accents, and `&` as `and`. */\nconst wordsOf = (text: string): string[] =>\n text\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/\\p{M}/gu, \"\")\n .replace(/&/g, \" and \")\n .replace(/[^\\p{L}\\p{N} ]/gu, \" \")\n .split(/\\s+/)\n .filter((word) => word.length > 0 && !STOP_WORDS.has(word));\n\n/**\n * This function tells if a page names a venue: the page has the first word of the name and 60\n * percent of its words. A domain that a venue let go can show a different business. It gives\n * undefined for a name with no word.\n *\n * @beta\n */\nexport const namesVenue = (\n name: string,\n page: { readonly title: string; readonly text: string },\n): boolean | undefined => {\n const words = wordsOf(name);\n if (words.length === 0) return undefined;\n const onPage = new Set(wordsOf(`${page.title} ${page.text}`));\n const named = new Set(words);\n const found = [...named].filter((word) => onPage.has(word)).length;\n return onPage.has(words[0]!) && found / named.size >= 0.6;\n};\n\n// ── the links of the site ───────────────────────────────────────────────────\n\n/** The words of a link that tell what its page holds, with their weights. */\nconst LINK_WORDS: ReadonlyArray<readonly [RegExp, number]> = [\n [/hours|horario|horaires|opening/, 6],\n [/menu|food|drink|wine|cocktail/, 4],\n [/contact|location|visit|find.?us|directions/, 3],\n [/about|info/, 2],\n [/lunch|dinner|brunch|breakfast/, 2],\n];\n\n/** The hosts of review and social sites: a menu there is a different page about the venue. */\nconst SOCIAL =\n /(?:^|\\.)(?:yelp|tripadvisor|facebook|instagram|twitter|x|tiktok|google|apple|spotify)\\.[a-z.]+$/i;\n\n/** The files that hold no hours as text. */\nconst FILE = /\\.(?:pdf|jpe?g|png|gif|webp|svg|mp4)(?:[?#]|$)/i;\n\nconst hostOf = (url: string): string | undefined => {\n try {\n return new URL(url).hostname.toLowerCase().replace(/^www\\./, \"\");\n } catch {\n return undefined;\n }\n};\n\nconst withoutHash = (url: string): string => url.replace(/#.*$/s, \"\").replace(/\\/$/, \"\");\n\n/**\n * This function ranks the links of a page by the words of their text and path. The links to\n * the hours or a menu come first. It keeps only the links on the site of `home`,\n * and it leaves out the files (a PDF, an image) and the home page itself.\n *\n * @beta\n */\nexport const rankHoursLinks = (links: readonly PageLink[], home: string): PageLink[] => {\n const site = hostOf(home);\n const self = withoutHash(home);\n const seen = new Set<string>();\n const scored: Array<{ readonly link: PageLink; readonly score: number }> = [];\n for (const link of links.slice(0, VENUE_LIMITS.links)) {\n if (typeof link?.href !== \"string\" || hostOf(link.href) !== site || FILE.test(link.href))\n continue;\n const key = withoutHash(link.href);\n if (key === self || seen.has(key)) continue;\n let path = \"\";\n try {\n path = new URL(link.href).pathname;\n } catch {\n continue;\n }\n const words = `${typeof link.text === \"string\" ? link.text : \"\"} ${path}`.toLowerCase();\n const score = LINK_WORDS.reduce(\n (sum, [pattern, weight]) => sum + (pattern.test(words) ? weight : 0),\n 0,\n );\n if (score <= 0) continue;\n seen.add(key);\n scored.push({ link, score });\n }\n // a stable sort: links of equal score keep the order of the page\n return scored.sort((a, b) => b.score - a.score).map((entry) => entry.link);\n};\n\n/** A link to a menu. */\nexport interface VenueLink {\n readonly url: string;\n readonly text: string;\n}\n\n// ── the location of a venue ─────────────────────────────────────────────────\n\n/** The place of a venue, to choose its hours on a page that lists more than one location. */\nexport interface VenueLocation {\n /** This text is the street address, for example `12 Via Esempio`. The number and the street count. */\n readonly address?: string | undefined;\n readonly lat?: number | undefined;\n readonly lon?: number | undefined;\n}\n\ninterface Point {\n readonly lat: number;\n readonly lon: number;\n}\n\nconst EARTH_METRES = 6_371_000;\n\n/** This function gives the distance in metres between two points on the earth (the haversine formula). */\nconst metresBetween = (a: Point, b: Point): number => {\n const rad = Math.PI / 180;\n const dLat = (b.lat - a.lat) * rad;\n const dLon = (b.lon - a.lon) * rad;\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(dLon / 2) ** 2;\n return 2 * EARTH_METRES * Math.asin(Math.min(1, Math.sqrt(h)));\n};\n\nconst pointOf = (lat: unknown, lon: unknown): Point | undefined => {\n const la = typeof lat === \"string\" ? Number.parseFloat(lat) : lat;\n const lo = typeof lon === \"string\" ? Number.parseFloat(lon) : lon;\n return typeof la === \"number\" &&\n typeof lo === \"number\" &&\n Number.isFinite(la) &&\n Number.isFinite(lo) &&\n Math.abs(la) <= 90 &&\n Math.abs(lo) <= 180\n ? { lat: la, lon: lo }\n : undefined;\n};\n\n/** The words of a street that tell nothing about which street: directions and kinds of road. */\nconst STREET_NOISE: ReadonlySet<string> = new Set([\n \"n\",\n \"s\",\n \"e\",\n \"w\",\n \"north\",\n \"south\",\n \"east\",\n \"west\",\n \"st\",\n \"street\",\n \"ave\",\n \"avenue\",\n \"rd\",\n \"road\",\n \"blvd\",\n \"boulevard\",\n \"dr\",\n \"drive\",\n \"ln\",\n \"lane\",\n \"ct\",\n \"court\",\n \"pl\",\n \"place\",\n \"sq\",\n \"square\",\n \"hwy\",\n \"highway\",\n \"pkwy\",\n \"parkway\",\n \"way\",\n \"ter\",\n \"terrace\",\n \"suite\",\n \"ste\",\n \"unit\",\n]);\n\n/** This function gives the number of a street address and the words that name its street. */\nconst streetOf = (\n address: string,\n): { readonly number: string; readonly words: ReadonlySet<string> } | undefined => {\n const words = wordsOf((address.split(\",\")[0] ?? address).slice(0, 200));\n const number = words.find((word) => /^\\d+[a-z]?$/.test(word));\n if (number === undefined) return undefined;\n return {\n number,\n words: new Set(words.filter((word) => !/^\\d/.test(word) && !STREET_NOISE.has(word))),\n };\n};\n\nconst addressText = (value: unknown): string | undefined => {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return addressText(value[0]);\n if (typeof value === \"object\" && value !== null) {\n const street = (value as Record<string, unknown>)[\"streetAddress\"];\n return typeof street === \"string\" ? street : undefined;\n }\n return undefined;\n};\n\n/**\n * This function tells if a JSON-LD location record is the venue. The record must have the same\n * number on a street that shares a word, or a position near the venue (`VENUE_LIMITS.nearMetres`).\n *\n * @beta\n */\nexport const locationMatches = (\n record: Readonly<Record<string, unknown>>,\n location: VenueLocation,\n): boolean => {\n const geo = record[\"geo\"];\n const at =\n typeof geo === \"object\" && geo !== null\n ? pointOf(\n (geo as Record<string, unknown>)[\"latitude\"],\n (geo as Record<string, unknown>)[\"longitude\"],\n )\n : undefined;\n const ours = pointOf(location.lat, location.lon);\n if (at !== undefined && ours !== undefined && metresBetween(at, ours) <= VENUE_LIMITS.nearMetres)\n return true;\n const theirs = addressText(record[\"address\"]);\n if (theirs === undefined || location.address === undefined) return false;\n const a = streetOf(theirs);\n const b = streetOf(location.address);\n return (\n a !== undefined &&\n b !== undefined &&\n a.number === b.number &&\n [...a.words].some((word) => b.words.has(word))\n );\n};\n\nconst located = (record: Readonly<Record<string, unknown>>): boolean =>\n record[\"address\"] !== undefined || record[\"geo\"] !== undefined;\n\n// ── the readings ────────────────────────────────────────────────────────────\n\n/** One reading of the hours, with the source that it came from. */\nexport interface VenueReading {\n /** The value `jsonld` is the structured data of a page, `text` the text of a page, and `osm` the value of the caller. */\n readonly from: \"jsonld\" | \"osm\" | \"text\";\n /**\n * This value orders the readings, 0 first. JSON-LD is 0, a text under an hours heading is 1 and\n * OpenStreetMap is 2. A text with no label of a different schedule is 3. A different schedule is\n * 4, for example a brunch, a kitchen, a happy hour or an office.\n */\n readonly rank: number;\n /** The value `opening` is the opening hours of the venue (a rank of 3 or less). Else it is `other`. */\n readonly schedule: \"opening\" | \"other\";\n /** This text is the line above a reading of the text, for example `Hours` or `Happy hour`. */\n readonly label: string | null;\n /** This text is the week as words, from `describeWeek`. */\n readonly hours: string;\n readonly week: Week;\n /** This value is true when the reader guessed an am or a pm. */\n readonly inferred: boolean;\n /** This value is true when the reader left out a rule or a line (a holiday, a date). */\n readonly partial: boolean;\n /** This text names the season or the dates of the hours. */\n readonly season: string | null;\n /** This text is the words that the hours came from. A reading of the text can be a quote for `verify`. */\n readonly raw: string;\n /** This path names the JSON-LD record of the reading in the metadata of the document, for example `jsonld[2]`. */\n readonly path: string | null;\n /** This URL is the page of the reading. */\n readonly url: string | null;\n readonly doc: string | null;\n /** This id is the capture of the text that the reading came from. A citation names it. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n /** This value tells that the page is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n}\n\n/** A stored document, or a version of one: what `venueReadingsOf` reads. */\nexport interface VenueSource {\n readonly doc?: string | null | undefined;\n readonly url: string | null;\n readonly text: string;\n readonly meta: Readonly<Record<string, unknown>>;\n readonly capture?: string | null | undefined;\n readonly fetched_at?: string | null | undefined;\n}\n\n/** The result of `venueReadingsOf`: what one document says. */\nexport interface VenueRead {\n readonly readings: readonly VenueReading[];\n /** This value is the first `telephone` of the JSON-LD records of the venue. */\n readonly phone: string | null;\n readonly menus: readonly VenueLink[];\n readonly notes: readonly string[];\n /**\n * This value counts the work of the reader. The tests use it, because it does not change with the computer.\n *\n * @beta\n */\n readonly steps: number;\n}\n\n/** The types of JSON-LD records that have the hours of a venue. */\nconst HOURS_TYPES =\n /Restaurant|LocalBusiness|FoodEstablishment|BarOrPub|CafeOrCoffeeShop|Bakery|Store|Place|Winery|Brewery|NightClub|Museum|TouristAttraction|Library|EntertainmentBusiness/;\n\n/**\n * A label that names a schedule that is not the opening hours. The line above a schedule is often a\n * different thing, for example a phone number, and such a line tells nothing.\n */\nconst OTHER_SCHEDULE =\n /brunch|lunch|dinner|breakfast|happy|kitchen|\bbar\b|deliver|take.?out|pick.?up|patio|holiday|season|summer|winter|special|late|menu/i;\n\n/**\n * A label of a service beside the opening hours, or of an office. Its hours are a different\n * schedule, also when the label says \"hours\" (\"Happy hour\", \"Kitchen hours\", \"Office hours\").\n */\nconst SERVICE =\n /happy|kitchen|deliver|take.?out|pick.?up|brunch|holiday|special|patio|\bbar\b|late.?night|office/i;\n\nconst rankOf = (from: VenueReading[\"from\"], label: string | null): number => {\n if (from === \"jsonld\") return 0;\n if (from === \"osm\") return 2;\n const text = label ?? \"\";\n if (SERVICE.test(text)) return 4;\n if (/hour|open/i.test(text)) return 1;\n return OTHER_SCHEDULE.test(text) ? 4 : 3;\n};\n\nconst reading = (\n from: VenueReading[\"from\"],\n hours: Hours,\n label: string | null,\n source: {\n readonly url: string | null;\n readonly doc: string | null;\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly path: string | null;\n readonly archived?: Archived | undefined;\n },\n): VenueReading => {\n const rank = rankOf(from, label);\n return {\n from,\n rank,\n schedule: rank <= 3 ? \"opening\" : \"other\",\n label,\n hours: describeWeek(hours.week),\n week: hours.week,\n inferred: hours.inferred,\n partial: hours.partial === true,\n season: hours.season ?? null,\n raw: hours.raw,\n path: source.path,\n url: source.url,\n doc: source.doc,\n capture: source.capture,\n fetched_at: source.fetched_at,\n ...(source.archived === undefined ? {} : { archived: source.archived }),\n };\n};\n\nconst recordsOf = (meta: Readonly<Record<string, unknown>>): Record<string, unknown>[] => {\n const jsonld = meta[\"jsonld\"];\n return Array.isArray(jsonld)\n ? jsonld.filter(\n (record): record is Record<string, unknown> =>\n record !== null && typeof record === \"object\" && !Array.isArray(record),\n )\n : [];\n};\n\nconst linksOf = (meta: Readonly<Record<string, unknown>>): PageLink[] => {\n const links = meta[\"links\"];\n return Array.isArray(links)\n ? links.filter(\n (link): link is PageLink =>\n link !== null && typeof link === \"object\" && typeof (link as PageLink).href === \"string\",\n )\n : [];\n};\n\n/**\n * This function reads the hours of one stored document, or of one version of it. It is pure, and\n * it sends no request. Thus it also reads a version at a time (`versionAtTime`) or an archived copy.\n *\n * - It reads each JSON-LD record of a venue type with `fromSchemaOrg`, and the text with `fromText`.\n * - On a page that lists more than one location, it reads only the location that matches\n * `location`. With no `location`, it reads none of them, and a note says so.\n * - It gives the first `telephone` and the menu links that are not on a review or social site.\n */\nexport const venueReadingsOf = (\n source: VenueSource,\n options: { readonly location?: VenueLocation | undefined } = {},\n): VenueRead => {\n let steps = 0;\n const notes: string[] = [];\n const archived = archivedOf(source.meta);\n const where = {\n url: archived?.original ?? source.url,\n doc: source.doc ?? null,\n capture: source.capture ?? null,\n fetched_at: source.fetched_at ?? null,\n ...(archived === undefined ? {} : { archived }),\n };\n const all = recordsOf(source.meta);\n steps += all.length;\n const venues = all\n .map((record, index) => ({ record, index }))\n .filter(({ record }) => typesOf(record).some((type) => HOURS_TYPES.test(type)));\n // a page of a group lists each of its locations: only the hours of this venue are its hours\n let usable = venues;\n const placed = venues.filter(({ record }) => located(record));\n if (placed.length > 1) {\n const location = options.location;\n const ours =\n location === undefined\n ? []\n : placed.filter(({ record }) => locationMatches(record, location));\n if (ours.length === 0) {\n notes.push(\n location === undefined\n ? `${where.url ?? \"the page\"} lists ${String(placed.length)} locations: give location to read the hours of one`\n : `${where.url ?? \"the page\"} lists ${String(placed.length)} locations, and none matched the location: their hours were not read`,\n );\n }\n usable = [...ours, ...venues.filter(({ record }) => !located(record))];\n }\n const readings: VenueReading[] = [];\n let phone: string | null = null;\n for (const { record, index } of usable) {\n steps += 1;\n const hours = fromSchemaOrg(record);\n if (hours !== undefined && readings.length < VENUE_LIMITS.readingsPerPage) {\n readings.push(reading(\"jsonld\", hours, null, { ...where, path: `jsonld[${String(index)}]` }));\n }\n const telephone = record[\"telephone\"];\n if (phone === null && typeof telephone === \"string\" && telephone.trim().length > 0)\n phone = telephone.trim().slice(0, 100);\n }\n steps += source.text.length;\n for (const hours of fromText(source.text)) {\n if (readings.length >= VENUE_LIMITS.readingsPerPage) {\n notes.push(\n `${where.url ?? \"the page\"} gives more than ${String(VENUE_LIMITS.readingsPerPage)} readings: the rest were not read`,\n );\n break;\n }\n readings.push(reading(\"text\", hours, hours.label, { ...where, path: null }));\n }\n const menus: VenueLink[] = [];\n for (const link of linksOf(source.meta).slice(0, VENUE_LIMITS.links)) {\n steps += 1;\n if (menus.length >= VENUE_LIMITS.menus) break;\n const text = typeof link.text === \"string\" ? link.text.slice(0, 200) : \"\";\n const host = hostOf(link.href);\n if (host === undefined || SOCIAL.test(host) || !/menu/i.test(`${text} ${link.href}`)) continue;\n if (menus.some((menu) => menu.url === link.href)) continue;\n menus.push({ url: link.href, text });\n }\n return { readings, phone, menus, notes, steps };\n};\n\n// ── the recipe ──────────────────────────────────────────────────────────────\n\n/** An OpenStreetMap `opening_hours` value, with where the caller got it. */\nexport interface VenueOsm {\n /** This text is the value of the tag, for example `Mo-Fr 08:00-18:00; Sa 09:00-13:00`. */\n readonly value: string;\n readonly url?: string | null | undefined;\n readonly doc?: string | null | undefined;\n readonly capture?: string | null | undefined;\n readonly fetched_at?: string | null | undefined;\n}\n\n/** These options control `readVenue`. */\nexport interface VenueOptions {\n /** This value is the number of pages after the home page, from 0 to `VENUE_LIMITS.maxPages`. The default is 3. */\n readonly pages?: number | undefined;\n /** This text is the name of the venue. The result tells if the home page names it (`names_venue`). */\n readonly name?: string | undefined;\n /** With true, a home page that does not name the venue gives no reading, and the recipe fetches no more pages. */\n readonly requireName?: boolean | undefined;\n /** This value is the place of the venue, for a page that lists more than one location. */\n readonly location?: VenueLocation | undefined;\n /** This value is an OpenStreetMap `opening_hours` value. The recipe reads it, and sends no request for it. */\n readonly osm?: string | VenueOsm | undefined;\n /** The value `auto` renders a page only when it shows almost nothing without JavaScript. */\n readonly render?: \"auto\" | \"never\" | \"always\" | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for example `Claude-User`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n readonly signal?: AbortSignal | undefined;\n /** No request starts after this time from the start of the read, for example `\"2m\"`. */\n readonly budget?: Duration | undefined;\n readonly scope?: string | undefined;\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** This name connects the captures to the job that asked for them. The default is `venue`. */\n readonly job?: string | undefined;\n /** This value is true when a person gave this URL directly. It is the only way to read a held-back page. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /** The value `archive` reads an archived copy of a page that the site refuses, or when the site is down. */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** One page that the venue recipe fetched. */\nexport interface VenuePage {\n readonly url: string;\n readonly doc: string | null;\n readonly outcome: string;\n readonly status: number;\n readonly rendered: boolean;\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly from_cache: boolean;\n readonly held_back: boolean;\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\n/** The result of `readVenue`. */\nexport interface VenueResult {\n /** This URL is the site that the caller gave. */\n readonly url: string;\n /** This URL is where the home page landed, after the redirects. */\n readonly home: string | null;\n /** This URL is the new site, when the home page landed on a different host. */\n readonly moved: string | null;\n /** This value tells if the home page names the venue. It is null with no `name`. */\n readonly names_venue: boolean | null;\n readonly pages: readonly VenuePage[];\n /** These are the readings, one for each source, label and week: the best first. */\n readonly readings: readonly VenueReading[];\n /** This reading is the best reading of the opening hours, from any source. */\n readonly best: VenueReading | null;\n /** This reading is the best reading of the site itself (not OpenStreetMap) with no guess. A check for a change compares it. */\n readonly from_site: VenueReading | null;\n /** These days are the days where two readings of the whole schedule (a rank of 2 or less) disagree. */\n readonly disagree: readonly Day[];\n readonly menus: readonly VenueLink[];\n readonly phone: string | null;\n /** These ids are the captures of the pages that the recipe read. */\n readonly captures: readonly string[];\n /** This value is true when a page asked not to be AI input, so the recipe read nothing from it. */\n readonly held_back: boolean;\n readonly note: string | null;\n}\n\nconst answered = (page: PageResult): boolean =>\n page.outcome === \"ok\" || page.outcome === \"not-modified\" || page.archived !== undefined;\n\n/**\n * This function reads the hours of a venue from its site. It fetches the home page and the pages\n * that most likely hold hours or a menu. Each one is a fetch of the crawler. Thus robots.txt, the\n * pace, the stops, the capture log and the rule for a held-back page apply. It renders a page only\n * when the page needs JavaScript, as `fetch` does.\n *\n * It gives each reading with its source, the best reading, and the days where the readings\n * disagree. It writes nothing but the capture log. To choose the true hours is the decision of\n * the application.\n */\nexport const readVenue = async (\n target: RecipeTarget,\n url: string,\n options: VenueOptions = {},\n): Promise<VenueResult> => {\n const runtime = runtimeOf(target);\n const extra = options.pages ?? VENUE_LIMITS.pages;\n if (!Number.isInteger(extra) || extra < 0 || extra > VENUE_LIMITS.maxPages) {\n throw new RangeError(\n `pages must be a whole number from 0 to ${String(VENUE_LIMITS.maxPages)}, not ${String(extra)}`,\n );\n }\n let site: URL;\n try {\n site = new URL(url);\n } catch {\n throw new TypeError(`readVenue needs the URL of a site, not ${JSON.stringify(url)}`);\n }\n if (site.protocol !== \"http:\" && site.protocol !== \"https:\")\n throw new TypeError(`readVenue needs an http or https URL, not ${url}`);\n const maxAge = options.maxAge ?? \"24h\";\n const job = options.job ?? \"venue\";\n const userProvided = options.userProvided ?? false;\n const pageOptions: PageOptions = {\n render: options.render ?? \"auto\",\n maxAgeMs: maxAge === \"auto\" ? \"auto\" : durationMs(maxAge),\n onBehalfOf: options.onBehalfOf,\n job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline: deadlineOf(runtime, options.budget),\n fallback: options.fallback,\n archiveDespiteRobots: options.archiveDespiteRobots,\n };\n const notes: string[] = [];\n const readings: VenueReading[] = [];\n const pages: VenuePage[] = [];\n const menus: VenueLink[] = [];\n const captures = new Set<string>();\n let phone: string | null = null;\n let heldBack = false;\n\n if (options.osm !== undefined) {\n const osm: VenueOsm = typeof options.osm === \"string\" ? { value: options.osm } : options.osm;\n const hours = typeof osm.value === \"string\" ? fromCompact(osm.value, \"osm\") : undefined;\n if (hours === undefined) {\n notes.push(\n `the OpenStreetMap hours cannot be read: ${JSON.stringify(String(osm.value).slice(0, 200))}`,\n );\n } else {\n readings.push(\n reading(\"osm\", hours, null, {\n url: osm.url ?? null,\n doc: osm.doc ?? null,\n capture: osm.capture ?? null,\n fetched_at: osm.fetched_at ?? null,\n path: null,\n }),\n );\n if (typeof osm.capture === \"string\") captures.add(osm.capture);\n }\n }\n\n /** This function fetches one page, and gives its document when the recipe can read it. */\n const readPage = async (pageUrl: string): Promise<Doc | undefined> => {\n const page = await fetchPage(runtime, pageUrl, pageOptions);\n const doc = page.doc === null ? undefined : runtime.index.get(page.doc);\n const held = doc !== undefined && isHeldBack(doc, userProvided);\n pages.push({\n url: pageUrl,\n doc: page.doc,\n outcome: page.outcome,\n status: page.status,\n rendered: page.rendered,\n capture: page.capture,\n fetched_at: page.fetched_at,\n from_cache: page.from_cache,\n held_back: held,\n ...(page.archived === undefined ? {} : { archived: page.archived }),\n note: page.note,\n });\n if (!answered(page)) {\n notes.push(`${pageUrl}: ${page.outcome}${page.note === null ? \"\" : ` (${page.note})`}`);\n }\n if (doc === undefined) return undefined;\n if (held) {\n heldBack = true;\n return undefined;\n }\n if (doc.ai_input === \"no\") await recordOverride(runtime, doc, job);\n captures.add(doc.capture);\n return doc;\n };\n\n const readDoc = (doc: Doc): void => {\n const read = venueReadingsOf(doc, { location: options.location });\n readings.push(...read.readings);\n notes.push(...read.notes);\n phone ??= read.phone;\n for (const menu of read.menus) {\n if (menus.length < VENUE_LIMITS.menus && !menus.some((known) => known.url === menu.url))\n menus.push(menu);\n }\n };\n\n let homeUrl: string | null = null;\n let namesVenueResult: boolean | null = null;\n const home = await readPage(site.href);\n if (home !== undefined) {\n homeUrl = archivedOf(home.meta)?.original ?? home.url ?? site.href;\n if (options.name !== undefined) {\n namesVenueResult = namesVenue(options.name, home) ?? null;\n if (namesVenueResult === false) {\n notes.push(\n `${homeUrl} does not name ${options.name}: the domain can belong to a different business now`,\n );\n }\n }\n if (namesVenueResult === false && options.requireName === true) {\n notes.push(\"requireName: the recipe read no hours from a site that does not name the venue\");\n } else {\n readDoc(home);\n for (const link of rankHoursLinks(linksOf(home.meta), homeUrl).slice(0, extra)) {\n if (options.signal?.aborted === true) break;\n const doc = await readPage(link.href);\n if (doc !== undefined) readDoc(doc);\n }\n }\n }\n const moved = homeUrl !== null && hostOf(homeUrl) !== hostOf(site.href) ? homeUrl : null;\n if (moved !== null) notes.push(`${site.href} now goes to ${moved}`);\n if (heldBack) notes.push(HELD_BACK);\n\n // one reading for each source, label and week: a site that repeats its JSON-LD on each page said it once\n const firstOf = new Map<string, VenueReading>();\n for (const one of readings) {\n const key = `${one.from}|${one.label ?? \"\"}|${one.hours}|${one.season ?? \"\"}`;\n if (!firstOf.has(key)) firstOf.set(key, one);\n }\n // the best first; of two equal readings, the reading as written before a guess\n const unique = [...firstOf.values()].sort(\n (a, b) => a.rank - b.rank || Number(a.inferred) - Number(b.inferred),\n );\n const disagree = new Set<Day>();\n const whole = unique.filter((one) => one.rank <= 2);\n for (let i = 0; i < whole.length; i += 1) {\n for (let j = i + 1; j < whole.length; j += 1) {\n for (const day of disagreements(whole[i]!.week, whole[j]!.week)) disagree.add(day);\n }\n }\n const best = unique.find((one) => one.schedule === \"opening\") ?? null;\n const fromSite =\n unique.find((one) => one.from !== \"osm\" && one.schedule === \"opening\" && !one.inferred) ?? null;\n return {\n url: site.href,\n home: homeUrl,\n moved,\n names_venue: namesVenueResult,\n pages,\n readings: unique,\n best,\n from_site: fromSite,\n disagree: DAYS.filter((day) => disagree.has(day)),\n menus,\n phone,\n captures: [...captures],\n held_back: heldBack,\n note: notes.length > 0 ? notes.join(\"; \") : null,\n };\n};\n","/*\n * The product recipe reads a product page: its schema.org Product data. Most shop systems write\n * this data on each product page. The recipe gives the name, the brand, the codes (SKU and GTIN),\n * the price as an amount and a currency, a range of prices across the variants, the availability,\n * the seller, the image, and the variant that the URL names.\n *\n * A shop that refuses the crawler is a clean stop: the result says `wall`, with a note, and the\n * crawler does not try a different identity.\n */\nimport type { Doc } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedOf } from \"./fallback.ts\";\nimport { extractPage, typesOf } from \"./html.ts\";\nimport type { RecipeTarget } from \"./recipe.ts\";\nimport { deadlineOf, runtimeOf } from \"./recipe.ts\";\nimport type { CrawlRuntime, PageOptions, PageResult } from \"./runtime.ts\";\nimport { HELD_BACK, fetchPage, isHeldBack, recordOverride } from \"./runtime.ts\";\nimport { decodeBody } from \"./text.ts\";\n\n/** This is a JSON object, for example a schema.org record of a page. */\nexport type JsonRecord = Readonly<Record<string, unknown>>;\n\n/**\n * These are the limits of the product reader. A page with more is hostile or broken, and the reader stops there.\n *\n * @beta\n */\nexport const PRODUCT_LIMITS: {\n /** The most JSON-LD records that the reader examines. */\n readonly records: number;\n /** The most offers of one product or variant. */\n readonly offers: number;\n /** The most variants of one product. */\n readonly variants: number;\n /** The levels of `hasVariant` and of nested offers that the reader follows. */\n readonly depth: number;\n /** The longest text of a price. */\n readonly priceChars: number;\n} = { records: 500, offers: 200, variants: 200, depth: 2, priceChars: 64 };\n\n/** An amount of money, as schema.org Product data gives it. */\nexport interface Money {\n /** This value is the amount in the unit of the currency, for example 12.5. */\n readonly amount: number;\n /** This value is the ISO 4217 code of the currency, for example `EUR`. */\n readonly currency: string;\n /** This text is the price as the page wrote it, for a check with `extract`. */\n readonly raw: string;\n}\n\n/** The variant of a product that the URL names. */\nexport interface ProductVariant {\n readonly name: string | null;\n readonly sku: string | null;\n readonly price: Money | null;\n readonly availability: string | null;\n}\n\n/** The result of `productOf`: what the Product data of a page says. */\nexport interface ProductRead {\n /** This value is true when the page has a Product record. */\n readonly found: boolean;\n readonly name: string | null;\n readonly brand: string | null;\n readonly sku: string | null;\n readonly gtin: string | null;\n /** This value is the lowest price of the offers, in the first currency of the page. */\n readonly price: Money | null;\n /** This value is the highest price, when the offers give a range (sizes, colors, variants). */\n readonly price_to: Money | null;\n /**\n * This value is the schema.org name of the availability, for example `InStock`. When the offers\n * do not agree, it is `InStock` if one offer is in stock, and `availabilities` gives each value.\n */\n readonly availability: string | null;\n readonly availabilities: readonly string[];\n /** This value is the variant that the URL names (`?variant=`, `sku` or `gtin`), read alone. */\n readonly variant: ProductVariant | null;\n readonly seller: string | null;\n readonly image: string | null;\n readonly notes: readonly string[];\n /**\n * This value counts the work of the reader. The tests use it, because it does not change with the computer.\n *\n * @beta\n */\n readonly steps: number;\n}\n\nconst PRODUCT_TYPES = /^(?:Product|ProductGroup|IndividualProduct|ProductModel|SomeProducts)$/;\n\n/** A list price or a struck-through price is the old price, not the price. */\nconst WAS_PRICE = /ListPrice|StrikethroughPrice|MSRP|SRP/i;\n\nconst GTIN_KEYS = [\"gtin13\", \"gtin12\", \"gtin14\", \"gtin8\", \"gtin\"] as const;\n\n/** Two symbols that name one currency only. The other symbols (`$`, `¥`) name more than one. */\nconst SYMBOLS: Readonly<Record<string, string>> = { \"€\": \"EUR\", \"£\": \"GBP\" };\n\nlet knownCurrencies: ReadonlySet<string> | undefined;\n/** This function tells if a code is an ISO 4217 currency that the runtime knows. */\nconst isCurrency = (code: string): boolean => {\n if (!/^[A-Z]{3}$/.test(code)) return false;\n if (knownCurrencies === undefined) {\n try {\n knownCurrencies = new Set(Intl.supportedValuesOf(\"currency\"));\n } catch {\n // a runtime with no list: each code of three capital letters counts\n knownCurrencies = new Set();\n }\n }\n return knownCurrencies.size === 0 || knownCurrencies.has(code);\n};\n\n/** This function gives the number of digits after the decimal point of a currency: 2 for EUR, 0 for JPY. */\nconst fractionDigits = (currency: string | undefined): number | undefined => {\n if (currency === undefined) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions()\n .maximumFractionDigits;\n } catch {\n return undefined;\n }\n};\n\n/**\n * This function reads an amount as a shop writes it: `19.99`, `1,299.00`, `1.299,00`, `$19.99` or\n * the number 19.99. The last separator is the decimal point, with one exception. One kind of\n * separator, each followed by three digits, separates thousands: `1.299` is 1299. For a\n * currency with three decimal digits, one point with three digits is the decimal point. A text\n * with two numbers (a range), a negative amount or zero gives undefined.\n *\n * @beta\n */\nexport const readAmount = (value: unknown, currency?: string): number | undefined => {\n if (typeof value === \"number\") return Number.isFinite(value) && value > 0 ? value : undefined;\n if (typeof value !== \"string\" || value.length > PRODUCT_LIMITS.priceChars) return undefined;\n // one number, with the currency and the spaces around it\n const match = /^[^\\d-]*?(\\d(?:[\\d.,' ]*\\d)?)[^\\d]*$/.exec(value.trim());\n if (match === null) return undefined;\n let digits = match[1]!.replace(/[' ]/g, \"\");\n const last = Math.max(digits.lastIndexOf(\".\"), digits.lastIndexOf(\",\"));\n if (last !== -1) {\n const kinds = new Set(digits.match(/[.,]/g));\n const groups = kinds.size === 1 && /^[1-9]\\d{0,2}(?:[.,]\\d{3})+$/.test(digits);\n const decimalThree = groups && fractionDigits(currency) === 3 && /^\\d+\\.\\d{3}$/.test(digits);\n digits =\n groups && !decimalThree\n ? digits.replace(/[.,]/g, \"\")\n : `${digits.slice(0, last).replace(/[.,]/g, \"\")}.${digits.slice(last + 1)}`;\n }\n const amount = Number(digits);\n return Number.isFinite(amount) && amount > 0 ? amount : undefined;\n};\n\nconst list = (value: unknown, limit: number): unknown[] =>\n (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]).slice(\n 0,\n limit,\n );\n\nconst objects = (value: unknown, limit: number): JsonRecord[] =>\n list(value, limit).filter(\n (item): item is JsonRecord => typeof item === \"object\" && item !== null && !Array.isArray(item),\n );\n\n/** This function gives a name, written as a text or as an object that has one. */\nconst nameOf = (value: unknown): string | null => {\n for (const item of list(value, 20)) {\n if (typeof item === \"string\" && item.trim().length > 0) return item.trim().slice(0, 500);\n if (typeof item === \"object\" && item !== null) {\n const named = (item as JsonRecord)[\"name\"];\n if (typeof named === \"string\" && named.trim().length > 0) return named.trim().slice(0, 500);\n }\n }\n return null;\n};\n\nconst textOf = (value: unknown): string | null =>\n typeof value === \"string\" && value.trim().length > 0\n ? value.trim().slice(0, 200)\n : typeof value === \"number\" && Number.isFinite(value)\n ? String(value)\n : null;\n\nconst imageOf = (value: unknown): string | null => {\n for (const item of list(value, 20)) {\n if (typeof item === \"string\" && /^https?:/i.test(item)) return item;\n const url = typeof item === \"object\" && item !== null ? (item as JsonRecord)[\"url\"] : undefined;\n if (typeof url === \"string\" && /^https?:/i.test(url)) return url;\n }\n return null;\n};\n\ninterface Offers {\n readonly prices: Money[];\n readonly unknownCurrencies: Set<string>;\n readonly availability: Set<string>;\n readonly sellers: Set<string>;\n /** An amount with no currency, which is not a price. */\n unstated: boolean;\n steps: number;\n}\n\nconst emptyOffers = (): Offers => ({\n prices: [],\n unknownCurrencies: new Set(),\n availability: new Set(),\n sellers: new Set(),\n unstated: false,\n steps: 0,\n});\n\n/** This function reads the offers of a product into `read`, and the offers of its variants to `depth` levels. */\nconst readOffers = (product: JsonRecord, read: Offers, depth: number): void => {\n const money = (value: unknown, currency: unknown): void => {\n const written = typeof currency === \"string\" ? currency.trim() : \"\";\n const code = SYMBOLS[written] ?? written.toUpperCase();\n const amount = readAmount(value, code.length > 0 ? code : undefined);\n if (amount === undefined) return;\n if (code.length === 0) {\n read.unstated = true;\n return;\n }\n if (!isCurrency(code)) {\n read.unknownCurrencies.add(written.slice(0, 20));\n return;\n }\n read.prices.push({\n amount,\n currency: code,\n raw: String(value).slice(0, PRODUCT_LIMITS.priceChars),\n });\n };\n const offer = (o: JsonRecord, level: number): void => {\n read.steps += 1;\n const spec = objects(o[\"priceSpecification\"], PRODUCT_LIMITS.offers).find((entry) => {\n const kind = entry[\"priceType\"];\n return !(typeof kind === \"string\" && WAS_PRICE.test(kind));\n });\n const currency = o[\"priceCurrency\"] ?? spec?.[\"priceCurrency\"];\n money(o[\"price\"] ?? spec?.[\"price\"], currency);\n money(o[\"lowPrice\"], currency);\n money(o[\"highPrice\"], currency);\n const availability =\n typeof o[\"availability\"] === \"string\"\n ? o[\"availability\"]\n .trim()\n .replace(/^.*[/#:]/, \"\")\n .slice(0, 60)\n : undefined;\n if (availability !== undefined && availability.length > 0) read.availability.add(availability);\n const seller = nameOf(o[\"seller\"]);\n if (seller !== null) read.sellers.add(seller);\n // an AggregateOffer can list its offers\n if (level < PRODUCT_LIMITS.depth) {\n for (const inner of objects(o[\"offers\"], PRODUCT_LIMITS.offers)) offer(inner, level + 1);\n }\n };\n for (const o of objects(product[\"offers\"], PRODUCT_LIMITS.offers)) offer(o, 0);\n if (depth <= 0) return;\n for (const variant of objects(product[\"hasVariant\"], PRODUCT_LIMITS.variants)) {\n read.steps += 1;\n readOffers(variant, read, depth - 1);\n }\n};\n\nconst availabilityOf = (read: Offers): string | null =>\n read.availability.size === 0\n ? null\n : read.availability.size === 1 || !read.availability.has(\"InStock\")\n ? [...read.availability][0]!\n : \"InStock\";\n\nconst pathKey = (value: string, page: string): string | undefined => {\n // \"#product\" names a part of this page: a related product can have such an id too\n if (value.startsWith(\"#\")) return undefined;\n try {\n const url = new URL(value, page);\n return `${url.hostname.toLowerCase().replace(/^www\\./, \"\")}${url.pathname.replace(/\\/$/, \"\")}`;\n } catch {\n return undefined;\n }\n};\n\n/** This function gives the size of a record as JSON, or 0 for a record that JSON cannot write (a nesting too deep). */\nconst sizeOf = (record: JsonRecord): number => {\n try {\n return JSON.stringify(record).length;\n } catch {\n return 0;\n }\n};\n\n/**\n * This function picks the product of a page from its JSON-LD records. A page often has more\n * records of products: the related products, the products that others bought. It picks:\n *\n * 1. The product whose `url` or `@id` is the page (the path, with no query).\n * 2. Else the largest product with offers or variants.\n * 3. Else the largest product.\n *\n * It also reads the `mainEntity` of a record (a `WebPage` or an `ItemPage` that names its product).\n *\n * @beta\n */\nexport const pickProduct = (\n records: readonly JsonRecord[],\n page: string,\n): { readonly product: JsonRecord | undefined; readonly steps: number } => {\n let steps = 0;\n const products: JsonRecord[] = [];\n for (const record of records.slice(0, PRODUCT_LIMITS.records)) {\n steps += 1;\n if (typesOf(record as Record<string, unknown>).some((type) => PRODUCT_TYPES.test(type)))\n products.push(record);\n for (const inner of objects(record[\"mainEntity\"], 20)) {\n steps += 1;\n if (typesOf(inner as Record<string, unknown>).some((type) => PRODUCT_TYPES.test(type)))\n products.push(inner);\n }\n }\n const here = pathKey(page, page);\n const own = products.find((product) =>\n [product[\"url\"], product[\"@id\"]].some(\n (value) => typeof value === \"string\" && here !== undefined && pathKey(value, page) === here,\n ),\n );\n if (own !== undefined) return { product: own, steps };\n const selling = products.filter(\n (product) => list(product[\"offers\"], 1).length > 0 || list(product[\"hasVariant\"], 1).length > 0,\n );\n const pool = selling.length > 0 ? selling : products;\n let best: JsonRecord | undefined;\n let size = -1;\n for (const product of pool) {\n const length = sizeOf(product);\n steps += length;\n if (length > size) {\n best = product;\n size = length;\n }\n }\n return { product: best, steps };\n};\n\n/**\n * This function gives the variant that a URL names: the `variant` parameter of Shopify, or a `sku` or `gtin` parameter.\n *\n * @beta\n */\nexport const pickVariant = (product: JsonRecord, page: string): JsonRecord | undefined => {\n let params: URLSearchParams;\n try {\n params = new URL(page).searchParams;\n } catch {\n return undefined;\n }\n const variantId = params.get(\"variant\");\n const code = params.get(\"sku\") ?? params.get(\"gtin\");\n if (variantId === null && code === null) return undefined;\n const names = (value: unknown): boolean => {\n if (typeof value !== \"string\" || variantId === null) return false;\n try {\n return (\n new URL(value, page).searchParams.get(\"variant\") === variantId ||\n value.endsWith(`/${variantId}`)\n );\n } catch {\n return false;\n }\n };\n return objects(product[\"hasVariant\"], PRODUCT_LIMITS.variants).find((variant) => {\n const offers = objects(variant[\"offers\"], PRODUCT_LIMITS.offers);\n const codes = [\n variant[\"sku\"],\n variant[\"gtin\"],\n variant[\"gtin13\"],\n variant[\"gtin12\"],\n variant[\"productID\"],\n ]\n .map(textOf)\n .filter((one): one is string => one !== null);\n return (\n (code !== null && codes.includes(code)) ||\n [variant[\"@id\"], variant[\"url\"], ...offers.map((offer) => offer[\"url\"])].some(names) ||\n (variantId !== null && codes.includes(variantId))\n );\n });\n};\n\nconst byAmount = (a: Money, b: Money): number => a.amount - b.amount;\n\n/**\n * This function reads the Product data of a page from its JSON-LD records. It is pure, and it sends\n * no request. Thus it also reads a version at a time, or the stored JSON-LD of a document.\n *\n * - The prices are in the first currency of the offers. A price with no currency is not a price.\n * - A list price or a struck-through price is not the price.\n * - A currency must be an ISO 4217 code (`€` and `£` count too). The notes name each other one.\n */\nexport const productOf = (records: readonly JsonRecord[], page: string): ProductRead => {\n const picked = pickProduct(records, page);\n const product = picked.product;\n if (product === undefined) {\n return {\n found: false,\n name: null,\n brand: null,\n sku: null,\n gtin: null,\n price: null,\n price_to: null,\n availability: null,\n availabilities: [],\n variant: null,\n seller: null,\n image: null,\n notes: [\n \"no schema.org Product data on the page: a list of products, or a shop that does not publish it\",\n ],\n steps: picked.steps,\n };\n }\n const read = emptyOffers();\n readOffers(product, read, PRODUCT_LIMITS.depth);\n const currency = read.prices[0]?.currency;\n const sorted = read.prices.filter((price) => price.currency === currency).sort(byAmount);\n const low = sorted[0] ?? null;\n const high = sorted.at(-1) ?? null;\n const gtin = GTIN_KEYS.map((key) => textOf(product[key])).find((value) => value !== null);\n const chosen = pickVariant(product, page);\n let variant: ProductVariant | null = null;\n let steps = picked.steps + read.steps;\n if (chosen !== undefined) {\n const own = emptyOffers();\n readOffers(chosen, own, 0);\n steps += own.steps;\n const prices = own.prices.filter((price) => price.currency === currency).sort(byAmount);\n variant = {\n name: nameOf(chosen[\"name\"]),\n sku: textOf(chosen[\"sku\"]),\n price: prices[0] ?? null,\n availability: availabilityOf(own),\n };\n }\n const notes = [\n read.unknownCurrencies.size > 0\n ? `a currency that is not an ISO 4217 code: ${[...read.unknownCurrencies].join(\", \")}`\n : null,\n read.unstated && sorted.length === 0\n ? \"the page gives a price with no currency, so the price is not read as money\"\n : sorted.length === 0\n ? \"the page names the product and gives no price\"\n : null,\n new Set(read.prices.map((price) => price.currency)).size > 1\n ? `the offers have more than one currency: the prices are in ${currency ?? \"\"}`\n : null,\n /[?&](?:variant|sku|gtin)=/.test(page) && chosen === undefined\n ? \"the URL names a variant that the Product data does not list: the price is the whole range\"\n : null,\n ].filter((note): note is string => note !== null);\n return {\n found: true,\n name: nameOf(product[\"name\"]),\n brand: nameOf(product[\"brand\"]),\n sku: textOf(product[\"sku\"]),\n gtin: gtin ?? null,\n price: low,\n price_to: high !== null && low !== null && high.amount > low.amount ? high : null,\n availability: availabilityOf(read),\n availabilities: [...read.availability],\n variant,\n seller: read.sellers.size === 0 ? null : [...read.sellers].join(\", \"),\n image: imageOf(product[\"image\"]),\n notes,\n steps,\n };\n};\n\n// ── the recipe ──────────────────────────────────────────────────────────────\n\n/** These options control `readProduct`. */\nexport interface ProductOptions {\n /** The value `auto` (the default) renders once more when the plain page has no Product data. `never` does not render. */\n readonly render?: \"auto\" | \"never\" | \"always\" | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for example `Claude-User`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n readonly signal?: AbortSignal | undefined;\n /** No request starts after this time from the start of the read, for example `\"1m\"`. */\n readonly budget?: Duration | undefined;\n readonly scope?: string | undefined;\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** This name connects the captures to the job that asked for them. The default is `product`. */\n readonly job?: string | undefined;\n /** This value is true when a person gave this URL directly. It is the only way to read a held-back page. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /** The value `archive` reads an archived copy of a page that the shop refuses. Its prices are of the date of the copy. */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** The result of `readProduct`. */\nexport interface ProductResult extends Omit<ProductRead, \"steps\" | \"notes\"> {\n readonly url: string;\n /** This value is true when the page says what it sells, in structured data: the Product has a name. */\n readonly verified: boolean;\n /**\n * This value is true when the site refused the crawler. The causes are a 401, a 402 or a 403, a\n * challenge, a bot check, a stop of the host or robots.txt. The recipe stops there, and the note\n * says why. With `fallback: \"archive\"`, it stays true when an archived copy answers.\n */\n readonly wall: boolean;\n readonly outcome: string;\n readonly status: number;\n readonly rendered: boolean;\n readonly doc: string | null;\n /** This id is the capture of the text that the recipe read. A citation names it. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly captures: readonly string[];\n readonly from_cache: boolean;\n /** This value is true when the page asked not to be AI input, so the recipe read nothing from it. */\n readonly held_back: boolean;\n /** This value tells that the page is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\nconst WALLS: ReadonlySet<string> = new Set([\"blocked\", \"challenge\", \"payment-required\"]);\n\n/** This function tells if the site refused the crawler. */\nconst wallOf = (page: PageResult): boolean =>\n WALLS.has(page.outcome) ||\n (page.outcome === \"skipped\" && /^(?:robots\\.txt disallows|host stopped)/.test(page.note ?? \"\")) ||\n (page.doc === null && /\\bbot check\\b/.test(page.note ?? \"\"));\n\nconst answered = (page: PageResult): boolean =>\n page.doc !== null &&\n (page.outcome === \"ok\" || page.outcome === \"not-modified\" || page.archived !== undefined);\n\n/**\n * This function gives the JSON-LD records of a document from its stored body. The index keeps a\n * part of the JSON-LD of a large page only, and a product with many variants can be larger. An\n * archived copy gives the JSON-LD that the index keeps.\n */\nconst recordsOfDoc = async (runtime: CrawlRuntime, doc: Doc): Promise<readonly JsonRecord[]> => {\n const kept = Array.isArray(doc.meta[\"jsonld\"]) ? (doc.meta[\"jsonld\"] as JsonRecord[]) : [];\n if (doc.blob === null || archivedOf(doc.meta) !== undefined) return kept;\n const body = await runtime.blobs.get(doc.blob);\n if (body === undefined) return kept;\n const at = doc.url ?? \"\";\n const type = doc.rendered\n ? \"text/html; charset=utf-8\"\n : (runtime.index.latest(at, \"plain\")?.content_type ?? null);\n try {\n return extractPage(decodeBody(body, type), at).jsonld;\n } catch {\n return kept;\n }\n};\n\n/**\n * This function reads a product page. It fetches the page through the crawler: robots.txt, the\n * pace, the stops, the capture log and the rule for a held-back page apply. When the plain page has\n * no Product data, it renders the page once, because some shops write that data with JavaScript.\n *\n * A shop that refuses the crawler gives `wall: true` and a note. The recipe writes nothing but the\n * capture log. `extract` can check each value against the stored page.\n */\nexport const readProduct = async (\n target: RecipeTarget,\n url: string,\n options: ProductOptions = {},\n): Promise<ProductResult> => {\n const runtime = runtimeOf(target);\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") throw new TypeError(url);\n } catch {\n throw new TypeError(`readProduct needs an http or https URL, not ${JSON.stringify(url)}`);\n }\n const maxAge = options.maxAge ?? \"24h\";\n const job = options.job ?? \"product\";\n const userProvided = options.userProvided ?? false;\n const render = options.render ?? \"auto\";\n const deadline = deadlineOf(runtime, options.budget);\n const pageOptions = (mode: \"auto\" | \"never\" | \"always\"): PageOptions => ({\n render: mode,\n maxAgeMs: maxAge === \"auto\" ? \"auto\" : durationMs(maxAge),\n onBehalfOf: options.onBehalfOf,\n job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline,\n fallback: options.fallback,\n archiveDespiteRobots: options.archiveDespiteRobots,\n });\n let page = await fetchPage(runtime, url, pageOptions(render));\n const notes: string[] = [];\n const result = (\n from: PageResult,\n read: Omit<ProductRead, \"steps\"> | undefined,\n more: { readonly held_back?: boolean } = {},\n ): ProductResult => {\n const all = [from.note, ...notes, ...(read?.notes ?? [])].filter(\n (part): part is string => part !== null && part !== undefined && part.length > 0,\n );\n return {\n url,\n found: read?.found ?? false,\n verified: read?.found === true && read.name !== null,\n name: read?.name ?? null,\n brand: read?.brand ?? null,\n sku: read?.sku ?? null,\n gtin: read?.gtin ?? null,\n price: read?.price ?? null,\n price_to: read?.price_to ?? null,\n availability: read?.availability ?? null,\n availabilities: read?.availabilities ?? [],\n variant: read?.variant ?? null,\n seller: read?.seller ?? null,\n image: read?.image ?? null,\n wall: wallOf(from),\n outcome: from.outcome,\n status: from.status,\n rendered: from.rendered,\n doc: from.doc,\n capture: from.capture,\n fetched_at: from.fetched_at,\n captures: [...from.captures],\n from_cache: from.from_cache,\n held_back: more.held_back ?? false,\n ...(from.archived === undefined ? {} : { archived: from.archived }),\n note: all.length > 0 ? [...new Set(all)].join(\"; \") : null,\n };\n };\n if (!answered(page)) {\n const wall = wallOf(page);\n notes.push(\n wall\n ? `the shop refused the crawler (${page.outcome}): the recipe read nothing, and it does not try a different identity`\n : `the page gave no document (${page.outcome})`,\n );\n return result(page, undefined);\n }\n const held = (from: PageResult): Doc | undefined => {\n const doc = from.doc === null ? undefined : runtime.index.get(from.doc);\n return doc !== undefined && isHeldBack(doc, userProvided) ? doc : undefined;\n };\n if (held(page) !== undefined) {\n notes.push(HELD_BACK);\n return result(page, undefined, { held_back: true });\n }\n const docOf = (from: PageResult): Doc | undefined =>\n from.doc === null ? undefined : runtime.index.get(from.doc);\n let doc = docOf(page);\n let read = doc === undefined ? undefined : productOf(await recordsOfDoc(runtime, doc), url);\n // some shops write their Product data with JavaScript: one render before the recipe says that the page has none\n if (read?.found !== true && !page.rendered && render === \"auto\" && page.archived === undefined) {\n const rendered = await fetchPage(runtime, url, pageOptions(\"always\"));\n const shown = docOf(rendered);\n if (\n rendered.rendered &&\n answered(rendered) &&\n shown !== undefined &&\n held(rendered) === undefined\n ) {\n page = rendered;\n doc = shown;\n read = productOf(await recordsOfDoc(runtime, shown), url);\n } else if (rendered.note !== null && !rendered.rendered) {\n notes.push(`the render gave nothing more: ${rendered.note}`);\n }\n }\n if (userProvided && doc?.ai_input === \"no\") await recordOverride(runtime, doc, job);\n return result(page, read);\n};\n"],"mappings":";;;;;;;;;;;AAuCA,MAAa,eAaT;CAAE,OAAO;CAAG,UAAU;CAAG,OAAO;CAAG,iBAAiB;CAAI,OAAO;CAAK,YAAY;AAAI;AAIxF,MAAM,6BAAkC,IAAI,IAAI;CAAC;CAAO;CAAK;CAAM;CAAO;CAAM;AAAI,CAAC;;AAGrF,MAAM,WAAW,SACf,KACG,YAAY,CAAC,CACb,UAAU,MAAM,CAAC,CACjB,QAAQ,WAAW,EAAE,CAAC,CACtB,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,oBAAoB,GAAG,CAAC,CAChC,MAAM,KAAK,CAAC,CACZ,QAAQ,SAAS,KAAK,SAAS,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC;;;;;;;;AAS9D,MAAa,cACX,MACA,SACwB;CACxB,MAAM,QAAQ,QAAQ,IAAI;CAC1B,IAAI,MAAM,WAAW,GAAG,OAAO,KAAA;CAC/B,MAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,KAAK,MAAM,GAAG,KAAK,MAAM,CAAC;CAC5D,MAAM,QAAQ,IAAI,IAAI,KAAK;CAC3B,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,SAAS,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC;CAC5D,OAAO,OAAO,IAAI,MAAM,EAAG,KAAK,QAAQ,MAAM,QAAQ;AACxD;;AAKA,MAAM,aAAuD;CAC3D,CAAC,kCAAkC,CAAC;CACpC,CAAC,iCAAiC,CAAC;CACnC,CAAC,8CAA8C,CAAC;CAChD,CAAC,cAAc,CAAC;CAChB,CAAC,iCAAiC,CAAC;AACrC;;AAGA,MAAM,SACJ;;AAGF,MAAM,OAAO;AAEb,MAAM,UAAU,QAAoC;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,QAAQ,UAAU,EAAE;CACjE,QAAQ;EACN;CACF;AACF;AAEA,MAAM,eAAe,QAAwB,IAAI,QAAQ,SAAS,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE;;;;;;;;AASvF,MAAa,kBAAkB,OAA4B,SAA6B;CACtF,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,OAAO,YAAY,IAAI;CAC7B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAqE,CAAC;CAC5E,KAAK,MAAM,QAAQ,MAAM,MAAM,GAAG,aAAa,KAAK,GAAG;EACrD,IAAI,OAAO,MAAM,SAAS,YAAY,OAAO,KAAK,IAAI,MAAM,QAAQ,KAAK,KAAK,KAAK,IAAI,GACrF;EACF,MAAM,MAAM,YAAY,KAAK,IAAI;EACjC,IAAI,QAAQ,QAAQ,KAAK,IAAI,GAAG,GAAG;EACnC,IAAI,OAAO;EACX,IAAI;GACF,OAAO,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC;EAC5B,QAAQ;GACN;EACF;EACA,MAAM,QAAQ,GAAG,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,GAAG,GAAG,OAAO,YAAY;EACtF,MAAM,QAAQ,WAAW,QACtB,KAAK,CAAC,SAAS,YAAY,OAAO,QAAQ,KAAK,KAAK,IAAI,SAAS,IAClE,CACF;EACA,IAAI,SAAS,GAAG;EAChB,KAAK,IAAI,GAAG;EACZ,OAAO,KAAK;GAAE;GAAM;EAAM,CAAC;CAC7B;CAEA,OAAO,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,KAAK,UAAU,MAAM,IAAI;AAC3E;AAuBA,MAAM,eAAe;;AAGrB,MAAM,iBAAiB,GAAU,MAAqB;CACpD,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,QAAQ,EAAE,MAAM,EAAE,OAAO;CAC/B,MAAM,QAAQ,EAAE,MAAM,EAAE,OAAO;CAC/B,MAAM,IACJ,KAAK,IAAI,OAAO,CAAC,KAAK,IACtB,KAAK,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK;CACxE,OAAO,IAAI,eAAe,KAAK,KAAK,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC;AAC/D;AAEA,MAAM,WAAW,KAAc,QAAoC;CACjE,MAAM,KAAK,OAAO,QAAQ,WAAW,OAAO,WAAW,GAAG,IAAI;CAC9D,MAAM,KAAK,OAAO,QAAQ,WAAW,OAAO,WAAW,GAAG,IAAI;CAC9D,OAAO,OAAO,OAAO,YACnB,OAAO,OAAO,YACd,OAAO,SAAS,EAAE,KAClB,OAAO,SAAS,EAAE,KAClB,KAAK,IAAI,EAAE,KAAK,MAChB,KAAK,IAAI,EAAE,KAAK,MACd;EAAE,KAAK;EAAI,KAAK;CAAG,IACnB,KAAA;AACN;;AAGA,MAAM,+BAAoC,IAAI,IAAI;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,YACJ,YACiF;CACjF,MAAM,QAAQ,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,QAAA,CAAS,MAAM,GAAG,GAAG,CAAC;CACtE,MAAM,SAAS,MAAM,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC;CAC5D,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;CACjC,OAAO;EACL;EACA,OAAO,IAAI,IAAI,MAAM,QAAQ,SAAS,CAAC,MAAM,KAAK,IAAI,KAAK,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC;CACrF;AACF;AAEA,MAAM,eAAe,UAAuC;CAC1D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,MAAM,EAAE;CACrD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,SAAU,MAAkC;EAClD,OAAO,OAAO,WAAW,WAAW,SAAS,KAAA;CAC/C;AAEF;;;;;;;AAQA,MAAa,mBACX,QACA,aACY;CACZ,MAAM,MAAM,OAAO;CACnB,MAAM,KACJ,OAAO,QAAQ,YAAY,QAAQ,OAC/B,QACG,IAAgC,aAChC,IAAgC,YACnC,IACA,KAAA;CACN,MAAM,OAAO,QAAQ,SAAS,KAAK,SAAS,GAAG;CAC/C,IAAI,OAAO,KAAA,KAAa,SAAS,KAAA,KAAa,cAAc,IAAI,IAAI,KAAK,aAAa,YACpF,OAAO;CACT,MAAM,SAAS,YAAY,OAAO,UAAU;CAC5C,IAAI,WAAW,KAAA,KAAa,SAAS,YAAY,KAAA,GAAW,OAAO;CACnE,MAAM,IAAI,SAAS,MAAM;CACzB,MAAM,IAAI,SAAS,SAAS,OAAO;CACnC,OACE,MAAM,KAAA,KACN,MAAM,KAAA,KACN,EAAE,WAAW,EAAE,UACf,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,MAAM,SAAS,EAAE,MAAM,IAAI,IAAI,CAAC;AAEjD;AAEA,MAAM,WAAW,WACf,OAAO,eAAe,KAAA,KAAa,OAAO,WAAW,KAAA;;AAmEvD,MAAM,cACJ;;;;;AAMF,MAAM,iBACJ;;;;;AAMF,MAAM,UACJ;AAEF,MAAM,UAAU,MAA4B,UAAiC;CAC3E,IAAI,SAAS,UAAU,OAAO;CAC9B,IAAI,SAAS,OAAO,OAAO;CAC3B,MAAM,OAAO,SAAS;CACtB,IAAI,QAAQ,KAAK,IAAI,GAAG,OAAO;CAC/B,IAAI,aAAa,KAAK,IAAI,GAAG,OAAO;CACpC,OAAO,eAAe,KAAK,IAAI,IAAI,IAAI;AACzC;AAEA,MAAM,WACJ,MACA,OACA,OACA,WAQiB;CACjB,MAAM,OAAO,OAAO,MAAM,KAAK;CAC/B,OAAO;EACL;EACA;EACA,UAAU,QAAQ,IAAI,YAAY;EAClC;EACA,OAAO,aAAa,MAAM,IAAI;EAC9B,MAAM,MAAM;EACZ,UAAU,MAAM;EAChB,SAAS,MAAM,YAAY;EAC3B,QAAQ,MAAM,UAAU;EACxB,KAAK,MAAM;EACX,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,SAAS,OAAO;EAChB,YAAY,OAAO;EACnB,GAAI,OAAO,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,OAAO,SAAS;CACvE;AACF;AAEA,MAAM,aAAa,SAAuE;CACxF,MAAM,SAAS,KAAK;CACpB,OAAO,MAAM,QAAQ,MAAM,IACvB,OAAO,QACJ,WACC,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,CAC1E,IACA,CAAC;AACP;AAEA,MAAM,WAAW,SAAwD;CACvE,MAAM,QAAQ,KAAK;CACnB,OAAO,MAAM,QAAQ,KAAK,IACtB,MAAM,QACH,SACC,SAAS,QAAQ,OAAO,SAAS,YAAY,OAAQ,KAAkB,SAAS,QACpF,IACA,CAAC;AACP;;;;;;;;;;AAWA,MAAa,mBACX,QACA,UAA6D,CAAC,MAChD;CACd,IAAI,QAAQ;CACZ,MAAM,QAAkB,CAAC;CACzB,MAAM,WAAW,WAAW,OAAO,IAAI;CACvC,MAAM,QAAQ;EACZ,KAAK,UAAU,YAAY,OAAO;EAClC,KAAK,OAAO,OAAO;EACnB,SAAS,OAAO,WAAW;EAC3B,YAAY,OAAO,cAAc;EACjC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;CAC/C;CACA,MAAM,MAAM,UAAU,OAAO,IAAI;CACjC,SAAS,IAAI;CACb,MAAM,SAAS,IACZ,KAAK,QAAQ,WAAW;EAAE;EAAQ;CAAM,EAAE,CAAC,CAC3C,QAAQ,EAAE,aAAa,QAAQ,MAAM,CAAC,CAAC,MAAM,SAAS,YAAY,KAAK,IAAI,CAAC,CAAC;CAEhF,IAAI,SAAS;CACb,MAAM,SAAS,OAAO,QAAQ,EAAE,aAAa,QAAQ,MAAM,CAAC;CAC5D,IAAI,OAAO,SAAS,GAAG;EACrB,MAAM,WAAW,QAAQ;EACzB,MAAM,OACJ,aAAa,KAAA,IACT,CAAC,IACD,OAAO,QAAQ,EAAE,aAAa,gBAAgB,QAAQ,QAAQ,CAAC;EACrE,IAAI,KAAK,WAAW,GAClB,MAAM,KACJ,aAAa,KAAA,IACT,GAAG,MAAM,OAAO,WAAW,SAAS,OAAO,OAAO,MAAM,EAAE,sDAC1D,GAAG,MAAM,OAAO,WAAW,SAAS,OAAO,OAAO,MAAM,EAAE,qEAChE;EAEF,SAAS,CAAC,GAAG,MAAM,GAAG,OAAO,QAAQ,EAAE,aAAa,CAAC,QAAQ,MAAM,CAAC,CAAC;CACvE;CACA,MAAM,WAA2B,CAAC;CAClC,IAAI,QAAuB;CAC3B,KAAK,MAAM,EAAE,QAAQ,WAAW,QAAQ;EACtC,SAAS;EACT,MAAM,QAAQ,cAAc,MAAM;EAClC,IAAI,UAAU,KAAA,KAAa,SAAS,SAAS,aAAa,iBACxD,SAAS,KAAK,QAAQ,UAAU,OAAO,MAAM;GAAE,GAAG;GAAO,MAAM,UAAU,OAAO,KAAK,EAAE;EAAG,CAAC,CAAC;EAE9F,MAAM,YAAY,OAAO;EACzB,IAAI,UAAU,QAAQ,OAAO,cAAc,YAAY,UAAU,KAAK,CAAC,CAAC,SAAS,GAC/E,QAAQ,UAAU,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;CACzC;CACA,SAAS,OAAO,KAAK;CACrB,KAAK,MAAM,SAAS,SAAS,OAAO,IAAI,GAAG;EACzC,IAAI,SAAS,UAAU,aAAa,iBAAiB;GACnD,MAAM,KACJ,GAAG,MAAM,OAAO,WAAW,mBAAmB,OAAO,aAAa,eAAe,EAAE,kCACrF;GACA;EACF;EACA,SAAS,KAAK,QAAQ,QAAQ,OAAO,MAAM,OAAO;GAAE,GAAG;GAAO,MAAM;EAAK,CAAC,CAAC;CAC7E;CACA,MAAM,QAAqB,CAAC;CAC5B,KAAK,MAAM,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAAC,MAAM,GAAG,aAAa,KAAK,GAAG;EACpE,SAAS;EACT,IAAI,MAAM,UAAU,aAAa,OAAO;EACxC,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI;EACvE,MAAM,OAAO,OAAO,KAAK,IAAI;EAC7B,IAAI,SAAS,KAAA,KAAa,OAAO,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK,MAAM,GAAG;EACtF,IAAI,MAAM,MAAM,SAAS,KAAK,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,KAAK;GAAE,KAAK,KAAK;GAAM;EAAK,CAAC;CACrC;CACA,OAAO;EAAE;EAAU;EAAO;EAAO;EAAO;CAAM;AAChD;AAyFA,MAAMA,cAAY,SAChB,KAAK,YAAY,QAAQ,KAAK,YAAY,kBAAkB,KAAK,aAAa,KAAA;;;;;;;;;;;AAYhF,MAAa,YAAY,OACvB,QACA,KACA,UAAwB,CAAC,MACA;CACzB,MAAM,UAAU,UAAU,MAAM;CAChC,MAAM,QAAQ,QAAQ,SAAS,aAAa;CAC5C,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,aAAa,UAChE,MAAM,IAAI,WACR,0CAA0C,OAAO,aAAa,QAAQ,EAAE,QAAQ,OAAO,KAAK,GAC9F;CAEF,IAAI;CACJ,IAAI;EACF,OAAO,IAAI,IAAI,GAAG;CACpB,QAAQ;EACN,MAAM,IAAI,UAAU,0CAA0C,KAAK,UAAU,GAAG,GAAG;CACrF;CACA,IAAI,KAAK,aAAa,WAAW,KAAK,aAAa,UACjD,MAAM,IAAI,UAAU,6CAA6C,KAAK;CACxE,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,cAA2B;EAC/B,QAAQ,QAAQ,UAAU;EAC1B,UAAU,WAAW,SAAS,SAAS,WAAW,MAAM;EACxD,YAAY,QAAQ;EACpB;EACA,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB,UAAU,WAAW,SAAS,QAAQ,MAAM;EAC5C,UAAU,QAAQ;EAClB,sBAAsB,QAAQ;CAChC;CACA,MAAM,QAAkB,CAAC;CACzB,MAAM,WAA2B,CAAC;CAClC,MAAM,QAAqB,CAAC;CAC5B,MAAM,QAAqB,CAAC;CAC5B,MAAM,2BAAW,IAAI,IAAY;CACjC,IAAI,QAAuB;CAC3B,IAAI,WAAW;CAEf,IAAI,QAAQ,QAAQ,KAAA,GAAW;EAC7B,MAAM,MAAgB,OAAO,QAAQ,QAAQ,WAAW,EAAE,OAAO,QAAQ,IAAI,IAAI,QAAQ;EACzF,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,YAAY,IAAI,OAAO,KAAK,IAAI,KAAA;EAC9E,IAAI,UAAU,KAAA,GACZ,MAAM,KACJ,2CAA2C,KAAK,UAAU,OAAO,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAC3F;OACK;GACL,SAAS,KACP,QAAQ,OAAO,OAAO,MAAM;IAC1B,KAAK,IAAI,OAAO;IAChB,KAAK,IAAI,OAAO;IAChB,SAAS,IAAI,WAAW;IACxB,YAAY,IAAI,cAAc;IAC9B,MAAM;GACR,CAAC,CACH;GACA,IAAI,OAAO,IAAI,YAAY,UAAU,SAAS,IAAI,IAAI,OAAO;EAC/D;CACF;;CAGA,MAAM,WAAW,OAAO,YAA8C;EACpE,MAAM,OAAO,MAAM,UAAU,SAAS,SAAS,WAAW;EAC1D,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;EACtE,MAAM,OAAO,QAAQ,KAAA,KAAa,WAAW,KAAK,YAAY;EAC9D,MAAM,KAAK;GACT,KAAK;GACL,KAAK,KAAK;GACV,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,SAAS,KAAK;GACd,YAAY,KAAK;GACjB,YAAY,KAAK;GACjB,WAAW;GACX,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS;GACjE,MAAM,KAAK;EACb,CAAC;EACD,IAAI,CAACA,WAAS,IAAI,GAChB,MAAM,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,KAAK,SAAS,OAAO,KAAK,KAAK,KAAK,KAAK,IAAI;EAExF,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAA;EAC9B,IAAI,MAAM;GACR,WAAW;GACX;EACF;EACA,IAAI,IAAI,aAAa,MAAM,MAAM,eAAe,SAAS,KAAK,GAAG;EACjE,SAAS,IAAI,IAAI,OAAO;EACxB,OAAO;CACT;CAEA,MAAM,WAAW,QAAmB;EAClC,MAAM,OAAO,gBAAgB,KAAK,EAAE,UAAU,QAAQ,SAAS,CAAC;EAChE,SAAS,KAAK,GAAG,KAAK,QAAQ;EAC9B,MAAM,KAAK,GAAG,KAAK,KAAK;EACxB,UAAU,KAAK;EACf,KAAK,MAAM,QAAQ,KAAK,OACtB,IAAI,MAAM,SAAS,aAAa,SAAS,CAAC,MAAM,MAAM,UAAU,MAAM,QAAQ,KAAK,GAAG,GACpF,MAAM,KAAK,IAAI;CAErB;CAEA,IAAI,UAAyB;CAC7B,IAAI,mBAAmC;CACvC,MAAM,OAAO,MAAM,SAAS,KAAK,IAAI;CACrC,IAAI,SAAS,KAAA,GAAW;EACtB,UAAU,WAAW,KAAK,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,KAAK;EAC9D,IAAI,QAAQ,SAAS,KAAA,GAAW;GAC9B,mBAAmB,WAAW,QAAQ,MAAM,IAAI,KAAK;GACrD,IAAI,qBAAqB,OACvB,MAAM,KACJ,GAAG,QAAQ,iBAAiB,QAAQ,KAAK,oDAC3C;EAEJ;EACA,IAAI,qBAAqB,SAAS,QAAQ,gBAAgB,MACxD,MAAM,KAAK,gFAAgF;OACtF;GACL,QAAQ,IAAI;GACZ,KAAK,MAAM,QAAQ,eAAe,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,KAAK,GAAG;IAC9E,IAAI,QAAQ,QAAQ,YAAY,MAAM;IACtC,MAAM,MAAM,MAAM,SAAS,KAAK,IAAI;IACpC,IAAI,QAAQ,KAAA,GAAW,QAAQ,GAAG;GACpC;EACF;CACF;CACA,MAAM,QAAQ,YAAY,QAAQ,OAAO,OAAO,MAAM,OAAO,KAAK,IAAI,IAAI,UAAU;CACpF,IAAI,UAAU,MAAM,MAAM,KAAK,GAAG,KAAK,KAAK,eAAe,OAAO;CAClE,IAAI,UAAU,MAAM,KAAK,SAAS;CAGlC,MAAM,0BAAU,IAAI,IAA0B;CAC9C,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,IAAI,SAAS,GAAG,GAAG,IAAI,MAAM,GAAG,IAAI,UAAU;EACzE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,QAAQ,IAAI,KAAK,GAAG;CAC7C;CAEA,MAAM,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,MAClC,GAAG,MAAM,EAAE,OAAO,EAAE,QAAQ,OAAO,EAAE,QAAQ,IAAI,OAAO,EAAE,QAAQ,CACrE;CACA,MAAM,2BAAW,IAAI,IAAS;CAC9B,MAAM,QAAQ,OAAO,QAAQ,QAAQ,IAAI,QAAQ,CAAC;CAClD,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACrC,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACzC,KAAK,MAAM,OAAO,cAAc,MAAM,EAAE,CAAE,MAAM,MAAM,EAAE,CAAE,IAAI,GAAG,SAAS,IAAI,GAAG;CAGrF,MAAM,OAAO,OAAO,MAAM,QAAQ,IAAI,aAAa,SAAS,KAAK;CACjE,MAAM,WACJ,OAAO,MAAM,QAAQ,IAAI,SAAS,SAAS,IAAI,aAAa,aAAa,CAAC,IAAI,QAAQ,KAAK;CAC7F,OAAO;EACL,KAAK,KAAK;EACV,MAAM;EACN;EACA,aAAa;EACb;EACA,UAAU;EACV;EACA,WAAW;EACX,UAAU,KAAK,QAAQ,QAAQ,SAAS,IAAI,GAAG,CAAC;EAChD;EACA;EACA,UAAU,CAAC,GAAG,QAAQ;EACtB,WAAW;EACX,MAAM,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI;CAC9C;AACF;;;;;;;;AC7vBA,MAAa,iBAWT;CAAE,SAAS;CAAK,QAAQ;CAAK,UAAU;CAAK,OAAO;CAAG,YAAY;AAAG;AAmDzE,MAAM,gBAAgB;;AAGtB,MAAM,YAAY;AAElB,MAAM,YAAY;CAAC;CAAU;CAAU;CAAU;CAAS;AAAM;;AAGhE,MAAM,UAA4C;CAAE,KAAK;CAAO,KAAK;AAAM;AAE3E,IAAI;;AAEJ,MAAM,cAAc,SAA0B;CAC5C,IAAI,CAAC,aAAa,KAAK,IAAI,GAAG,OAAO;CACrC,IAAI,oBAAoB,KAAA,GACtB,IAAI;EACF,kBAAkB,IAAI,IAAI,KAAK,kBAAkB,UAAU,CAAC;CAC9D,QAAQ;EAEN,kCAAkB,IAAI,IAAI;CAC5B;CAEF,OAAO,gBAAgB,SAAS,KAAK,gBAAgB,IAAI,IAAI;AAC/D;;AAGA,MAAM,kBAAkB,aAAqD;CAC3E,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;CACnC,IAAI;EACF,OAAO,IAAI,KAAK,aAAa,MAAM;GAAE,OAAO;GAAY;EAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAClF;CACL,QAAQ;EACN;CACF;AACF;;;;;;;;;;AAWA,MAAa,cAAc,OAAgB,aAA0C;CACnF,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ,KAAA;CACpF,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,eAAe,YAAY,OAAO,KAAA;CAElF,MAAM,QAAQ,yCAAyC,KAAK,MAAM,KAAK,CAAC;CACxE,IAAI,UAAU,MAAM,OAAO,KAAA;CAC3B,IAAI,SAAS,MAAM,EAAE,CAAE,QAAQ,WAAW,EAAE;CAC5C,MAAM,OAAO,KAAK,IAAI,OAAO,YAAY,GAAG,GAAG,OAAO,YAAY,GAAG,CAAC;CACtE,IAAI,SAAS,IAAI;EAEf,MAAM,SAAS,IADG,IAAI,OAAO,MAAM,OAAO,CACvB,CAAC,CAAC,SAAS,KAAK,+BAA+B,KAAK,MAAM;EAC7E,MAAM,eAAe,UAAU,eAAe,QAAQ,MAAM,KAAK,eAAe,KAAK,MAAM;EAC3F,SACE,UAAU,CAAC,eACP,OAAO,QAAQ,SAAS,EAAE,IAC1B,GAAG,OAAO,MAAM,GAAG,IAAI,CAAC,CAAC,QAAQ,SAAS,EAAE,EAAE,GAAG,OAAO,MAAM,OAAO,CAAC;CAC9E;CACA,MAAM,SAAS,OAAO,MAAM;CAC5B,OAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS,KAAA;AAC1D;AAEA,MAAM,QAAQ,OAAgB,WAC3B,MAAM,QAAQ,KAAK,IAAI,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO,CAAC,IAAI,CAAC,KAAK,EAAA,CAAG,MACpF,GACA,KACF;AAEF,MAAM,WAAW,OAAgB,UAC/B,KAAK,OAAO,KAAK,CAAC,CAAC,QAChB,SAA6B,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAChG;;AAGF,MAAM,UAAU,UAAkC;CAChD,KAAK,MAAM,QAAQ,KAAK,OAAO,EAAE,GAAG;EAClC,IAAI,OAAO,SAAS,YAAY,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;EACvF,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAC7C,MAAM,QAAS,KAAoB;GACnC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;EAC5F;CACF;CACA,OAAO;AACT;AAEA,MAAM,UAAU,UACd,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAC/C,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,IACzB,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAChD,OAAO,KAAK,IACZ;AAER,MAAM,WAAW,UAAkC;CACjD,KAAK,MAAM,QAAQ,KAAK,OAAO,EAAE,GAAG;EAClC,IAAI,OAAO,SAAS,YAAY,YAAY,KAAK,IAAI,GAAG,OAAO;EAC/D,MAAM,MAAM,OAAO,SAAS,YAAY,SAAS,OAAQ,KAAoB,SAAS,KAAA;EACtF,IAAI,OAAO,QAAQ,YAAY,YAAY,KAAK,GAAG,GAAG,OAAO;CAC/D;CACA,OAAO;AACT;AAYA,MAAM,qBAA6B;CACjC,QAAQ,CAAC;CACT,mCAAmB,IAAI,IAAI;CAC3B,8BAAc,IAAI,IAAI;CACtB,yBAAS,IAAI,IAAI;CACjB,UAAU;CACV,OAAO;AACT;;AAGA,MAAM,cAAc,SAAqB,MAAc,UAAwB;CAC7E,MAAM,SAAS,OAAgB,aAA4B;EACzD,MAAM,UAAU,OAAO,aAAa,WAAW,SAAS,KAAK,IAAI;EACjE,MAAM,OAAO,QAAQ,YAAY,QAAQ,YAAY;EACrD,MAAM,SAAS,WAAW,OAAO,KAAK,SAAS,IAAI,OAAO,KAAA,CAAS;EACnE,IAAI,WAAW,KAAA,GAAW;EAC1B,IAAI,KAAK,WAAW,GAAG;GACrB,KAAK,WAAW;GAChB;EACF;EACA,IAAI,CAAC,WAAW,IAAI,GAAG;GACrB,KAAK,kBAAkB,IAAI,QAAQ,MAAM,GAAG,EAAE,CAAC;GAC/C;EACF;EACA,KAAK,OAAO,KAAK;GACf;GACA,UAAU;GACV,KAAK,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,eAAe,UAAU;EACvD,CAAC;CACH;CACA,MAAM,SAAS,GAAe,UAAwB;EACpD,KAAK,SAAS;EACd,MAAM,OAAO,QAAQ,EAAE,uBAAuB,eAAe,MAAM,CAAC,CAAC,MAAM,UAAU;GACnF,MAAM,OAAO,MAAM;GACnB,OAAO,EAAE,OAAO,SAAS,YAAY,UAAU,KAAK,IAAI;EAC1D,CAAC;EACD,MAAM,WAAW,EAAE,oBAAoB,OAAO;EAC9C,MAAM,EAAE,YAAY,OAAO,UAAU,QAAQ;EAC7C,MAAM,EAAE,aAAa,QAAQ;EAC7B,MAAM,EAAE,cAAc,QAAQ;EAC9B,MAAM,eACJ,OAAO,EAAE,oBAAoB,WACzB,EAAE,eAAe,CACd,KAAK,CAAC,CACN,QAAQ,YAAY,EAAE,CAAC,CACvB,MAAM,GAAG,EAAE,IACd,KAAA;EACN,IAAI,iBAAiB,KAAA,KAAa,aAAa,SAAS,GAAG,KAAK,aAAa,IAAI,YAAY;EAC7F,MAAM,SAAS,OAAO,EAAE,SAAS;EACjC,IAAI,WAAW,MAAM,KAAK,QAAQ,IAAI,MAAM;EAE5C,IAAI,QAAQ,eAAe,OACzB,KAAK,MAAM,SAAS,QAAQ,EAAE,WAAW,eAAe,MAAM,GAAG,MAAM,OAAO,QAAQ,CAAC;CAE3F;CACA,KAAK,MAAM,KAAK,QAAQ,QAAQ,WAAW,eAAe,MAAM,GAAG,MAAM,GAAG,CAAC;CAC7E,IAAI,SAAS,GAAG;CAChB,KAAK,MAAM,WAAW,QAAQ,QAAQ,eAAe,eAAe,QAAQ,GAAG;EAC7E,KAAK,SAAS;EACd,WAAW,SAAS,MAAM,QAAQ,CAAC;CACrC;AACF;AAEA,MAAM,kBAAkB,SACtB,KAAK,aAAa,SAAS,IACvB,OACA,KAAK,aAAa,SAAS,KAAK,CAAC,KAAK,aAAa,IAAI,SAAS,IAC9D,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,KACvB;AAER,MAAM,WAAW,OAAe,SAAqC;CAEnE,IAAI,MAAM,WAAW,GAAG,GAAG,OAAO,KAAA;CAClC,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,OAAO,IAAI;EAC/B,OAAO,GAAG,IAAI,SAAS,YAAY,CAAC,CAAC,QAAQ,UAAU,EAAE,IAAI,IAAI,SAAS,QAAQ,OAAO,EAAE;CAC7F,QAAQ;EACN;CACF;AACF;;AAGA,MAAM,UAAU,WAA+B;CAC7C,IAAI;EACF,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;CAChC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;AAcA,MAAa,eACX,SACA,SACyE;CACzE,IAAI,QAAQ;CACZ,MAAM,WAAyB,CAAC;CAChC,KAAK,MAAM,UAAU,QAAQ,MAAM,GAAG,eAAe,OAAO,GAAG;EAC7D,SAAS;EACT,IAAI,QAAQ,MAAiC,CAAC,CAAC,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC,GACpF,SAAS,KAAK,MAAM;EACtB,KAAK,MAAM,SAAS,QAAQ,OAAO,eAAe,EAAE,GAAG;GACrD,SAAS;GACT,IAAI,QAAQ,KAAgC,CAAC,CAAC,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC,GACnF,SAAS,KAAK,KAAK;EACvB;CACF;CACA,MAAM,OAAO,QAAQ,MAAM,IAAI;CAC/B,MAAM,MAAM,SAAS,MAAM,YACzB,CAAC,QAAQ,QAAQ,QAAQ,MAAM,CAAC,CAAC,MAC9B,UAAU,OAAO,UAAU,YAAY,SAAS,KAAA,KAAa,QAAQ,OAAO,IAAI,MAAM,IACzF,CACF;CACA,IAAI,QAAQ,KAAA,GAAW,OAAO;EAAE,SAAS;EAAK;CAAM;CACpD,MAAM,UAAU,SAAS,QACtB,YAAY,KAAK,QAAQ,WAAW,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,QAAQ,eAAe,CAAC,CAAC,CAAC,SAAS,CAChG;CACA,MAAM,OAAO,QAAQ,SAAS,IAAI,UAAU;CAC5C,IAAI;CACJ,IAAI,OAAO;CACX,KAAK,MAAM,WAAW,MAAM;EAC1B,MAAM,SAAS,OAAO,OAAO;EAC7B,SAAS;EACT,IAAI,SAAS,MAAM;GACjB,OAAO;GACP,OAAO;EACT;CACF;CACA,OAAO;EAAE,SAAS;EAAM;CAAM;AAChC;;;;;;AAOA,MAAa,eAAe,SAAqB,SAAyC;CACxF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC;CACzB,QAAQ;EACN;CACF;CACA,MAAM,YAAY,OAAO,IAAI,SAAS;CACtC,MAAM,OAAO,OAAO,IAAI,KAAK,KAAK,OAAO,IAAI,MAAM;CACnD,IAAI,cAAc,QAAQ,SAAS,MAAM,OAAO,KAAA;CAChD,MAAM,SAAS,UAA4B;EACzC,IAAI,OAAO,UAAU,YAAY,cAAc,MAAM,OAAO;EAC5D,IAAI;GACF,OACE,IAAI,IAAI,OAAO,IAAI,CAAC,CAAC,aAAa,IAAI,SAAS,MAAM,aACrD,MAAM,SAAS,IAAI,WAAW;EAElC,QAAQ;GACN,OAAO;EACT;CACF;CACA,OAAO,QAAQ,QAAQ,eAAe,eAAe,QAAQ,CAAC,CAAC,MAAM,YAAY;EAC/E,MAAM,SAAS,QAAQ,QAAQ,WAAW,eAAe,MAAM;EAC/D,MAAM,QAAQ;GACZ,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;EACV,CAAC,CACE,IAAI,MAAM,CAAC,CACX,QAAQ,QAAuB,QAAQ,IAAI;EAC9C,OACG,SAAS,QAAQ,MAAM,SAAS,IAAI,KACrC;GAAC,QAAQ;GAAQ,QAAQ;GAAQ,GAAG,OAAO,KAAK,UAAU,MAAM,MAAM;EAAC,CAAC,CAAC,KAAK,KAAK,KAClF,cAAc,QAAQ,MAAM,SAAS,SAAS;CAEnD,CAAC;AACH;AAEA,MAAM,YAAY,GAAU,MAAqB,EAAE,SAAS,EAAE;;;;;;;;;AAU9D,MAAa,aAAa,SAAgC,SAA8B;CACtF,MAAM,SAAS,YAAY,SAAS,IAAI;CACxC,MAAM,UAAU,OAAO;CACvB,IAAI,YAAY,KAAA,GACd,OAAO;EACL,OAAO;EACP,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,OAAO;EACP,UAAU;EACV,cAAc;EACd,gBAAgB,CAAC;EACjB,SAAS;EACT,QAAQ;EACR,OAAO;EACP,OAAO,CACL,gGACF;EACA,OAAO,OAAO;CAChB;CAEF,MAAM,OAAO,YAAY;CACzB,WAAW,SAAS,MAAM,eAAe,KAAK;CAC9C,MAAM,WAAW,KAAK,OAAO,EAAE,EAAE;CACjC,MAAM,SAAS,KAAK,OAAO,QAAQ,UAAU,MAAM,aAAa,QAAQ,CAAC,CAAC,KAAK,QAAQ;CACvF,MAAM,MAAM,OAAO,MAAM;CACzB,MAAM,OAAO,OAAO,GAAG,EAAE,KAAK;CAC9B,MAAM,OAAO,UAAU,KAAK,QAAQ,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,UAAU,UAAU,IAAI;CACxF,MAAM,SAAS,YAAY,SAAS,IAAI;CACxC,IAAI,UAAiC;CACrC,IAAI,QAAQ,OAAO,QAAQ,KAAK;CAChC,IAAI,WAAW,KAAA,GAAW;EACxB,MAAM,MAAM,YAAY;EACxB,WAAW,QAAQ,KAAK,CAAC;EACzB,SAAS,IAAI;EACb,MAAM,SAAS,IAAI,OAAO,QAAQ,UAAU,MAAM,aAAa,QAAQ,CAAC,CAAC,KAAK,QAAQ;EACtF,UAAU;GACR,MAAM,OAAO,OAAO,OAAO;GAC3B,KAAK,OAAO,OAAO,MAAM;GACzB,OAAO,OAAO,MAAM;GACpB,cAAc,eAAe,GAAG;EAClC;CACF;CACA,MAAM,QAAQ;EACZ,KAAK,kBAAkB,OAAO,IAC1B,4CAA4C,CAAC,GAAG,KAAK,iBAAiB,CAAC,CAAC,KAAK,IAAI,MACjF;EACJ,KAAK,YAAY,OAAO,WAAW,IAC/B,+EACA,OAAO,WAAW,IAChB,kDACA;EACN,IAAI,IAAI,KAAK,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC,CAAC,OAAO,IACvD,6DAA6D,YAAY,OACzE;EACJ,4BAA4B,KAAK,IAAI,KAAK,WAAW,KAAA,IACjD,8FACA;CACN,CAAC,CAAC,QAAQ,SAAyB,SAAS,IAAI;CAChD,OAAO;EACL,OAAO;EACP,MAAM,OAAO,QAAQ,OAAO;EAC5B,OAAO,OAAO,QAAQ,QAAQ;EAC9B,KAAK,OAAO,QAAQ,MAAM;EAC1B,MAAM,QAAQ;EACd,OAAO;EACP,UAAU,SAAS,QAAQ,QAAQ,QAAQ,KAAK,SAAS,IAAI,SAAS,OAAO;EAC7E,cAAc,eAAe,IAAI;EACjC,gBAAgB,CAAC,GAAG,KAAK,YAAY;EACrC;EACA,QAAQ,KAAK,QAAQ,SAAS,IAAI,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI;EACpE,OAAO,QAAQ,QAAQ,QAAQ;EAC/B;EACA;CACF;AACF;AAqDA,MAAM,wBAA6B,IAAI,IAAI;CAAC;CAAW;CAAa;AAAkB,CAAC;;AAGvF,MAAM,UAAU,SACd,MAAM,IAAI,KAAK,OAAO,KACrB,KAAK,YAAY,aAAa,0CAA0C,KAAK,KAAK,QAAQ,EAAE,KAC5F,KAAK,QAAQ,QAAQ,gBAAgB,KAAK,KAAK,QAAQ,EAAE;AAE5D,MAAM,YAAY,SAChB,KAAK,QAAQ,SACZ,KAAK,YAAY,QAAQ,KAAK,YAAY,kBAAkB,KAAK,aAAa,KAAA;;;;;;AAOjF,MAAM,eAAe,OAAO,SAAuB,QAA6C;CAC9F,MAAM,OAAO,MAAM,QAAQ,IAAI,KAAK,SAAS,IAAK,IAAI,KAAK,YAA6B,CAAC;CACzF,IAAI,IAAI,SAAS,QAAQ,WAAW,IAAI,IAAI,MAAM,KAAA,GAAW,OAAO;CACpE,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,IAAI;CAC7C,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,MAAM,KAAK,IAAI,OAAO;CACtB,MAAM,OAAO,IAAI,WACb,6BACC,QAAQ,MAAM,OAAO,IAAI,OAAO,CAAC,EAAE,gBAAgB;CACxD,IAAI;EACF,OAAO,YAAY,WAAW,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;CACjD,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;AAUA,MAAa,cAAc,OACzB,QACA,KACA,UAA0B,CAAC,MACA;CAC3B,MAAM,UAAU,UAAU,MAAM;CAChC,IAAI;EACF,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU,MAAM,IAAI,UAAU,GAAG;CAC1F,QAAQ;EACN,MAAM,IAAI,UAAU,+CAA+C,KAAK,UAAU,GAAG,GAAG;CAC1F;CACA,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,WAAW,WAAW,SAAS,QAAQ,MAAM;CACnD,MAAM,eAAe,UAAoD;EACvE,QAAQ;EACR,UAAU,WAAW,SAAS,SAAS,WAAW,MAAM;EACxD,YAAY,QAAQ;EACpB;EACA,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB;EACA,UAAU,QAAQ;EAClB,sBAAsB,QAAQ;CAChC;CACA,IAAI,OAAO,MAAM,UAAU,SAAS,KAAK,YAAY,MAAM,CAAC;CAC5D,MAAM,QAAkB,CAAC;CACzB,MAAM,UACJ,MACA,MACA,OAAyC,CAAC,MACxB;EAClB,MAAM,MAAM;GAAC,KAAK;GAAM,GAAG;GAAO,GAAI,MAAM,SAAS,CAAC;EAAE,CAAC,CAAC,QACvD,SAAyB,SAAS,QAAQ,SAAS,KAAA,KAAa,KAAK,SAAS,CACjF;EACA,OAAO;GACL;GACA,OAAO,MAAM,SAAS;GACtB,UAAU,MAAM,UAAU,QAAQ,KAAK,SAAS;GAChD,MAAM,MAAM,QAAQ;GACpB,OAAO,MAAM,SAAS;GACtB,KAAK,MAAM,OAAO;GAClB,MAAM,MAAM,QAAQ;GACpB,OAAO,MAAM,SAAS;GACtB,UAAU,MAAM,YAAY;GAC5B,cAAc,MAAM,gBAAgB;GACpC,gBAAgB,MAAM,kBAAkB,CAAC;GACzC,SAAS,MAAM,WAAW;GAC1B,QAAQ,MAAM,UAAU;GACxB,OAAO,MAAM,SAAS;GACtB,MAAM,OAAO,IAAI;GACjB,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,KAAK,KAAK;GACV,SAAS,KAAK;GACd,YAAY,KAAK;GACjB,UAAU,CAAC,GAAG,KAAK,QAAQ;GAC3B,YAAY,KAAK;GACjB,WAAW,KAAK,aAAa;GAC7B,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS;GACjE,MAAM,IAAI,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI;EACxD;CACF;CACA,IAAI,CAAC,SAAS,IAAI,GAAG;EACnB,MAAM,OAAO,OAAO,IAAI;EACxB,MAAM,KACJ,OACI,iCAAiC,KAAK,QAAQ,wEAC9C,8BAA8B,KAAK,QAAQ,EACjD;EACA,OAAO,OAAO,MAAM,KAAA,CAAS;CAC/B;CACA,MAAM,QAAQ,SAAsC;EAClD,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;EACtE,OAAO,QAAQ,KAAA,KAAa,WAAW,KAAK,YAAY,IAAI,MAAM,KAAA;CACpE;CACA,IAAI,KAAK,IAAI,MAAM,KAAA,GAAW;EAC5B,MAAM,KAAK,SAAS;EACpB,OAAO,OAAO,MAAM,KAAA,GAAW,EAAE,WAAW,KAAK,CAAC;CACpD;CACA,MAAM,SAAS,SACb,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;CAC5D,IAAI,MAAM,MAAM,IAAI;CACpB,IAAI,OAAO,QAAQ,KAAA,IAAY,KAAA,IAAY,UAAU,MAAM,aAAa,SAAS,GAAG,GAAG,GAAG;CAE1F,IAAI,MAAM,UAAU,QAAQ,CAAC,KAAK,YAAY,WAAW,UAAU,KAAK,aAAa,KAAA,GAAW;EAC9F,MAAM,WAAW,MAAM,UAAU,SAAS,KAAK,YAAY,QAAQ,CAAC;EACpE,MAAM,QAAQ,MAAM,QAAQ;EAC5B,IACE,SAAS,YACT,SAAS,QAAQ,KACjB,UAAU,KAAA,KACV,KAAK,QAAQ,MAAM,KAAA,GACnB;GACA,OAAO;GACP,MAAM;GACN,OAAO,UAAU,MAAM,aAAa,SAAS,KAAK,GAAG,GAAG;EAC1D,OAAO,IAAI,SAAS,SAAS,QAAQ,CAAC,SAAS,UAC7C,MAAM,KAAK,iCAAiC,SAAS,MAAM;CAE/D;CACA,IAAI,gBAAgB,KAAK,aAAa,MAAM,MAAM,eAAe,SAAS,KAAK,GAAG;CAClF,OAAO,OAAO,MAAM,IAAI;AAC1B"}
|
|
1
|
+
{"version":3,"file":"recipe-product-B6XmkpB3.mjs","names":["answered"],"sources":["../src/recipe-venue.ts","../src/recipe-product.ts"],"sourcesContent":["/*\n * The venue recipe reads the opening hours of a venue (a restaurant, a shop, a museum) from its own\n * site. It reads the cheap sources first:\n *\n * 1. The home page: its JSON-LD (`openingHoursSpecification` and `openingHours`) and its text.\n * 2. The few pages of the site that most likely hold hours or a menu, by the words of their links.\n * 3. An OpenStreetMap `opening_hours` value, when the caller gives one. The recipe sends no\n * request to OpenStreetMap.\n *\n * Each reading names the page, the document and the capture that it came from. The recipe gives\n * readings only. To compare them with the hours that an application keeps is the work of the\n * application, and `compareHours` is the pure helper for it.\n */\nimport type { Doc } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedOf } from \"./fallback.ts\";\nimport type { Day, Hours, Week } from \"./hours.ts\";\nimport {\n DAYS,\n describeWeek,\n disagreements,\n fromCompact,\n fromSchemaOrg,\n fromText,\n} from \"./hours.ts\";\nimport type { PageLink } from \"./html.ts\";\nimport { typesOf } from \"./html.ts\";\nimport type { RecipeTarget } from \"./recipe.ts\";\nimport { deadlineOf, runtimeOf } from \"./recipe.ts\";\nimport type { PageOptions, PageResult } from \"./runtime.ts\";\nimport { HELD_BACK, fetchPage, isHeldBack, recordOverride } from \"./runtime.ts\";\n\n/**\n * These are the limits of the venue recipe.\n *\n * @beta\n */\nexport const VENUE_LIMITS: {\n /** The pages of the site that a read fetches after the home page, by default. */\n readonly pages: number;\n /** The most pages after the home page. */\n readonly maxPages: number;\n /** The most menu links of a result. */\n readonly menus: number;\n /** The most readings of one page. A page with more is a list of something else. */\n readonly readingsPerPage: number;\n /** The most links of one page that the recipe ranks. */\n readonly links: number;\n /** The distance in metres within which a JSON-LD location is the venue. */\n readonly nearMetres: number;\n} = { pages: 3, maxPages: 8, menus: 8, readingsPerPage: 20, links: 200, nearMetres: 150 };\n\n// ── words ───────────────────────────────────────────────────────────────────\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\"the\", \"a\", \"an\", \"and\", \"of\", \"at\"]);\n\n/** This function gives the words of a text in one form: lower case, no accents, and `&` as `and`. */\nconst wordsOf = (text: string): string[] =>\n text\n .toLowerCase()\n .normalize(\"NFKD\")\n .replace(/\\p{M}/gu, \"\")\n .replace(/&/g, \" and \")\n .replace(/[^\\p{L}\\p{N} ]/gu, \" \")\n .split(/\\s+/)\n .filter((word) => word.length > 0 && !STOP_WORDS.has(word));\n\n/**\n * This function tells if a page names a venue: the page has the first word of the name and 60\n * percent of its words. A domain that a venue let go can show a different business. It gives\n * undefined for a name with no word.\n *\n * @beta\n */\nexport const namesVenue = (\n name: string,\n page: { readonly title: string; readonly text: string },\n): boolean | undefined => {\n const words = wordsOf(name);\n if (words.length === 0) return undefined;\n const onPage = new Set(wordsOf(`${page.title} ${page.text}`));\n const named = new Set(words);\n const found = [...named].filter((word) => onPage.has(word)).length;\n return onPage.has(words[0]!) && found / named.size >= 0.6;\n};\n\n// ── the links of the site ───────────────────────────────────────────────────\n\n/** The words of a link that tell what its page holds, with their weights. */\nconst LINK_WORDS: ReadonlyArray<readonly [RegExp, number]> = [\n [/hours|horario|horaires|opening/, 6],\n [/menu|food|drink|wine|cocktail/, 4],\n [/contact|location|visit|find.?us|directions/, 3],\n [/about|info/, 2],\n [/lunch|dinner|brunch|breakfast/, 2],\n];\n\n/** The hosts of review and social sites: a menu there is a different page about the venue. */\nconst SOCIAL =\n /(?:^|\\.)(?:yelp|tripadvisor|facebook|instagram|twitter|x|tiktok|google|apple|spotify)\\.[a-z.]+$/i;\n\n/** The files that hold no hours as text. */\nconst FILE = /\\.(?:pdf|jpe?g|png|gif|webp|svg|mp4)(?:[?#]|$)/i;\n\nconst hostOf = (url: string): string | undefined => {\n try {\n return new URL(url).hostname.toLowerCase().replace(/^www\\./, \"\");\n } catch {\n return undefined;\n }\n};\n\nconst withoutHash = (url: string): string => url.replace(/#.*$/s, \"\").replace(/\\/$/, \"\");\n\n/**\n * This function ranks the links of a page by the words of their text and path. The links to\n * the hours or a menu come first. It keeps only the links on the site of `home`,\n * and it leaves out the files (a PDF, an image) and the home page itself.\n *\n * @beta\n */\nexport const rankHoursLinks = (links: readonly PageLink[], home: string): PageLink[] => {\n const site = hostOf(home);\n const self = withoutHash(home);\n const seen = new Set<string>();\n const scored: Array<{ readonly link: PageLink; readonly score: number }> = [];\n for (const link of links.slice(0, VENUE_LIMITS.links)) {\n if (typeof link?.href !== \"string\" || hostOf(link.href) !== site || FILE.test(link.href))\n continue;\n const key = withoutHash(link.href);\n if (key === self || seen.has(key)) continue;\n let path = \"\";\n try {\n path = new URL(link.href).pathname;\n } catch {\n continue;\n }\n const words = `${typeof link.text === \"string\" ? link.text : \"\"} ${path}`.toLowerCase();\n const score = LINK_WORDS.reduce(\n (sum, [pattern, weight]) => sum + (pattern.test(words) ? weight : 0),\n 0,\n );\n if (score <= 0) continue;\n seen.add(key);\n scored.push({ link, score });\n }\n // a stable sort: links of equal score keep the order of the page\n return scored.sort((a, b) => b.score - a.score).map((entry) => entry.link);\n};\n\n/** A link to a menu. */\nexport interface VenueLink {\n readonly url: string;\n readonly text: string;\n}\n\n// ── the location of a venue ─────────────────────────────────────────────────\n\n/** The place of a venue, to choose its hours on a page that lists more than one location. */\nexport interface VenueLocation {\n /** This text is the street address, for example `12 Via Esempio`. The number and the street count. */\n readonly address?: string | undefined;\n readonly lat?: number | undefined;\n readonly lon?: number | undefined;\n}\n\ninterface Point {\n readonly lat: number;\n readonly lon: number;\n}\n\nconst EARTH_METRES = 6_371_000;\n\n/** This function gives the distance in metres between two points on the earth (the haversine formula). */\nconst metresBetween = (a: Point, b: Point): number => {\n const rad = Math.PI / 180;\n const dLat = (b.lat - a.lat) * rad;\n const dLon = (b.lon - a.lon) * rad;\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(dLon / 2) ** 2;\n return 2 * EARTH_METRES * Math.asin(Math.min(1, Math.sqrt(h)));\n};\n\nconst pointOf = (lat: unknown, lon: unknown): Point | undefined => {\n const la = typeof lat === \"string\" ? Number.parseFloat(lat) : lat;\n const lo = typeof lon === \"string\" ? Number.parseFloat(lon) : lon;\n return typeof la === \"number\" &&\n typeof lo === \"number\" &&\n Number.isFinite(la) &&\n Number.isFinite(lo) &&\n Math.abs(la) <= 90 &&\n Math.abs(lo) <= 180\n ? { lat: la, lon: lo }\n : undefined;\n};\n\n/** The words of a street that tell nothing about which street: directions and kinds of road. */\nconst STREET_NOISE: ReadonlySet<string> = new Set([\n \"n\",\n \"s\",\n \"e\",\n \"w\",\n \"north\",\n \"south\",\n \"east\",\n \"west\",\n \"st\",\n \"street\",\n \"ave\",\n \"avenue\",\n \"rd\",\n \"road\",\n \"blvd\",\n \"boulevard\",\n \"dr\",\n \"drive\",\n \"ln\",\n \"lane\",\n \"ct\",\n \"court\",\n \"pl\",\n \"place\",\n \"sq\",\n \"square\",\n \"hwy\",\n \"highway\",\n \"pkwy\",\n \"parkway\",\n \"way\",\n \"ter\",\n \"terrace\",\n \"suite\",\n \"ste\",\n \"unit\",\n]);\n\n/** This function gives the number of a street address and the words that name its street. */\nconst streetOf = (\n address: string,\n): { readonly number: string; readonly words: ReadonlySet<string> } | undefined => {\n const words = wordsOf((address.split(\",\")[0] ?? address).slice(0, 200));\n const number = words.find((word) => /^\\d+[a-z]?$/.test(word));\n if (number === undefined) return undefined;\n return {\n number,\n words: new Set(words.filter((word) => !/^\\d/.test(word) && !STREET_NOISE.has(word))),\n };\n};\n\nconst addressText = (value: unknown): string | undefined => {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return addressText(value[0]);\n if (typeof value === \"object\" && value !== null) {\n const street = (value as Record<string, unknown>)[\"streetAddress\"];\n return typeof street === \"string\" ? street : undefined;\n }\n return undefined;\n};\n\n/**\n * This function tells if a JSON-LD location record is the venue. The record must have the same\n * number on a street that shares a word, or a position near the venue (`VENUE_LIMITS.nearMetres`).\n *\n * @beta\n */\nexport const locationMatches = (\n record: Readonly<Record<string, unknown>>,\n location: VenueLocation,\n): boolean => {\n const geo = record[\"geo\"];\n const at =\n typeof geo === \"object\" && geo !== null\n ? pointOf(\n (geo as Record<string, unknown>)[\"latitude\"],\n (geo as Record<string, unknown>)[\"longitude\"],\n )\n : undefined;\n const ours = pointOf(location.lat, location.lon);\n if (at !== undefined && ours !== undefined && metresBetween(at, ours) <= VENUE_LIMITS.nearMetres)\n return true;\n const theirs = addressText(record[\"address\"]);\n if (theirs === undefined || location.address === undefined) return false;\n const a = streetOf(theirs);\n const b = streetOf(location.address);\n return (\n a !== undefined &&\n b !== undefined &&\n a.number === b.number &&\n [...a.words].some((word) => b.words.has(word))\n );\n};\n\nconst located = (record: Readonly<Record<string, unknown>>): boolean =>\n record[\"address\"] !== undefined || record[\"geo\"] !== undefined;\n\n// ── the readings ────────────────────────────────────────────────────────────\n\n/** One reading of the hours, with the source that it came from. */\nexport interface VenueReading {\n /** The value `jsonld` is the structured data of a page, `text` the text of a page, and `osm` the value of the caller. */\n readonly from: \"jsonld\" | \"osm\" | \"text\";\n /**\n * This value orders the readings, 0 first. JSON-LD is 0, a text under an hours heading is 1 and\n * OpenStreetMap is 2. A text with no label of a different schedule is 3. A different schedule is\n * 4, for example a brunch, a kitchen, a happy hour or an office.\n */\n readonly rank: number;\n /** The value `opening` is the opening hours of the venue (a rank of 3 or less). Else it is `other`. */\n readonly schedule: \"opening\" | \"other\";\n /** This text is the line above a reading of the text, for example `Hours` or `Happy hour`. */\n readonly label: string | null;\n /** This text is the week as words, from `describeWeek`. */\n readonly hours: string;\n readonly week: Week;\n /** This value is true when the reader guessed an am or a pm. */\n readonly inferred: boolean;\n /** This value is true when the reader left out a rule or a line (a holiday, a date). */\n readonly partial: boolean;\n /** This text names the season or the dates of the hours. */\n readonly season: string | null;\n /** This text is the words that the hours came from. A reading of the text can be a quote for `verify`. */\n readonly raw: string;\n /** This path names the JSON-LD record of the reading in the metadata of the document, for example `jsonld[2]`. */\n readonly path: string | null;\n /** This URL is the page of the reading. */\n readonly url: string | null;\n readonly doc: string | null;\n /** This id is the capture of the text that the reading came from. A citation names it. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n /** This value tells that the page is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n}\n\n/** A stored document, or a version of one: what `venueReadingsOf` reads. */\nexport interface VenueSource {\n readonly doc?: string | null | undefined;\n readonly url: string | null;\n readonly text: string;\n readonly meta: Readonly<Record<string, unknown>>;\n readonly capture?: string | null | undefined;\n readonly fetched_at?: string | null | undefined;\n}\n\n/** The result of `venueReadingsOf`: what one document says. */\nexport interface VenueRead {\n readonly readings: readonly VenueReading[];\n /** This value is the first `telephone` of the JSON-LD records of the venue. */\n readonly phone: string | null;\n readonly menus: readonly VenueLink[];\n readonly notes: readonly string[];\n /**\n * This value counts the work of the reader. The tests use it, because it does not change with the computer.\n *\n * @beta\n */\n readonly steps: number;\n}\n\n/** The types of JSON-LD records that have the hours of a venue. */\nconst HOURS_TYPES =\n /Restaurant|LocalBusiness|FoodEstablishment|BarOrPub|CafeOrCoffeeShop|Bakery|Store|Place|Winery|Brewery|NightClub|Museum|TouristAttraction|Library|EntertainmentBusiness/;\n\n/**\n * A label that names a schedule that is not the opening hours. The line above a schedule is often a\n * different thing, for example a phone number, and such a line tells nothing.\n */\nconst OTHER_SCHEDULE =\n /brunch|lunch|dinner|breakfast|happy|kitchen|\bbar\b|deliver|take.?out|pick.?up|patio|holiday|season|summer|winter|special|late|menu/i;\n\n/**\n * A label of a service beside the opening hours, or of an office. Its hours are a different\n * schedule, also when the label says \"hours\" (\"Happy hour\", \"Kitchen hours\", \"Office hours\").\n */\nconst SERVICE =\n /happy|kitchen|deliver|take.?out|pick.?up|brunch|holiday|special|patio|\bbar\b|late.?night|office/i;\n\nconst rankOf = (from: VenueReading[\"from\"], label: string | null): number => {\n if (from === \"jsonld\") return 0;\n if (from === \"osm\") return 2;\n const text = label ?? \"\";\n if (SERVICE.test(text)) return 4;\n if (/hour|open/i.test(text)) return 1;\n return OTHER_SCHEDULE.test(text) ? 4 : 3;\n};\n\nconst reading = (\n from: VenueReading[\"from\"],\n hours: Hours,\n label: string | null,\n source: {\n readonly url: string | null;\n readonly doc: string | null;\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly path: string | null;\n readonly archived?: Archived | undefined;\n },\n): VenueReading => {\n const rank = rankOf(from, label);\n return {\n from,\n rank,\n schedule: rank <= 3 ? \"opening\" : \"other\",\n label,\n hours: describeWeek(hours.week),\n week: hours.week,\n inferred: hours.inferred,\n partial: hours.partial === true,\n season: hours.season ?? null,\n raw: hours.raw,\n path: source.path,\n url: source.url,\n doc: source.doc,\n capture: source.capture,\n fetched_at: source.fetched_at,\n ...(source.archived === undefined ? {} : { archived: source.archived }),\n };\n};\n\nconst recordsOf = (meta: Readonly<Record<string, unknown>>): Record<string, unknown>[] => {\n const jsonld = meta[\"jsonld\"];\n return Array.isArray(jsonld)\n ? jsonld.filter(\n (record): record is Record<string, unknown> =>\n record !== null && typeof record === \"object\" && !Array.isArray(record),\n )\n : [];\n};\n\nconst linksOf = (meta: Readonly<Record<string, unknown>>): PageLink[] => {\n const links = meta[\"links\"];\n return Array.isArray(links)\n ? links.filter(\n (link): link is PageLink =>\n link !== null && typeof link === \"object\" && typeof (link as PageLink).href === \"string\",\n )\n : [];\n};\n\n/**\n * This function reads the hours of one stored document, or of one version of it. It is pure, and\n * it sends no request. Thus it also reads a version at a time (`versionAtTime`) or an archived copy.\n *\n * - It reads each JSON-LD record of a venue type with `fromSchemaOrg`, and the text with `fromText`.\n * - On a page that lists more than one location, it reads only the location that matches\n * `location`. With no `location`, it reads none of them, and a note says so.\n * - It gives the first `telephone` and the menu links that are not on a review or social site.\n */\nexport const venueReadingsOf = (\n source: VenueSource,\n options: { readonly location?: VenueLocation | undefined } = {},\n): VenueRead => {\n let steps = 0;\n const notes: string[] = [];\n const archived = archivedOf(source.meta);\n const where = {\n url: archived?.original ?? source.url,\n doc: source.doc ?? null,\n capture: source.capture ?? null,\n fetched_at: source.fetched_at ?? null,\n ...(archived === undefined ? {} : { archived }),\n };\n const all = recordsOf(source.meta);\n steps += all.length;\n const venues = all\n .map((record, index) => ({ record, index }))\n .filter(({ record }) => typesOf(record).some((type) => HOURS_TYPES.test(type)));\n // a page of a group lists each of its locations: only the hours of this venue are its hours\n let usable = venues;\n const placed = venues.filter(({ record }) => located(record));\n if (placed.length > 1) {\n const location = options.location;\n const ours =\n location === undefined\n ? []\n : placed.filter(({ record }) => locationMatches(record, location));\n if (ours.length === 0) {\n notes.push(\n location === undefined\n ? `${where.url ?? \"the page\"} lists ${String(placed.length)} locations: give location to read the hours of one`\n : `${where.url ?? \"the page\"} lists ${String(placed.length)} locations, and none matched the location: their hours were not read`,\n );\n }\n usable = [...ours, ...venues.filter(({ record }) => !located(record))];\n }\n const readings: VenueReading[] = [];\n let phone: string | null = null;\n for (const { record, index } of usable) {\n steps += 1;\n const hours = fromSchemaOrg(record);\n if (hours !== undefined && readings.length < VENUE_LIMITS.readingsPerPage) {\n readings.push(reading(\"jsonld\", hours, null, { ...where, path: `jsonld[${String(index)}]` }));\n }\n const telephone = record[\"telephone\"];\n if (phone === null && typeof telephone === \"string\" && telephone.trim().length > 0)\n phone = telephone.trim().slice(0, 100);\n }\n steps += source.text.length;\n for (const hours of fromText(source.text)) {\n if (readings.length >= VENUE_LIMITS.readingsPerPage) {\n notes.push(\n `${where.url ?? \"the page\"} gives more than ${String(VENUE_LIMITS.readingsPerPage)} readings: the rest were not read`,\n );\n break;\n }\n readings.push(reading(\"text\", hours, hours.label, { ...where, path: null }));\n }\n const menus: VenueLink[] = [];\n for (const link of linksOf(source.meta).slice(0, VENUE_LIMITS.links)) {\n steps += 1;\n if (menus.length >= VENUE_LIMITS.menus) break;\n const text = typeof link.text === \"string\" ? link.text.slice(0, 200) : \"\";\n const host = hostOf(link.href);\n if (host === undefined || SOCIAL.test(host) || !/menu/i.test(`${text} ${link.href}`)) continue;\n if (menus.some((menu) => menu.url === link.href)) continue;\n menus.push({ url: link.href, text });\n }\n return { readings, phone, menus, notes, steps };\n};\n\n// ── the recipe ──────────────────────────────────────────────────────────────\n\n/** An OpenStreetMap `opening_hours` value, with where the caller got it. */\nexport interface VenueOsm {\n /** This text is the value of the tag, for example `Mo-Fr 08:00-18:00; Sa 09:00-13:00`. */\n readonly value: string;\n readonly url?: string | null | undefined;\n readonly doc?: string | null | undefined;\n readonly capture?: string | null | undefined;\n readonly fetched_at?: string | null | undefined;\n}\n\n/** These options control `readVenue`. */\nexport interface VenueOptions {\n /** This value is the number of pages after the home page, from 0 to `VENUE_LIMITS.maxPages`. The default is 3. */\n readonly pages?: number | undefined;\n /** This text is the name of the venue. The result tells if the home page names it (`names_venue`). */\n readonly name?: string | undefined;\n /** With true, a home page that does not name the venue gives no reading, and the recipe fetches no more pages. */\n readonly requireName?: boolean | undefined;\n /** This value is the place of the venue, for a page that lists more than one location. */\n readonly location?: VenueLocation | undefined;\n /** This value is an OpenStreetMap `opening_hours` value. The recipe reads it, and sends no request for it. */\n readonly osm?: string | VenueOsm | undefined;\n /** The value `auto` renders a page only when it shows almost nothing without JavaScript. */\n readonly render?: \"auto\" | \"never\" | \"always\" | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for example `Claude-User`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n readonly signal?: AbortSignal | undefined;\n /** No request starts after this time from the start of the read, for example `\"2m\"`. */\n readonly budget?: Duration | undefined;\n readonly scope?: string | undefined;\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** This name connects the captures to the job that asked for them. The default is `venue`. */\n readonly job?: string | undefined;\n /** This value is true when a person gave this URL directly. It is the only way to read a held-back page. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /** The value `archive` reads an archived copy of a page that the site refuses, or when the site is down. */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** One page that the venue recipe fetched. */\nexport interface VenuePage {\n readonly url: string;\n readonly doc: string | null;\n readonly outcome: string;\n readonly status: number;\n readonly rendered: boolean;\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly from_cache: boolean;\n readonly held_back: boolean;\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\n/** The result of `readVenue`. */\nexport interface VenueResult {\n /** This URL is the site that the caller gave. */\n readonly url: string;\n /** This URL is where the home page landed, after the redirects. */\n readonly home: string | null;\n /** This URL is the new site, when the home page landed on a different host. */\n readonly moved: string | null;\n /** This value tells if the home page names the venue. It is null with no `name`. */\n readonly names_venue: boolean | null;\n readonly pages: readonly VenuePage[];\n /** These are the readings, one for each source, label and week: the best first. */\n readonly readings: readonly VenueReading[];\n /** This reading is the best reading of the opening hours, from any source. */\n readonly best: VenueReading | null;\n /** This reading is the best reading of the site itself (not OpenStreetMap) with no guess. A check for a change compares it. */\n readonly from_site: VenueReading | null;\n /** These days are the days where two readings of the whole schedule (a rank of 2 or less) disagree. */\n readonly disagree: readonly Day[];\n readonly menus: readonly VenueLink[];\n readonly phone: string | null;\n /** These ids are the captures of the pages that the recipe read. */\n readonly captures: readonly string[];\n /** This value is true when a page asked not to be AI input, so the recipe read nothing from it. */\n readonly held_back: boolean;\n readonly note: string | null;\n}\n\nconst answered = (page: PageResult): boolean =>\n page.outcome === \"ok\" || page.outcome === \"not-modified\" || page.archived !== undefined;\n\n/**\n * This function reads the hours of a venue from its site. It fetches the home page and the pages\n * that most likely hold hours or a menu. Each one is a fetch of the crawler. Thus robots.txt, the\n * pace, the stops, the capture log and the rule for a held-back page apply. It renders a page only\n * when the page needs JavaScript, as `fetch` does.\n *\n * It gives each reading with its source, the best reading, and the days where the readings\n * disagree. It writes nothing but the capture log. To choose the true hours is the decision of\n * the application.\n */\nexport const readVenue = async (\n target: RecipeTarget,\n url: string,\n options: VenueOptions = {},\n): Promise<VenueResult> => {\n const runtime = runtimeOf(target);\n const extra = options.pages ?? VENUE_LIMITS.pages;\n if (!Number.isInteger(extra) || extra < 0 || extra > VENUE_LIMITS.maxPages) {\n throw new RangeError(\n `pages must be a whole number from 0 to ${String(VENUE_LIMITS.maxPages)}, not ${String(extra)}`,\n );\n }\n let site: URL;\n try {\n site = new URL(url);\n } catch {\n throw new TypeError(`readVenue needs the URL of a site, not ${JSON.stringify(url)}`);\n }\n if (site.protocol !== \"http:\" && site.protocol !== \"https:\")\n throw new TypeError(`readVenue needs an http or https URL, not ${url}`);\n const maxAge = options.maxAge ?? \"24h\";\n const job = options.job ?? \"venue\";\n const userProvided = options.userProvided ?? false;\n const pageOptions: PageOptions = {\n render: options.render ?? \"auto\",\n maxAgeMs: maxAge === \"auto\" ? \"auto\" : durationMs(maxAge),\n onBehalfOf: options.onBehalfOf,\n job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline: deadlineOf(runtime, options.budget),\n fallback: options.fallback,\n archiveDespiteRobots: options.archiveDespiteRobots,\n };\n const notes: string[] = [];\n const readings: VenueReading[] = [];\n const pages: VenuePage[] = [];\n const menus: VenueLink[] = [];\n const captures = new Set<string>();\n let phone: string | null = null;\n let heldBack = false;\n\n if (options.osm !== undefined) {\n const osm: VenueOsm = typeof options.osm === \"string\" ? { value: options.osm } : options.osm;\n const hours = typeof osm.value === \"string\" ? fromCompact(osm.value, \"osm\") : undefined;\n if (hours === undefined) {\n notes.push(\n `the OpenStreetMap hours cannot be read: ${JSON.stringify(String(osm.value).slice(0, 200))}`,\n );\n } else {\n readings.push(\n reading(\"osm\", hours, null, {\n url: osm.url ?? null,\n doc: osm.doc ?? null,\n capture: osm.capture ?? null,\n fetched_at: osm.fetched_at ?? null,\n path: null,\n }),\n );\n if (typeof osm.capture === \"string\") captures.add(osm.capture);\n }\n }\n\n /** This function fetches one page, and gives its document when the recipe can read it. */\n const readPage = async (pageUrl: string): Promise<Doc | undefined> => {\n const page = await fetchPage(runtime, pageUrl, pageOptions);\n const doc = page.doc === null ? undefined : runtime.index.get(page.doc);\n const held = doc !== undefined && isHeldBack(doc, userProvided);\n pages.push({\n url: pageUrl,\n doc: page.doc,\n outcome: page.outcome,\n status: page.status,\n rendered: page.rendered,\n capture: page.capture,\n fetched_at: page.fetched_at,\n from_cache: page.from_cache,\n held_back: held,\n ...(page.archived === undefined ? {} : { archived: page.archived }),\n note: page.note,\n });\n if (!answered(page)) {\n notes.push(`${pageUrl}: ${page.outcome}${page.note === null ? \"\" : ` (${page.note})`}`);\n }\n if (doc === undefined) return undefined;\n if (held) {\n heldBack = true;\n return undefined;\n }\n if (doc.ai_input === \"no\") await recordOverride(runtime, doc, job);\n captures.add(doc.capture);\n return doc;\n };\n\n const readDoc = (doc: Doc): void => {\n const read = venueReadingsOf(doc, { location: options.location });\n readings.push(...read.readings);\n notes.push(...read.notes);\n phone ??= read.phone;\n for (const menu of read.menus) {\n if (menus.length < VENUE_LIMITS.menus && !menus.some((known) => known.url === menu.url))\n menus.push(menu);\n }\n };\n\n let homeUrl: string | null = null;\n let namesVenueResult: boolean | null = null;\n const home = await readPage(site.href);\n if (home !== undefined) {\n homeUrl = archivedOf(home.meta)?.original ?? home.url ?? site.href;\n if (options.name !== undefined) {\n namesVenueResult = namesVenue(options.name, home) ?? null;\n if (namesVenueResult === false) {\n notes.push(\n `${homeUrl} does not name ${options.name}: the domain can belong to a different business now`,\n );\n }\n }\n if (namesVenueResult === false && options.requireName === true) {\n notes.push(\"requireName: the recipe read no hours from a site that does not name the venue\");\n } else {\n readDoc(home);\n for (const link of rankHoursLinks(linksOf(home.meta), homeUrl).slice(0, extra)) {\n if (options.signal?.aborted === true) break;\n const doc = await readPage(link.href);\n if (doc !== undefined) readDoc(doc);\n }\n }\n }\n const moved = homeUrl !== null && hostOf(homeUrl) !== hostOf(site.href) ? homeUrl : null;\n if (moved !== null) notes.push(`${site.href} now goes to ${moved}`);\n if (heldBack) notes.push(HELD_BACK);\n\n // one reading for each source, label and week: a site that repeats its JSON-LD on each page said it once\n const firstOf = new Map<string, VenueReading>();\n for (const one of readings) {\n const key = `${one.from}|${one.label ?? \"\"}|${one.hours}|${one.season ?? \"\"}`;\n if (!firstOf.has(key)) firstOf.set(key, one);\n }\n // the best first; of two equal readings, the reading as written before a guess\n const unique = [...firstOf.values()].sort(\n (a, b) => a.rank - b.rank || Number(a.inferred) - Number(b.inferred),\n );\n const disagree = new Set<Day>();\n const whole = unique.filter((one) => one.rank <= 2);\n for (let i = 0; i < whole.length; i += 1) {\n for (let j = i + 1; j < whole.length; j += 1) {\n for (const day of disagreements(whole[i]!.week, whole[j]!.week)) disagree.add(day);\n }\n }\n const best = unique.find((one) => one.schedule === \"opening\") ?? null;\n const fromSite =\n unique.find((one) => one.from !== \"osm\" && one.schedule === \"opening\" && !one.inferred) ?? null;\n return {\n url: site.href,\n home: homeUrl,\n moved,\n names_venue: namesVenueResult,\n pages,\n readings: unique,\n best,\n from_site: fromSite,\n disagree: DAYS.filter((day) => disagree.has(day)),\n menus,\n phone,\n captures: [...captures],\n held_back: heldBack,\n note: notes.length > 0 ? notes.join(\"; \") : null,\n };\n};\n","/*\n * The product recipe reads a product page: its schema.org Product data. Most shop systems write\n * this data on each product page. The recipe gives the name, the brand, the codes (SKU and GTIN),\n * the price as an amount and a currency, a range of prices across the variants, the availability,\n * the seller, the image, and the variant that the URL names.\n *\n * A shop that refuses the crawler is a clean stop: the result says `wall`, with a note, and the\n * crawler does not try a different identity.\n */\nimport type { Doc } from \"./docs.ts\";\nimport type { Duration } from \"./duration.ts\";\nimport { durationMs } from \"./duration.ts\";\nimport type { Archived } from \"./fallback.ts\";\nimport { archivedOf } from \"./fallback.ts\";\nimport { extractPage, typesOf } from \"./html.ts\";\nimport type { RecipeTarget } from \"./recipe.ts\";\nimport { deadlineOf, runtimeOf } from \"./recipe.ts\";\nimport type { CrawlRuntime, PageOptions, PageResult } from \"./runtime.ts\";\nimport { HELD_BACK, fetchPage, isHeldBack, recordOverride } from \"./runtime.ts\";\nimport { decodeBody } from \"./text.ts\";\n\n/** This is a JSON object, for example a schema.org record of a page. */\nexport type JsonRecord = Readonly<Record<string, unknown>>;\n\n/**\n * These are the limits of the product reader. A page with more is hostile or broken, and the reader stops there.\n *\n * @beta\n */\nexport const PRODUCT_LIMITS: {\n /** The most JSON-LD records that the reader examines. */\n readonly records: number;\n /** The most offers of one product or variant. */\n readonly offers: number;\n /** The most variants of one product. */\n readonly variants: number;\n /** The levels of `hasVariant` and of nested offers that the reader follows. */\n readonly depth: number;\n /** The longest text of a price. */\n readonly priceChars: number;\n} = { records: 500, offers: 200, variants: 200, depth: 2, priceChars: 64 };\n\n/** An amount of money, as schema.org Product data gives it. */\nexport interface Money {\n /** This value is the amount in the unit of the currency, for example 12.5. */\n readonly amount: number;\n /** This value is the ISO 4217 code of the currency, for example `EUR`. */\n readonly currency: string;\n /** This text is the price as the page wrote it, for a check with `extract`. */\n readonly raw: string;\n}\n\n/** The variant of a product that the URL names. */\nexport interface ProductVariant {\n readonly name: string | null;\n readonly sku: string | null;\n readonly price: Money | null;\n readonly availability: string | null;\n}\n\n/** The result of `productOf`: what the Product data of a page says. */\nexport interface ProductRead {\n /** This value is true when the page has a Product record. */\n readonly found: boolean;\n readonly name: string | null;\n readonly brand: string | null;\n readonly sku: string | null;\n readonly gtin: string | null;\n /** This value is the lowest price of the offers, in the first currency of the page. */\n readonly price: Money | null;\n /** This value is the highest price, when the offers give a range (sizes, colors, variants). */\n readonly price_to: Money | null;\n /**\n * This value is the schema.org name of the availability, for example `InStock`. When the offers\n * do not agree, it is `InStock` if one offer is in stock, and `availabilities` gives each value.\n */\n readonly availability: string | null;\n readonly availabilities: readonly string[];\n /** This value is the variant that the URL names (`?variant=`, `sku` or `gtin`), read alone. */\n readonly variant: ProductVariant | null;\n readonly seller: string | null;\n readonly image: string | null;\n readonly notes: readonly string[];\n /**\n * This value counts the work of the reader. The tests use it, because it does not change with the computer.\n *\n * @beta\n */\n readonly steps: number;\n}\n\nconst PRODUCT_TYPES = /^(?:Product|ProductGroup|IndividualProduct|ProductModel|SomeProducts)$/;\n\n/** A list price or a struck-through price is the old price, not the price. */\nconst WAS_PRICE = /ListPrice|StrikethroughPrice|MSRP|SRP/i;\n\nconst GTIN_KEYS = [\"gtin13\", \"gtin12\", \"gtin14\", \"gtin8\", \"gtin\"] as const;\n\n/** Two symbols that name one currency only. The other symbols (`$`, `¥`) name more than one. */\nconst SYMBOLS: Readonly<Record<string, string>> = { \"€\": \"EUR\", \"£\": \"GBP\" };\n\nlet knownCurrencies: ReadonlySet<string> | undefined;\n/** This function tells if a code is an ISO 4217 currency that the runtime knows. */\nconst isCurrency = (code: string): boolean => {\n if (!/^[A-Z]{3}$/.test(code)) return false;\n if (knownCurrencies === undefined) {\n try {\n knownCurrencies = new Set(Intl.supportedValuesOf(\"currency\"));\n } catch {\n // a runtime with no list: each code of three capital letters counts\n knownCurrencies = new Set();\n }\n }\n return knownCurrencies.size === 0 || knownCurrencies.has(code);\n};\n\n/** This function gives the number of digits after the decimal point of a currency: 2 for EUR, 0 for JPY. */\nconst fractionDigits = (currency: string | undefined): number | undefined => {\n if (currency === undefined) return undefined;\n try {\n return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions()\n .maximumFractionDigits;\n } catch {\n return undefined;\n }\n};\n\n/**\n * This function reads an amount as a shop writes it: `19.99`, `1,299.00`, `1.299,00`, `$19.99` or\n * the number 19.99. The last separator is the decimal point, with one exception. One kind of\n * separator, each followed by three digits, separates thousands: `1.299` is 1299. For a\n * currency with three decimal digits, one point with three digits is the decimal point. A text\n * with two numbers (a range), a negative amount or zero gives undefined.\n *\n * @beta\n */\nexport const readAmount = (value: unknown, currency?: string): number | undefined => {\n if (typeof value === \"number\") return Number.isFinite(value) && value > 0 ? value : undefined;\n if (typeof value !== \"string\" || value.length > PRODUCT_LIMITS.priceChars) return undefined;\n // one number, with the currency and the spaces around it\n const match = /^[^\\d-]*?(\\d(?:[\\d.,' ]*\\d)?)[^\\d]*$/.exec(value.trim());\n if (match === null) return undefined;\n let digits = match[1]!.replace(/[' ]/g, \"\");\n const last = Math.max(digits.lastIndexOf(\".\"), digits.lastIndexOf(\",\"));\n if (last !== -1) {\n const kinds = new Set(digits.match(/[.,]/g));\n const groups = kinds.size === 1 && /^[1-9]\\d{0,2}(?:[.,]\\d{3})+$/.test(digits);\n const decimalThree = groups && fractionDigits(currency) === 3 && /^\\d+\\.\\d{3}$/.test(digits);\n digits =\n groups && !decimalThree\n ? digits.replace(/[.,]/g, \"\")\n : `${digits.slice(0, last).replace(/[.,]/g, \"\")}.${digits.slice(last + 1)}`;\n }\n const amount = Number(digits);\n return Number.isFinite(amount) && amount > 0 ? amount : undefined;\n};\n\nconst list = (value: unknown, limit: number): unknown[] =>\n (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]).slice(\n 0,\n limit,\n );\n\nconst objects = (value: unknown, limit: number): JsonRecord[] =>\n list(value, limit).filter(\n (item): item is JsonRecord => typeof item === \"object\" && item !== null && !Array.isArray(item),\n );\n\n/** This function gives a name, written as a text or as an object that has one. */\nconst nameOf = (value: unknown): string | null => {\n for (const item of list(value, 20)) {\n if (typeof item === \"string\" && item.trim().length > 0) return item.trim().slice(0, 500);\n if (typeof item === \"object\" && item !== null) {\n const named = (item as JsonRecord)[\"name\"];\n if (typeof named === \"string\" && named.trim().length > 0) return named.trim().slice(0, 500);\n }\n }\n return null;\n};\n\nconst textOf = (value: unknown): string | null =>\n typeof value === \"string\" && value.trim().length > 0\n ? value.trim().slice(0, 200)\n : typeof value === \"number\" && Number.isFinite(value)\n ? String(value)\n : null;\n\nconst imageOf = (value: unknown): string | null => {\n for (const item of list(value, 20)) {\n if (typeof item === \"string\" && /^https?:/i.test(item)) return item;\n const url = typeof item === \"object\" && item !== null ? (item as JsonRecord)[\"url\"] : undefined;\n if (typeof url === \"string\" && /^https?:/i.test(url)) return url;\n }\n return null;\n};\n\ninterface Offers {\n readonly prices: Money[];\n readonly unknownCurrencies: Set<string>;\n readonly availability: Set<string>;\n readonly sellers: Set<string>;\n /** An amount with no currency, which is not a price. */\n unstated: boolean;\n steps: number;\n}\n\nconst emptyOffers = (): Offers => ({\n prices: [],\n unknownCurrencies: new Set(),\n availability: new Set(),\n sellers: new Set(),\n unstated: false,\n steps: 0,\n});\n\n/** This function reads the offers of a product into `read`, and the offers of its variants to `depth` levels. */\nconst readOffers = (product: JsonRecord, read: Offers, depth: number): void => {\n const money = (value: unknown, currency: unknown): void => {\n const written = typeof currency === \"string\" ? currency.trim() : \"\";\n const code = SYMBOLS[written] ?? written.toUpperCase();\n const amount = readAmount(value, code.length > 0 ? code : undefined);\n if (amount === undefined) return;\n if (code.length === 0) {\n read.unstated = true;\n return;\n }\n if (!isCurrency(code)) {\n read.unknownCurrencies.add(written.slice(0, 20));\n return;\n }\n read.prices.push({\n amount,\n currency: code,\n raw: String(value).slice(0, PRODUCT_LIMITS.priceChars),\n });\n };\n const offer = (o: JsonRecord, level: number): void => {\n read.steps += 1;\n const spec = objects(o[\"priceSpecification\"], PRODUCT_LIMITS.offers).find((entry) => {\n const kind = entry[\"priceType\"];\n return !(typeof kind === \"string\" && WAS_PRICE.test(kind));\n });\n const currency = o[\"priceCurrency\"] ?? spec?.[\"priceCurrency\"];\n money(o[\"price\"] ?? spec?.[\"price\"], currency);\n money(o[\"lowPrice\"], currency);\n money(o[\"highPrice\"], currency);\n const availability =\n typeof o[\"availability\"] === \"string\"\n ? o[\"availability\"]\n .trim()\n .replace(/^.*[/#:]/, \"\")\n .slice(0, 60)\n : undefined;\n if (availability !== undefined && availability.length > 0) read.availability.add(availability);\n const seller = nameOf(o[\"seller\"]);\n if (seller !== null) read.sellers.add(seller);\n // an AggregateOffer can list its offers\n if (level < PRODUCT_LIMITS.depth) {\n for (const inner of objects(o[\"offers\"], PRODUCT_LIMITS.offers)) offer(inner, level + 1);\n }\n };\n for (const o of objects(product[\"offers\"], PRODUCT_LIMITS.offers)) offer(o, 0);\n if (depth <= 0) return;\n for (const variant of objects(product[\"hasVariant\"], PRODUCT_LIMITS.variants)) {\n read.steps += 1;\n readOffers(variant, read, depth - 1);\n }\n};\n\nconst availabilityOf = (read: Offers): string | null =>\n read.availability.size === 0\n ? null\n : read.availability.size === 1 || !read.availability.has(\"InStock\")\n ? [...read.availability][0]!\n : \"InStock\";\n\nconst pathKey = (value: string, page: string): string | undefined => {\n // \"#product\" names a part of this page: a related product can have such an id too\n if (value.startsWith(\"#\")) return undefined;\n try {\n const url = new URL(value, page);\n return `${url.hostname.toLowerCase().replace(/^www\\./, \"\")}${url.pathname.replace(/\\/$/, \"\")}`;\n } catch {\n return undefined;\n }\n};\n\n/** This function gives the size of a record as JSON, or 0 for a record that JSON cannot write (a nesting too deep). */\nconst sizeOf = (record: JsonRecord): number => {\n try {\n return JSON.stringify(record).length;\n } catch {\n return 0;\n }\n};\n\n/**\n * This function picks the product of a page from its JSON-LD records. A page often has more\n * records of products: the related products, the products that others bought. It picks:\n *\n * 1. The product whose `url` or `@id` is the page (the path, with no query).\n * 2. Else the largest product with offers or variants.\n * 3. Else the largest product.\n *\n * It also reads the `mainEntity` of a record (a `WebPage` or an `ItemPage` that names its product).\n *\n * @beta\n */\nexport const pickProduct = (\n records: readonly JsonRecord[],\n page: string,\n): { readonly product: JsonRecord | undefined; readonly steps: number } => {\n let steps = 0;\n const products: JsonRecord[] = [];\n for (const record of records.slice(0, PRODUCT_LIMITS.records)) {\n steps += 1;\n if (typesOf(record as Record<string, unknown>).some((type) => PRODUCT_TYPES.test(type)))\n products.push(record);\n for (const inner of objects(record[\"mainEntity\"], 20)) {\n steps += 1;\n if (typesOf(inner as Record<string, unknown>).some((type) => PRODUCT_TYPES.test(type)))\n products.push(inner);\n }\n }\n const here = pathKey(page, page);\n const own = products.find((product) =>\n [product[\"url\"], product[\"@id\"]].some(\n (value) => typeof value === \"string\" && here !== undefined && pathKey(value, page) === here,\n ),\n );\n if (own !== undefined) return { product: own, steps };\n const selling = products.filter(\n (product) => list(product[\"offers\"], 1).length > 0 || list(product[\"hasVariant\"], 1).length > 0,\n );\n const pool = selling.length > 0 ? selling : products;\n let best: JsonRecord | undefined;\n let size = -1;\n for (const product of pool) {\n const length = sizeOf(product);\n steps += length;\n if (length > size) {\n best = product;\n size = length;\n }\n }\n return { product: best, steps };\n};\n\n/**\n * This function gives the variant that a URL names: the `variant` parameter of Shopify, or a `sku` or `gtin` parameter.\n *\n * @beta\n */\nexport const pickVariant = (product: JsonRecord, page: string): JsonRecord | undefined => {\n let params: URLSearchParams;\n try {\n params = new URL(page).searchParams;\n } catch {\n return undefined;\n }\n const variantId = params.get(\"variant\");\n const code = params.get(\"sku\") ?? params.get(\"gtin\");\n if (variantId === null && code === null) return undefined;\n const names = (value: unknown): boolean => {\n if (typeof value !== \"string\" || variantId === null) return false;\n try {\n return (\n new URL(value, page).searchParams.get(\"variant\") === variantId ||\n value.endsWith(`/${variantId}`)\n );\n } catch {\n return false;\n }\n };\n return objects(product[\"hasVariant\"], PRODUCT_LIMITS.variants).find((variant) => {\n const offers = objects(variant[\"offers\"], PRODUCT_LIMITS.offers);\n const codes = [\n variant[\"sku\"],\n variant[\"gtin\"],\n variant[\"gtin13\"],\n variant[\"gtin12\"],\n variant[\"productID\"],\n ]\n .map(textOf)\n .filter((one): one is string => one !== null);\n return (\n (code !== null && codes.includes(code)) ||\n [variant[\"@id\"], variant[\"url\"], ...offers.map((offer) => offer[\"url\"])].some(names) ||\n (variantId !== null && codes.includes(variantId))\n );\n });\n};\n\nconst byAmount = (a: Money, b: Money): number => a.amount - b.amount;\n\n/**\n * This function reads the Product data of a page from its JSON-LD records. It is pure, and it sends\n * no request. Thus it also reads a version at a time, or the stored JSON-LD of a document.\n *\n * - The prices are in the first currency of the offers. A price with no currency is not a price.\n * - A list price or a struck-through price is not the price.\n * - A currency must be an ISO 4217 code (`€` and `£` count too). The notes name each other one.\n */\nexport const productOf = (records: readonly JsonRecord[], page: string): ProductRead => {\n const picked = pickProduct(records, page);\n const product = picked.product;\n if (product === undefined) {\n return {\n found: false,\n name: null,\n brand: null,\n sku: null,\n gtin: null,\n price: null,\n price_to: null,\n availability: null,\n availabilities: [],\n variant: null,\n seller: null,\n image: null,\n notes: [\n \"no schema.org Product data on the page: a list of products, or a shop that does not publish it\",\n ],\n steps: picked.steps,\n };\n }\n const read = emptyOffers();\n readOffers(product, read, PRODUCT_LIMITS.depth);\n const currency = read.prices[0]?.currency;\n const sorted = read.prices.filter((price) => price.currency === currency).sort(byAmount);\n const low = sorted[0] ?? null;\n const high = sorted.at(-1) ?? null;\n const gtin = GTIN_KEYS.map((key) => textOf(product[key])).find((value) => value !== null);\n const chosen = pickVariant(product, page);\n let variant: ProductVariant | null = null;\n let steps = picked.steps + read.steps;\n if (chosen !== undefined) {\n const own = emptyOffers();\n readOffers(chosen, own, 0);\n steps += own.steps;\n const prices = own.prices.filter((price) => price.currency === currency).sort(byAmount);\n variant = {\n name: nameOf(chosen[\"name\"]),\n sku: textOf(chosen[\"sku\"]),\n price: prices[0] ?? null,\n availability: availabilityOf(own),\n };\n }\n const notes = [\n read.unknownCurrencies.size > 0\n ? `a currency that is not an ISO 4217 code: ${[...read.unknownCurrencies].join(\", \")}`\n : null,\n read.unstated && sorted.length === 0\n ? \"the page gives a price with no currency, so the price is not read as money\"\n : sorted.length === 0\n ? \"the page names the product and gives no price\"\n : null,\n new Set(read.prices.map((price) => price.currency)).size > 1\n ? `the offers have more than one currency: the prices are in ${currency ?? \"\"}`\n : null,\n /[?&](?:variant|sku|gtin)=/.test(page) && chosen === undefined\n ? \"the URL names a variant that the Product data does not list: the price is the whole range\"\n : null,\n ].filter((note): note is string => note !== null);\n return {\n found: true,\n name: nameOf(product[\"name\"]),\n brand: nameOf(product[\"brand\"]),\n sku: textOf(product[\"sku\"]),\n gtin: gtin ?? null,\n price: low,\n price_to: high !== null && low !== null && high.amount > low.amount ? high : null,\n availability: availabilityOf(read),\n availabilities: [...read.availability],\n variant,\n seller: read.sellers.size === 0 ? null : [...read.sellers].join(\", \"),\n image: imageOf(product[\"image\"]),\n notes,\n steps,\n };\n};\n\n// ── the recipe ──────────────────────────────────────────────────────────────\n\n/** These options control `readProduct`. */\nexport interface ProductOptions {\n /** The value `auto` (the default) renders once more when the plain page has no Product data. `never` does not render. */\n readonly render?: \"auto\" | \"never\" | \"always\" | undefined;\n /** A stored copy younger than this age answers with no request. The default is `24h`. */\n readonly maxAge?: Duration | undefined;\n /** These agents get their robots.txt rules applied too, for example `Claude-User`. */\n readonly onBehalfOf?: readonly string[] | undefined;\n readonly signal?: AbortSignal | undefined;\n /** No request starts after this time from the start of the read, for example `\"1m\"`. */\n readonly budget?: Duration | undefined;\n readonly scope?: string | undefined;\n readonly labels?: Readonly<Record<string, string>> | undefined;\n /** This name connects the captures to the job that asked for them. The default is `product`. */\n readonly job?: string | undefined;\n /** This value is true when a person gave this URL directly. It is the only way to read a held-back page. The crawler logs it. */\n readonly userProvided?: boolean | undefined;\n /** The value `archive` reads an archived copy of a page that the shop refuses. Its prices are of the date of the copy. */\n readonly fallback?: \"archive\" | \"none\" | undefined;\n readonly archiveDespiteRobots?: boolean | undefined;\n}\n\n/** The result of `readProduct`. */\nexport interface ProductResult extends Omit<ProductRead, \"steps\" | \"notes\"> {\n readonly url: string;\n /** This value is true when the page says what it sells, in structured data: the Product has a name. */\n readonly verified: boolean;\n /**\n * This value is true when the site refused the crawler. The causes are a 401, a 402 or a 403, a\n * challenge, a bot check, a stop of the host or robots.txt. The recipe stops there, and the note\n * says why. With `fallback: \"archive\"`, it stays true when an archived copy answers.\n */\n readonly wall: boolean;\n readonly outcome: string;\n readonly status: number;\n readonly rendered: boolean;\n readonly doc: string | null;\n /** This id is the capture of the text that the recipe read. A citation names it. */\n readonly capture: string | null;\n readonly fetched_at: string | null;\n readonly captures: readonly string[];\n readonly from_cache: boolean;\n /** This value is true when the page asked not to be AI input, so the recipe read nothing from it. */\n readonly held_back: boolean;\n /** This value tells that the page is an archived copy, with the archive and the date of the copy. */\n readonly archived?: Archived | undefined;\n readonly note: string | null;\n}\n\nconst WALLS: ReadonlySet<string> = new Set([\"blocked\", \"challenge\", \"payment-required\"]);\n\n/** This function tells if the site refused the crawler. */\nconst wallOf = (page: PageResult): boolean =>\n WALLS.has(page.outcome) ||\n (page.outcome === \"skipped\" && /^(?:robots\\.txt disallows|host stopped)/.test(page.note ?? \"\")) ||\n (page.doc === null && /\\bbot check\\b/.test(page.note ?? \"\"));\n\nconst answered = (page: PageResult): boolean =>\n page.doc !== null &&\n (page.outcome === \"ok\" || page.outcome === \"not-modified\" || page.archived !== undefined);\n\n/**\n * This function gives the JSON-LD records of a document from its stored body. The index keeps a\n * part of the JSON-LD of a large page only, and a product with many variants can be larger. An\n * archived copy gives the JSON-LD that the index keeps.\n */\nconst recordsOfDoc = async (runtime: CrawlRuntime, doc: Doc): Promise<readonly JsonRecord[]> => {\n const kept = Array.isArray(doc.meta[\"jsonld\"]) ? (doc.meta[\"jsonld\"] as JsonRecord[]) : [];\n if (doc.blob === null || archivedOf(doc.meta) !== undefined) return kept;\n const body = await runtime.blobs.get(doc.blob);\n if (body === undefined) return kept;\n const at = doc.url ?? \"\";\n const type = doc.rendered\n ? \"text/html; charset=utf-8\"\n : (runtime.index.latest(at, \"plain\")?.content_type ?? null);\n try {\n return extractPage(decodeBody(body, type), at).jsonld;\n } catch {\n return kept;\n }\n};\n\n/**\n * This function reads a product page. It fetches the page through the crawler: robots.txt, the\n * pace, the stops, the capture log and the rule for a held-back page apply. When the plain page has\n * no Product data, it renders the page once, because some shops write that data with JavaScript.\n *\n * A shop that refuses the crawler gives `wall: true` and a note. The recipe writes nothing but the\n * capture log. `extract` can check each value against the stored page.\n */\nexport const readProduct = async (\n target: RecipeTarget,\n url: string,\n options: ProductOptions = {},\n): Promise<ProductResult> => {\n const runtime = runtimeOf(target);\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") throw new TypeError(url);\n } catch {\n throw new TypeError(`readProduct needs an http or https URL, not ${JSON.stringify(url)}`);\n }\n const maxAge = options.maxAge ?? \"24h\";\n const job = options.job ?? \"product\";\n const userProvided = options.userProvided ?? false;\n const render = options.render ?? \"auto\";\n const deadline = deadlineOf(runtime, options.budget);\n const pageOptions = (mode: \"auto\" | \"never\" | \"always\"): PageOptions => ({\n render: mode,\n maxAgeMs: maxAge === \"auto\" ? \"auto\" : durationMs(maxAge),\n onBehalfOf: options.onBehalfOf,\n job,\n signal: options.signal,\n scope: options.scope,\n labels: options.labels,\n deadline,\n fallback: options.fallback,\n archiveDespiteRobots: options.archiveDespiteRobots,\n });\n let page = await fetchPage(runtime, url, pageOptions(render));\n const notes: string[] = [];\n const result = (\n from: PageResult,\n read: Omit<ProductRead, \"steps\"> | undefined,\n more: { readonly held_back?: boolean } = {},\n ): ProductResult => {\n const all = [from.note, ...notes, ...(read?.notes ?? [])].filter(\n (part): part is string => part !== null && part !== undefined && part.length > 0,\n );\n return {\n url,\n found: read?.found ?? false,\n verified: read?.found === true && read.name !== null,\n name: read?.name ?? null,\n brand: read?.brand ?? null,\n sku: read?.sku ?? null,\n gtin: read?.gtin ?? null,\n price: read?.price ?? null,\n price_to: read?.price_to ?? null,\n availability: read?.availability ?? null,\n availabilities: read?.availabilities ?? [],\n variant: read?.variant ?? null,\n seller: read?.seller ?? null,\n image: read?.image ?? null,\n wall: wallOf(from),\n outcome: from.outcome,\n status: from.status,\n rendered: from.rendered,\n doc: from.doc,\n capture: from.capture,\n fetched_at: from.fetched_at,\n captures: [...from.captures],\n from_cache: from.from_cache,\n held_back: more.held_back ?? false,\n ...(from.archived === undefined ? {} : { archived: from.archived }),\n note: all.length > 0 ? [...new Set(all)].join(\"; \") : null,\n };\n };\n if (!answered(page)) {\n const wall = wallOf(page);\n notes.push(\n wall\n ? `the shop refused the crawler (${page.outcome}): the recipe read nothing, and it does not try a different identity`\n : `the page gave no document (${page.outcome})`,\n );\n return result(page, undefined);\n }\n const held = (from: PageResult): Doc | undefined => {\n const doc = from.doc === null ? undefined : runtime.index.get(from.doc);\n return doc !== undefined && isHeldBack(doc, userProvided) ? doc : undefined;\n };\n if (held(page) !== undefined) {\n notes.push(HELD_BACK);\n return result(page, undefined, { held_back: true });\n }\n const docOf = (from: PageResult): Doc | undefined =>\n from.doc === null ? undefined : runtime.index.get(from.doc);\n let doc = docOf(page);\n let read = doc === undefined ? undefined : productOf(await recordsOfDoc(runtime, doc), url);\n // some shops write their Product data with JavaScript: one render before the recipe says that the page has none\n if (read?.found !== true && !page.rendered && render === \"auto\" && page.archived === undefined) {\n const rendered = await fetchPage(runtime, url, pageOptions(\"always\"));\n const shown = docOf(rendered);\n if (\n rendered.rendered &&\n answered(rendered) &&\n shown !== undefined &&\n held(rendered) === undefined\n ) {\n page = rendered;\n doc = shown;\n read = productOf(await recordsOfDoc(runtime, shown), url);\n } else if (rendered.note !== null && !rendered.rendered) {\n notes.push(`the render gave nothing more: ${rendered.note}`);\n }\n }\n if (userProvided && doc?.ai_input === \"no\") await recordOverride(runtime, doc, job);\n return result(page, read);\n};\n"],"mappings":";;;;;;;;;;;AAuCA,MAAa,eAaT;CAAE,OAAO;CAAG,UAAU;CAAG,OAAO;CAAG,iBAAiB;CAAI,OAAO;CAAK,YAAY;AAAI;AAIxF,MAAM,6BAAkC,IAAI,IAAI;CAAC;CAAO;CAAK;CAAM;CAAO;CAAM;AAAI,CAAC;;AAGrF,MAAM,WAAW,SACf,KACG,YAAY,CAAC,CACb,UAAU,MAAM,CAAC,CACjB,QAAQ,WAAW,EAAE,CAAC,CACtB,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,oBAAoB,GAAG,CAAC,CAChC,MAAM,KAAK,CAAC,CACZ,QAAQ,SAAS,KAAK,SAAS,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC;;;;;;;;AAS9D,MAAa,cACX,MACA,SACwB;CACxB,MAAM,QAAQ,QAAQ,IAAI;CAC1B,IAAI,MAAM,WAAW,GAAG,OAAO,KAAA;CAC/B,MAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,KAAK,MAAM,GAAG,KAAK,MAAM,CAAC;CAC5D,MAAM,QAAQ,IAAI,IAAI,KAAK;CAC3B,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,SAAS,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC;CAC5D,OAAO,OAAO,IAAI,MAAM,EAAG,KAAK,QAAQ,MAAM,QAAQ;AACxD;;AAKA,MAAM,aAAuD;CAC3D,CAAC,kCAAkC,CAAC;CACpC,CAAC,iCAAiC,CAAC;CACnC,CAAC,8CAA8C,CAAC;CAChD,CAAC,cAAc,CAAC;CAChB,CAAC,iCAAiC,CAAC;AACrC;;AAGA,MAAM,SACJ;;AAGF,MAAM,OAAO;AAEb,MAAM,UAAU,QAAoC;CAClD,IAAI;EACF,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,QAAQ,UAAU,EAAE;CACjE,QAAQ;EACN;CACF;AACF;AAEA,MAAM,eAAe,QAAwB,IAAI,QAAQ,SAAS,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE;;;;;;;;AASvF,MAAa,kBAAkB,OAA4B,SAA6B;CACtF,MAAM,OAAO,OAAO,IAAI;CACxB,MAAM,OAAO,YAAY,IAAI;CAC7B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAqE,CAAC;CAC5E,KAAK,MAAM,QAAQ,MAAM,MAAM,GAAG,aAAa,KAAK,GAAG;EACrD,IAAI,OAAO,MAAM,SAAS,YAAY,OAAO,KAAK,IAAI,MAAM,QAAQ,KAAK,KAAK,KAAK,IAAI,GACrF;EACF,MAAM,MAAM,YAAY,KAAK,IAAI;EACjC,IAAI,QAAQ,QAAQ,KAAK,IAAI,GAAG,GAAG;EACnC,IAAI,OAAO;EACX,IAAI;GACF,OAAO,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC;EAC5B,QAAQ;GACN;EACF;EACA,MAAM,QAAQ,GAAG,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO,GAAG,GAAG,OAAO,YAAY;EACtF,MAAM,QAAQ,WAAW,QACtB,KAAK,CAAC,SAAS,YAAY,OAAO,QAAQ,KAAK,KAAK,IAAI,SAAS,IAClE,CACF;EACA,IAAI,SAAS,GAAG;EAChB,KAAK,IAAI,GAAG;EACZ,OAAO,KAAK;GAAE;GAAM;EAAM,CAAC;CAC7B;CAEA,OAAO,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,KAAK,UAAU,MAAM,IAAI;AAC3E;AAuBA,MAAM,eAAe;;AAGrB,MAAM,iBAAiB,GAAU,MAAqB;CACpD,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,QAAQ,EAAE,MAAM,EAAE,OAAO;CAC/B,MAAM,QAAQ,EAAE,MAAM,EAAE,OAAO;CAC/B,MAAM,IACJ,KAAK,IAAI,OAAO,CAAC,KAAK,IACtB,KAAK,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK;CACxE,OAAO,IAAI,eAAe,KAAK,KAAK,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC;AAC/D;AAEA,MAAM,WAAW,KAAc,QAAoC;CACjE,MAAM,KAAK,OAAO,QAAQ,WAAW,OAAO,WAAW,GAAG,IAAI;CAC9D,MAAM,KAAK,OAAO,QAAQ,WAAW,OAAO,WAAW,GAAG,IAAI;CAC9D,OAAO,OAAO,OAAO,YACnB,OAAO,OAAO,YACd,OAAO,SAAS,EAAE,KAClB,OAAO,SAAS,EAAE,KAClB,KAAK,IAAI,EAAE,KAAK,MAChB,KAAK,IAAI,EAAE,KAAK,MACd;EAAE,KAAK;EAAI,KAAK;CAAG,IACnB,KAAA;AACN;;AAGA,MAAM,+BAAoC,IAAI,IAAI;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,YACJ,YACiF;CACjF,MAAM,QAAQ,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM,QAAA,CAAS,MAAM,GAAG,GAAG,CAAC;CACtE,MAAM,SAAS,MAAM,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC;CAC5D,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;CACjC,OAAO;EACL;EACA,OAAO,IAAI,IAAI,MAAM,QAAQ,SAAS,CAAC,MAAM,KAAK,IAAI,KAAK,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC;CACrF;AACF;AAEA,MAAM,eAAe,UAAuC;CAC1D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,MAAM,EAAE;CACrD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,SAAU,MAAkC;EAClD,OAAO,OAAO,WAAW,WAAW,SAAS,KAAA;CAC/C;AAEF;;;;;;;AAQA,MAAa,mBACX,QACA,aACY;CACZ,MAAM,MAAM,OAAO;CACnB,MAAM,KACJ,OAAO,QAAQ,YAAY,QAAQ,OAC/B,QACG,IAAgC,aAChC,IAAgC,YACnC,IACA,KAAA;CACN,MAAM,OAAO,QAAQ,SAAS,KAAK,SAAS,GAAG;CAC/C,IAAI,OAAO,KAAA,KAAa,SAAS,KAAA,KAAa,cAAc,IAAI,IAAI,KAAK,aAAa,YACpF,OAAO;CACT,MAAM,SAAS,YAAY,OAAO,UAAU;CAC5C,IAAI,WAAW,KAAA,KAAa,SAAS,YAAY,KAAA,GAAW,OAAO;CACnE,MAAM,IAAI,SAAS,MAAM;CACzB,MAAM,IAAI,SAAS,SAAS,OAAO;CACnC,OACE,MAAM,KAAA,KACN,MAAM,KAAA,KACN,EAAE,WAAW,EAAE,UACf,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,MAAM,SAAS,EAAE,MAAM,IAAI,IAAI,CAAC;AAEjD;AAEA,MAAM,WAAW,WACf,OAAO,eAAe,KAAA,KAAa,OAAO,WAAW,KAAA;;AAmEvD,MAAM,cACJ;;;;;AAMF,MAAM,iBACJ;;;;;AAMF,MAAM,UACJ;AAEF,MAAM,UAAU,MAA4B,UAAiC;CAC3E,IAAI,SAAS,UAAU,OAAO;CAC9B,IAAI,SAAS,OAAO,OAAO;CAC3B,MAAM,OAAO,SAAS;CACtB,IAAI,QAAQ,KAAK,IAAI,GAAG,OAAO;CAC/B,IAAI,aAAa,KAAK,IAAI,GAAG,OAAO;CACpC,OAAO,eAAe,KAAK,IAAI,IAAI,IAAI;AACzC;AAEA,MAAM,WACJ,MACA,OACA,OACA,WAQiB;CACjB,MAAM,OAAO,OAAO,MAAM,KAAK;CAC/B,OAAO;EACL;EACA;EACA,UAAU,QAAQ,IAAI,YAAY;EAClC;EACA,OAAO,aAAa,MAAM,IAAI;EAC9B,MAAM,MAAM;EACZ,UAAU,MAAM;EAChB,SAAS,MAAM,YAAY;EAC3B,QAAQ,MAAM,UAAU;EACxB,KAAK,MAAM;EACX,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,SAAS,OAAO;EAChB,YAAY,OAAO;EACnB,GAAI,OAAO,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,OAAO,SAAS;CACvE;AACF;AAEA,MAAM,aAAa,SAAuE;CACxF,MAAM,SAAS,KAAK;CACpB,OAAO,MAAM,QAAQ,MAAM,IACvB,OAAO,QACJ,WACC,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,CAC1E,IACA,CAAC;AACP;AAEA,MAAM,WAAW,SAAwD;CACvE,MAAM,QAAQ,KAAK;CACnB,OAAO,MAAM,QAAQ,KAAK,IACtB,MAAM,QACH,SACC,SAAS,QAAQ,OAAO,SAAS,YAAY,OAAQ,KAAkB,SAAS,QACpF,IACA,CAAC;AACP;;;;;;;;;;AAWA,MAAa,mBACX,QACA,UAA6D,CAAC,MAChD;CACd,IAAI,QAAQ;CACZ,MAAM,QAAkB,CAAC;CACzB,MAAM,WAAW,WAAW,OAAO,IAAI;CACvC,MAAM,QAAQ;EACZ,KAAK,UAAU,YAAY,OAAO;EAClC,KAAK,OAAO,OAAO;EACnB,SAAS,OAAO,WAAW;EAC3B,YAAY,OAAO,cAAc;EACjC,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;CAC/C;CACA,MAAM,MAAM,UAAU,OAAO,IAAI;CACjC,SAAS,IAAI;CACb,MAAM,SAAS,IACZ,KAAK,QAAQ,WAAW;EAAE;EAAQ;CAAM,EAAE,CAAC,CAC3C,QAAQ,EAAE,aAAa,QAAQ,MAAM,CAAC,CAAC,MAAM,SAAS,YAAY,KAAK,IAAI,CAAC,CAAC;CAEhF,IAAI,SAAS;CACb,MAAM,SAAS,OAAO,QAAQ,EAAE,aAAa,QAAQ,MAAM,CAAC;CAC5D,IAAI,OAAO,SAAS,GAAG;EACrB,MAAM,WAAW,QAAQ;EACzB,MAAM,OACJ,aAAa,KAAA,IACT,CAAC,IACD,OAAO,QAAQ,EAAE,aAAa,gBAAgB,QAAQ,QAAQ,CAAC;EACrE,IAAI,KAAK,WAAW,GAClB,MAAM,KACJ,aAAa,KAAA,IACT,GAAG,MAAM,OAAO,WAAW,SAAS,OAAO,OAAO,MAAM,EAAE,sDAC1D,GAAG,MAAM,OAAO,WAAW,SAAS,OAAO,OAAO,MAAM,EAAE,qEAChE;EAEF,SAAS,CAAC,GAAG,MAAM,GAAG,OAAO,QAAQ,EAAE,aAAa,CAAC,QAAQ,MAAM,CAAC,CAAC;CACvE;CACA,MAAM,WAA2B,CAAC;CAClC,IAAI,QAAuB;CAC3B,KAAK,MAAM,EAAE,QAAQ,WAAW,QAAQ;EACtC,SAAS;EACT,MAAM,QAAQ,cAAc,MAAM;EAClC,IAAI,UAAU,KAAA,KAAa,SAAS,SAAS,aAAa,iBACxD,SAAS,KAAK,QAAQ,UAAU,OAAO,MAAM;GAAE,GAAG;GAAO,MAAM,UAAU,OAAO,KAAK,EAAE;EAAG,CAAC,CAAC;EAE9F,MAAM,YAAY,OAAO;EACzB,IAAI,UAAU,QAAQ,OAAO,cAAc,YAAY,UAAU,KAAK,CAAC,CAAC,SAAS,GAC/E,QAAQ,UAAU,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;CACzC;CACA,SAAS,OAAO,KAAK;CACrB,KAAK,MAAM,SAAS,SAAS,OAAO,IAAI,GAAG;EACzC,IAAI,SAAS,UAAU,aAAa,iBAAiB;GACnD,MAAM,KACJ,GAAG,MAAM,OAAO,WAAW,mBAAmB,OAAO,aAAa,eAAe,EAAE,kCACrF;GACA;EACF;EACA,SAAS,KAAK,QAAQ,QAAQ,OAAO,MAAM,OAAO;GAAE,GAAG;GAAO,MAAM;EAAK,CAAC,CAAC;CAC7E;CACA,MAAM,QAAqB,CAAC;CAC5B,KAAK,MAAM,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAAC,MAAM,GAAG,aAAa,KAAK,GAAG;EACpE,SAAS;EACT,IAAI,MAAM,UAAU,aAAa,OAAO;EACxC,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,KAAK,MAAM,GAAG,GAAG,IAAI;EACvE,MAAM,OAAO,OAAO,KAAK,IAAI;EAC7B,IAAI,SAAS,KAAA,KAAa,OAAO,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,KAAK,GAAG,KAAK,MAAM,GAAG;EACtF,IAAI,MAAM,MAAM,SAAS,KAAK,QAAQ,KAAK,IAAI,GAAG;EAClD,MAAM,KAAK;GAAE,KAAK,KAAK;GAAM;EAAK,CAAC;CACrC;CACA,OAAO;EAAE;EAAU;EAAO;EAAO;EAAO;CAAM;AAChD;AAyFA,MAAMA,cAAY,SAChB,KAAK,YAAY,QAAQ,KAAK,YAAY,kBAAkB,KAAK,aAAa,KAAA;;;;;;;;;;;AAYhF,MAAa,YAAY,OACvB,QACA,KACA,UAAwB,CAAC,MACA;CACzB,MAAM,UAAU,UAAU,MAAM;CAChC,MAAM,QAAQ,QAAQ,SAAS,aAAa;CAC5C,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,aAAa,UAChE,MAAM,IAAI,WACR,0CAA0C,OAAO,aAAa,QAAQ,EAAE,QAAQ,OAAO,KAAK,GAC9F;CAEF,IAAI;CACJ,IAAI;EACF,OAAO,IAAI,IAAI,GAAG;CACpB,QAAQ;EACN,MAAM,IAAI,UAAU,0CAA0C,KAAK,UAAU,GAAG,GAAG;CACrF;CACA,IAAI,KAAK,aAAa,WAAW,KAAK,aAAa,UACjD,MAAM,IAAI,UAAU,6CAA6C,KAAK;CACxE,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,cAA2B;EAC/B,QAAQ,QAAQ,UAAU;EAC1B,UAAU,WAAW,SAAS,SAAS,WAAW,MAAM;EACxD,YAAY,QAAQ;EACpB;EACA,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB,UAAU,WAAW,SAAS,QAAQ,MAAM;EAC5C,UAAU,QAAQ;EAClB,sBAAsB,QAAQ;CAChC;CACA,MAAM,QAAkB,CAAC;CACzB,MAAM,WAA2B,CAAC;CAClC,MAAM,QAAqB,CAAC;CAC5B,MAAM,QAAqB,CAAC;CAC5B,MAAM,2BAAW,IAAI,IAAY;CACjC,IAAI,QAAuB;CAC3B,IAAI,WAAW;CAEf,IAAI,QAAQ,QAAQ,KAAA,GAAW;EAC7B,MAAM,MAAgB,OAAO,QAAQ,QAAQ,WAAW,EAAE,OAAO,QAAQ,IAAI,IAAI,QAAQ;EACzF,MAAM,QAAQ,OAAO,IAAI,UAAU,WAAW,YAAY,IAAI,OAAO,KAAK,IAAI,KAAA;EAC9E,IAAI,UAAU,KAAA,GACZ,MAAM,KACJ,2CAA2C,KAAK,UAAU,OAAO,IAAI,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAC3F;OACK;GACL,SAAS,KACP,QAAQ,OAAO,OAAO,MAAM;IAC1B,KAAK,IAAI,OAAO;IAChB,KAAK,IAAI,OAAO;IAChB,SAAS,IAAI,WAAW;IACxB,YAAY,IAAI,cAAc;IAC9B,MAAM;GACR,CAAC,CACH;GACA,IAAI,OAAO,IAAI,YAAY,UAAU,SAAS,IAAI,IAAI,OAAO;EAC/D;CACF;;CAGA,MAAM,WAAW,OAAO,YAA8C;EACpE,MAAM,OAAO,MAAM,UAAU,SAAS,SAAS,WAAW;EAC1D,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;EACtE,MAAM,OAAO,QAAQ,KAAA,KAAa,WAAW,KAAK,YAAY;EAC9D,MAAM,KAAK;GACT,KAAK;GACL,KAAK,KAAK;GACV,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,SAAS,KAAK;GACd,YAAY,KAAK;GACjB,YAAY,KAAK;GACjB,WAAW;GACX,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS;GACjE,MAAM,KAAK;EACb,CAAC;EACD,IAAI,CAACA,WAAS,IAAI,GAChB,MAAM,KAAK,GAAG,QAAQ,IAAI,KAAK,UAAU,KAAK,SAAS,OAAO,KAAK,KAAK,KAAK,KAAK,IAAI;EAExF,IAAI,QAAQ,KAAA,GAAW,OAAO,KAAA;EAC9B,IAAI,MAAM;GACR,WAAW;GACX;EACF;EACA,IAAI,IAAI,aAAa,MAAM,MAAM,eAAe,SAAS,KAAK,GAAG;EACjE,SAAS,IAAI,IAAI,OAAO;EACxB,OAAO;CACT;CAEA,MAAM,WAAW,QAAmB;EAClC,MAAM,OAAO,gBAAgB,KAAK,EAAE,UAAU,QAAQ,SAAS,CAAC;EAChE,SAAS,KAAK,GAAG,KAAK,QAAQ;EAC9B,MAAM,KAAK,GAAG,KAAK,KAAK;EACxB,UAAU,KAAK;EACf,KAAK,MAAM,QAAQ,KAAK,OACtB,IAAI,MAAM,SAAS,aAAa,SAAS,CAAC,MAAM,MAAM,UAAU,MAAM,QAAQ,KAAK,GAAG,GACpF,MAAM,KAAK,IAAI;CAErB;CAEA,IAAI,UAAyB;CAC7B,IAAI,mBAAmC;CACvC,MAAM,OAAO,MAAM,SAAS,KAAK,IAAI;CACrC,IAAI,SAAS,KAAA,GAAW;EACtB,UAAU,WAAW,KAAK,IAAI,CAAC,EAAE,YAAY,KAAK,OAAO,KAAK;EAC9D,IAAI,QAAQ,SAAS,KAAA,GAAW;GAC9B,mBAAmB,WAAW,QAAQ,MAAM,IAAI,KAAK;GACrD,IAAI,qBAAqB,OACvB,MAAM,KACJ,GAAG,QAAQ,iBAAiB,QAAQ,KAAK,oDAC3C;EAEJ;EACA,IAAI,qBAAqB,SAAS,QAAQ,gBAAgB,MACxD,MAAM,KAAK,gFAAgF;OACtF;GACL,QAAQ,IAAI;GACZ,KAAK,MAAM,QAAQ,eAAe,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,KAAK,GAAG;IAC9E,IAAI,QAAQ,QAAQ,YAAY,MAAM;IACtC,MAAM,MAAM,MAAM,SAAS,KAAK,IAAI;IACpC,IAAI,QAAQ,KAAA,GAAW,QAAQ,GAAG;GACpC;EACF;CACF;CACA,MAAM,QAAQ,YAAY,QAAQ,OAAO,OAAO,MAAM,OAAO,KAAK,IAAI,IAAI,UAAU;CACpF,IAAI,UAAU,MAAM,MAAM,KAAK,GAAG,KAAK,KAAK,eAAe,OAAO;CAClE,IAAI,UAAU,MAAM,KAAK,SAAS;CAGlC,MAAM,0BAAU,IAAI,IAA0B;CAC9C,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,IAAI,SAAS,GAAG,GAAG,IAAI,MAAM,GAAG,IAAI,UAAU;EACzE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,QAAQ,IAAI,KAAK,GAAG;CAC7C;CAEA,MAAM,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,MAClC,GAAG,MAAM,EAAE,OAAO,EAAE,QAAQ,OAAO,EAAE,QAAQ,IAAI,OAAO,EAAE,QAAQ,CACrE;CACA,MAAM,2BAAW,IAAI,IAAS;CAC9B,MAAM,QAAQ,OAAO,QAAQ,QAAQ,IAAI,QAAQ,CAAC;CAClD,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACrC,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GACzC,KAAK,MAAM,OAAO,cAAc,MAAM,EAAE,CAAE,MAAM,MAAM,EAAE,CAAE,IAAI,GAAG,SAAS,IAAI,GAAG;CAGrF,MAAM,OAAO,OAAO,MAAM,QAAQ,IAAI,aAAa,SAAS,KAAK;CACjE,MAAM,WACJ,OAAO,MAAM,QAAQ,IAAI,SAAS,SAAS,IAAI,aAAa,aAAa,CAAC,IAAI,QAAQ,KAAK;CAC7F,OAAO;EACL,KAAK,KAAK;EACV,MAAM;EACN;EACA,aAAa;EACb;EACA,UAAU;EACV;EACA,WAAW;EACX,UAAU,KAAK,QAAQ,QAAQ,SAAS,IAAI,GAAG,CAAC;EAChD;EACA;EACA,UAAU,CAAC,GAAG,QAAQ;EACtB,WAAW;EACX,MAAM,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI;CAC9C;AACF;;;;;;;;AC7vBA,MAAa,iBAWT;CAAE,SAAS;CAAK,QAAQ;CAAK,UAAU;CAAK,OAAO;CAAG,YAAY;AAAG;AAmDzE,MAAM,gBAAgB;;AAGtB,MAAM,YAAY;AAElB,MAAM,YAAY;CAAC;CAAU;CAAU;CAAU;CAAS;AAAM;;AAGhE,MAAM,UAA4C;CAAE,KAAK;CAAO,KAAK;AAAM;AAE3E,IAAI;;AAEJ,MAAM,cAAc,SAA0B;CAC5C,IAAI,CAAC,aAAa,KAAK,IAAI,GAAG,OAAO;CACrC,IAAI,oBAAoB,KAAA,GACtB,IAAI;EACF,kBAAkB,IAAI,IAAI,KAAK,kBAAkB,UAAU,CAAC;CAC9D,QAAQ;EAEN,kCAAkB,IAAI,IAAI;CAC5B;CAEF,OAAO,gBAAgB,SAAS,KAAK,gBAAgB,IAAI,IAAI;AAC/D;;AAGA,MAAM,kBAAkB,aAAqD;CAC3E,IAAI,aAAa,KAAA,GAAW,OAAO,KAAA;CACnC,IAAI;EACF,OAAO,IAAI,KAAK,aAAa,MAAM;GAAE,OAAO;GAAY;EAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAClF;CACL,QAAQ;EACN;CACF;AACF;;;;;;;;;;AAWA,MAAa,cAAc,OAAgB,aAA0C;CACnF,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,QAAQ,KAAA;CACpF,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,eAAe,YAAY,OAAO,KAAA;CAElF,MAAM,QAAQ,yCAAyC,KAAK,MAAM,KAAK,CAAC;CACxE,IAAI,UAAU,MAAM,OAAO,KAAA;CAC3B,IAAI,SAAS,MAAM,EAAE,CAAE,QAAQ,WAAW,EAAE;CAC5C,MAAM,OAAO,KAAK,IAAI,OAAO,YAAY,GAAG,GAAG,OAAO,YAAY,GAAG,CAAC;CACtE,IAAI,SAAS,IAAI;EAEf,MAAM,SAAS,IADG,IAAI,OAAO,MAAM,OAAO,CACvB,CAAC,CAAC,SAAS,KAAK,+BAA+B,KAAK,MAAM;EAC7E,MAAM,eAAe,UAAU,eAAe,QAAQ,MAAM,KAAK,eAAe,KAAK,MAAM;EAC3F,SACE,UAAU,CAAC,eACP,OAAO,QAAQ,SAAS,EAAE,IAC1B,GAAG,OAAO,MAAM,GAAG,IAAI,CAAC,CAAC,QAAQ,SAAS,EAAE,EAAE,GAAG,OAAO,MAAM,OAAO,CAAC;CAC9E;CACA,MAAM,SAAS,OAAO,MAAM;CAC5B,OAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS,KAAA;AAC1D;AAEA,MAAM,QAAQ,OAAgB,WAC3B,MAAM,QAAQ,KAAK,IAAI,QAAQ,UAAU,KAAA,KAAa,UAAU,OAAO,CAAC,IAAI,CAAC,KAAK,EAAA,CAAG,MACpF,GACA,KACF;AAEF,MAAM,WAAW,OAAgB,UAC/B,KAAK,OAAO,KAAK,CAAC,CAAC,QAChB,SAA6B,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAChG;;AAGF,MAAM,UAAU,UAAkC;CAChD,KAAK,MAAM,QAAQ,KAAK,OAAO,EAAE,GAAG;EAClC,IAAI,OAAO,SAAS,YAAY,KAAK,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;EACvF,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;GAC7C,MAAM,QAAS,KAAoB;GACnC,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG;EAC5F;CACF;CACA,OAAO;AACT;AAEA,MAAM,UAAU,UACd,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAC/C,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,IACzB,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAChD,OAAO,KAAK,IACZ;AAER,MAAM,WAAW,UAAkC;CACjD,KAAK,MAAM,QAAQ,KAAK,OAAO,EAAE,GAAG;EAClC,IAAI,OAAO,SAAS,YAAY,YAAY,KAAK,IAAI,GAAG,OAAO;EAC/D,MAAM,MAAM,OAAO,SAAS,YAAY,SAAS,OAAQ,KAAoB,SAAS,KAAA;EACtF,IAAI,OAAO,QAAQ,YAAY,YAAY,KAAK,GAAG,GAAG,OAAO;CAC/D;CACA,OAAO;AACT;AAYA,MAAM,qBAA6B;CACjC,QAAQ,CAAC;CACT,mCAAmB,IAAI,IAAI;CAC3B,8BAAc,IAAI,IAAI;CACtB,yBAAS,IAAI,IAAI;CACjB,UAAU;CACV,OAAO;AACT;;AAGA,MAAM,cAAc,SAAqB,MAAc,UAAwB;CAC7E,MAAM,SAAS,OAAgB,aAA4B;EACzD,MAAM,UAAU,OAAO,aAAa,WAAW,SAAS,KAAK,IAAI;EACjE,MAAM,OAAO,QAAQ,YAAY,QAAQ,YAAY;EACrD,MAAM,SAAS,WAAW,OAAO,KAAK,SAAS,IAAI,OAAO,KAAA,CAAS;EACnE,IAAI,WAAW,KAAA,GAAW;EAC1B,IAAI,KAAK,WAAW,GAAG;GACrB,KAAK,WAAW;GAChB;EACF;EACA,IAAI,CAAC,WAAW,IAAI,GAAG;GACrB,KAAK,kBAAkB,IAAI,QAAQ,MAAM,GAAG,EAAE,CAAC;GAC/C;EACF;EACA,KAAK,OAAO,KAAK;GACf;GACA,UAAU;GACV,KAAK,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,eAAe,UAAU;EACvD,CAAC;CACH;CACA,MAAM,SAAS,GAAe,UAAwB;EACpD,KAAK,SAAS;EACd,MAAM,OAAO,QAAQ,EAAE,uBAAuB,eAAe,MAAM,CAAC,CAAC,MAAM,UAAU;GACnF,MAAM,OAAO,MAAM;GACnB,OAAO,EAAE,OAAO,SAAS,YAAY,UAAU,KAAK,IAAI;EAC1D,CAAC;EACD,MAAM,WAAW,EAAE,oBAAoB,OAAO;EAC9C,MAAM,EAAE,YAAY,OAAO,UAAU,QAAQ;EAC7C,MAAM,EAAE,aAAa,QAAQ;EAC7B,MAAM,EAAE,cAAc,QAAQ;EAC9B,MAAM,eACJ,OAAO,EAAE,oBAAoB,WACzB,EAAE,eAAe,CACd,KAAK,CAAC,CACN,QAAQ,YAAY,EAAE,CAAC,CACvB,MAAM,GAAG,EAAE,IACd,KAAA;EACN,IAAI,iBAAiB,KAAA,KAAa,aAAa,SAAS,GAAG,KAAK,aAAa,IAAI,YAAY;EAC7F,MAAM,SAAS,OAAO,EAAE,SAAS;EACjC,IAAI,WAAW,MAAM,KAAK,QAAQ,IAAI,MAAM;EAE5C,IAAI,QAAQ,eAAe,OACzB,KAAK,MAAM,SAAS,QAAQ,EAAE,WAAW,eAAe,MAAM,GAAG,MAAM,OAAO,QAAQ,CAAC;CAE3F;CACA,KAAK,MAAM,KAAK,QAAQ,QAAQ,WAAW,eAAe,MAAM,GAAG,MAAM,GAAG,CAAC;CAC7E,IAAI,SAAS,GAAG;CAChB,KAAK,MAAM,WAAW,QAAQ,QAAQ,eAAe,eAAe,QAAQ,GAAG;EAC7E,KAAK,SAAS;EACd,WAAW,SAAS,MAAM,QAAQ,CAAC;CACrC;AACF;AAEA,MAAM,kBAAkB,SACtB,KAAK,aAAa,SAAS,IACvB,OACA,KAAK,aAAa,SAAS,KAAK,CAAC,KAAK,aAAa,IAAI,SAAS,IAC9D,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,KACvB;AAER,MAAM,WAAW,OAAe,SAAqC;CAEnE,IAAI,MAAM,WAAW,GAAG,GAAG,OAAO,KAAA;CAClC,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,OAAO,IAAI;EAC/B,OAAO,GAAG,IAAI,SAAS,YAAY,CAAC,CAAC,QAAQ,UAAU,EAAE,IAAI,IAAI,SAAS,QAAQ,OAAO,EAAE;CAC7F,QAAQ;EACN;CACF;AACF;;AAGA,MAAM,UAAU,WAA+B;CAC7C,IAAI;EACF,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC;CAChC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;;;AAcA,MAAa,eACX,SACA,SACyE;CACzE,IAAI,QAAQ;CACZ,MAAM,WAAyB,CAAC;CAChC,KAAK,MAAM,UAAU,QAAQ,MAAM,GAAG,eAAe,OAAO,GAAG;EAC7D,SAAS;EACT,IAAI,QAAQ,MAAiC,CAAC,CAAC,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC,GACpF,SAAS,KAAK,MAAM;EACtB,KAAK,MAAM,SAAS,QAAQ,OAAO,eAAe,EAAE,GAAG;GACrD,SAAS;GACT,IAAI,QAAQ,KAAgC,CAAC,CAAC,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC,GACnF,SAAS,KAAK,KAAK;EACvB;CACF;CACA,MAAM,OAAO,QAAQ,MAAM,IAAI;CAC/B,MAAM,MAAM,SAAS,MAAM,YACzB,CAAC,QAAQ,QAAQ,QAAQ,MAAM,CAAC,CAAC,MAC9B,UAAU,OAAO,UAAU,YAAY,SAAS,KAAA,KAAa,QAAQ,OAAO,IAAI,MAAM,IACzF,CACF;CACA,IAAI,QAAQ,KAAA,GAAW,OAAO;EAAE,SAAS;EAAK;CAAM;CACpD,MAAM,UAAU,SAAS,QACtB,YAAY,KAAK,QAAQ,WAAW,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,QAAQ,eAAe,CAAC,CAAC,CAAC,SAAS,CAChG;CACA,MAAM,OAAO,QAAQ,SAAS,IAAI,UAAU;CAC5C,IAAI;CACJ,IAAI,OAAO;CACX,KAAK,MAAM,WAAW,MAAM;EAC1B,MAAM,SAAS,OAAO,OAAO;EAC7B,SAAS;EACT,IAAI,SAAS,MAAM;GACjB,OAAO;GACP,OAAO;EACT;CACF;CACA,OAAO;EAAE,SAAS;EAAM;CAAM;AAChC;;;;;;AAOA,MAAa,eAAe,SAAqB,SAAyC;CACxF,IAAI;CACJ,IAAI;EACF,SAAS,IAAI,IAAI,IAAI,CAAC,CAAC;CACzB,QAAQ;EACN;CACF;CACA,MAAM,YAAY,OAAO,IAAI,SAAS;CACtC,MAAM,OAAO,OAAO,IAAI,KAAK,KAAK,OAAO,IAAI,MAAM;CACnD,IAAI,cAAc,QAAQ,SAAS,MAAM,OAAO,KAAA;CAChD,MAAM,SAAS,UAA4B;EACzC,IAAI,OAAO,UAAU,YAAY,cAAc,MAAM,OAAO;EAC5D,IAAI;GACF,OACE,IAAI,IAAI,OAAO,IAAI,CAAC,CAAC,aAAa,IAAI,SAAS,MAAM,aACrD,MAAM,SAAS,IAAI,WAAW;EAElC,QAAQ;GACN,OAAO;EACT;CACF;CACA,OAAO,QAAQ,QAAQ,eAAe,eAAe,QAAQ,CAAC,CAAC,MAAM,YAAY;EAC/E,MAAM,SAAS,QAAQ,QAAQ,WAAW,eAAe,MAAM;EAC/D,MAAM,QAAQ;GACZ,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;GACR,QAAQ;EACV,CAAC,CACE,IAAI,MAAM,CAAC,CACX,QAAQ,QAAuB,QAAQ,IAAI;EAC9C,OACG,SAAS,QAAQ,MAAM,SAAS,IAAI,KACrC;GAAC,QAAQ;GAAQ,QAAQ;GAAQ,GAAG,OAAO,KAAK,UAAU,MAAM,MAAM;EAAC,CAAC,CAAC,KAAK,KAAK,KAClF,cAAc,QAAQ,MAAM,SAAS,SAAS;CAEnD,CAAC;AACH;AAEA,MAAM,YAAY,GAAU,MAAqB,EAAE,SAAS,EAAE;;;;;;;;;AAU9D,MAAa,aAAa,SAAgC,SAA8B;CACtF,MAAM,SAAS,YAAY,SAAS,IAAI;CACxC,MAAM,UAAU,OAAO;CACvB,IAAI,YAAY,KAAA,GACd,OAAO;EACL,OAAO;EACP,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,OAAO;EACP,UAAU;EACV,cAAc;EACd,gBAAgB,CAAC;EACjB,SAAS;EACT,QAAQ;EACR,OAAO;EACP,OAAO,CACL,gGACF;EACA,OAAO,OAAO;CAChB;CAEF,MAAM,OAAO,YAAY;CACzB,WAAW,SAAS,MAAM,eAAe,KAAK;CAC9C,MAAM,WAAW,KAAK,OAAO,EAAE,EAAE;CACjC,MAAM,SAAS,KAAK,OAAO,QAAQ,UAAU,MAAM,aAAa,QAAQ,CAAC,CAAC,KAAK,QAAQ;CACvF,MAAM,MAAM,OAAO,MAAM;CACzB,MAAM,OAAO,OAAO,GAAG,EAAE,KAAK;CAC9B,MAAM,OAAO,UAAU,KAAK,QAAQ,OAAO,QAAQ,IAAI,CAAC,CAAC,CAAC,MAAM,UAAU,UAAU,IAAI;CACxF,MAAM,SAAS,YAAY,SAAS,IAAI;CACxC,IAAI,UAAiC;CACrC,IAAI,QAAQ,OAAO,QAAQ,KAAK;CAChC,IAAI,WAAW,KAAA,GAAW;EACxB,MAAM,MAAM,YAAY;EACxB,WAAW,QAAQ,KAAK,CAAC;EACzB,SAAS,IAAI;EACb,MAAM,SAAS,IAAI,OAAO,QAAQ,UAAU,MAAM,aAAa,QAAQ,CAAC,CAAC,KAAK,QAAQ;EACtF,UAAU;GACR,MAAM,OAAO,OAAO,OAAO;GAC3B,KAAK,OAAO,OAAO,MAAM;GACzB,OAAO,OAAO,MAAM;GACpB,cAAc,eAAe,GAAG;EAClC;CACF;CACA,MAAM,QAAQ;EACZ,KAAK,kBAAkB,OAAO,IAC1B,4CAA4C,CAAC,GAAG,KAAK,iBAAiB,CAAC,CAAC,KAAK,IAAI,MACjF;EACJ,KAAK,YAAY,OAAO,WAAW,IAC/B,+EACA,OAAO,WAAW,IAChB,kDACA;EACN,IAAI,IAAI,KAAK,OAAO,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC,CAAC,OAAO,IACvD,6DAA6D,YAAY,OACzE;EACJ,4BAA4B,KAAK,IAAI,KAAK,WAAW,KAAA,IACjD,8FACA;CACN,CAAC,CAAC,QAAQ,SAAyB,SAAS,IAAI;CAChD,OAAO;EACL,OAAO;EACP,MAAM,OAAO,QAAQ,OAAO;EAC5B,OAAO,OAAO,QAAQ,QAAQ;EAC9B,KAAK,OAAO,QAAQ,MAAM;EAC1B,MAAM,QAAQ;EACd,OAAO;EACP,UAAU,SAAS,QAAQ,QAAQ,QAAQ,KAAK,SAAS,IAAI,SAAS,OAAO;EAC7E,cAAc,eAAe,IAAI;EACjC,gBAAgB,CAAC,GAAG,KAAK,YAAY;EACrC;EACA,QAAQ,KAAK,QAAQ,SAAS,IAAI,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI;EACpE,OAAO,QAAQ,QAAQ,QAAQ;EAC/B;EACA;CACF;AACF;AAqDA,MAAM,wBAA6B,IAAI,IAAI;CAAC;CAAW;CAAa;AAAkB,CAAC;;AAGvF,MAAM,UAAU,SACd,MAAM,IAAI,KAAK,OAAO,KACrB,KAAK,YAAY,aAAa,0CAA0C,KAAK,KAAK,QAAQ,EAAE,KAC5F,KAAK,QAAQ,QAAQ,gBAAgB,KAAK,KAAK,QAAQ,EAAE;AAE5D,MAAM,YAAY,SAChB,KAAK,QAAQ,SACZ,KAAK,YAAY,QAAQ,KAAK,YAAY,kBAAkB,KAAK,aAAa,KAAA;;;;;;AAOjF,MAAM,eAAe,OAAO,SAAuB,QAA6C;CAC9F,MAAM,OAAO,MAAM,QAAQ,IAAI,KAAK,SAAS,IAAK,IAAI,KAAK,YAA6B,CAAC;CACzF,IAAI,IAAI,SAAS,QAAQ,WAAW,IAAI,IAAI,MAAM,KAAA,GAAW,OAAO;CACpE,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,IAAI,IAAI;CAC7C,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,MAAM,KAAK,IAAI,OAAO;CACtB,MAAM,OAAO,IAAI,WACb,6BACC,QAAQ,MAAM,OAAO,IAAI,OAAO,CAAC,EAAE,gBAAgB;CACxD,IAAI;EACF,OAAO,YAAY,WAAW,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;CACjD,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;AAUA,MAAa,cAAc,OACzB,QACA,KACA,UAA0B,CAAC,MACA;CAC3B,MAAM,UAAU,UAAU,MAAM;CAChC,IAAI;EACF,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU,MAAM,IAAI,UAAU,GAAG;CAC1F,QAAQ;EACN,MAAM,IAAI,UAAU,+CAA+C,KAAK,UAAU,GAAG,GAAG;CAC1F;CACA,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,WAAW,WAAW,SAAS,QAAQ,MAAM;CACnD,MAAM,eAAe,UAAoD;EACvE,QAAQ;EACR,UAAU,WAAW,SAAS,SAAS,WAAW,MAAM;EACxD,YAAY,QAAQ;EACpB;EACA,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,QAAQ,QAAQ;EAChB;EACA,UAAU,QAAQ;EAClB,sBAAsB,QAAQ;CAChC;CACA,IAAI,OAAO,MAAM,UAAU,SAAS,KAAK,YAAY,MAAM,CAAC;CAC5D,MAAM,QAAkB,CAAC;CACzB,MAAM,UACJ,MACA,MACA,OAAyC,CAAC,MACxB;EAClB,MAAM,MAAM;GAAC,KAAK;GAAM,GAAG;GAAO,GAAI,MAAM,SAAS,CAAC;EAAE,CAAC,CAAC,QACvD,SAAyB,SAAS,QAAQ,SAAS,KAAA,KAAa,KAAK,SAAS,CACjF;EACA,OAAO;GACL;GACA,OAAO,MAAM,SAAS;GACtB,UAAU,MAAM,UAAU,QAAQ,KAAK,SAAS;GAChD,MAAM,MAAM,QAAQ;GACpB,OAAO,MAAM,SAAS;GACtB,KAAK,MAAM,OAAO;GAClB,MAAM,MAAM,QAAQ;GACpB,OAAO,MAAM,SAAS;GACtB,UAAU,MAAM,YAAY;GAC5B,cAAc,MAAM,gBAAgB;GACpC,gBAAgB,MAAM,kBAAkB,CAAC;GACzC,SAAS,MAAM,WAAW;GAC1B,QAAQ,MAAM,UAAU;GACxB,OAAO,MAAM,SAAS;GACtB,MAAM,OAAO,IAAI;GACjB,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,KAAK,KAAK;GACV,SAAS,KAAK;GACd,YAAY,KAAK;GACjB,UAAU,CAAC,GAAG,KAAK,QAAQ;GAC3B,YAAY,KAAK;GACjB,WAAW,KAAK,aAAa;GAC7B,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS;GACjE,MAAM,IAAI,SAAS,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI;EACxD;CACF;CACA,IAAI,CAAC,SAAS,IAAI,GAAG;EACnB,MAAM,OAAO,OAAO,IAAI;EACxB,MAAM,KACJ,OACI,iCAAiC,KAAK,QAAQ,wEAC9C,8BAA8B,KAAK,QAAQ,EACjD;EACA,OAAO,OAAO,MAAM,KAAA,CAAS;CAC/B;CACA,MAAM,QAAQ,SAAsC;EAClD,MAAM,MAAM,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;EACtE,OAAO,QAAQ,KAAA,KAAa,WAAW,KAAK,YAAY,IAAI,MAAM,KAAA;CACpE;CACA,IAAI,KAAK,IAAI,MAAM,KAAA,GAAW;EAC5B,MAAM,KAAK,SAAS;EACpB,OAAO,OAAO,MAAM,KAAA,GAAW,EAAE,WAAW,KAAK,CAAC;CACpD;CACA,MAAM,SAAS,SACb,KAAK,QAAQ,OAAO,KAAA,IAAY,QAAQ,MAAM,IAAI,KAAK,GAAG;CAC5D,IAAI,MAAM,MAAM,IAAI;CACpB,IAAI,OAAO,QAAQ,KAAA,IAAY,KAAA,IAAY,UAAU,MAAM,aAAa,SAAS,GAAG,GAAG,GAAG;CAE1F,IAAI,MAAM,UAAU,QAAQ,CAAC,KAAK,YAAY,WAAW,UAAU,KAAK,aAAa,KAAA,GAAW;EAC9F,MAAM,WAAW,MAAM,UAAU,SAAS,KAAK,YAAY,QAAQ,CAAC;EACpE,MAAM,QAAQ,MAAM,QAAQ;EAC5B,IACE,SAAS,YACT,SAAS,QAAQ,KACjB,UAAU,KAAA,KACV,KAAK,QAAQ,MAAM,KAAA,GACnB;GACA,OAAO;GACP,MAAM;GACN,OAAO,UAAU,MAAM,aAAa,SAAS,KAAK,GAAG,GAAG;EAC1D,OAAO,IAAI,SAAS,SAAS,QAAQ,CAAC,SAAS,UAC7C,MAAM,KAAK,iCAAiC,SAAS,MAAM;CAE/D;CACA,IAAI,gBAAgB,KAAK,aAAa,MAAM,MAAM,eAAe,SAAS,KAAK,GAAG;CAClF,OAAO,OAAO,MAAM,IAAI;AAC1B"}
|