mcp-databnf 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -4
- package/dist/bnf/client.js +1 -1
- package/dist/bnf/client.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +4 -4
package/dist/bnf/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config.ts","../../src/version.ts","../../src/errors.ts","../../src/bnf/cache.ts","../../src/bnf/endpoint.ts","../../src/bnf/http.ts","../../src/bnf/sparql.ts","../../src/bnf/queries.ts","../../src/bnf/parse.ts","../../src/bnf/rateLimiter.ts","../../src/bnf/client.ts"],"sourcesContent":["/**\n * Settings, read from the environment.\n *\n * A value that cannot be read warns and falls back rather than stopping the\n * server: a typo in one variable should not take away every tool. Warnings go\n * to stderr, because stdout carries the protocol and anything written there\n * corrupts the session.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport const LOG_LEVELS = [\"silent\", \"error\", \"info\", \"debug\"] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\n/**\n * The narrowest spacing this client will ever use, in milliseconds.\n *\n * The BnF publishes no rate for the SPARQL endpoint. It publishes\n * `Crawl-delay: 5` for gallica.bnf.fr and enforces it there by banning an\n * address, so five seconds is the only figure the institution has stated about\n * how fast it wants to be read. Three seconds is the floor here because a\n * SPARQL query is one request where a crawl is thousands, and because every\n * answer is cached: the spacing that matters is the one between two distinct\n * questions.\n *\n * Configuration can slow the server down. It cannot take the spacing below this\n * floor, whether the setting arrives through the environment or through a\n * configuration object handed to the published client.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 3000;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_MS = 120_000;\n/** The spacing in force when nothing asks for more. */\nexport const DEFAULT_INTERVAL_MS = 3000;\n\n/**\n * The deadline one query is given.\n *\n * Measured against the live endpoint, a lookup by identifier answers in well\n * under a second, while a full-text query over titles takes several seconds and\n * a query the planner handles badly is dropped by the server with an empty\n * body. Sixty seconds clears the slow honest case without holding the single\n * request slot for a query that will never answer.\n */\nexport const DEFAULT_TIMEOUT_MS = 60_000;\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-databnf/${PKG_VERSION} (+${REPO_URL})`;\n\nexport interface Logger {\n debug: (message: string) => void;\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport function createLogger(level: LogLevel): Logger {\n const rank = LOG_LEVELS.indexOf(level);\n const write = (at: LogLevel, message: string) => {\n if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {\n return;\n }\n process.stderr.write(`[mcp-databnf] ${at}: ${message}\\n`);\n };\n return {\n debug: (m) => write(\"debug\", m),\n info: (m) => write(\"info\", m),\n // A warning goes out at the error level so it survives the default\n // setting: a caller has to know that rows were dropped.\n warn: (m) => write(\"error\", m),\n error: (m) => write(\"error\", m),\n };\n}\n\nfunction readInteger(\n env: NodeJS.ProcessEnv,\n name: string,\n fallback: number,\n min: number,\n max: number,\n): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return fallback;\n }\n\n const value = Number(raw);\n if (!(Number.isFinite(value) && Number.isInteger(value))) {\n process.stderr.write(\n `[mcp-databnf] error: ${name}=\"${raw}\" is not a whole number; using ${fallback}.\\n`,\n );\n return fallback;\n }\n if (value < min || value > max) {\n // Clamping silently would let a caller believe a setting took effect when\n // the opposite is true, so the refusal is stated and the default stands.\n process.stderr.write(\n `[mcp-databnf] error: ${name}=${value} is outside ${min}..${max}; using ${fallback}.\\n`,\n );\n return fallback;\n }\n return value;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const level = env.BNF_LOG_LEVEL as LogLevel | undefined;\n const logLevel = level && LOG_LEVELS.includes(level) ? level : \"error\";\n if (level && !LOG_LEVELS.includes(level)) {\n process.stderr.write(\n `[mcp-databnf] error: BNF_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.BNF_USER_AGENT?.trim();\n\n return {\n // A caller who wants to be recognised may say who they are, and the\n // contact address stays attached: the BnF has to be able to reach a human\n // about traffic it did not expect.\n userAgent: custom ? `${custom} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT,\n minIntervalMs: readInteger(\n env,\n \"BNF_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"BNF_TIMEOUT_MS\", DEFAULT_TIMEOUT_MS, 1000, 300_000),\n maxRetries: readInteger(env, \"BNF_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"BNF_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"BNF_CACHE_MAX_ENTRIES\", 200, 1, 5000),\n logLevel,\n };\n}\n","/**\n * Kept in step with the version in package.json. The User-Agent embeds it, so\n * the BnF can tell which build is calling from its own logs.\n */\nexport const PKG_VERSION = \"2.1.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-databnf\";\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"the BnF describes no such\n * thing\" and \"the question could not be asked\". Collapsing the two lets a model\n * report an absence it never established, which is a false statement about a\n * catalogue rather than a missing feature.\n */\n\nexport type ErrorCode =\n /** data.bnf.fr answered, and describes no such person, work or edition. */\n | \"not_found\"\n /** The arguments cannot produce a query. */\n | \"invalid_input\"\n /** data.bnf.fr asked this client to slow down or refused it for now. */\n | \"rate_limited\"\n /** An answer arrived in a shape this server cannot read. */\n | \"parse_failure\"\n /** The request could not be completed. */\n | \"network_error\"\n /** The request was abandoned before an answer arrived. */\n | \"timeout\";\n\nexport interface ErrorDetails {\n /** What the caller can do about it, when there is something. */\n hint?: string;\n /** The address that produced the failure, for a bug report. */\n url?: string;\n status?: number;\n /** What was raised underneath, kept for the bug report the hint asks for. */\n cause?: unknown;\n}\n\nexport class BnfError extends Error {\n readonly code: ErrorCode;\n readonly details: ErrorDetails;\n\n constructor(code: ErrorCode, message: string, details: ErrorDetails = {}) {\n super(message, details.cause === undefined ? undefined : { cause: details.cause });\n this.name = \"BnfError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new BnfError(\"not_found\", message, details);\n\nexport const invalidInput = (message: string, hint?: string) =>\n new BnfError(\"invalid_input\", message, hint ? { hint } : {});\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new BnfError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the BnF describes what you asked for.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new BnfError(\"parse_failure\", message, {\n hint: \"data.bnf.fr may have changed how it answers. Please report this at https://github.com/smeet666/mcp-databnf/issues with the arguments you used.\",\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new BnfError(\"network_error\", message, details);\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new BnfError(\"timeout\", message, details);\n","/**\n * A small in-memory store, keyed by the query that produced the value.\n *\n * It exists to keep a conversation that asks the same thing twice from asking\n * the endpoint twice, which matters here because a conversation about one\n * author walks the same records repeatedly: the person, then the works, then\n * the editions of one work, then back to the person.\n *\n * Entries expire, the store is bounded, and it holds only what was successfully\n * read: storing a response nobody could parse would serve that failure back for\n * the rest of its lifetime.\n *\n * Each entry keeps the moment it was read. The licence asks for the date the\n * metadata was retrieved, and a cached answer was retrieved when it entered the\n * store rather than when it was served, so the stored moment is what travels\n * with it.\n */\n\ninterface Entry<T> {\n value: T;\n expiresAt: number;\n retrievedAt: number;\n}\n\nexport interface Hit<T> {\n value: T;\n retrievedAt: number;\n}\n\nexport class Cache<T> {\n private readonly entries = new Map<string, Entry<T>>();\n\n constructor(\n private readonly ttlMs: number,\n private readonly maxEntries: number,\n ) {}\n\n get(key: string): Hit<T> | undefined {\n const entry = this.entries.get(key);\n if (!entry) {\n return undefined;\n }\n if (entry.expiresAt <= Date.now()) {\n this.entries.delete(key);\n return undefined;\n }\n // Re-inserting marks it as the most recently used, which is what the\n // eviction below reads.\n this.entries.delete(key);\n this.entries.set(key, entry);\n return { value: entry.value, retrievedAt: entry.retrievedAt };\n }\n\n set(key: string, value: T, retrievedAt: number): void {\n // A lifetime of zero turns the store off rather than expiring at once:\n // nothing is written, so nothing has to be checked on the way out.\n if (this.ttlMs <= 0) {\n return;\n }\n this.entries.delete(key);\n this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs, retrievedAt });\n while (this.entries.size > this.maxEntries) {\n const oldest = this.entries.keys().next();\n if (oldest.done) {\n break;\n }\n this.entries.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n","/**\n * Every host, address and vocabulary term this server reads, in one place. An\n * upstream rename is then a one-file change rather than a hunt through the\n * parsers.\n *\n * The robots file at data.bnf.fr withholds one path, the one ending in\n * `cross-documents`, and nothing built here reaches it. The endpoint below is\n * addressed without a trailing slash: with one, the same address serves a\n * JavaScript query editor rather than the query service.\n */\n\nimport { BnfError } from \"../errors.js\";\n\n/** The root a host name may end on, which names the same host without it. */\nconst TRAILING_DOT = /\\.$/;\nconst FRAGMENT = /#.*$/;\nconst HTTP_SCHEME = /^http:/;\n\n/** The one address this server sends a request to. */\nexport const SPARQL_ENDPOINT = \"https://data.bnf.fr/sparql\";\n\n/** The host every request must be addressed to, checked before each call. */\nexport const ALLOWED_HOST = \"data.bnf.fr\";\n\n/**\n * The host this server describes and never requests.\n *\n * The BnF places its metadata and its digitised contents under two different\n * regimes. The metadata served here is free to reuse with its source and its\n * date of retrieval named. The contents on gallica.bnf.fr are not: their terms\n * make use inside an artificial-intelligence project subject to a paid licence\n * outside academic research, and the site refuses ClaudeBot and GPTBot at the\n * server, then bans the calling address after about fifteen requests whatever\n * the pace.\n *\n * So a Gallica address is a piece of metadata here, rendered as a link for a\n * person to open. `assertRequestable` below is what keeps that a rule rather\n * than an intention.\n */\nexport const GALLICA_HOST = \"gallica.bnf.fr\";\n\n/**\n * Refuse to request anything but the query service.\n *\n * This sits between the query builders and `fetch`, so a future route, a\n * redirect followed by hand, or an address read out of a response cannot become\n * a request to a host this server has no licence to read.\n */\nexport function assertRequestable(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch (cause) {\n throw refusal(`mcp-databnf refuses to request \"${url}\": it is not an address.`, cause);\n }\n const host = parsed.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n if (host !== ALLOWED_HOST) {\n throw refusal(\n `mcp-databnf requests ${ALLOWED_HOST} and nothing else; \"${host}\" was refused. ` +\n \"Gallica in particular is described from the catalogue and never read.\",\n );\n }\n}\n\n/**\n * The refusal, raised as an error the transport will not retry.\n *\n * A plain error here would be read as a request that failed to complete, and the\n * next attempt would be made against the same address. A decision not to call a\n * host is settled, so it has to arrive as an answer rather than as a mishap.\n */\nconst refusal = (message: string, cause?: unknown) =>\n new BnfError(\"network_error\", message, {\n hint: \"This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host.\",\n cause,\n });\n\n/** True when an address points at Gallica, whatever the scheme or the case. */\nexport function isGallicaAddress(address: string): boolean {\n try {\n // A trailing dot names the root of the domain tree and resolves to the same\n // host. Comparing without stripping it would read a Gallica address as\n // somewhere else, and the link would be dropped from the answer.\n const host = new URL(address).hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n return host === GALLICA_HOST || host.endsWith(`.${GALLICA_HOST}`);\n } catch {\n return false;\n }\n}\n\n/** Namespaces the queries use, emitted as a PREFIX block. */\nexport const PREFIXES = {\n foaf: \"http://xmlns.com/foaf/0.1/\",\n rdf: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\n rdfs: \"http://www.w3.org/2000/01/rdf-schema#\",\n owl: \"http://www.w3.org/2002/07/owl#\",\n skos: \"http://www.w3.org/2004/02/skos/core#\",\n dcterms: \"http://purl.org/dc/terms/\",\n bio: \"http://vocab.org/bio/0.1/\",\n bnf: \"http://data.bnf.fr/ontology/bnf-onto/\",\n bibo: \"http://purl.org/ontology/bibo/\",\n isni: \"http://isni.org/ontology#\",\n /** RDA group 2 elements, which carry the biographical fields. */\n rdag2: \"http://rdvocab.info/ElementsGr2/\",\n /** RDA elements, which carry the bibliographic fields. */\n rdae: \"http://rdvocab.info/Elements/\",\n /** RDA relationships between work, expression, manifestation and item. */\n rdarel: \"http://rdvocab.info/RDARelationshipsWEMI/\",\n /** The FRBR classes data.bnf.fr types its entities with. */\n rdafrbr: \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/\",\n} as const;\n\nexport const PREFIX_BLOCK = Object.entries(PREFIXES)\n .map(([name, iri]) => `PREFIX ${name}: <${iri}>`)\n .join(\"\\n\");\n\n/** The two shapes an entity address takes in this dataset. */\nexport const ARK_BASE = \"http://data.bnf.fr/ark:/12148/\";\nexport const TEMP_WORK_BASE = \"http://data.bnf.fr/temp-work/\";\n\n/** The page a person can open for a record, which every result carries. */\nexport const publicPageFor = (entityIri: string): string =>\n entityIri.replace(FRAGMENT, \"\").replace(HTTP_SCHEME, \"https:\");\n\n/** The BnF general catalogue, where a record is described for a reader. */\nexport const catalogueUrlFor = (arkId: string): string =>\n `https://catalogue.bnf.fr/ark:/12148/${arkId}`;\n\n/**\n * The licence this dataset is published under, and what it asks for.\n *\n * The BnF states it in one sentence: the metadata may be used freely and at no\n * cost, provided the source is named and the date of retrieval is stated. Both\n * halves are therefore attached to every answer rather than left to a caller.\n */\nexport const LICENCE_FR =\n \"L'utilisation de ces métadonnées est libre et gratuite sous réserve du maintien de la mention de leur source et de l'indication de leur date de récupération.\";\n\nexport const LICENCE_EN =\n \"These metadata may be used freely and at no cost, provided their source is named and the date they were retrieved is stated.\";\n","/**\n * One query sent to the endpoint, with a deadline and bounded retries.\n *\n * Two things separate a retry worth making from one that only adds load. A\n * refusal that carries a time to come back is obeyed rather than guessed at,\n * and an answer the service meant is never retried: asking again for something\n * that is not there wastes a request and delays the honest answer.\n *\n * The query travels in the body of a POST. A long query in a query string is\n * refused by an intermediary before it reaches the service, and a POST keeps the\n * text out of every access log between here and there.\n */\n\nimport {\n invalidInput,\n networkError,\n parseFailure,\n rateLimited,\n timeout as timeoutError,\n} from \"../errors.js\";\nimport type { Logger } from \"../config.js\";\nimport { SPARQL_ENDPOINT, assertRequestable } from \"./endpoint.js\";\nimport type { RateLimiter } from \"./rateLimiter.js\";\n\n/** The shape the endpoint's own engine writes a refusal in. */\nconst VIRTUOSO_ERROR = /^Virtuoso\\s+(\\w+)\\s+Error\\s+([\\s\\S]*)$/;\n\n/** The JSON shape a SPARQL SELECT answers with. */\nexport interface SparqlTerm {\n type: \"uri\" | \"literal\" | \"typed-literal\" | \"bnode\";\n value: string;\n datatype?: string;\n \"xml:lang\"?: string;\n}\n\nexport type SparqlRow = Record<string, SparqlTerm | undefined>;\n\nexport interface SparqlResults {\n head: { vars?: string[] };\n results: { bindings: SparqlRow[] };\n}\n\nexport interface QueryOptions {\n query: string;\n userAgent: string;\n timeoutMs: number;\n maxRetries: number;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Statuses worth another attempt: the service is busy, not answering \"no\". */\nconst RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);\n/**\n * Statuses that mean the service is refusing for now and naming a delay.\n *\n * These produce a `rate_limited` error and honour a `Retry-After` header. A\n * server error is a different claim, so it keeps its own code even though it\n * widens the spacing the same way.\n */\nconst PUSH_BACK = new Set([429, 503]);\n\n/**\n * The longest wait worth taking rather than reporting.\n *\n * A refusal may name any delay, and an hour is a legal answer. Sleeping through\n * it holds the one request slot this server has, so every other tool waits\n * behind a call whose caller has long since given up. Past this point the wait\n * is the answer, and the caller decides what to do with it.\n */\nconst LONGEST_WAIT_MS = 30_000;\n\n/**\n * How many times a query that never answered is worth repeating.\n *\n * A query that did not respond within its budget is one the planner is\n * struggling with. Repeating it adds the same load again, and each attempt\n * holds the slot for the full deadline.\n */\nconst RETRIES_AFTER_SILENCE = 1;\n\n/**\n * Redirects worth following before calling it a loop.\n *\n * The query service answers directly, so one hop covers a host that has moved\n * and two cover a scheme change on top of it. Beyond that the answer is a\n * redirect rather than a result.\n */\nconst MAX_REDIRECTS = 2;\n\n/**\n * Read a Retry-After header, which is either a number of seconds or a date.\n * Returns null when it says neither, so the caller falls back to its own wait.\n */\nexport function parseRetryAfter(value: string | null, now = Date.now()): number | null {\n if (!value) {\n return null;\n }\n const trimmed = value.trim();\n\n const seconds = Number(trimmed);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n\n const at = Date.parse(trimmed);\n if (Number.isNaN(at)) {\n return null;\n }\n return Math.max(0, at - now);\n}\n\n/** Growing wait with jitter, so several clients do not return in step. */\nfunction backoffMs(attempt: number): number {\n const base = Math.min(16_000, 1000 * 2 ** attempt);\n return base + Math.floor(Math.random() * 500);\n}\n\n/**\n * What a Virtuoso says when it will not run a query.\n *\n * It answers in plain text rather than in the result format, and the leading\n * code says which half went wrong: a compiler code means the query as written\n * could not be understood, and a runtime code means it was understood and then\n * abandoned, usually because it asked for more of the graph than the service\n * will spend on one caller.\n */\nexport function readEngineError(\n body: string,\n): { kind: \"compile\" | \"runtime\"; text: string } | null {\n const match = VIRTUOSO_ERROR.exec(body.trim());\n if (!match) {\n return null;\n }\n const state = match[1] ?? \"\";\n const text = (match[2] ?? \"\").split(\"\\n\")[0]?.trim() ?? \"\";\n // 37000 is the compiler's own state; the rest come from execution.\n return { kind: state === \"37000\" ? \"compile\" : \"runtime\", text };\n}\n\nasync function postOnce(\n options: QueryOptions,\n signal: AbortSignal,\n): Promise<{ status: number; headers: Headers; body: string; ok: boolean }> {\n assertRequestable(SPARQL_ENDPOINT);\n const doFetch = options.fetchImpl ?? fetch;\n\n const form = new URLSearchParams();\n form.set(\"query\", options.query);\n // The endpoint answers in XML when only the Accept header asks for JSON, so\n // the format is stated in the request itself as well.\n form.set(\"format\", \"application/sparql-results+json\");\n\n // Redirects are handled here rather than by fetch. Following one automatically\n // would take the request to whatever host the answer names, and the guard\n // above only ever sees the address this file wrote: a redirect to Gallica\n // would cross the licence boundary with no code having decided to.\n let target = SPARQL_ENDPOINT;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n const response = await doFetch(target, {\n method: \"POST\",\n signal,\n redirect: \"manual\",\n headers: {\n \"user-agent\": options.userAgent,\n accept: \"application/sparql-results+json\",\n \"content-type\": \"application/x-www-form-urlencoded\",\n },\n body: form.toString(),\n });\n\n const location = response.headers.get(\"location\");\n if (response.status >= 300 && response.status < 400 && location !== null) {\n const next = new URL(location, target).toString();\n // Checked before the next request rather than after it, so a refused\n // address is one this client never called.\n assertRequestable(next);\n target = next;\n continue;\n }\n\n return {\n status: response.status,\n headers: response.headers,\n body: await response.text(),\n ok: response.ok,\n };\n }\n\n throw networkError(`data.bnf.fr redirected this request more than ${MAX_REDIRECTS} times.`, {\n url: SPARQL_ENDPOINT,\n });\n}\n\n/** What a refusal from the endpoint amounts to, and what it costs the pacing. */\ntype Refusal =\n | { kind: \"refused\"; error: Error; pushBack: boolean }\n | { kind: \"again\"; waitMs: number; pushBack: boolean };\n\n/**\n * Read a status the endpoint answered with, apart from the loop that retries.\n *\n * Anything worth another attempt means the service is struggling, so the gap\n * widens whether or not the status is one that names a delay: a Virtuoso under\n * load answers 500 as readily as 503. A status the service read the query for\n * and would not run is not a network failure, and calling it one invites a\n * retry of something only the query can fix.\n */\nfunction readRefusal(\n response: { status: number; body: string; headers: Headers },\n attempt: number,\n maxRetries: number,\n): Refusal {\n const pushBack = RETRYABLE.has(response.status);\n\n if (PUSH_BACK.has(response.status)) {\n const asked = parseRetryAfter(response.headers.get(\"retry-after\"));\n\n if (asked !== null && asked > LONGEST_WAIT_MS) {\n return {\n kind: \"refused\",\n pushBack,\n error: rateLimited(\n `data.bnf.fr asked this client to wait ${Math.round(asked / 1000)} seconds (HTTP ${response.status}).`,\n { url: SPARQL_ENDPOINT, status: response.status },\n ),\n };\n }\n if (attempt >= maxRetries) {\n return {\n kind: \"refused\",\n pushBack,\n error: rateLimited(\n `data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,\n {\n url: SPARQL_ENDPOINT,\n status: response.status,\n },\n ),\n };\n }\n return { kind: \"again\", pushBack, waitMs: asked ?? backoffMs(attempt) };\n }\n\n if (RETRYABLE.has(response.status) && attempt < maxRetries) {\n return { kind: \"again\", pushBack, waitMs: backoffMs(attempt) };\n }\n\n if (response.status === 400 || response.status === 422) {\n const engine = readEngineError(response.body);\n return {\n kind: \"refused\",\n pushBack,\n error: invalidInput(\n `data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : \".\"}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n ),\n };\n }\n\n return {\n kind: \"refused\",\n pushBack,\n error: networkError(`data.bnf.fr answered HTTP ${response.status}.`, {\n url: SPARQL_ENDPOINT,\n status: response.status,\n }),\n };\n}\n\n/**\n * What a thrown attempt amounts to, or the error it has become.\n *\n * An error this module raised on purpose already says what happened, and is\n * passed straight on. Silence is given fewer attempts than a refusal: a query\n * spanning the whole catalogue can take longer than the service will spend on\n * it, and asking again costs both sides the same wait.\n */\nfunction readFailure(\n error: unknown,\n attempts: { attempt: number; maxRetries: number; timeoutMs: number },\n): Error {\n const { attempt, maxRetries, timeoutMs } = attempts;\n\n if (error instanceof Error && error.name === \"BnfError\") {\n throw error;\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {\n throw timeoutError(\n `No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,\n { url: SPARQL_ENDPOINT },\n );\n }\n return error;\n }\n\n const failure = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) {\n throw networkError(`Could not reach data.bnf.fr: ${failure.message}`, { url: SPARQL_ENDPOINT });\n }\n return failure;\n}\n\n/** Send one query and return the parsed result set. */\nexport async function runQuery(options: QueryOptions): Promise<SparqlResults> {\n const { timeoutMs, maxRetries, limiter, logger } = options;\n\n let lastError: Error | null = null;\n /** Honoured before the next attempt rather than slept after the last one. */\n let askedWaitMs = 0;\n\n for (let attempt = 0; attempt <= maxRetries; attempt += 1) {\n if (askedWaitMs > 0) {\n logger.debug(`waiting ${askedWaitMs}ms, as asked`);\n // Read once into a constant: the timer closes over what this attempt was\n // told to wait, not over whatever a later attempt puts there.\n const asked = askedWaitMs;\n await new Promise((resolve) => setTimeout(resolve, asked));\n askedWaitMs = 0;\n }\n await limiter.beforeRequest();\n\n const controller = new AbortController();\n const deadline = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n logger.debug(`POST ${SPARQL_ENDPOINT} (${options.query.length} characters)`);\n const response = await postOnce(options, controller.signal);\n\n if (response.ok) {\n // The body decides whether this was an answer. This endpoint says \"I\n // will spend no more on you\" with a status of 200 and either an empty\n // body or a runtime error in plain text, so crediting the status line\n // would narrow the spacing at the one moment it has to widen.\n let parsed: SparqlResults;\n try {\n parsed = parseResults(response.body);\n } catch (error) {\n limiter.pushBack();\n throw error;\n }\n limiter.succeeded();\n return parsed;\n }\n\n const verdict = readRefusal(response, attempt, maxRetries);\n if (verdict.pushBack) {\n limiter.pushBack();\n }\n if (verdict.kind === \"refused\") {\n throw verdict.error;\n }\n askedWaitMs = verdict.waitMs;\n lastError = new Error(`HTTP ${response.status}`);\n } catch (error) {\n clearTimeout(deadline);\n\n lastError = readFailure(error, { attempt, maxRetries, timeoutMs });\n askedWaitMs = backoffMs(attempt);\n } finally {\n clearTimeout(deadline);\n }\n }\n\n throw networkError(\n `Could not reach data.bnf.fr: ${lastError?.message ?? \"no attempt was made\"}`,\n {\n url: SPARQL_ENDPOINT,\n },\n );\n}\n\n/**\n * Read a result set out of a body that arrived with a success status.\n *\n * The endpoint answers 200 with an empty body when it abandons a query part way\n * through, which is the same silence as a result set holding no rows and means\n * the opposite. An empty body is therefore a failure to read rather than an\n * absence, so nothing downstream can report \"the BnF describes none\" on the\n * strength of a query that was never finished.\n */\nexport function parseResults(body: string): SparqlResults {\n if (body.trim() === \"\") {\n throw parseFailure(\n \"data.bnf.fr returned an empty body, which is what it does when it abandons a query part way through.\",\n { url: SPARQL_ENDPOINT },\n );\n }\n\n const engine = readEngineError(body);\n if (engine) {\n if (engine.kind === \"compile\") {\n throw invalidInput(\n `data.bnf.fr could not read this query: ${engine.text}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n );\n }\n throw timeoutError(`data.bnf.fr started this query and gave it up: ${engine.text}`, {\n url: SPARQL_ENDPOINT,\n });\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(body);\n } catch (cause) {\n throw parseFailure(\"data.bnf.fr answered with something that is not JSON.\", {\n url: SPARQL_ENDPOINT,\n cause,\n });\n }\n\n if (\n typeof payload !== \"object\" ||\n payload === null ||\n !(\"results\" in payload) ||\n typeof (payload as SparqlResults).results !== \"object\" ||\n !Array.isArray((payload as SparqlResults).results?.bindings)\n ) {\n throw parseFailure(\"data.bnf.fr answered with JSON that is not a SPARQL result set.\", {\n url: SPARQL_ENDPOINT,\n });\n }\n\n return payload as SparqlResults;\n}\n","/**\n * How caller text becomes part of a query, and what it can never become.\n *\n * A query is a program, and every character of it that came from outside is a\n * chance to write a different program. Three separate defences apply here, and\n * each is narrower than the one before:\n *\n * 1. Text that reaches the full-text index is reduced to words. Letters,\n * digits, apostrophes inside a word and hyphens inside a word survive; every\n * quote, brace, backslash, angle bracket and newline is dropped before the\n * query exists. There is no escaping to get right because there is nothing\n * left to escape.\n * 2. An identifier is matched against the two shapes this dataset uses and\n * rebuilt from the match, so the address in the query is one this file\n * wrote rather than one a caller supplied.\n * 3. Numbers reach the query as numbers, after being bounded.\n *\n * `escapeLiteral` exists for the values that are neither: it is the last line\n * rather than the first, and the tests hold it to producing a literal that\n * closes where it opened whatever it is handed.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\n/** What a term can carry: letters, digits, the joiners and the spaces between them. */\nconst KEPT_CHARACTER = /[\\p{L}\\p{N}'\\-\\s]/u;\nconst TYPOGRAPHIC_APOSTROPHE = /[‘’ʼ]/u;\nconst LEADING_JOINERS = /^['-]+/u;\nconst TRAILING_JOINERS = /['-]+$/u;\nconst JOINERS = /['-]+/u;\nconst ARK_IN_INPUT = /(?:^|\\/)ark:\\/12148\\/(c[bc][0-9a-z]+)/i;\nconst LEADING_HASH = /^#/;\nconst FRAGMENT_OR_QUERY = /[#?]/;\nconst TEMP_WORK_IN_INPUT = /temp-work\\/([0-9a-f]{32})/i;\nconst GALLICA_ARK = /ark:\\/12148\\/([0-9a-z]+)/i;\n\n/**\n * Escape a string for a SPARQL short literal in double quotes.\n *\n * Backslash goes first: escaping it after the others would escape the\n * backslashes they just introduced. The characters a SPARQL short literal\n * cannot carry raw are a quote, a backslash, a line feed and a carriage\n * return; the rest are legal and pass through as published, since mangling a\n * title is its own kind of lie.\n */\nexport function escapeLiteral(value: string): string {\n return (\n value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\")\n // The remaining control characters are legal inside a SPARQL literal and\n // meaningless in a catalogue query. A null byte in particular is where a\n // C string ends, so an intermediary or the engine can truncate the query\n // at one and run a shorter query than the one that was written.\n .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n );\n}\n\n/** A literal, quoted and escaped, ready to be dropped into a query. */\nexport const literal = (value: string): string => `\"${escapeLiteral(value)}\"`;\n\n/**\n * Characters that occupy a position in a string and mark nothing on a screen.\n *\n * The control characters other than the whitespace ones, the zero-width spaces\n * and joiners, the soft hyphen, the bidirectional marks and the byte order\n * mark. A space, a tab and a line break are absent from the list: they are\n * visible as a gap and they do separate two words.\n *\n * They are written as escapes because they would be invisible in this file as\n * well, and a character nobody can see in the source of a guard is a guard\n * nobody can review.\n */\nconst INVISIBLE =\n /[\\u0000-\\u0008\\u000e-\\u001f\\u007f-\\u009f\\u00ad\\u061c\\u180e\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]/gu;\n\n/**\n * The text with the characters that mark nothing removed.\n *\n * A separator is something a person wrote to hold two words apart. One of these\n * holds nothing apart, so treating it as a separator turns one word into two\n * terms the index then requires separately, and a name that matches comes back\n * as a name the catalogue does not hold.\n */\nexport function withoutInvisibles(input: string): string {\n return input.replace(INVISIBLE, \"\");\n}\n\n/** The characters that marked nothing, listed once each, in the order written. */\nexport function invisibleCharacters(input: string): string[] {\n const found: string[] = [];\n for (const character of input.match(INVISIBLE) ?? []) {\n if (!found.includes(character)) {\n found.push(character);\n }\n }\n return found;\n}\n\n/**\n * The visible characters a search dropped, listed once each.\n *\n * Punctuation reaches no index here, so a caller comparing what they wrote\n * against what was searched for needs to see which marks were left out. A\n * space is not one of them: it separates words and is what the words are cut\n * along.\n */\nexport function charactersSetAside(input: string): string[] {\n const found: string[] = [];\n for (const character of withoutInvisibles(input).normalize(\"NFC\")) {\n if (KEPT_CHARACTER.test(character) || TYPOGRAPHIC_APOSTROPHE.test(character)) {\n continue;\n }\n if (!found.includes(character)) {\n found.push(character);\n }\n }\n return found;\n}\n\n/** How a piece of caller text became the terms the index was given. */\nexport interface SearchReading {\n /** The words the text was cut into, each one quoted into the query. */\n words: string[];\n /** The terms the index requires, after it splits inside a word. */\n terms: string[];\n /** The characters that marked nothing and were removed before the cutting. */\n invisible: string[];\n /** The visible characters that reached no term. */\n setAside: string[];\n}\n\n/** Read a piece of caller text the way the query builder and the index will. */\nexport function readSearchText(input: string): SearchReading {\n const words = toSearchWords(input);\n return {\n words,\n terms: toIndexTerms(words),\n invisible: invisibleCharacters(input),\n setAside: charactersSetAside(input),\n };\n}\n\n/**\n * Words a full-text query may carry.\n *\n * The endpoint is a Virtuoso, whose `bif:contains` takes its own miniature\n * language: words in single quotes joined by AND, OR and NOT, with `*` for a\n * prefix. Handing caller text to that language directly would let a caller\n * write the operators, so the text is cut into words instead and this file\n * writes every operator.\n *\n * A word keeps letters and digits in any script, and keeps an apostrophe or a\n * hyphen that sits between two other characters, because \"aujourd'hui\" and\n * \"Charleville-Mézières\" are one word each. A leading or trailing one is\n * dropped, so nothing can close the quoting this file opens.\n *\n * The same rule holds a span of years together: \"1871-1933\" stays one word.\n * Nothing here can tell a range from a compound name, and splitting on the\n * chance that it is a range would break every hyphenated place in France.\n */\nexport function toSearchWords(input: string): string[] {\n const cleaned = withoutInvisibles(input)\n .normalize(\"NFC\")\n // Anything that is not a letter, a digit, an apostrophe or a hyphen becomes\n // a separator. Typographic apostrophes are folded onto the plain one first,\n // since a name is written with either.\n .replace(/[‘’ʼ]/gu, \"'\")\n .replace(/[^\\p{L}\\p{N}'-]+/gu, \" \");\n\n const words: string[] = [];\n for (const raw of cleaned.split(\" \")) {\n // Trim the joiners at the edges: they only mean something between letters.\n const word = raw.replace(LEADING_JOINERS, \"\").replace(TRAILING_JOINERS, \"\");\n if (word === \"\") {\n continue;\n }\n // A word this long is a paste rather than a word, and the index will not\n // match it anyway.\n words.push(word.length > 60 ? word.slice(0, 60) : word);\n if (words.length >= 12) {\n break;\n }\n }\n return words;\n}\n\n/**\n * The terms the index will require, read off the words a caller wrote.\n *\n * The index tokenises inside a quoted term: an apostrophe and a hyphen are\n * separators there, so `O'Brien` reaches it as two terms and a record written\n * `Pat O Brien` carries both and matches. Reporting the word back as one term\n * would leave a caller checking why a row is on the list with no way of getting\n * there from the fields shown.\n *\n * A piece repeated across words is kept once, since requiring a term twice is\n * requiring it.\n */\nexport function toIndexTerms(words: readonly string[]): string[] {\n const terms: string[] = [];\n for (const word of words) {\n for (const piece of word.split(JOINERS)) {\n if (piece !== \"\" && !terms.includes(piece)) {\n terms.push(piece);\n }\n }\n }\n return terms;\n}\n\n/**\n * A `bif:contains` argument requiring every word, built from words this file\n * quoted.\n *\n * The result is a SPARQL literal, so the double quotes belong to SPARQL and the\n * single quotes belong to the index. Two quotings are therefore in play, and\n * each has to be closed by the layer that opened it.\n *\n * The apostrophe kept inside a word is also the index's own quoting character,\n * so `rimbaud'OR'hugo` arrives as one word and emits `'rimbaud'OR'hugo'`, where\n * `OR` has escaped the quotes and become an operator. Doubling it puts it back\n * inside the term, which is how the index writes a literal quote. That is the\n * whole of the exposure: the word cannot reach the SPARQL layer as anything but\n * text, because `escapeLiteral` runs over the finished clause.\n */\nexport function containsAllWords(words: readonly string[]): string {\n if (words.length === 0) {\n throw invalidInput(\n \"No word to search for: the text held no letters or digits.\",\n \"Write the words to look for, such as a surname or a title.\",\n );\n }\n const clause = words.map((word) => `'${word.replace(/'/g, \"''\")}'`).join(\" AND \");\n return literal(clause);\n}\n\n/**\n * The two identifier shapes this dataset uses, and nothing else.\n *\n * An ARK identifier is `cb` followed by digits and one final check character,\n * which the BnF also writes as `cc…` on a few records. A provisional work is\n * named by a thirty-two character hexadecimal digest. Both are matched whole,\n * so an identifier reaches a query only after this file has rebuilt it.\n */\nconst ARK_ID = /^c[bc][0-9a-z]{6,20}$/;\nconst TEMP_WORK_ID = /^[0-9a-f]{32}$/;\n\nexport type EntityKind = \"ark\" | \"temp-work\";\n\nexport interface EntityId {\n kind: EntityKind;\n /** The bare identifier, as a person would quote it. */\n id: string;\n /** The address of the thing itself, which the queries bind to. */\n iri: string;\n /** The page a person can open. */\n pageUrl: string;\n}\n\n/**\n * Read an identifier a caller wrote, in any of the forms they see.\n *\n * Rows carry `http://data.bnf.fr/ark:/12148/cb119219976#about`, prose carries\n * `cb119219976`, and a browser carries `https://data.bnf.fr/ark:/12148/cb119219976`.\n * All three name one record. Anything else is refused rather than guessed at,\n * because an identifier built from a guess sends the next call to a record that\n * does not exist and the answer reads as an absence.\n */\nexport function parseEntityId(input: string): EntityId {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"An identifier is required.\");\n }\n\n const arkMatch = ARK_IN_INPUT.exec(trimmed);\n const bare = arkMatch\n ? arkMatch[1]\n : trimmed.replace(LEADING_HASH, \"\").split(FRAGMENT_OR_QUERY)[0];\n const candidate = (bare ?? \"\").toLowerCase();\n\n if (ARK_ID.test(candidate)) {\n return {\n kind: \"ark\",\n id: candidate,\n iri: `http://data.bnf.fr/ark:/12148/${candidate}#about`,\n pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`,\n };\n }\n\n const tempMatch = TEMP_WORK_IN_INPUT.exec(trimmed);\n const digest = (tempMatch?.[1] ?? candidate).toLowerCase();\n if (TEMP_WORK_ID.test(digest)) {\n return {\n kind: \"temp-work\",\n id: `temp-work/${digest}`,\n iri: `http://data.bnf.fr/temp-work/${digest}/#about`,\n pageUrl: `https://data.bnf.fr/temp-work/${digest}/`,\n };\n }\n\n throw invalidInput(`\"${input}\" is not a data.bnf.fr identifier.`, hintFor(trimmed));\n}\n\n/**\n * Two shapes people paste, which are addresses of something else entirely.\n *\n * A Gallica document identifier and a data.bnf.fr page address both look like\n * identifiers to whoever is holding one, and both are refused here. Refusing\n * them with the general advice reads as the server failing to know a record it\n * plainly has, so each is named for what it is and told what to do instead.\n */\nconst GALLICA_DOCUMENT = /^(?:bpt6k|btv1b|bd6t|cb32|btv1b)[0-9a-z]+$/i;\n/** `https://data.bnf.fr/fr/11907966/marguerite_duras/`, which is what a browser shows. */\nconst PAGE_ADDRESS = /data\\.bnf\\.fr\\/(?:[a-z]{2}\\/)?(\\d{6,12})\\/([a-z0-9_]+)/i;\n\nconst GENERAL_HINT =\n \"An identifier looks like 'cb119219976' for an established record, or 'temp-work/22d7f68c1a4bdd081ad7ca791fd3b730' for a provisional one. Take it from a search result rather than building one.\";\n\nfunction hintFor(input: string): string {\n const page = PAGE_ADDRESS.exec(input);\n if (page) {\n const name = (page[2] ?? \"\").replace(/_/g, \" \");\n return (\n `That is the readable address of a data.bnf.fr page, and ${page[1]} in it is the record's FRBNF number rather than the identifier these tools take. ` +\n `Search for \"${name}\" with search_authors or search_works, and use the identifier the row carries.`\n );\n }\n\n const gallicaArk = GALLICA_ARK.exec(input)?.[1] ?? input;\n if (GALLICA_DOCUMENT.test(gallicaArk)) {\n return (\n \"That is a Gallica identifier: it names a digitised document rather than a record in the catalogue, and these tools read the catalogue. \" +\n \"Search for the work with search_works, then list_editions will give you the Gallica link alongside the edition it belongs to.\"\n );\n }\n\n return GENERAL_HINT;\n}\n\n/** An identifier written back as the address of the entity itself. */\nexport const iriOf = (id: EntityId): string => `<${id.iri}>`;\n\n/**\n * A whole number, bounded, written into a query.\n *\n * LIMIT and OFFSET take a bare number rather than a quoted value, so this is\n * the one place where something that is not a literal is assembled. Bounding\n * before writing is what makes that safe, and it also keeps a caller from\n * asking the endpoint for a page a hundred thousand rows deep.\n */\nexport function boundedInteger(value: number, min: number, max: number, name: string): number {\n if (!(Number.isFinite(value) && Number.isInteger(value))) {\n throw invalidInput(`'${name}' has to be a whole number.`);\n }\n if (value < min || value > max) {\n throw invalidInput(`'${name}' has to be between ${min} and ${max}.`);\n }\n return value;\n}\n","/**\n * The queries this server sends, and the reasons they are shaped as they are.\n *\n * Every one of them describes the endpoint as it actually answers: the shapes\n * below are the ones the live service returns, rather than the ones the\n * vocabularies promise.\n *\n * Three facts about this dataset govern most of what follows.\n *\n * **An entity and its record are two addresses.** A person is\n * `ark:/12148/cb119219976#about`, typed `foaf:Person`, and carries the dates,\n * the places and the alignments. The authority record is the same ARK without\n * the fragment, typed `skos:Concept`, and carries the headings, the editorial\n * notes and the date the record was last changed. A query that reads only one\n * of the two misses half the record.\n *\n * **Work, expression and manifestation are three levels.** A work links to\n * expressions, and a manifestation names both the expression it realises and\n * the work it manifests. `workManifested` is what makes the editions of a work\n * reachable in one step rather than two.\n *\n * **The full-text index drives, or the query is abandoned.** Virtuoso needs the\n * triple whose object it will search to appear before `bif:contains` in the same\n * group, and it needs that pattern to be the one that starts the plan. Asking\n * for `?w a rdafrbr:Work` first makes the planner walk every work in the\n * catalogue and the service gives the query up, answering 200 with an empty\n * body. Putting the text search in a subquery and filtering its output is what\n * keeps the index in front.\n *\n * **A paged query is ordered at both levels.** The subquery decides which\n * entities a page holds; the outer query decides the sequence its rows arrive\n * in, and SPARQL gives an unordered outer query no sequence at all. Since a\n * page is read by taking one entity beyond it and dropping the last, an outer\n * form without `ORDER BY` drops an entity chosen by the service: that entity is\n * then missing from the walk, and one of its neighbours is served on two pages.\n * Both levels therefore sort on the same variable, so the sequence a caller\n * reads is the sequence the pages were cut along. The outer sort runs over the\n * rows of one page, so it costs a sort of a few dozen rows rather than of the\n * matching set.\n */\n\nimport { PREFIX_BLOCK } from \"./endpoint.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { boundedInteger, containsAllWords, iriOf, literal } from \"./sparql.js\";\n\n/**\n * How many rows a text search reads from the index before filtering.\n *\n * `bif:contains` answers in index order, which is not an order of relevance, so\n * a window that is too small leaves out the record a caller meant. A window this\n * size costs the service one pass and gives the filter enough to work with.\n *\n * It is also the end of what a text search can page through. An offset past it\n * lands beyond what was ever read, and the answer would be an empty page that\n * looks exactly like the catalogue holding nothing. The tools refuse such a page\n * rather than buying the query, which is why this is exported.\n */\nexport const TEXT_WINDOW = 400;\n\nconst prefixed = (body: string): string => `${PREFIX_BLOCK}\\n${body.trim()}\\n`;\n\n/**\n * How many rows the window returned, asked for in the request that reads it.\n *\n * The window bounds the answer, and it is read before the type filter, so a\n * page of a few dozen rows can rest on a window that came back full. From the\n * page alone the two readings are identical: an index with nothing more to give\n * and an index holding a great deal more that was never asked. The occupancy is\n * what separates them, and it costs one extra pass of the same index inside a\n * request that was already being sent, rather than a second call.\n *\n * It arrives as a row of its own, binding the count and no entity, because a\n * branch of a UNION answers even when the other branch holds nothing. A page\n * emptied by the filter is the reading that most needs the fact.\n */\nfunction windowOccupancy(predicate: string, words: readonly string[]): string {\n return `\n {\n SELECT (COUNT(*) AS ?windowRows) WHERE {\n SELECT DISTINCT ?windowEntity ?windowText WHERE {\n ?windowEntity ${predicate} ?windowText .\n ?windowText bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n }`;\n}\n\n/**\n * People whose name carries every word given.\n *\n * The name is searched rather than the authority heading, because a heading\n * carries the dates and a caller writing \"Arthur Rimbaud 1854\" would otherwise\n * match on the year. Both are returned, so an answer can show which of several\n * people bearing one name is which.\n *\n * `limit + 1` rows are asked for. The extra row is never shown; it is how the\n * answer can say that more exist without a second query counting them.\n */\nexport function searchAuthorsQuery(\n words: readonly string[],\n limit: number,\n offset: number,\n): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // The page is cut in the middle subquery, over people rather than over rows,\n // and the cutting projection names the person alone. Selecting the name\n // alongside would give a person recorded under two names two of the page's\n // slots; the parser then folds them back into one row, and a page of ten\n // comes back holding eight.\n // The columns of the page are read inside the branch that produces it. Read\n // outside it, they would be joined against an unbound subject on the row the\n // occupancy arrives on, which asks the service for every name in the file.\n return prefixed(`\nSELECT ?person ?name ?label ?birthYear ?deathYear ?role ?windowRows WHERE {\n {\n {\n SELECT DISTINCT ?person WHERE {\n {\n SELECT DISTINCT ?person ?name WHERE {\n ?person foaf:name ?name .\n ?name bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?person a foaf:Person .\n }\n ORDER BY ?person\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?person foaf:name ?name }\n OPTIONAL { ?record foaf:focus ?person ; skos:prefLabel ?label }\n OPTIONAL { ?person bnf:firstYear ?birthYear }\n OPTIONAL { ?person bnf:lastYear ?deathYear }\n OPTIONAL { ?person rdag2:biographicalInformation ?role }\n }\n UNION${windowOccupancy(\"foaf:name\", words)}\n}\nORDER BY ?person`);\n}\n\n/**\n * The most triples one record's detail query will read.\n *\n * A record that hits this ceiling has been cut, and a cut record renders as a\n * record whose missing statements are absences. The parsers compare the row\n * count against it and say so rather than reporting the silence as fact.\n */\nexport const MOST_TRIPLES = 2000;\n\n/**\n * Everything both halves of one person's record carry.\n *\n * The two subjects are read in one query through a UNION rather than in two\n * requests: the spacing between requests is the cost that matters here, and one\n * person is one question.\n */\nexport function authorQuery(id: EntityId): string {\n const person = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang WHERE {\n {\n ${person} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n UNION\n {\n ?record foaf:focus ${person} ; ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * Works whose title carries every word given.\n *\n * Both established works and provisional ones are returned, and the row says\n * which is which: leaving the provisional ones out would hide the only record\n * the BnF holds for a great many recent books, and mixing them in silently\n * would let a provisional record be quoted as an established one.\n */\nexport function searchWorksQuery(words: readonly string[], limit: number, offset: number): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // As with people, the page is cut over works rather than over rows, on the\n // work alone: a work with three authors occupies three rows, and a work\n // recorded under two titles would take two of the page's slots.\n return prefixed(`\nSELECT ?work ?title ?date ?status ?creator ?creatorName ?windowRows WHERE {\n {\n {\n SELECT DISTINCT ?work WHERE {\n {\n SELECT DISTINCT ?work ?title WHERE {\n ?work dcterms:title ?title .\n ?title bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?work a rdafrbr:Work .\n }\n ORDER BY ?work\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?work dcterms:title ?title }\n OPTIONAL { ?work dcterms:date ?date }\n OPTIONAL { ?work rdae:statusOfIdentification ?status }\n OPTIONAL {\n ?work dcterms:creator ?creator .\n OPTIONAL { ?creator foaf:name ?creatorName }\n }\n }\n UNION${windowOccupancy(\"dcterms:title\", words)}\n}\nORDER BY ?work`);\n}\n\n/**\n * The works one person is named the creator of.\n *\n * `dcterms:creator` is the statement that ties a work to the person who made\n * it, and it is the widest one at that level: every work reached through the\n * relator vocabularies is either reached through this one as well, or is a\n * record of a performance rather than of a work. So one statement answers the\n * question, and a second path would add rows of a different kind.\n *\n * The page is cut in a subquery over works alone, ordered by their address. A\n * work carrying two form codes arrives as two rows, and cutting the page over\n * rows would give that work two of the page's slots. The order is arbitrary and\n * it is stable, which is what a caller reading page after page needs.\n */\nexport function worksByCreatorQuery(id: EntityId, limit: number, offset: number): string {\n const person = iriOf(id);\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n return prefixed(`\nSELECT ?work ?title ?date ?year ?status ?form WHERE {\n {\n SELECT DISTINCT ?work WHERE {\n ?work dcterms:creator ${person} .\n }\n ORDER BY ?work\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?work dcterms:title ?title }\n OPTIONAL { ?work dcterms:date ?date }\n OPTIONAL { ?work bnf:firstYear ?year }\n OPTIONAL { ?work rdae:statusOfIdentification ?status }\n OPTIONAL { ?work rdae:formOfWork ?form }\n}\nORDER BY ?work`);\n}\n\n/** Everything one work's record carries, with the names of its creators. */\nexport function workQuery(id: EntityId): string {\n const work = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang ?name WHERE {\n ${work} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n OPTIONAL { ?o foaf:name ?name }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * The published editions of one work.\n *\n * A manifestation naming the same work twice, or carrying two digitised copies,\n * comes back as several rows describing one edition. The parser groups them,\n * which is why the manifestation's own address is selected first.\n */\nexport function editionsQuery(id: EntityId, limit: number, offset: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n return prefixed(`\nSELECT ?edition ?title ?date ?year ?publisher ?place ?editionStatement\n ?extent ?isbn ?note ?catalogue ?reproduction ?ocr WHERE {\n {\n SELECT DISTINCT ?edition WHERE {\n ?edition rdarel:workManifested ${work} .\n }\n ORDER BY ?edition\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?edition dcterms:title ?title }\n OPTIONAL { ?edition dcterms:date ?date }\n OPTIONAL { ?edition bnf:firstYear ?year }\n OPTIONAL { ?edition rdae:publishersName ?publisher }\n OPTIONAL { ?edition rdae:placeOfPublication ?place }\n OPTIONAL { ?edition rdae:editionStatement ?editionStatement }\n OPTIONAL { ?edition dcterms:description ?extent }\n OPTIONAL { ?edition bnf:isbn ?isbn }\n OPTIONAL { ?edition rdae:note ?note }\n OPTIONAL { ?edition rdfs:seeAlso ?catalogue }\n OPTIONAL { ?edition rdarel:electronicReproduction ?reproduction }\n OPTIONAL { ?edition bnf:OCR ?ocr }\n}\nORDER BY ?edition`);\n}\n\n/**\n * Every digitised document one work's editions point at.\n *\n * Held apart from the edition listing because the two answer different\n * questions: one asks what was published, the other asks what can be opened.\n */\nexport function digitisedForWorkQuery(id: EntityId, limit: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?edition rdarel:workManifested ${work} .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${work} foaf:depiction ?depiction .\n BIND(${work} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Every digitised document attached to one person.\n *\n * Two paths lead there and they mean different things. `foaf:depiction` hangs\n * off the person and illustrates them. A reproduction hangs off an edition of a\n * work they are credited with, and is that edition digitised. The parser keeps\n * the two apart, and the answer says which is which.\n *\n * Reproductions are ordered first because a well-known person carries dozens of\n * illustrations, and a page filled with those would answer \"what of this author\n * can be read\" with a list of pictures of him.\n */\nexport function digitisedForPersonQuery(id: EntityId, limit: number): string {\n const person = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?work dcterms:creator ${person} .\n ?edition rdarel:workManifested ?work .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${person} foaf:depiction ?depiction .\n BIND(${person} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Whether one address is described at all, and what it is.\n *\n * Asked when a lookup came back with nothing, so an answer can tell \"the BnF\n * describes no such record\" from \"that record exists and is not a person\".\n */\nexport function kindQuery(id: EntityId): string {\n return prefixed(`\nSELECT DISTINCT ?type WHERE {\n ${iriOf(id)} a ?type .\n}\nLIMIT 20`);\n}\n\n/**\n * A query written for the live suite alone, naming one record by its label.\n *\n * It exists so the canary exercises `literal` against text a person typed,\n * rather than only against words the tokeniser produced.\n */\nexport function labelProbeQuery(label: string): string {\n return prefixed(`\nSELECT ?person WHERE {\n ?person foaf:name ${literal(label)} .\n}\nLIMIT 3`);\n}\n","/**\n * Turning result rows into the shapes the tools render.\n *\n * The rule the whole file follows: a statement the record does not carry comes\n * out as `null` or as an empty list, never as a plausible value. A person with\n * no recorded year of death is alive, or unresearched, or died in a year nobody\n * wrote down, and the three are different claims. Only the record decides.\n */\n\nimport { isGallicaAddress, publicPageFor } from \"./endpoint.js\";\nimport { MOST_TRIPLES, TEXT_WINDOW } from \"./queries.js\";\nimport type { SparqlResults, SparqlRow, SparqlTerm } from \"./http.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n AuthoredWork,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\n\nconst ARK_IN_IRI = /ark:\\/12148\\/(c[bc][0-9a-z]+)/i;\nconst TEMP_WORK_IN_IRI = /temp-work\\/([0-9a-f]{32})/i;\nconst TRAILING_SEPARATOR = /[/#]$/;\nconst PATH_SEPARATOR = /[/#]/;\nconst ARK_SEGMENT = /ark:\\/12148\\/([^/?#]+)/;\nconst QUERY_START = /[?#]/;\nconst LEADING_DOTS = /^[./]+/;\nconst TRAILING_SLASHES = /\\/+$/;\n\n/** Predicates read off a record. Named once, so a rename is one edit. */\nconst P = {\n type: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\",\n label: \"http://www.w3.org/2000/01/rdf-schema#label\",\n seeAlso: \"http://www.w3.org/2000/01/rdf-schema#seeAlso\",\n sameAs: \"http://www.w3.org/2002/07/owl#sameAs\",\n exactMatch: \"http://www.w3.org/2004/02/skos/core#exactMatch\",\n closeMatch: \"http://www.w3.org/2004/02/skos/core#closeMatch\",\n prefLabel: \"http://www.w3.org/2004/02/skos/core#prefLabel\",\n altLabel: \"http://www.w3.org/2004/02/skos/core#altLabel\",\n created: \"http://purl.org/dc/terms/created\",\n modified: \"http://purl.org/dc/terms/modified\",\n title: \"http://purl.org/dc/terms/title\",\n date: \"http://purl.org/dc/terms/date\",\n creator: \"http://purl.org/dc/terms/creator\",\n language: \"http://purl.org/dc/terms/language\",\n subject: \"http://purl.org/dc/terms/subject\",\n name: \"http://xmlns.com/foaf/0.1/name\",\n givenName: \"http://xmlns.com/foaf/0.1/givenName\",\n familyName: \"http://xmlns.com/foaf/0.1/familyName\",\n depiction: \"http://xmlns.com/foaf/0.1/depiction\",\n birth: \"http://vocab.org/bio/0.1/birth\",\n death: \"http://vocab.org/bio/0.1/death\",\n firstYear: \"http://data.bnf.fr/ontology/bnf-onto/firstYear\",\n lastYear: \"http://data.bnf.fr/ontology/bnf-onto/lastYear\",\n subjectLabel: \"http://data.bnf.fr/ontology/bnf-onto/subject\",\n isni: \"http://isni.org/ontology#identifierValid\",\n placeOfBirth: \"http://rdvocab.info/ElementsGr2/placeOfBirth\",\n placeOfDeath: \"http://rdvocab.info/ElementsGr2/placeOfDeath\",\n biographicalInformation: \"http://rdvocab.info/ElementsGr2/biographicalInformation\",\n languageOfThePerson: \"http://rdvocab.info/ElementsGr2/languageOfThePerson\",\n countryAssociated: \"http://rdvocab.info/ElementsGr2/countryAssociatedWithThePerson\",\n fieldOfActivity: \"http://rdvocab.info/ElementsGr2/fieldOfActivityOfThePerson\",\n formOfWork: \"http://rdvocab.info/Elements/formOfWork\",\n statusOfIdentification: \"http://rdvocab.info/Elements/statusOfIdentification\",\n /** The occupation, which RDA numbers rather than names. */\n occupation: \"http://rdaregistry.info/Elements/a/#P50113\",\n /** Expressions of a work, which are not editions. */\n expressionOfWork: \"http://rdaregistry.info/Elements/w/#P10078\",\n} as const;\n\n/** Files whose addresses this dataset aligns records with. */\nconst ALIGNMENT_HOSTS: Array<{ host: string; name: string }> = [\n { host: \"viaf.org\", name: \"viaf\" },\n { host: \"idref.fr\", name: \"idref\" },\n { host: \"dbpedia.org\", name: \"dbpedia\" },\n { host: \"wikidata.org\", name: \"wikidata\" },\n { host: \"isni.org\", name: \"isni\" },\n { host: \"d-nb.info\", name: \"gnd\" },\n { host: \"id.loc.gov\", name: \"loc\" },\n { host: \"datos.bne.es\", name: \"bne\" },\n { host: \"wikipedia.org\", name: \"wikipedia\" },\n { host: \"musicbrainz.org\", name: \"musicbrainz\" },\n { host: \"francearchives.gouv.fr\", name: \"francearchives\" },\n { host: \"imslp.org\", name: \"imslp\" },\n { host: \"catalogue.bnf.fr\", name: \"catalogue\" },\n];\n\nconst text = (term: SparqlTerm | undefined): string | null => {\n if (!term) {\n return null;\n }\n const value = term.value.trim();\n return value === \"\" ? null : value;\n};\n\nconst integer = (term: SparqlTerm | undefined): number | null => {\n const raw = text(term);\n if (raw === null) {\n return null;\n }\n const value = Number(raw);\n return Number.isInteger(value) ? value : null;\n};\n\n/** The identifier a caller quotes, read back off the address of a record. */\nexport function idFromIri(iri: string): string | null {\n const ark = ARK_IN_IRI.exec(iri);\n if (ark?.[1]) {\n return ark[1].toLowerCase();\n }\n const temp = TEMP_WORK_IN_IRI.exec(iri);\n if (temp?.[1]) {\n return `temp-work/${temp[1].toLowerCase()}`;\n }\n return null;\n}\n\n/** The last segment of a vocabulary address, which is the term itself. */\nconst vocabularyTerm = (iri: string): string =>\n decodeURIComponent(iri.replace(TRAILING_SEPARATOR, \"\").split(PATH_SEPARATOR).pop() ?? iri);\n\n/**\n * A Gallica address turned into a link.\n *\n * Returns null for anything that is not a Gallica address, so a record pointing\n * somewhere else does not arrive labelled as a digitised BnF document. The\n * address is carried through as published: it is what a person will open.\n */\nexport function toDigitisedLink(\n address: string | null,\n role: DigitisedLink[\"role\"],\n fromId: string,\n fromTitle: string | null,\n): DigitisedLink | null {\n if (!(address && isGallicaAddress(address))) {\n return null;\n }\n const segment = ARK_SEGMENT.exec(address)?.[1] ?? null;\n if (!segment) {\n return null;\n }\n // An address can carry a rendering after the name: `.thumbnail` asks for a\n // small image of the document, `.item` for one leaf of it. An ARK name holds\n // no full stop, so what precedes the first one is the identifier and the rest\n // describes a view of it. Reporting the view as the identifier would give two\n // views of one document two identifiers.\n const ark = segment.split(\".\")[0] ?? segment;\n if (ark === \"\") {\n return null;\n }\n return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };\n}\n\n/**\n * The view an address asks for, read off what follows the ARK name in it.\n *\n * `bpt6k90000030` names a document; `bpt6k90000030.thumbnail` asks for a small\n * image of it and `btv1b90000002/f3.item.thumbnail` for one leaf of it as an\n * image. Carrying that as a field of its own is what keeps `url` and `ark` from\n * reading as two names for one thing: a caller told only the ARK would quote a\n * document while having been handed a picture of a page of it.\n */\nfunction renderingOf(address: string, ark: string): string | null {\n const path = address.split(QUERY_START)[0] ?? address;\n const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);\n const rendering = after.replace(LEADING_DOTS, \"\").replace(TRAILING_SLASHES, \"\");\n return rendering === \"\" ? null : rendering;\n}\n\n/**\n * A page, with the rows beyond it read as more existing.\n *\n * `hasMore` is true when the endpoint sent the extra row that was asked for, and\n * also when a row had to be dropped: a page that came back short because this\n * client could not read part of it is not a page with room to spare.\n */\nfunction page<T>(rows: T[], limit: number, skipped: number): Page<T> {\n const more = rows.length > limit;\n return skipped > 0\n ? { rows: rows.slice(0, limit), hasMore: more, skipped }\n : { rows: rows.slice(0, limit), hasMore: more };\n}\n\n/**\n * Whether the index window a text search reads came back full.\n *\n * The endpoint sends the occupancy on a row of its own, binding the count and\n * naming no entity. A count that reaches the size of the window means the\n * reading stopped where the window did rather than where the matches did.\n *\n * Null when no such row arrived, since a window nobody measured supports no\n * statement about what was left out.\n */\nfunction readWindowOccupancy(results: SparqlResults): boolean | null {\n for (const row of results.results.bindings) {\n const raw = text(row.windowRows);\n if (raw === null) {\n continue;\n }\n const count = Number(raw);\n if (Number.isInteger(count)) {\n return count >= TEXT_WINDOW;\n }\n }\n return null;\n}\n\n/**\n * True for the row carrying the occupancy and nothing else.\n *\n * It names no record, which is the shape of a row that had to be dropped. It is\n * neither: counting it as dropped would report the page as short of a record\n * the endpoint never claimed to send.\n */\nconst isOccupancyRow = (row: SparqlRow, entity: string): boolean =>\n row.windowRows !== undefined && row[entity] === undefined;\n\n/** Index a `?p ?o` result set by predicate, keeping every value. */\nexport function byPredicate(results: SparqlResults): Map<string, SparqlRow[]> {\n const index = new Map<string, SparqlRow[]>();\n for (const row of results.results.bindings) {\n const predicate = text(row.p);\n if (predicate === null) {\n continue;\n }\n const bucket = index.get(predicate);\n if (bucket) {\n bucket.push(row);\n } else {\n index.set(predicate, [row]);\n }\n }\n return index;\n}\n\nconst firstValue = (index: Map<string, SparqlRow[]>, predicate: string): string | null =>\n text(index.get(predicate)?.[0]?.o);\n\nconst allValues = (index: Map<string, SparqlRow[]>, predicate: string): string[] => {\n const seen = new Set<string>();\n for (const row of index.get(predicate) ?? []) {\n const value = text(row.o);\n if (value !== null) {\n seen.add(value);\n }\n }\n return [...seen];\n};\n\n/** Values that are addresses of vocabulary terms, reduced to the term. */\nconst allTerms = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.startsWith(\"http\"))\n .map(vocabularyTerm);\n\n/**\n * Dewey classes, and nothing else that happens to share the predicate.\n *\n * `dcterms:subject` on a work points at a Dewey class and at subject-heading\n * ARKs alike. Reducing an ARK to its last segment yields the string `about`,\n * which would then be published as a Dewey class and printed beside a real one.\n */\nconst deweyClasses = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.includes(\"dewey.info/class/\"))\n .map(vocabularyTerm);\n\n/** Values that are plain words rather than addresses. */\nconst allWords = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate).filter((value) => !value.startsWith(\"http\"));\n\n/**\n * Alignments to the same thing in other files, grouped by file.\n *\n * `owl:sameAs`, `skos:exactMatch` and `skos:closeMatch` are gathered together\n * because a caller wanting the VIAF address does not care which of the three\n * carried it, and the dataset uses all three for the same alignments. A\n * `closeMatch` is a weaker claim than the other two, and the record makes that\n * distinction nowhere a caller could act on it, so the grouping does not\n * pretend otherwise: the field is named for the addresses it holds.\n */\nfunction alignments(\n index: Map<string, SparqlRow[]>,\n selfId: string | null,\n): Record<string, string[]> {\n const found: Record<string, Set<string>> = {};\n for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {\n for (const address of allValues(index, predicate)) {\n if (!address.startsWith(\"http\")) {\n continue;\n }\n // An address pointing back at this record says nothing about anywhere\n // else, and the dataset carries several of those per record.\n if (selfId && address.includes(selfId)) {\n continue;\n }\n let host: string;\n try {\n host = new URL(address).hostname.toLowerCase();\n } catch {\n continue;\n }\n const known = ALIGNMENT_HOSTS.find(\n (entry) => host === entry.host || host.endsWith(`.${entry.host}`),\n );\n if (!known) {\n continue;\n }\n const addresses = found[known.name] ?? new Set<string>();\n found[known.name] = addresses;\n addresses.add(address);\n }\n }\n return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));\n}\n\n/** ISNI is written as a bare number rather than an address. */\nfunction isniAddresses(index: Map<string, SparqlRow[]>): string[] {\n return allValues(index, P.isni).map(\n (value) => `https://isni.org/isni/${value.replace(/\\s/g, \"\")}`,\n );\n}\n\n/** Rows of an author search, with the extra row read as \"more exist\". */\nexport function toAuthorSummaries(results: SparqlResults, limit: number): Page<AuthorSummary> {\n const rows: AuthorSummary[] = [];\n const seen = new Set<string>();\n let skipped = 0;\n\n for (const row of results.results.bindings) {\n if (isOccupancyRow(row, \"person\")) {\n continue;\n }\n const iri = text(row.person);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n if (seen.has(id)) {\n continue;\n }\n seen.add(id);\n rows.push({\n id,\n name: text(row.name),\n label: text(row.label),\n birthYear: integer(row.birthYear),\n deathYear: integer(row.deathYear),\n role: text(row.role),\n sourceUrl: publicPageFor(iri),\n });\n }\n\n return { ...page(rows, limit, skipped), indexWindowFull: readWindowOccupancy(results) };\n}\n\n/** One person, assembled from both halves of their record. */\nexport function toAuthorDetail(results: SparqlResults, id: string, pageUrl: string): AuthorDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n\n const otherNames: Array<{ label: string; language: string | null }> = [];\n const seenLabels = new Set<string>();\n for (const row of index.get(P.altLabel) ?? []) {\n const label = text(row.o);\n if (label === null || seenLabels.has(label)) {\n continue;\n }\n seenLabels.add(label);\n otherNames.push({ label, language: text(row.lang) });\n }\n\n const sameAs = alignments(index, id);\n const isni = isniAddresses(index);\n if (isni.length > 0) {\n sameAs.isni = [...new Set([...(sameAs.isni ?? []), ...isni])];\n }\n\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) {\n depictions.push(link);\n }\n }\n\n return {\n id,\n name: firstValue(index, P.name),\n label: firstValue(index, P.prefLabel),\n givenName: firstValue(index, P.givenName),\n familyName: firstValue(index, P.familyName),\n otherNames,\n birthDate: firstValue(index, P.birth),\n deathDate: firstValue(index, P.death),\n birthYear: integer(index.get(P.firstYear)?.[0]?.o),\n deathYear: integer(index.get(P.lastYear)?.[0]?.o),\n birthPlace: firstValue(index, P.placeOfBirth),\n deathPlace: firstValue(index, P.placeOfDeath),\n biographicalInformation: firstValue(index, P.biographicalInformation),\n occupation: firstValue(index, P.occupation),\n languages: allTerms(index, P.languageOfThePerson),\n countries: allTerms(index, P.countryAssociated),\n fields: allWords(index, P.fieldOfActivity),\n deweyClasses: deweyClasses(index, P.fieldOfActivity),\n sameAs,\n catalogueUrl,\n recordCreated: firstValue(index, P.created),\n recordModified: firstValue(index, P.modified),\n depictions,\n types,\n truncated: results.results.bindings.length >= MOST_TRIPLES,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * How settled a record is.\n *\n * The record states it in words, and the address states it in its shape: a\n * provisional work is addressed under `temp-work` rather than by an ARK. The\n * statement governs when there is one, since a cataloguer can settle a record\n * before it is re-addressed.\n */\nexport function readStatus(statement: string | null, iri: string): \"established\" | \"provisional\" {\n if (statement !== null) {\n const said = statement.toLowerCase();\n if (said.includes(\"provisional\")) {\n return \"provisional\";\n }\n if (said.includes(\"established\")) {\n return \"established\";\n }\n }\n return iri.includes(\"/temp-work/\") ? \"provisional\" : \"established\";\n}\n\n/** Rows of a work search, gathering the creators of each work. */\nexport function toWorkSummaries(results: SparqlResults, limit: number): Page<WorkSummary> {\n const order: string[] = [];\n const byId = new Map<string, WorkSummary>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n if (isOccupancyRow(row, \"work\")) {\n continue;\n }\n const iri = text(row.work);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let work = byId.get(id);\n if (!work) {\n work = {\n id,\n title: text(row.title),\n date: text(row.date),\n creators: [],\n status: readStatus(text(row.status), iri),\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, work);\n order.push(id);\n }\n\n const creatorIri = text(row.creator);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (creatorId && !work.creators.some((c) => c.id === creatorId)) {\n work.creators.push({ id: creatorId, name: text(row.creatorName) });\n }\n }\n\n return {\n ...page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n ),\n indexWindowFull: readWindowOccupancy(results),\n };\n}\n\n/**\n * The works one person is credited with, gathered from rows that repeat.\n *\n * A work pointing at two form terms arrives as two rows differing in one\n * column. Counting rows would report two works where the catalogue links one,\n * so the address of the work is what a work is counted by.\n */\nexport function toAuthoredWorks(results: SparqlResults, limit: number): Page<AuthoredWork> {\n const order: string[] = [];\n const byId = new Map<string, AuthoredWork>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.work);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let work = byId.get(id);\n if (!work) {\n work = {\n id,\n title: text(row.title),\n date: text(row.date),\n year: integer(row.year),\n forms: [],\n status: readStatus(text(row.status), iri),\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, work);\n order.push(id);\n }\n\n const form = text(row.form);\n if (form?.startsWith(\"http\")) {\n const term = vocabularyTerm(form);\n if (!work.forms.includes(term)) {\n work.forms.push(term);\n }\n }\n }\n\n return page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n );\n}\n\n/** One work, assembled from its record. */\nexport function toWorkDetail(\n results: SparqlResults,\n id: string,\n iri: string,\n pageUrl: string,\n): WorkDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n const truncated = results.results.bindings.length >= MOST_TRIPLES;\n\n const creators: Array<{ id: string; name: string | null }> = [];\n for (const row of index.get(P.creator) ?? []) {\n const creatorIri = text(row.o);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (!creatorId || creators.some((c) => c.id === creatorId)) {\n continue;\n }\n creators.push({ id: creatorId, name: text(row.name) });\n }\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) {\n depictions.push(link);\n }\n }\n\n const statusStatement = firstValue(index, P.statusOfIdentification);\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n return {\n id,\n title: firstValue(index, P.title),\n label: firstValue(index, P.label),\n date: firstValue(index, P.date),\n firstYear: integer(index.get(P.firstYear)?.[0]?.o),\n creators,\n languages: allTerms(index, P.language),\n forms: allTerms(index, P.formOfWork),\n subjects: allWords(index, P.subjectLabel),\n deweyClasses: deweyClasses(index, P.subject),\n status: readStatus(statusStatement, iri),\n statusStatement,\n // A cut record makes this the ceiling rather than the count, so it is\n // withheld rather than published as a number the record does not support.\n expressionCount: truncated ? null : allValues(index, P.expressionOfWork).length,\n sameAs: alignments(index, id),\n catalogueUrl,\n depictions,\n types,\n truncated,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * Editions, gathered from rows that repeat.\n *\n * One manifestation carrying two digitised copies arrives as two rows differing\n * in one column. Counting rows would report two editions where the record\n * describes one, so the address of the manifestation is what an edition is\n * counted by.\n */\nexport function toEditions(results: SparqlResults, limit: number): Page<Edition> {\n const order: string[] = [];\n const byId = new Map<string, Edition>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.edition);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let edition = byId.get(id);\n if (!edition) {\n edition = {\n id,\n title: text(row.title),\n date: text(row.date),\n year: integer(row.year),\n publisher: text(row.publisher),\n place: text(row.place),\n editionStatement: text(row.editionStatement),\n extent: text(row.extent),\n isbn: text(row.isbn),\n note: text(row.note),\n catalogueUrl: text(row.catalogue),\n digitised: [],\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, edition);\n order.push(id);\n }\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, id, edition.title);\n if (link && !edition.digitised.some((existing) => existing.url === link.url)) {\n edition.digitised.push(link);\n }\n }\n }\n\n return page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n );\n}\n\n/**\n * Every digitised document a set of rows points at, kept in one list.\n *\n * One document can be reached by several paths, so the same address arrives\n * more than once and is kept once. That makes the length of the list a poor\n * signal of whether more exist: twenty-one rows holding seventeen addresses\n * would otherwise be reported as a page with room to spare. The endpoint was\n * asked for one row beyond the page, so the count of rows is what answers that.\n */\nexport function toDigitisedLinks(results: SparqlResults, limit: number): Page<DigitisedLink> {\n const links: DigitisedLink[] = [];\n const seen = new Set<string>();\n const rowCount = results.results.bindings.length;\n\n for (const row of results.results.bindings) {\n const fromIri = text(row.edition);\n const fromId = fromIri ? (idFromIri(fromIri) ?? fromIri) : \"\";\n const title = text(row.title);\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n [\"depiction\", \"depiction\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, fromId, title);\n if (!link || seen.has(link.url)) {\n continue;\n }\n seen.add(link.url);\n links.push(link);\n }\n }\n\n // `rowCount` counts what the endpoint sent, including rows whose address was\n // not a Gallica one and rows naming a document already listed. Raising the\n // limit after such a page returns the same list, so the claim is confined to\n // the case where the links themselves fill the page.\n return { rows: links.slice(0, limit), hasMore: links.length > limit || rowCount > limit };\n}\n\n/** The classes an address is typed with, reduced to their vocabulary terms. */\nexport function toTypes(results: SparqlResults): string[] {\n const types = new Set<string>();\n for (const row of results.results.bindings) {\n const iri = text(row.type);\n if (iri !== null) {\n types.add(iri);\n }\n }\n return [...types];\n}\n","/**\n * Keeps this client to one request at a time, spaced out.\n *\n * data.bnf.fr is a query service a public institution runs at its own cost, and\n * a SPARQL endpoint answers an expensive question per request rather than\n * serving a file. Serialising also means a burst of tool calls cannot turn into\n * a burst of concurrent queries on one planner.\n *\n * The spacing widens when the service pushes back and narrows again on a run of\n * quiet successes, so a slow patch does not become the permanent speed.\n */\n\nexport interface RateLimiterOptions {\n /** Spacing between requests when nothing has gone wrong. */\n intervalMs: number;\n /** The widest the spacing may become under push-back. */\n maxIntervalMs?: number;\n}\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n private lastStartedAt = 0;\n private queue: Promise<void> = Promise.resolve();\n private calmStreak = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = options.intervalMs;\n this.maxIntervalMs = options.maxIntervalMs ?? options.intervalMs * 8;\n this.intervalMs = options.intervalMs;\n }\n\n /** The spacing in force, which callers report rather than guess. */\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /**\n * Run `task` with nothing else in flight.\n *\n * This only serialises. Spacing is claimed per attempt through\n * `beforeRequest`, because a task that retries makes several requests and\n * each of them owes the service the same gap.\n */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const result = this.queue.then(task, task);\n this.queue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n\n /** Wait until enough time has passed since the previous request began. */\n async beforeRequest(): Promise<void> {\n const elapsed = Date.now() - this.lastStartedAt;\n // Capped at one whole interval, so a clock that moved backwards cannot\n // produce a wait longer than the spacing itself.\n const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (wait > 0) {\n await sleep(wait);\n }\n this.lastStartedAt = Date.now();\n }\n\n /** Called when the service asks for room: double the gap, up to the ceiling. */\n pushBack(): void {\n this.calmStreak = 0;\n this.intervalMs = Math.min(this.maxIntervalMs, this.intervalMs * 2);\n }\n\n /**\n * Called on a clean answer. Recovery takes several in a row, so one lucky\n * response after a rough patch does not undo the caution that earned it.\n */\n succeeded(): void {\n if (this.intervalMs === this.baseIntervalMs) {\n return;\n }\n this.calmStreak += 1;\n if (this.calmStreak < 3) {\n return;\n }\n this.calmStreak = 0;\n this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * The one place that talks to data.bnf.fr.\n *\n * It holds a single rate limiter and a single cache, so pacing applies to the\n * server as a whole rather than to whichever tool happens to be running. It\n * imports nothing from the MCP layer and is published on its own, so the same\n * code serves a plain script.\n *\n * Every read runs a query, parses it and only then stores: an answer nobody\n * could parse must not be served back for the rest of the cache's lifetime.\n *\n * Every read also carries the moment the metadata came off the endpoint. The\n * licence asks for the date of retrieval alongside the source, so that date is\n * a value the layer produces rather than something a caller is trusted to add.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n MAX_ALLOWED_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { notFound } from \"../errors.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n AuthoredWork,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\nimport { REPO_URL } from \"../version.js\";\nimport { Cache } from \"./cache.js\";\nimport type { SparqlResults } from \"./http.js\";\nimport { runQuery } from \"./http.js\";\nimport {\n authorQuery,\n digitisedForPersonQuery,\n digitisedForWorkQuery,\n editionsQuery,\n kindQuery,\n searchAuthorsQuery,\n searchWorksQuery,\n workQuery,\n worksByCreatorQuery,\n} from \"./queries.js\";\nimport {\n toAuthorDetail,\n toAuthorSummaries,\n toAuthoredWorks,\n toDigitisedLinks,\n toEditions,\n toTypes,\n toWorkDetail,\n toWorkSummaries,\n} from \"./parse.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { toSearchWords } from \"./sparql.js\";\n\n/** The class data.bnf.fr types a person with. */\nconst PERSON_CLASS = \"http://xmlns.com/foaf/0.1/Person\";\n\n/**\n * The classes data.bnf.fr types a work with.\n *\n * Two vocabularies say the same thing here, and a record carries both. Either\n * one is enough, because a record carrying one has always carried the other.\n */\nconst WORK_CLASSES = new Set([\n \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/Work\",\n \"http://rdaregistry.info/Elements/c/#C10001\",\n]);\n\nexport interface ClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Every read reports where the value came from and when it was retrieved. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n /**\n * When the metadata came off data.bnf.fr, as an ISO 8601 instant.\n *\n * A cached answer keeps the moment it was first read. Reporting the moment it\n * was served would state a date of retrieval that never happened, which is\n * exactly the claim the licence asks to be accurate.\n */\n retrievedAt: string;\n /** Rows the endpoint sent that could not be read, which paging still counts. */\n skipped?: number;\n}\n\n/**\n * What this server owes the BnF, applied to whatever it is handed.\n *\n * A configuration object assembled by a caller has not been through\n * `loadConfig`, so it can carry a missing value, a value of the wrong shape, or\n * a User-Agent that names somebody else. Every bound the environment path\n * enforces is enforced here too: the published `./client` entry point is the\n * one a program uses, and it would be a strange guarantee that held only for\n * the executable.\n *\n * Three of these bounds are about what one call can cost the service. A retry\n * budget of two hundred turns a single lookup into two hundred requests against\n * an endpoint that is already failing. A deadline of an hour holds the one\n * request slot for an hour. A cache of zero entries evicts each value as it is\n * written, which switches off the caching that the three-second floor is set\n * against.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\n\n /**\n * A setting that is absent or unreadable falls back, and one outside its\n * range is brought inside it. Refusing outright is what the environment path\n * does, because a person reads stderr; a program handed an object does not.\n */\n const bounded = (value: unknown, fallback: number, min: number, max: number): number => {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n return fallback;\n }\n return Math.min(max, Math.max(min, parsed));\n };\n\n const claimed = typeof config.userAgent === \"string\" ? config.userAgent.trim() : \"\";\n const identifier = defaults.userAgent;\n\n return {\n ...config,\n // A caller may say who they are. Appending rather than replacing means the\n // BnF can always tell which software is calling, and reach someone.\n userAgent:\n claimed === \"\" || claimed.includes(REPO_URL) ? identifier : `${claimed} ${identifier}`,\n minIntervalMs: bounded(\n config.minIntervalMs,\n defaults.minIntervalMs,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: bounded(config.timeoutMs, defaults.timeoutMs, 1000, 300_000),\n maxRetries: bounded(config.maxRetries, defaults.maxRetries, 0, 8),\n cacheTtlMs: bounded(config.cacheTtlMs, defaults.cacheTtlMs, 0, 86_400_000),\n cacheMaxEntries: bounded(config.cacheMaxEntries, defaults.cacheMaxEntries, 1, 5000),\n };\n}\n\nexport class BnfClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: Cache<unknown>;\n /** Questions already on the wire, so an identical one waits instead of asking. */\n private readonly inFlight = new Map<string, Promise<Read<unknown>>>();\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: ClientOptions = {}) {\n const base = { ...loadConfig(), ...options.config };\n this.config = withGuarantees(base);\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ intervalMs: this.config.minIntervalMs });\n this.cache = new Cache(this.config.cacheTtlMs, this.config.cacheMaxEntries);\n this.fetchImpl = options.fetchImpl;\n }\n\n /** The pacing in force, which widens when the endpoint pushes back. */\n get intervalMs(): number {\n return this.limiter.currentIntervalMs;\n }\n\n /** What the BnF sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n /** The deadline one query is given, which callers report rather than guess. */\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n /** The one address this client sends anything to. */\n /**\n * `cacheKey` is stated rather than taken from the query, because two reads of\n * one query can produce different values: a list cut to five rows is not the\n * same value as the same list cut to twenty, and serving one for the other\n * would answer a size the caller never asked for.\n */\n private read<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const hit = this.cache.get(cacheKey) as { value: T; retrievedAt: number } | undefined;\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${cacheKey}`);\n return Promise.resolve({\n data: hit.value,\n cached: true,\n retrievedAt: new Date(hit.retrievedAt).toISOString(),\n });\n }\n\n // Two callers asking one question at the same moment both miss the cache,\n // because the first has not answered yet. Sharing the flight means the BnF\n // is asked once, which is the point of the cache in the first place.\n const flying = this.inFlight.get(cacheKey);\n if (flying !== undefined) {\n this.logger.debug(`joining a request already in flight for ${cacheKey}`);\n return flying as Promise<Read<T>>;\n }\n\n const flight = this.fetchAndParse(cacheKey, query, parse);\n this.inFlight.set(cacheKey, flight as Promise<Read<unknown>>);\n // A failure is never remembered, so the next caller asks again rather than\n // being handed the failure of somebody else's request.\n return flight.finally(() => this.inFlight.delete(cacheKey));\n }\n\n private async fetchAndParse<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const results = await this.limiter.schedule(() =>\n runQuery({\n query,\n userAgent: this.config.userAgent,\n timeoutMs: this.config.timeoutMs,\n maxRetries: this.config.maxRetries,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n }),\n );\n\n // The moment is taken once the answer is in hand, so it names when the\n // metadata was read rather than when the request left.\n const retrievedAt = Date.now();\n const data = parse(results);\n this.cache.set(cacheKey, data, retrievedAt);\n return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };\n }\n\n /** Read an identifier a caller wrote, refusing anything this dataset cannot address. */\n /** The words a search will actually look for, which a tool reports back. */\n searchAuthors(name: string, limit: number, offset: number): Promise<Read<Page<AuthorSummary>>> {\n const words = toSearchWords(name);\n const query = searchAuthorsQuery(words, limit, offset);\n return this.read(`authors:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toAuthorSummaries(results, limit),\n );\n }\n\n async getAuthor(id: EntityId): Promise<Read<AuthorDetail>> {\n const result = await this.read(`author:${id.id}`, authorQuery(id), (results) =>\n toAuthorDetail(results, id.id, id.pageUrl),\n );\n\n // The record decides what it is, and a name is not the test: a subject\n // heading and a corporate body both carry one. Answering with a person\n // record for either would put a date of birth beside something that was\n // never born, and report its absence as a fact about a life.\n if (!result.data.types.includes(PERSON_CLASS)) {\n await this.refuse(id, \"a person\", \"get_work reads a work.\");\n }\n return result;\n }\n\n /**\n * Say what an address is, when it is not the kind of thing that was asked for.\n *\n * A record the catalogue does not describe at all and a record describing\n * something else are two different answers, and only one of them means the\n * caller should go looking elsewhere.\n */\n private async refuse(id: EntityId, wanted: string, hint: string): Promise<never> {\n // A detail query gathers the classes of everything it walks through, so the\n // list it leaves behind is longer on one tool than on another and one\n // record would be described differently depending on which refused it. What\n // the address itself answers is one list, and it is the one every refusal\n // states, whatever the caller was reading at the time.\n const known = (await this.read(`types:${id.id}`, kindQuery(id), toTypes)).data;\n\n if (known.length === 0) {\n throw notFound(`data.bnf.fr describes nothing at \"${id.id}\".`, {\n hint: \"Find the identifier with search_authors or search_works rather than writing one.\",\n url: id.pageUrl,\n });\n }\n throw notFound(\n `\"${id.id}\" is described by data.bnf.fr, and it is not ${wanted}: it is typed ${known.join(\", \")}.`,\n { hint, url: id.pageUrl },\n );\n }\n\n searchWorks(title: string, limit: number, offset: number): Promise<Read<Page<WorkSummary>>> {\n const words = toSearchWords(title);\n const query = searchWorksQuery(words, limit, offset);\n return this.read(`works:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toWorkSummaries(results, limit),\n );\n }\n\n async getWork(id: EntityId): Promise<Read<WorkDetail>> {\n const result = await this.read(`work:${id.id}`, workQuery(id), (results) =>\n toWorkDetail(results, id.id, id.iri, id.pageUrl),\n );\n\n // An edition carries a title too, and its ARK names a manifestation rather\n // than a work. Accepting one here would hand back an identifier described\n // as the work's, which is the identifier a citation would then carry.\n if (!result.data.types.some((type) => WORK_CLASSES.has(type))) {\n await this.refuse(\n id,\n \"a work\",\n \"get_author reads a person, and list_editions reads the editions of a work.\",\n );\n }\n return result;\n }\n\n /**\n * The works one person is named the creator of.\n *\n * The record is not checked here. A person the catalogue credits with nothing\n * and an address that names no person both answer with no rows, and telling\n * them apart costs a second query that only an empty answer needs.\n */\n worksByAuthor(id: EntityId, limit: number, offset: number): Promise<Read<Page<AuthoredWork>>> {\n return this.read(\n `author-works:${id.id}:${limit}:${offset}`,\n worksByCreatorQuery(id, limit, offset),\n (results) => toAuthoredWorks(results, limit),\n );\n }\n\n listEditions(id: EntityId, limit: number, offset: number): Promise<Read<Page<Edition>>> {\n const query = editionsQuery(id, limit, offset);\n return this.read(`editions:${id.id}:${limit}:${offset}`, query, (results) =>\n toEditions(results, limit),\n );\n }\n\n /** Digitised documents attached to a work and to its editions. */\n digitisedForWork(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-work:${id.id}:${limit}`,\n digitisedForWorkQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** Digitised documents attached to a person and to the editions of their works. */\n digitisedForPerson(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-person:${id.id}:${limit}`,\n digitisedForPersonQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** What an address is typed as, which tells a person from a work. */\n types(id: EntityId): Promise<Read<string[]>> {\n return this.read(`types:${id.id}`, kindQuery(id), toTypes);\n }\n}\n\n/**\n * What a caller needs beside the client itself.\n *\n * Every route that reads one record takes an `EntityId` rather than a string,\n * because an identifier built from a guess sends the call to a record that does\n * not exist and the answer reads as an absence. A program importing this entry\n * point therefore has to be able to build one, so the reading of an address is\n * published alongside the client that consumes it.\n */\nexport { parseEntityId } from \"./sparql.js\";\nexport type { EntityId, EntityKind } from \"./sparql.js\";\n"],"mappings":"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","names":["text","page"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config.ts","../../src/version.ts","../../src/errors.ts","../../src/bnf/cache.ts","../../src/bnf/endpoint.ts","../../src/bnf/http.ts","../../src/bnf/sparql.ts","../../src/bnf/queries.ts","../../src/bnf/parse.ts","../../src/bnf/rateLimiter.ts","../../src/bnf/client.ts"],"sourcesContent":["/**\n * Settings, read from the environment.\n *\n * A value that cannot be read warns and falls back rather than stopping the\n * server: a typo in one variable should not take away every tool. Warnings go\n * to stderr, because stdout carries the protocol and anything written there\n * corrupts the session.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport const LOG_LEVELS = [\"silent\", \"error\", \"info\", \"debug\"] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\n/**\n * The narrowest spacing this client will ever use, in milliseconds.\n *\n * The BnF publishes no rate for the SPARQL endpoint. It publishes\n * `Crawl-delay: 5` for gallica.bnf.fr and enforces it there by banning an\n * address, so five seconds is the only figure the institution has stated about\n * how fast it wants to be read. Three seconds is the floor here because a\n * SPARQL query is one request where a crawl is thousands, and because every\n * answer is cached: the spacing that matters is the one between two distinct\n * questions.\n *\n * Configuration can slow the server down. It cannot take the spacing below this\n * floor, whether the setting arrives through the environment or through a\n * configuration object handed to the published client.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 3000;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_MS = 120_000;\n/** The spacing in force when nothing asks for more. */\nexport const DEFAULT_INTERVAL_MS = 3000;\n\n/**\n * The deadline one query is given.\n *\n * Measured against the live endpoint, a lookup by identifier answers in well\n * under a second, while a full-text query over titles takes several seconds and\n * a query the planner handles badly is dropped by the server with an empty\n * body. Sixty seconds clears the slow honest case without holding the single\n * request slot for a query that will never answer.\n */\nexport const DEFAULT_TIMEOUT_MS = 60_000;\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n logLevel: LogLevel;\n}\n\nexport const DEFAULT_USER_AGENT = `mcp-databnf/${PKG_VERSION} (+${REPO_URL})`;\n\nexport interface Logger {\n debug: (message: string) => void;\n info: (message: string) => void;\n warn: (message: string) => void;\n error: (message: string) => void;\n}\n\nexport function createLogger(level: LogLevel): Logger {\n const rank = LOG_LEVELS.indexOf(level);\n const write = (at: LogLevel, message: string) => {\n if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {\n return;\n }\n process.stderr.write(`[mcp-databnf] ${at}: ${message}\\n`);\n };\n return {\n debug: (m) => write(\"debug\", m),\n info: (m) => write(\"info\", m),\n // A warning goes out at the error level so it survives the default\n // setting: a caller has to know that rows were dropped.\n warn: (m) => write(\"error\", m),\n error: (m) => write(\"error\", m),\n };\n}\n\nfunction readInteger(\n env: NodeJS.ProcessEnv,\n name: string,\n fallback: number,\n min: number,\n max: number,\n): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return fallback;\n }\n\n const value = Number(raw);\n if (!(Number.isFinite(value) && Number.isInteger(value))) {\n process.stderr.write(\n `[mcp-databnf] error: ${name}=\"${raw}\" is not a whole number; using ${fallback}.\\n`,\n );\n return fallback;\n }\n if (value < min || value > max) {\n // Clamping silently would let a caller believe a setting took effect when\n // the opposite is true, so the refusal is stated and the default stands.\n process.stderr.write(\n `[mcp-databnf] error: ${name}=${value} is outside ${min}..${max}; using ${fallback}.\\n`,\n );\n return fallback;\n }\n return value;\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const level = env.BNF_LOG_LEVEL as LogLevel | undefined;\n const logLevel = level && LOG_LEVELS.includes(level) ? level : \"error\";\n if (level && !LOG_LEVELS.includes(level)) {\n process.stderr.write(\n `[mcp-databnf] error: BNF_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.BNF_USER_AGENT?.trim();\n\n return {\n // A caller who wants to be recognised may say who they are, and the\n // contact address stays attached: the BnF has to be able to reach a human\n // about traffic it did not expect.\n userAgent: custom ? `${custom} ${DEFAULT_USER_AGENT}` : DEFAULT_USER_AGENT,\n minIntervalMs: readInteger(\n env,\n \"BNF_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"BNF_TIMEOUT_MS\", DEFAULT_TIMEOUT_MS, 1000, 300_000),\n maxRetries: readInteger(env, \"BNF_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"BNF_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"BNF_CACHE_MAX_ENTRIES\", 200, 1, 5000),\n logLevel,\n };\n}\n","/**\n * Kept in step with the version in package.json. The User-Agent embeds it, so\n * the BnF can tell which build is calling from its own logs.\n */\nexport const PKG_VERSION = \"2.1.2\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-databnf\";\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"the BnF describes no such\n * thing\" and \"the question could not be asked\". Collapsing the two lets a model\n * report an absence it never established, which is a false statement about a\n * catalogue rather than a missing feature.\n */\n\nexport type ErrorCode =\n /** data.bnf.fr answered, and describes no such person, work or edition. */\n | \"not_found\"\n /** The arguments cannot produce a query. */\n | \"invalid_input\"\n /** data.bnf.fr asked this client to slow down or refused it for now. */\n | \"rate_limited\"\n /** An answer arrived in a shape this server cannot read. */\n | \"parse_failure\"\n /** The request could not be completed. */\n | \"network_error\"\n /** The request was abandoned before an answer arrived. */\n | \"timeout\";\n\nexport interface ErrorDetails {\n /** What the caller can do about it, when there is something. */\n hint?: string;\n /** The address that produced the failure, for a bug report. */\n url?: string;\n status?: number;\n /** What was raised underneath, kept for the bug report the hint asks for. */\n cause?: unknown;\n}\n\nexport class BnfError extends Error {\n readonly code: ErrorCode;\n readonly details: ErrorDetails;\n\n constructor(code: ErrorCode, message: string, details: ErrorDetails = {}) {\n super(message, details.cause === undefined ? undefined : { cause: details.cause });\n this.name = \"BnfError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new BnfError(\"not_found\", message, details);\n\nexport const invalidInput = (message: string, hint?: string) =>\n new BnfError(\"invalid_input\", message, hint ? { hint } : {});\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new BnfError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the BnF describes what you asked for.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new BnfError(\"parse_failure\", message, {\n hint: \"data.bnf.fr may have changed how it answers. Please report this at https://github.com/smeet666/mcp-databnf/issues with the arguments you used.\",\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new BnfError(\"network_error\", message, details);\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new BnfError(\"timeout\", message, details);\n","/**\n * A small in-memory store, keyed by the query that produced the value.\n *\n * It exists to keep a conversation that asks the same thing twice from asking\n * the endpoint twice, which matters here because a conversation about one\n * author walks the same records repeatedly: the person, then the works, then\n * the editions of one work, then back to the person.\n *\n * Entries expire, the store is bounded, and it holds only what was successfully\n * read: storing a response nobody could parse would serve that failure back for\n * the rest of its lifetime.\n *\n * Each entry keeps the moment it was read. The licence asks for the date the\n * metadata was retrieved, and a cached answer was retrieved when it entered the\n * store rather than when it was served, so the stored moment is what travels\n * with it.\n */\n\ninterface Entry<T> {\n value: T;\n expiresAt: number;\n retrievedAt: number;\n}\n\nexport interface Hit<T> {\n value: T;\n retrievedAt: number;\n}\n\nexport class Cache<T> {\n private readonly entries = new Map<string, Entry<T>>();\n\n constructor(\n private readonly ttlMs: number,\n private readonly maxEntries: number,\n ) {}\n\n get(key: string): Hit<T> | undefined {\n const entry = this.entries.get(key);\n if (!entry) {\n return undefined;\n }\n if (entry.expiresAt <= Date.now()) {\n this.entries.delete(key);\n return undefined;\n }\n // Re-inserting marks it as the most recently used, which is what the\n // eviction below reads.\n this.entries.delete(key);\n this.entries.set(key, entry);\n return { value: entry.value, retrievedAt: entry.retrievedAt };\n }\n\n set(key: string, value: T, retrievedAt: number): void {\n // A lifetime of zero turns the store off rather than expiring at once:\n // nothing is written, so nothing has to be checked on the way out.\n if (this.ttlMs <= 0) {\n return;\n }\n this.entries.delete(key);\n this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs, retrievedAt });\n while (this.entries.size > this.maxEntries) {\n const oldest = this.entries.keys().next();\n if (oldest.done) {\n break;\n }\n this.entries.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n","/**\n * Every host, address and vocabulary term this server reads, in one place. An\n * upstream rename is then a one-file change rather than a hunt through the\n * parsers.\n *\n * The robots file at data.bnf.fr withholds one path, the one ending in\n * `cross-documents`, and nothing built here reaches it. The endpoint below is\n * addressed without a trailing slash: with one, the same address serves a\n * JavaScript query editor rather than the query service.\n */\n\nimport { BnfError } from \"../errors.js\";\n\n/** The root a host name may end on, which names the same host without it. */\nconst TRAILING_DOT = /\\.$/;\nconst FRAGMENT = /#.*$/;\nconst HTTP_SCHEME = /^http:/;\n\n/** The one address this server sends a request to. */\nexport const SPARQL_ENDPOINT = \"https://data.bnf.fr/sparql\";\n\n/** The host every request must be addressed to, checked before each call. */\nexport const ALLOWED_HOST = \"data.bnf.fr\";\n\n/**\n * The host this server describes and never requests.\n *\n * The BnF places its metadata and its digitised contents under two different\n * regimes. The metadata served here is free to reuse with its source and its\n * date of retrieval named. The contents on gallica.bnf.fr are not: their terms\n * make use inside an artificial-intelligence project subject to a paid licence\n * outside academic research, and the site refuses ClaudeBot and GPTBot at the\n * server, then bans the calling address after about fifteen requests whatever\n * the pace.\n *\n * So a Gallica address is a piece of metadata here, rendered as a link for a\n * person to open. `assertRequestable` below is what keeps that a rule rather\n * than an intention.\n */\nexport const GALLICA_HOST = \"gallica.bnf.fr\";\n\n/**\n * Refuse to request anything but the query service.\n *\n * This sits between the query builders and `fetch`, so a future route, a\n * redirect followed by hand, or an address read out of a response cannot become\n * a request to a host this server has no licence to read.\n */\nexport function assertRequestable(url: string): void {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch (cause) {\n throw refusal(`mcp-databnf refuses to request \"${url}\": it is not an address.`, cause);\n }\n const host = parsed.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n if (host !== ALLOWED_HOST) {\n throw refusal(\n `mcp-databnf requests ${ALLOWED_HOST} and nothing else; \"${host}\" was refused. ` +\n \"Gallica in particular is described from the catalogue and never read.\",\n );\n }\n}\n\n/**\n * The refusal, raised as an error the transport will not retry.\n *\n * A plain error here would be read as a request that failed to complete, and the\n * next attempt would be made against the same address. A decision not to call a\n * host is settled, so it has to arrive as an answer rather than as a mishap.\n */\nconst refusal = (message: string, cause?: unknown) =>\n new BnfError(\"network_error\", message, {\n hint: \"This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host.\",\n cause,\n });\n\n/** True when an address points at Gallica, whatever the scheme or the case. */\nexport function isGallicaAddress(address: string): boolean {\n try {\n // A trailing dot names the root of the domain tree and resolves to the same\n // host. Comparing without stripping it would read a Gallica address as\n // somewhere else, and the link would be dropped from the answer.\n const host = new URL(address).hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n return host === GALLICA_HOST || host.endsWith(`.${GALLICA_HOST}`);\n } catch {\n return false;\n }\n}\n\n/** Namespaces the queries use, emitted as a PREFIX block. */\nexport const PREFIXES = {\n foaf: \"http://xmlns.com/foaf/0.1/\",\n rdf: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\n rdfs: \"http://www.w3.org/2000/01/rdf-schema#\",\n owl: \"http://www.w3.org/2002/07/owl#\",\n skos: \"http://www.w3.org/2004/02/skos/core#\",\n dcterms: \"http://purl.org/dc/terms/\",\n bio: \"http://vocab.org/bio/0.1/\",\n bnf: \"http://data.bnf.fr/ontology/bnf-onto/\",\n bibo: \"http://purl.org/ontology/bibo/\",\n isni: \"http://isni.org/ontology#\",\n /** RDA group 2 elements, which carry the biographical fields. */\n rdag2: \"http://rdvocab.info/ElementsGr2/\",\n /** RDA elements, which carry the bibliographic fields. */\n rdae: \"http://rdvocab.info/Elements/\",\n /** RDA relationships between work, expression, manifestation and item. */\n rdarel: \"http://rdvocab.info/RDARelationshipsWEMI/\",\n /** The FRBR classes data.bnf.fr types its entities with. */\n rdafrbr: \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/\",\n} as const;\n\nexport const PREFIX_BLOCK = Object.entries(PREFIXES)\n .map(([name, iri]) => `PREFIX ${name}: <${iri}>`)\n .join(\"\\n\");\n\n/** The two shapes an entity address takes in this dataset. */\nexport const ARK_BASE = \"http://data.bnf.fr/ark:/12148/\";\nexport const TEMP_WORK_BASE = \"http://data.bnf.fr/temp-work/\";\n\n/** The page a person can open for a record, which every result carries. */\nexport const publicPageFor = (entityIri: string): string =>\n entityIri.replace(FRAGMENT, \"\").replace(HTTP_SCHEME, \"https:\");\n\n/** The BnF general catalogue, where a record is described for a reader. */\nexport const catalogueUrlFor = (arkId: string): string =>\n `https://catalogue.bnf.fr/ark:/12148/${arkId}`;\n\n/**\n * The licence this dataset is published under, and what it asks for.\n *\n * The BnF states it in one sentence: the metadata may be used freely and at no\n * cost, provided the source is named and the date of retrieval is stated. Both\n * halves are therefore attached to every answer rather than left to a caller.\n */\nexport const LICENCE_FR =\n \"L'utilisation de ces métadonnées est libre et gratuite sous réserve du maintien de la mention de leur source et de l'indication de leur date de récupération.\";\n\nexport const LICENCE_EN =\n \"These metadata may be used freely and at no cost, provided their source is named and the date they were retrieved is stated.\";\n","/**\n * One query sent to the endpoint, with a deadline and bounded retries.\n *\n * Two things separate a retry worth making from one that only adds load. A\n * refusal that carries a time to come back is obeyed rather than guessed at,\n * and an answer the service meant is never retried: asking again for something\n * that is not there wastes a request and delays the honest answer.\n *\n * The query travels in the body of a POST. A long query in a query string is\n * refused by an intermediary before it reaches the service, and a POST keeps the\n * text out of every access log between here and there.\n */\n\nimport {\n invalidInput,\n networkError,\n parseFailure,\n rateLimited,\n timeout as timeoutError,\n} from \"../errors.js\";\nimport type { Logger } from \"../config.js\";\nimport { SPARQL_ENDPOINT, assertRequestable } from \"./endpoint.js\";\nimport type { RateLimiter } from \"./rateLimiter.js\";\n\n/** The shape the endpoint's own engine writes a refusal in. */\nconst VIRTUOSO_ERROR = /^Virtuoso\\s+(\\w+)\\s+Error\\s+([\\s\\S]*)$/;\n\n/** The JSON shape a SPARQL SELECT answers with. */\nexport interface SparqlTerm {\n type: \"uri\" | \"literal\" | \"typed-literal\" | \"bnode\";\n value: string;\n datatype?: string;\n \"xml:lang\"?: string;\n}\n\nexport type SparqlRow = Record<string, SparqlTerm | undefined>;\n\nexport interface SparqlResults {\n head: { vars?: string[] };\n results: { bindings: SparqlRow[] };\n}\n\nexport interface QueryOptions {\n query: string;\n userAgent: string;\n timeoutMs: number;\n maxRetries: number;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Statuses worth another attempt: the service is busy, not answering \"no\". */\nconst RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);\n/**\n * Statuses that mean the service is refusing for now and naming a delay.\n *\n * These produce a `rate_limited` error and honour a `Retry-After` header. A\n * server error is a different claim, so it keeps its own code even though it\n * widens the spacing the same way.\n */\nconst PUSH_BACK = new Set([429, 503]);\n\n/**\n * The longest wait worth taking rather than reporting.\n *\n * A refusal may name any delay, and an hour is a legal answer. Sleeping through\n * it holds the one request slot this server has, so every other tool waits\n * behind a call whose caller has long since given up. Past this point the wait\n * is the answer, and the caller decides what to do with it.\n */\nconst LONGEST_WAIT_MS = 30_000;\n\n/**\n * How many times a query that never answered is worth repeating.\n *\n * A query that did not respond within its budget is one the planner is\n * struggling with. Repeating it adds the same load again, and each attempt\n * holds the slot for the full deadline.\n */\nconst RETRIES_AFTER_SILENCE = 1;\n\n/**\n * Redirects worth following before calling it a loop.\n *\n * The query service answers directly, so one hop covers a host that has moved\n * and two cover a scheme change on top of it. Beyond that the answer is a\n * redirect rather than a result.\n */\nconst MAX_REDIRECTS = 2;\n\n/**\n * Read a Retry-After header, which is either a number of seconds or a date.\n * Returns null when it says neither, so the caller falls back to its own wait.\n */\nexport function parseRetryAfter(value: string | null, now = Date.now()): number | null {\n if (!value) {\n return null;\n }\n const trimmed = value.trim();\n\n const seconds = Number(trimmed);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n\n const at = Date.parse(trimmed);\n if (Number.isNaN(at)) {\n return null;\n }\n return Math.max(0, at - now);\n}\n\n/** Growing wait with jitter, so several clients do not return in step. */\nfunction backoffMs(attempt: number): number {\n const base = Math.min(16_000, 1000 * 2 ** attempt);\n return base + Math.floor(Math.random() * 500);\n}\n\n/**\n * What a Virtuoso says when it will not run a query.\n *\n * It answers in plain text rather than in the result format, and the leading\n * code says which half went wrong: a compiler code means the query as written\n * could not be understood, and a runtime code means it was understood and then\n * abandoned, usually because it asked for more of the graph than the service\n * will spend on one caller.\n */\nexport function readEngineError(\n body: string,\n): { kind: \"compile\" | \"runtime\"; text: string } | null {\n const match = VIRTUOSO_ERROR.exec(body.trim());\n if (!match) {\n return null;\n }\n const state = match[1] ?? \"\";\n const text = (match[2] ?? \"\").split(\"\\n\")[0]?.trim() ?? \"\";\n // 37000 is the compiler's own state; the rest come from execution.\n return { kind: state === \"37000\" ? \"compile\" : \"runtime\", text };\n}\n\nasync function postOnce(\n options: QueryOptions,\n signal: AbortSignal,\n): Promise<{ status: number; headers: Headers; body: string; ok: boolean }> {\n assertRequestable(SPARQL_ENDPOINT);\n const doFetch = options.fetchImpl ?? fetch;\n\n const form = new URLSearchParams();\n form.set(\"query\", options.query);\n // The endpoint answers in XML when only the Accept header asks for JSON, so\n // the format is stated in the request itself as well.\n form.set(\"format\", \"application/sparql-results+json\");\n\n // Redirects are handled here rather than by fetch. Following one automatically\n // would take the request to whatever host the answer names, and the guard\n // above only ever sees the address this file wrote: a redirect to Gallica\n // would cross the licence boundary with no code having decided to.\n let target = SPARQL_ENDPOINT;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {\n const response = await doFetch(target, {\n method: \"POST\",\n signal,\n redirect: \"manual\",\n headers: {\n \"user-agent\": options.userAgent,\n accept: \"application/sparql-results+json\",\n \"content-type\": \"application/x-www-form-urlencoded\",\n },\n body: form.toString(),\n });\n\n const location = response.headers.get(\"location\");\n if (response.status >= 300 && response.status < 400 && location !== null) {\n const next = new URL(location, target).toString();\n // Checked before the next request rather than after it, so a refused\n // address is one this client never called.\n assertRequestable(next);\n target = next;\n continue;\n }\n\n return {\n status: response.status,\n headers: response.headers,\n body: await response.text(),\n ok: response.ok,\n };\n }\n\n throw networkError(`data.bnf.fr redirected this request more than ${MAX_REDIRECTS} times.`, {\n url: SPARQL_ENDPOINT,\n });\n}\n\n/** What a refusal from the endpoint amounts to, and what it costs the pacing. */\ntype Refusal =\n | { kind: \"refused\"; error: Error; pushBack: boolean }\n | { kind: \"again\"; waitMs: number; pushBack: boolean };\n\n/**\n * Read a status the endpoint answered with, apart from the loop that retries.\n *\n * Anything worth another attempt means the service is struggling, so the gap\n * widens whether or not the status is one that names a delay: a Virtuoso under\n * load answers 500 as readily as 503. A status the service read the query for\n * and would not run is not a network failure, and calling it one invites a\n * retry of something only the query can fix.\n */\nfunction readRefusal(\n response: { status: number; body: string; headers: Headers },\n attempt: number,\n maxRetries: number,\n): Refusal {\n const pushBack = RETRYABLE.has(response.status);\n\n if (PUSH_BACK.has(response.status)) {\n const asked = parseRetryAfter(response.headers.get(\"retry-after\"));\n\n if (asked !== null && asked > LONGEST_WAIT_MS) {\n return {\n kind: \"refused\",\n pushBack,\n error: rateLimited(\n `data.bnf.fr asked this client to wait ${Math.round(asked / 1000)} seconds (HTTP ${response.status}).`,\n { url: SPARQL_ENDPOINT, status: response.status },\n ),\n };\n }\n if (attempt >= maxRetries) {\n return {\n kind: \"refused\",\n pushBack,\n error: rateLimited(\n `data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,\n {\n url: SPARQL_ENDPOINT,\n status: response.status,\n },\n ),\n };\n }\n return { kind: \"again\", pushBack, waitMs: asked ?? backoffMs(attempt) };\n }\n\n if (RETRYABLE.has(response.status) && attempt < maxRetries) {\n return { kind: \"again\", pushBack, waitMs: backoffMs(attempt) };\n }\n\n if (response.status === 400 || response.status === 422) {\n const engine = readEngineError(response.body);\n return {\n kind: \"refused\",\n pushBack,\n error: invalidInput(\n `data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : \".\"}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n ),\n };\n }\n\n return {\n kind: \"refused\",\n pushBack,\n error: networkError(`data.bnf.fr answered HTTP ${response.status}.`, {\n url: SPARQL_ENDPOINT,\n status: response.status,\n }),\n };\n}\n\n/**\n * What a thrown attempt amounts to, or the error it has become.\n *\n * An error this module raised on purpose already says what happened, and is\n * passed straight on. Silence is given fewer attempts than a refusal: a query\n * spanning the whole catalogue can take longer than the service will spend on\n * it, and asking again costs both sides the same wait.\n */\nfunction readFailure(\n error: unknown,\n attempts: { attempt: number; maxRetries: number; timeoutMs: number },\n): Error {\n const { attempt, maxRetries, timeoutMs } = attempts;\n\n if (error instanceof Error && error.name === \"BnfError\") {\n throw error;\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {\n throw timeoutError(\n `No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,\n { url: SPARQL_ENDPOINT },\n );\n }\n return error;\n }\n\n const failure = error instanceof Error ? error : new Error(String(error));\n if (attempt >= maxRetries) {\n throw networkError(`Could not reach data.bnf.fr: ${failure.message}`, { url: SPARQL_ENDPOINT });\n }\n return failure;\n}\n\n/** Send one query and return the parsed result set. */\nexport async function runQuery(options: QueryOptions): Promise<SparqlResults> {\n const { timeoutMs, maxRetries, limiter, logger } = options;\n\n let lastError: Error | null = null;\n /** Honoured before the next attempt rather than slept after the last one. */\n let askedWaitMs = 0;\n\n for (let attempt = 0; attempt <= maxRetries; attempt += 1) {\n if (askedWaitMs > 0) {\n logger.debug(`waiting ${askedWaitMs}ms, as asked`);\n // Read once into a constant: the timer closes over what this attempt was\n // told to wait, not over whatever a later attempt puts there.\n const asked = askedWaitMs;\n await new Promise((resolve) => setTimeout(resolve, asked));\n askedWaitMs = 0;\n }\n await limiter.beforeRequest();\n\n const controller = new AbortController();\n const deadline = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n logger.debug(`POST ${SPARQL_ENDPOINT} (${options.query.length} characters)`);\n const response = await postOnce(options, controller.signal);\n\n if (response.ok) {\n // The body decides whether this was an answer. This endpoint says \"I\n // will spend no more on you\" with a status of 200 and either an empty\n // body or a runtime error in plain text, so crediting the status line\n // would narrow the spacing at the one moment it has to widen.\n let parsed: SparqlResults;\n try {\n parsed = parseResults(response.body);\n } catch (error) {\n limiter.pushBack();\n throw error;\n }\n limiter.succeeded();\n return parsed;\n }\n\n const verdict = readRefusal(response, attempt, maxRetries);\n if (verdict.pushBack) {\n limiter.pushBack();\n }\n if (verdict.kind === \"refused\") {\n throw verdict.error;\n }\n askedWaitMs = verdict.waitMs;\n lastError = new Error(`HTTP ${response.status}`);\n } catch (error) {\n clearTimeout(deadline);\n\n lastError = readFailure(error, { attempt, maxRetries, timeoutMs });\n askedWaitMs = backoffMs(attempt);\n } finally {\n clearTimeout(deadline);\n }\n }\n\n throw networkError(\n `Could not reach data.bnf.fr: ${lastError?.message ?? \"no attempt was made\"}`,\n {\n url: SPARQL_ENDPOINT,\n },\n );\n}\n\n/**\n * Read a result set out of a body that arrived with a success status.\n *\n * The endpoint answers 200 with an empty body when it abandons a query part way\n * through, which is the same silence as a result set holding no rows and means\n * the opposite. An empty body is therefore a failure to read rather than an\n * absence, so nothing downstream can report \"the BnF describes none\" on the\n * strength of a query that was never finished.\n */\nexport function parseResults(body: string): SparqlResults {\n if (body.trim() === \"\") {\n throw parseFailure(\n \"data.bnf.fr returned an empty body, which is what it does when it abandons a query part way through.\",\n { url: SPARQL_ENDPOINT },\n );\n }\n\n const engine = readEngineError(body);\n if (engine) {\n if (engine.kind === \"compile\") {\n throw invalidInput(\n `data.bnf.fr could not read this query: ${engine.text}`,\n \"Try fewer or plainer words. If this keeps happening with ordinary input, please report it.\",\n );\n }\n throw timeoutError(`data.bnf.fr started this query and gave it up: ${engine.text}`, {\n url: SPARQL_ENDPOINT,\n });\n }\n\n let payload: unknown;\n try {\n payload = JSON.parse(body);\n } catch (cause) {\n throw parseFailure(\"data.bnf.fr answered with something that is not JSON.\", {\n url: SPARQL_ENDPOINT,\n cause,\n });\n }\n\n if (\n typeof payload !== \"object\" ||\n payload === null ||\n !(\"results\" in payload) ||\n typeof (payload as SparqlResults).results !== \"object\" ||\n !Array.isArray((payload as SparqlResults).results?.bindings)\n ) {\n throw parseFailure(\"data.bnf.fr answered with JSON that is not a SPARQL result set.\", {\n url: SPARQL_ENDPOINT,\n });\n }\n\n return payload as SparqlResults;\n}\n","/**\n * How caller text becomes part of a query, and what it can never become.\n *\n * A query is a program, and every character of it that came from outside is a\n * chance to write a different program. Three separate defences apply here, and\n * each is narrower than the one before:\n *\n * 1. Text that reaches the full-text index is reduced to words. Letters,\n * digits, apostrophes inside a word and hyphens inside a word survive; every\n * quote, brace, backslash, angle bracket and newline is dropped before the\n * query exists. There is no escaping to get right because there is nothing\n * left to escape.\n * 2. An identifier is matched against the two shapes this dataset uses and\n * rebuilt from the match, so the address in the query is one this file\n * wrote rather than one a caller supplied.\n * 3. Numbers reach the query as numbers, after being bounded.\n *\n * `escapeLiteral` exists for the values that are neither: it is the last line\n * rather than the first, and the tests hold it to producing a literal that\n * closes where it opened whatever it is handed.\n */\n\nimport { invalidInput } from \"../errors.js\";\n\n/** What a term can carry: letters, digits, the joiners and the spaces between them. */\nconst KEPT_CHARACTER = /[\\p{L}\\p{N}'\\-\\s]/u;\nconst TYPOGRAPHIC_APOSTROPHE = /[‘’ʼ]/u;\nconst LEADING_JOINERS = /^['-]+/u;\nconst TRAILING_JOINERS = /['-]+$/u;\nconst JOINERS = /['-]+/u;\nconst ARK_IN_INPUT = /(?:^|\\/)ark:\\/12148\\/(c[bc][0-9a-z]+)/i;\nconst LEADING_HASH = /^#/;\nconst FRAGMENT_OR_QUERY = /[#?]/;\nconst TEMP_WORK_IN_INPUT = /temp-work\\/([0-9a-f]{32})/i;\nconst GALLICA_ARK = /ark:\\/12148\\/([0-9a-z]+)/i;\n\n/**\n * Escape a string for a SPARQL short literal in double quotes.\n *\n * Backslash goes first: escaping it after the others would escape the\n * backslashes they just introduced. The characters a SPARQL short literal\n * cannot carry raw are a quote, a backslash, a line feed and a carriage\n * return; the rest are legal and pass through as published, since mangling a\n * title is its own kind of lie.\n */\nexport function escapeLiteral(value: string): string {\n return (\n value\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\")\n // The remaining control characters are legal inside a SPARQL literal and\n // meaningless in a catalogue query. A null byte in particular is where a\n // C string ends, so an intermediary or the engine can truncate the query\n // at one and run a shorter query than the one that was written.\n .replace(/[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/gu, \"\")\n );\n}\n\n/** A literal, quoted and escaped, ready to be dropped into a query. */\nexport const literal = (value: string): string => `\"${escapeLiteral(value)}\"`;\n\n/**\n * Characters that occupy a position in a string and mark nothing on a screen.\n *\n * The control characters other than the whitespace ones, the zero-width spaces\n * and joiners, the soft hyphen, the bidirectional marks and the byte order\n * mark. A space, a tab and a line break are absent from the list: they are\n * visible as a gap and they do separate two words.\n *\n * They are written as escapes because they would be invisible in this file as\n * well, and a character nobody can see in the source of a guard is a guard\n * nobody can review.\n */\nconst INVISIBLE =\n /[\\u0000-\\u0008\\u000e-\\u001f\\u007f-\\u009f\\u00ad\\u061c\\u180e\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u2069\\ufeff]/gu;\n\n/**\n * The text with the characters that mark nothing removed.\n *\n * A separator is something a person wrote to hold two words apart. One of these\n * holds nothing apart, so treating it as a separator turns one word into two\n * terms the index then requires separately, and a name that matches comes back\n * as a name the catalogue does not hold.\n */\nexport function withoutInvisibles(input: string): string {\n return input.replace(INVISIBLE, \"\");\n}\n\n/** The characters that marked nothing, listed once each, in the order written. */\nexport function invisibleCharacters(input: string): string[] {\n const found: string[] = [];\n for (const character of input.match(INVISIBLE) ?? []) {\n if (!found.includes(character)) {\n found.push(character);\n }\n }\n return found;\n}\n\n/**\n * The visible characters a search dropped, listed once each.\n *\n * Punctuation reaches no index here, so a caller comparing what they wrote\n * against what was searched for needs to see which marks were left out. A\n * space is not one of them: it separates words and is what the words are cut\n * along.\n */\nexport function charactersSetAside(input: string): string[] {\n const found: string[] = [];\n for (const character of withoutInvisibles(input).normalize(\"NFC\")) {\n if (KEPT_CHARACTER.test(character) || TYPOGRAPHIC_APOSTROPHE.test(character)) {\n continue;\n }\n if (!found.includes(character)) {\n found.push(character);\n }\n }\n return found;\n}\n\n/** How a piece of caller text became the terms the index was given. */\nexport interface SearchReading {\n /** The words the text was cut into, each one quoted into the query. */\n words: string[];\n /** The terms the index requires, after it splits inside a word. */\n terms: string[];\n /** The characters that marked nothing and were removed before the cutting. */\n invisible: string[];\n /** The visible characters that reached no term. */\n setAside: string[];\n}\n\n/** Read a piece of caller text the way the query builder and the index will. */\nexport function readSearchText(input: string): SearchReading {\n const words = toSearchWords(input);\n return {\n words,\n terms: toIndexTerms(words),\n invisible: invisibleCharacters(input),\n setAside: charactersSetAside(input),\n };\n}\n\n/**\n * Words a full-text query may carry.\n *\n * The endpoint is a Virtuoso, whose `bif:contains` takes its own miniature\n * language: words in single quotes joined by AND, OR and NOT, with `*` for a\n * prefix. Handing caller text to that language directly would let a caller\n * write the operators, so the text is cut into words instead and this file\n * writes every operator.\n *\n * A word keeps letters and digits in any script, and keeps an apostrophe or a\n * hyphen that sits between two other characters, because \"aujourd'hui\" and\n * \"Charleville-Mézières\" are one word each. A leading or trailing one is\n * dropped, so nothing can close the quoting this file opens.\n *\n * The same rule holds a span of years together: \"1871-1933\" stays one word.\n * Nothing here can tell a range from a compound name, and splitting on the\n * chance that it is a range would break every hyphenated place in France.\n */\nexport function toSearchWords(input: string): string[] {\n const cleaned = withoutInvisibles(input)\n .normalize(\"NFC\")\n // Anything that is not a letter, a digit, an apostrophe or a hyphen becomes\n // a separator. Typographic apostrophes are folded onto the plain one first,\n // since a name is written with either.\n .replace(/[‘’ʼ]/gu, \"'\")\n .replace(/[^\\p{L}\\p{N}'-]+/gu, \" \");\n\n const words: string[] = [];\n for (const raw of cleaned.split(\" \")) {\n // Trim the joiners at the edges: they only mean something between letters.\n const word = raw.replace(LEADING_JOINERS, \"\").replace(TRAILING_JOINERS, \"\");\n if (word === \"\") {\n continue;\n }\n // A word this long is a paste rather than a word, and the index will not\n // match it anyway.\n words.push(word.length > 60 ? word.slice(0, 60) : word);\n if (words.length >= 12) {\n break;\n }\n }\n return words;\n}\n\n/**\n * The terms the index will require, read off the words a caller wrote.\n *\n * The index tokenises inside a quoted term: an apostrophe and a hyphen are\n * separators there, so `O'Brien` reaches it as two terms and a record written\n * `Pat O Brien` carries both and matches. Reporting the word back as one term\n * would leave a caller checking why a row is on the list with no way of getting\n * there from the fields shown.\n *\n * A piece repeated across words is kept once, since requiring a term twice is\n * requiring it.\n */\nexport function toIndexTerms(words: readonly string[]): string[] {\n const terms: string[] = [];\n for (const word of words) {\n for (const piece of word.split(JOINERS)) {\n if (piece !== \"\" && !terms.includes(piece)) {\n terms.push(piece);\n }\n }\n }\n return terms;\n}\n\n/**\n * A `bif:contains` argument requiring every word, built from words this file\n * quoted.\n *\n * The result is a SPARQL literal, so the double quotes belong to SPARQL and the\n * single quotes belong to the index. Two quotings are therefore in play, and\n * each has to be closed by the layer that opened it.\n *\n * The apostrophe kept inside a word is also the index's own quoting character,\n * so `rimbaud'OR'hugo` arrives as one word and emits `'rimbaud'OR'hugo'`, where\n * `OR` has escaped the quotes and become an operator. Doubling it puts it back\n * inside the term, which is how the index writes a literal quote. That is the\n * whole of the exposure: the word cannot reach the SPARQL layer as anything but\n * text, because `escapeLiteral` runs over the finished clause.\n */\nexport function containsAllWords(words: readonly string[]): string {\n if (words.length === 0) {\n throw invalidInput(\n \"No word to search for: the text held no letters or digits.\",\n \"Write the words to look for, such as a surname or a title.\",\n );\n }\n const clause = words.map((word) => `'${word.replace(/'/g, \"''\")}'`).join(\" AND \");\n return literal(clause);\n}\n\n/**\n * The two identifier shapes this dataset uses, and nothing else.\n *\n * An ARK identifier is `cb` followed by digits and one final check character,\n * which the BnF also writes as `cc…` on a few records. A provisional work is\n * named by a thirty-two character hexadecimal digest. Both are matched whole,\n * so an identifier reaches a query only after this file has rebuilt it.\n */\nconst ARK_ID = /^c[bc][0-9a-z]{6,20}$/;\nconst TEMP_WORK_ID = /^[0-9a-f]{32}$/;\n\nexport type EntityKind = \"ark\" | \"temp-work\";\n\nexport interface EntityId {\n kind: EntityKind;\n /** The bare identifier, as a person would quote it. */\n id: string;\n /** The address of the thing itself, which the queries bind to. */\n iri: string;\n /** The page a person can open. */\n pageUrl: string;\n}\n\n/**\n * Read an identifier a caller wrote, in any of the forms they see.\n *\n * Rows carry `http://data.bnf.fr/ark:/12148/cb119219976#about`, prose carries\n * `cb119219976`, and a browser carries `https://data.bnf.fr/ark:/12148/cb119219976`.\n * All three name one record. Anything else is refused rather than guessed at,\n * because an identifier built from a guess sends the next call to a record that\n * does not exist and the answer reads as an absence.\n */\nexport function parseEntityId(input: string): EntityId {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"An identifier is required.\");\n }\n\n const arkMatch = ARK_IN_INPUT.exec(trimmed);\n const bare = arkMatch\n ? arkMatch[1]\n : trimmed.replace(LEADING_HASH, \"\").split(FRAGMENT_OR_QUERY)[0];\n const candidate = (bare ?? \"\").toLowerCase();\n\n if (ARK_ID.test(candidate)) {\n return {\n kind: \"ark\",\n id: candidate,\n iri: `http://data.bnf.fr/ark:/12148/${candidate}#about`,\n pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`,\n };\n }\n\n const tempMatch = TEMP_WORK_IN_INPUT.exec(trimmed);\n const digest = (tempMatch?.[1] ?? candidate).toLowerCase();\n if (TEMP_WORK_ID.test(digest)) {\n return {\n kind: \"temp-work\",\n id: `temp-work/${digest}`,\n iri: `http://data.bnf.fr/temp-work/${digest}/#about`,\n pageUrl: `https://data.bnf.fr/temp-work/${digest}/`,\n };\n }\n\n throw invalidInput(`\"${input}\" is not a data.bnf.fr identifier.`, hintFor(trimmed));\n}\n\n/**\n * Two shapes people paste, which are addresses of something else entirely.\n *\n * A Gallica document identifier and a data.bnf.fr page address both look like\n * identifiers to whoever is holding one, and both are refused here. Refusing\n * them with the general advice reads as the server failing to know a record it\n * plainly has, so each is named for what it is and told what to do instead.\n */\nconst GALLICA_DOCUMENT = /^(?:bpt6k|btv1b|bd6t|cb32|btv1b)[0-9a-z]+$/i;\n/** `https://data.bnf.fr/fr/11907966/marguerite_duras/`, which is what a browser shows. */\nconst PAGE_ADDRESS = /data\\.bnf\\.fr\\/(?:[a-z]{2}\\/)?(\\d{6,12})\\/([a-z0-9_]+)/i;\n\nconst GENERAL_HINT =\n \"An identifier looks like 'cb119219976' for an established record, or 'temp-work/22d7f68c1a4bdd081ad7ca791fd3b730' for a provisional one. Take it from a search result rather than building one.\";\n\nfunction hintFor(input: string): string {\n const page = PAGE_ADDRESS.exec(input);\n if (page) {\n const name = (page[2] ?? \"\").replace(/_/g, \" \");\n return (\n `That is the readable address of a data.bnf.fr page, and ${page[1]} in it is the record's FRBNF number rather than the identifier these tools take. ` +\n `Search for \"${name}\" with search_authors or search_works, and use the identifier the row carries.`\n );\n }\n\n const gallicaArk = GALLICA_ARK.exec(input)?.[1] ?? input;\n if (GALLICA_DOCUMENT.test(gallicaArk)) {\n return (\n \"That is a Gallica identifier: it names a digitised document rather than a record in the catalogue, and these tools read the catalogue. \" +\n \"Search for the work with search_works, then list_editions will give you the Gallica link alongside the edition it belongs to.\"\n );\n }\n\n return GENERAL_HINT;\n}\n\n/** An identifier written back as the address of the entity itself. */\nexport const iriOf = (id: EntityId): string => `<${id.iri}>`;\n\n/**\n * A whole number, bounded, written into a query.\n *\n * LIMIT and OFFSET take a bare number rather than a quoted value, so this is\n * the one place where something that is not a literal is assembled. Bounding\n * before writing is what makes that safe, and it also keeps a caller from\n * asking the endpoint for a page a hundred thousand rows deep.\n */\nexport function boundedInteger(value: number, min: number, max: number, name: string): number {\n if (!(Number.isFinite(value) && Number.isInteger(value))) {\n throw invalidInput(`'${name}' has to be a whole number.`);\n }\n if (value < min || value > max) {\n throw invalidInput(`'${name}' has to be between ${min} and ${max}.`);\n }\n return value;\n}\n","/**\n * The queries this server sends, and the reasons they are shaped as they are.\n *\n * Every one of them describes the endpoint as it actually answers: the shapes\n * below are the ones the live service returns, rather than the ones the\n * vocabularies promise.\n *\n * Three facts about this dataset govern most of what follows.\n *\n * **An entity and its record are two addresses.** A person is\n * `ark:/12148/cb119219976#about`, typed `foaf:Person`, and carries the dates,\n * the places and the alignments. The authority record is the same ARK without\n * the fragment, typed `skos:Concept`, and carries the headings, the editorial\n * notes and the date the record was last changed. A query that reads only one\n * of the two misses half the record.\n *\n * **Work, expression and manifestation are three levels.** A work links to\n * expressions, and a manifestation names both the expression it realises and\n * the work it manifests. `workManifested` is what makes the editions of a work\n * reachable in one step rather than two.\n *\n * **The full-text index drives, or the query is abandoned.** Virtuoso needs the\n * triple whose object it will search to appear before `bif:contains` in the same\n * group, and it needs that pattern to be the one that starts the plan. Asking\n * for `?w a rdafrbr:Work` first makes the planner walk every work in the\n * catalogue and the service gives the query up, answering 200 with an empty\n * body. Putting the text search in a subquery and filtering its output is what\n * keeps the index in front.\n *\n * **A paged query is ordered at both levels.** The subquery decides which\n * entities a page holds; the outer query decides the sequence its rows arrive\n * in, and SPARQL gives an unordered outer query no sequence at all. Since a\n * page is read by taking one entity beyond it and dropping the last, an outer\n * form without `ORDER BY` drops an entity chosen by the service: that entity is\n * then missing from the walk, and one of its neighbours is served on two pages.\n * Both levels therefore sort on the same variable, so the sequence a caller\n * reads is the sequence the pages were cut along. The outer sort runs over the\n * rows of one page, so it costs a sort of a few dozen rows rather than of the\n * matching set.\n */\n\nimport { PREFIX_BLOCK } from \"./endpoint.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { boundedInteger, containsAllWords, iriOf, literal } from \"./sparql.js\";\n\n/**\n * How many rows a text search reads from the index before filtering.\n *\n * `bif:contains` answers in index order, which is not an order of relevance, so\n * a window that is too small leaves out the record a caller meant. A window this\n * size costs the service one pass and gives the filter enough to work with.\n *\n * It is also the end of what a text search can page through. An offset past it\n * lands beyond what was ever read, and the answer would be an empty page that\n * looks exactly like the catalogue holding nothing. The tools refuse such a page\n * rather than buying the query, which is why this is exported.\n */\nexport const TEXT_WINDOW = 400;\n\nconst prefixed = (body: string): string => `${PREFIX_BLOCK}\\n${body.trim()}\\n`;\n\n/**\n * How many rows the window returned, asked for in the request that reads it.\n *\n * The window bounds the answer, and it is read before the type filter, so a\n * page of a few dozen rows can rest on a window that came back full. From the\n * page alone the two readings are identical: an index with nothing more to give\n * and an index holding a great deal more that was never asked. The occupancy is\n * what separates them, and it costs one extra pass of the same index inside a\n * request that was already being sent, rather than a second call.\n *\n * It arrives as a row of its own, binding the count and no entity, because a\n * branch of a UNION answers even when the other branch holds nothing. A page\n * emptied by the filter is the reading that most needs the fact.\n */\nfunction windowOccupancy(predicate: string, words: readonly string[]): string {\n return `\n {\n SELECT (COUNT(*) AS ?windowRows) WHERE {\n SELECT DISTINCT ?windowEntity ?windowText WHERE {\n ?windowEntity ${predicate} ?windowText .\n ?windowText bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n }`;\n}\n\n/**\n * People whose name carries every word given.\n *\n * The name is searched rather than the authority heading, because a heading\n * carries the dates and a caller writing \"Arthur Rimbaud 1854\" would otherwise\n * match on the year. Both are returned, so an answer can show which of several\n * people bearing one name is which.\n *\n * `limit + 1` rows are asked for. The extra row is never shown; it is how the\n * answer can say that more exist without a second query counting them.\n */\nexport function searchAuthorsQuery(\n words: readonly string[],\n limit: number,\n offset: number,\n): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // The page is cut in the middle subquery, over people rather than over rows,\n // and the cutting projection names the person alone. Selecting the name\n // alongside would give a person recorded under two names two of the page's\n // slots; the parser then folds them back into one row, and a page of ten\n // comes back holding eight.\n // The columns of the page are read inside the branch that produces it. Read\n // outside it, they would be joined against an unbound subject on the row the\n // occupancy arrives on, which asks the service for every name in the file.\n return prefixed(`\nSELECT ?person ?name ?label ?birthYear ?deathYear ?role ?windowRows WHERE {\n {\n {\n SELECT DISTINCT ?person WHERE {\n {\n SELECT DISTINCT ?person ?name WHERE {\n ?person foaf:name ?name .\n ?name bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?person a foaf:Person .\n }\n ORDER BY ?person\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?person foaf:name ?name }\n OPTIONAL { ?record foaf:focus ?person ; skos:prefLabel ?label }\n OPTIONAL { ?person bnf:firstYear ?birthYear }\n OPTIONAL { ?person bnf:lastYear ?deathYear }\n OPTIONAL { ?person rdag2:biographicalInformation ?role }\n }\n UNION${windowOccupancy(\"foaf:name\", words)}\n}\nORDER BY ?person`);\n}\n\n/**\n * The most triples one record's detail query will read.\n *\n * A record that hits this ceiling has been cut, and a cut record renders as a\n * record whose missing statements are absences. The parsers compare the row\n * count against it and say so rather than reporting the silence as fact.\n */\nexport const MOST_TRIPLES = 2000;\n\n/**\n * Everything both halves of one person's record carry.\n *\n * The two subjects are read in one query through a UNION rather than in two\n * requests: the spacing between requests is the cost that matters here, and one\n * person is one question.\n */\nexport function authorQuery(id: EntityId): string {\n const person = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang WHERE {\n {\n ${person} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n UNION\n {\n ?record foaf:focus ${person} ; ?p ?o .\n BIND(LANG(?o) AS ?lang)\n }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * Works whose title carries every word given.\n *\n * Both established works and provisional ones are returned, and the row says\n * which is which: leaving the provisional ones out would hide the only record\n * the BnF holds for a great many recent books, and mixing them in silently\n * would let a provisional record be quoted as an established one.\n */\nexport function searchWorksQuery(words: readonly string[], limit: number, offset: number): string {\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n // As with people, the page is cut over works rather than over rows, on the\n // work alone: a work with three authors occupies three rows, and a work\n // recorded under two titles would take two of the page's slots.\n return prefixed(`\nSELECT ?work ?title ?date ?status ?creator ?creatorName ?windowRows WHERE {\n {\n {\n SELECT DISTINCT ?work WHERE {\n {\n SELECT DISTINCT ?work ?title WHERE {\n ?work dcterms:title ?title .\n ?title bif:contains ${containsAllWords(words)} .\n }\n LIMIT ${TEXT_WINDOW}\n }\n ?work a rdafrbr:Work .\n }\n ORDER BY ?work\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?work dcterms:title ?title }\n OPTIONAL { ?work dcterms:date ?date }\n OPTIONAL { ?work rdae:statusOfIdentification ?status }\n OPTIONAL {\n ?work dcterms:creator ?creator .\n OPTIONAL { ?creator foaf:name ?creatorName }\n }\n }\n UNION${windowOccupancy(\"dcterms:title\", words)}\n}\nORDER BY ?work`);\n}\n\n/**\n * The works one person is named the creator of.\n *\n * `dcterms:creator` is the statement that ties a work to the person who made\n * it, and it is the widest one at that level: every work reached through the\n * relator vocabularies is either reached through this one as well, or is a\n * record of a performance rather than of a work. So one statement answers the\n * question, and a second path would add rows of a different kind.\n *\n * The page is cut in a subquery over works alone, ordered by their address. A\n * work carrying two form codes arrives as two rows, and cutting the page over\n * rows would give that work two of the page's slots. The order is arbitrary and\n * it is stable, which is what a caller reading page after page needs.\n */\nexport function worksByCreatorQuery(id: EntityId, limit: number, offset: number): string {\n const person = iriOf(id);\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n return prefixed(`\nSELECT ?work ?title ?date ?year ?status ?form WHERE {\n {\n SELECT DISTINCT ?work WHERE {\n ?work dcterms:creator ${person} .\n }\n ORDER BY ?work\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?work dcterms:title ?title }\n OPTIONAL { ?work dcterms:date ?date }\n OPTIONAL { ?work bnf:firstYear ?year }\n OPTIONAL { ?work rdae:statusOfIdentification ?status }\n OPTIONAL { ?work rdae:formOfWork ?form }\n}\nORDER BY ?work`);\n}\n\n/** Everything one work's record carries, with the names of its creators. */\nexport function workQuery(id: EntityId): string {\n const work = iriOf(id);\n return prefixed(`\nSELECT ?p ?o ?lang ?name WHERE {\n ${work} ?p ?o .\n BIND(LANG(?o) AS ?lang)\n OPTIONAL { ?o foaf:name ?name }\n}\nLIMIT ${MOST_TRIPLES}`);\n}\n\n/**\n * The published editions of one work.\n *\n * A manifestation naming the same work twice, or carrying two digitised copies,\n * comes back as several rows describing one edition. The parser groups them,\n * which is why the manifestation's own address is selected first.\n */\nexport function editionsQuery(id: EntityId, limit: number, offset: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 50, \"limit\") + 1;\n const skip = boundedInteger(offset, 0, 5000, \"offset\");\n\n return prefixed(`\nSELECT ?edition ?title ?date ?year ?publisher ?place ?editionStatement\n ?extent ?isbn ?note ?catalogue ?reproduction ?ocr WHERE {\n {\n SELECT DISTINCT ?edition WHERE {\n ?edition rdarel:workManifested ${work} .\n }\n ORDER BY ?edition\n LIMIT ${take} OFFSET ${skip}\n }\n OPTIONAL { ?edition dcterms:title ?title }\n OPTIONAL { ?edition dcterms:date ?date }\n OPTIONAL { ?edition bnf:firstYear ?year }\n OPTIONAL { ?edition rdae:publishersName ?publisher }\n OPTIONAL { ?edition rdae:placeOfPublication ?place }\n OPTIONAL { ?edition rdae:editionStatement ?editionStatement }\n OPTIONAL { ?edition dcterms:description ?extent }\n OPTIONAL { ?edition bnf:isbn ?isbn }\n OPTIONAL { ?edition rdae:note ?note }\n OPTIONAL { ?edition rdfs:seeAlso ?catalogue }\n OPTIONAL { ?edition rdarel:electronicReproduction ?reproduction }\n OPTIONAL { ?edition bnf:OCR ?ocr }\n}\nORDER BY ?edition`);\n}\n\n/**\n * Every digitised document one work's editions point at.\n *\n * Held apart from the edition listing because the two answer different\n * questions: one asks what was published, the other asks what can be opened.\n */\nexport function digitisedForWorkQuery(id: EntityId, limit: number): string {\n const work = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?edition rdarel:workManifested ${work} .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${work} foaf:depiction ?depiction .\n BIND(${work} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Every digitised document attached to one person.\n *\n * Two paths lead there and they mean different things. `foaf:depiction` hangs\n * off the person and illustrates them. A reproduction hangs off an edition of a\n * work they are credited with, and is that edition digitised. The parser keeps\n * the two apart, and the answer says which is which.\n *\n * Reproductions are ordered first because a well-known person carries dozens of\n * illustrations, and a page filled with those would answer \"what of this author\n * can be read\" with a list of pictures of him.\n */\nexport function digitisedForPersonQuery(id: EntityId, limit: number): string {\n const person = iriOf(id);\n const take = boundedInteger(limit, 1, 200, \"limit\") + 1;\n\n return prefixed(`\nSELECT DISTINCT ?rank ?edition ?title ?reproduction ?ocr ?depiction WHERE {\n {\n ?work dcterms:creator ${person} .\n ?edition rdarel:workManifested ?work .\n OPTIONAL { ?edition dcterms:title ?title }\n { ?edition rdarel:electronicReproduction ?reproduction }\n UNION\n { ?edition bnf:OCR ?ocr }\n BIND(0 AS ?rank)\n }\n UNION\n {\n ${person} foaf:depiction ?depiction .\n BIND(${person} AS ?edition)\n BIND(1 AS ?rank)\n }\n}\nORDER BY ?rank\nLIMIT ${take}`);\n}\n\n/**\n * Whether one address is described at all, and what it is.\n *\n * Asked when a lookup came back with nothing, so an answer can tell \"the BnF\n * describes no such record\" from \"that record exists and is not a person\".\n */\nexport function kindQuery(id: EntityId): string {\n return prefixed(`\nSELECT DISTINCT ?type WHERE {\n ${iriOf(id)} a ?type .\n}\nLIMIT 20`);\n}\n\n/**\n * A query written for the live suite alone, naming one record by its label.\n *\n * It exists so the canary exercises `literal` against text a person typed,\n * rather than only against words the tokeniser produced.\n */\nexport function labelProbeQuery(label: string): string {\n return prefixed(`\nSELECT ?person WHERE {\n ?person foaf:name ${literal(label)} .\n}\nLIMIT 3`);\n}\n","/**\n * Turning result rows into the shapes the tools render.\n *\n * The rule the whole file follows: a statement the record does not carry comes\n * out as `null` or as an empty list, never as a plausible value. A person with\n * no recorded year of death is alive, or unresearched, or died in a year nobody\n * wrote down, and the three are different claims. Only the record decides.\n */\n\nimport { isGallicaAddress, publicPageFor } from \"./endpoint.js\";\nimport { MOST_TRIPLES, TEXT_WINDOW } from \"./queries.js\";\nimport type { SparqlResults, SparqlRow, SparqlTerm } from \"./http.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n AuthoredWork,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\n\nconst ARK_IN_IRI = /ark:\\/12148\\/(c[bc][0-9a-z]+)/i;\nconst TEMP_WORK_IN_IRI = /temp-work\\/([0-9a-f]{32})/i;\nconst TRAILING_SEPARATOR = /[/#]$/;\nconst PATH_SEPARATOR = /[/#]/;\nconst ARK_SEGMENT = /ark:\\/12148\\/([^/?#]+)/;\nconst QUERY_START = /[?#]/;\nconst LEADING_DOTS = /^[./]+/;\nconst TRAILING_SLASHES = /\\/+$/;\n\n/** Predicates read off a record. Named once, so a rename is one edit. */\nconst P = {\n type: \"http://www.w3.org/1999/02/22-rdf-syntax-ns#type\",\n label: \"http://www.w3.org/2000/01/rdf-schema#label\",\n seeAlso: \"http://www.w3.org/2000/01/rdf-schema#seeAlso\",\n sameAs: \"http://www.w3.org/2002/07/owl#sameAs\",\n exactMatch: \"http://www.w3.org/2004/02/skos/core#exactMatch\",\n closeMatch: \"http://www.w3.org/2004/02/skos/core#closeMatch\",\n prefLabel: \"http://www.w3.org/2004/02/skos/core#prefLabel\",\n altLabel: \"http://www.w3.org/2004/02/skos/core#altLabel\",\n created: \"http://purl.org/dc/terms/created\",\n modified: \"http://purl.org/dc/terms/modified\",\n title: \"http://purl.org/dc/terms/title\",\n date: \"http://purl.org/dc/terms/date\",\n creator: \"http://purl.org/dc/terms/creator\",\n language: \"http://purl.org/dc/terms/language\",\n subject: \"http://purl.org/dc/terms/subject\",\n name: \"http://xmlns.com/foaf/0.1/name\",\n givenName: \"http://xmlns.com/foaf/0.1/givenName\",\n familyName: \"http://xmlns.com/foaf/0.1/familyName\",\n depiction: \"http://xmlns.com/foaf/0.1/depiction\",\n birth: \"http://vocab.org/bio/0.1/birth\",\n death: \"http://vocab.org/bio/0.1/death\",\n firstYear: \"http://data.bnf.fr/ontology/bnf-onto/firstYear\",\n lastYear: \"http://data.bnf.fr/ontology/bnf-onto/lastYear\",\n subjectLabel: \"http://data.bnf.fr/ontology/bnf-onto/subject\",\n isni: \"http://isni.org/ontology#identifierValid\",\n placeOfBirth: \"http://rdvocab.info/ElementsGr2/placeOfBirth\",\n placeOfDeath: \"http://rdvocab.info/ElementsGr2/placeOfDeath\",\n biographicalInformation: \"http://rdvocab.info/ElementsGr2/biographicalInformation\",\n languageOfThePerson: \"http://rdvocab.info/ElementsGr2/languageOfThePerson\",\n countryAssociated: \"http://rdvocab.info/ElementsGr2/countryAssociatedWithThePerson\",\n fieldOfActivity: \"http://rdvocab.info/ElementsGr2/fieldOfActivityOfThePerson\",\n formOfWork: \"http://rdvocab.info/Elements/formOfWork\",\n statusOfIdentification: \"http://rdvocab.info/Elements/statusOfIdentification\",\n /** The occupation, which RDA numbers rather than names. */\n occupation: \"http://rdaregistry.info/Elements/a/#P50113\",\n /** Expressions of a work, which are not editions. */\n expressionOfWork: \"http://rdaregistry.info/Elements/w/#P10078\",\n} as const;\n\n/** Files whose addresses this dataset aligns records with. */\nconst ALIGNMENT_HOSTS: Array<{ host: string; name: string }> = [\n { host: \"viaf.org\", name: \"viaf\" },\n { host: \"idref.fr\", name: \"idref\" },\n { host: \"dbpedia.org\", name: \"dbpedia\" },\n { host: \"wikidata.org\", name: \"wikidata\" },\n { host: \"isni.org\", name: \"isni\" },\n { host: \"d-nb.info\", name: \"gnd\" },\n { host: \"id.loc.gov\", name: \"loc\" },\n { host: \"datos.bne.es\", name: \"bne\" },\n { host: \"wikipedia.org\", name: \"wikipedia\" },\n { host: \"musicbrainz.org\", name: \"musicbrainz\" },\n { host: \"francearchives.gouv.fr\", name: \"francearchives\" },\n { host: \"imslp.org\", name: \"imslp\" },\n { host: \"catalogue.bnf.fr\", name: \"catalogue\" },\n];\n\nconst text = (term: SparqlTerm | undefined): string | null => {\n if (!term) {\n return null;\n }\n const value = term.value.trim();\n return value === \"\" ? null : value;\n};\n\nconst integer = (term: SparqlTerm | undefined): number | null => {\n const raw = text(term);\n if (raw === null) {\n return null;\n }\n const value = Number(raw);\n return Number.isInteger(value) ? value : null;\n};\n\n/** The identifier a caller quotes, read back off the address of a record. */\nexport function idFromIri(iri: string): string | null {\n const ark = ARK_IN_IRI.exec(iri);\n if (ark?.[1]) {\n return ark[1].toLowerCase();\n }\n const temp = TEMP_WORK_IN_IRI.exec(iri);\n if (temp?.[1]) {\n return `temp-work/${temp[1].toLowerCase()}`;\n }\n return null;\n}\n\n/** The last segment of a vocabulary address, which is the term itself. */\nconst vocabularyTerm = (iri: string): string =>\n decodeURIComponent(iri.replace(TRAILING_SEPARATOR, \"\").split(PATH_SEPARATOR).pop() ?? iri);\n\n/**\n * A Gallica address turned into a link.\n *\n * Returns null for anything that is not a Gallica address, so a record pointing\n * somewhere else does not arrive labelled as a digitised BnF document. The\n * address is carried through as published: it is what a person will open.\n */\nexport function toDigitisedLink(\n address: string | null,\n role: DigitisedLink[\"role\"],\n fromId: string,\n fromTitle: string | null,\n): DigitisedLink | null {\n if (!(address && isGallicaAddress(address))) {\n return null;\n }\n const segment = ARK_SEGMENT.exec(address)?.[1] ?? null;\n if (!segment) {\n return null;\n }\n // An address can carry a rendering after the name: `.thumbnail` asks for a\n // small image of the document, `.item` for one leaf of it. An ARK name holds\n // no full stop, so what precedes the first one is the identifier and the rest\n // describes a view of it. Reporting the view as the identifier would give two\n // views of one document two identifiers.\n const ark = segment.split(\".\")[0] ?? segment;\n if (ark === \"\") {\n return null;\n }\n return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };\n}\n\n/**\n * The view an address asks for, read off what follows the ARK name in it.\n *\n * `bpt6k90000030` names a document; `bpt6k90000030.thumbnail` asks for a small\n * image of it and `btv1b90000002/f3.item.thumbnail` for one leaf of it as an\n * image. Carrying that as a field of its own is what keeps `url` and `ark` from\n * reading as two names for one thing: a caller told only the ARK would quote a\n * document while having been handed a picture of a page of it.\n */\nfunction renderingOf(address: string, ark: string): string | null {\n const path = address.split(QUERY_START)[0] ?? address;\n const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);\n const rendering = after.replace(LEADING_DOTS, \"\").replace(TRAILING_SLASHES, \"\");\n return rendering === \"\" ? null : rendering;\n}\n\n/**\n * A page, with the rows beyond it read as more existing.\n *\n * `hasMore` is true when the endpoint sent the extra row that was asked for, and\n * also when a row had to be dropped: a page that came back short because this\n * client could not read part of it is not a page with room to spare.\n */\nfunction page<T>(rows: T[], limit: number, skipped: number): Page<T> {\n const more = rows.length > limit;\n return skipped > 0\n ? { rows: rows.slice(0, limit), hasMore: more, skipped }\n : { rows: rows.slice(0, limit), hasMore: more };\n}\n\n/**\n * Whether the index window a text search reads came back full.\n *\n * The endpoint sends the occupancy on a row of its own, binding the count and\n * naming no entity. A count that reaches the size of the window means the\n * reading stopped where the window did rather than where the matches did.\n *\n * Null when no such row arrived, since a window nobody measured supports no\n * statement about what was left out.\n */\nfunction readWindowOccupancy(results: SparqlResults): boolean | null {\n for (const row of results.results.bindings) {\n const raw = text(row.windowRows);\n if (raw === null) {\n continue;\n }\n const count = Number(raw);\n if (Number.isInteger(count)) {\n return count >= TEXT_WINDOW;\n }\n }\n return null;\n}\n\n/**\n * True for the row carrying the occupancy and nothing else.\n *\n * It names no record, which is the shape of a row that had to be dropped. It is\n * neither: counting it as dropped would report the page as short of a record\n * the endpoint never claimed to send.\n */\nconst isOccupancyRow = (row: SparqlRow, entity: string): boolean =>\n row.windowRows !== undefined && row[entity] === undefined;\n\n/** Index a `?p ?o` result set by predicate, keeping every value. */\nexport function byPredicate(results: SparqlResults): Map<string, SparqlRow[]> {\n const index = new Map<string, SparqlRow[]>();\n for (const row of results.results.bindings) {\n const predicate = text(row.p);\n if (predicate === null) {\n continue;\n }\n const bucket = index.get(predicate);\n if (bucket) {\n bucket.push(row);\n } else {\n index.set(predicate, [row]);\n }\n }\n return index;\n}\n\nconst firstValue = (index: Map<string, SparqlRow[]>, predicate: string): string | null =>\n text(index.get(predicate)?.[0]?.o);\n\nconst allValues = (index: Map<string, SparqlRow[]>, predicate: string): string[] => {\n const seen = new Set<string>();\n for (const row of index.get(predicate) ?? []) {\n const value = text(row.o);\n if (value !== null) {\n seen.add(value);\n }\n }\n return [...seen];\n};\n\n/** Values that are addresses of vocabulary terms, reduced to the term. */\nconst allTerms = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.startsWith(\"http\"))\n .map(vocabularyTerm);\n\n/**\n * Dewey classes, and nothing else that happens to share the predicate.\n *\n * `dcterms:subject` on a work points at a Dewey class and at subject-heading\n * ARKs alike. Reducing an ARK to its last segment yields the string `about`,\n * which would then be published as a Dewey class and printed beside a real one.\n */\nconst deweyClasses = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate)\n .filter((value) => value.includes(\"dewey.info/class/\"))\n .map(vocabularyTerm);\n\n/** Values that are plain words rather than addresses. */\nconst allWords = (index: Map<string, SparqlRow[]>, predicate: string): string[] =>\n allValues(index, predicate).filter((value) => !value.startsWith(\"http\"));\n\n/**\n * Alignments to the same thing in other files, grouped by file.\n *\n * `owl:sameAs`, `skos:exactMatch` and `skos:closeMatch` are gathered together\n * because a caller wanting the VIAF address does not care which of the three\n * carried it, and the dataset uses all three for the same alignments. A\n * `closeMatch` is a weaker claim than the other two, and the record makes that\n * distinction nowhere a caller could act on it, so the grouping does not\n * pretend otherwise: the field is named for the addresses it holds.\n */\nfunction alignments(\n index: Map<string, SparqlRow[]>,\n selfId: string | null,\n): Record<string, string[]> {\n const found: Record<string, Set<string>> = {};\n for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {\n for (const address of allValues(index, predicate)) {\n if (!address.startsWith(\"http\")) {\n continue;\n }\n // An address pointing back at this record says nothing about anywhere\n // else, and the dataset carries several of those per record.\n if (selfId && address.includes(selfId)) {\n continue;\n }\n let host: string;\n try {\n host = new URL(address).hostname.toLowerCase();\n } catch {\n continue;\n }\n const known = ALIGNMENT_HOSTS.find(\n (entry) => host === entry.host || host.endsWith(`.${entry.host}`),\n );\n if (!known) {\n continue;\n }\n const addresses = found[known.name] ?? new Set<string>();\n found[known.name] = addresses;\n addresses.add(address);\n }\n }\n return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));\n}\n\n/** ISNI is written as a bare number rather than an address. */\nfunction isniAddresses(index: Map<string, SparqlRow[]>): string[] {\n return allValues(index, P.isni).map(\n (value) => `https://isni.org/isni/${value.replace(/\\s/g, \"\")}`,\n );\n}\n\n/** Rows of an author search, with the extra row read as \"more exist\". */\nexport function toAuthorSummaries(results: SparqlResults, limit: number): Page<AuthorSummary> {\n const rows: AuthorSummary[] = [];\n const seen = new Set<string>();\n let skipped = 0;\n\n for (const row of results.results.bindings) {\n if (isOccupancyRow(row, \"person\")) {\n continue;\n }\n const iri = text(row.person);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n if (seen.has(id)) {\n continue;\n }\n seen.add(id);\n rows.push({\n id,\n name: text(row.name),\n label: text(row.label),\n birthYear: integer(row.birthYear),\n deathYear: integer(row.deathYear),\n role: text(row.role),\n sourceUrl: publicPageFor(iri),\n });\n }\n\n return { ...page(rows, limit, skipped), indexWindowFull: readWindowOccupancy(results) };\n}\n\n/** One person, assembled from both halves of their record. */\nexport function toAuthorDetail(results: SparqlResults, id: string, pageUrl: string): AuthorDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n\n const otherNames: Array<{ label: string; language: string | null }> = [];\n const seenLabels = new Set<string>();\n for (const row of index.get(P.altLabel) ?? []) {\n const label = text(row.o);\n if (label === null || seenLabels.has(label)) {\n continue;\n }\n seenLabels.add(label);\n otherNames.push({ label, language: text(row.lang) });\n }\n\n const sameAs = alignments(index, id);\n const isni = isniAddresses(index);\n if (isni.length > 0) {\n sameAs.isni = [...new Set([...(sameAs.isni ?? []), ...isni])];\n }\n\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) {\n depictions.push(link);\n }\n }\n\n return {\n id,\n name: firstValue(index, P.name),\n label: firstValue(index, P.prefLabel),\n givenName: firstValue(index, P.givenName),\n familyName: firstValue(index, P.familyName),\n otherNames,\n birthDate: firstValue(index, P.birth),\n deathDate: firstValue(index, P.death),\n birthYear: integer(index.get(P.firstYear)?.[0]?.o),\n deathYear: integer(index.get(P.lastYear)?.[0]?.o),\n birthPlace: firstValue(index, P.placeOfBirth),\n deathPlace: firstValue(index, P.placeOfDeath),\n biographicalInformation: firstValue(index, P.biographicalInformation),\n occupation: firstValue(index, P.occupation),\n languages: allTerms(index, P.languageOfThePerson),\n countries: allTerms(index, P.countryAssociated),\n fields: allWords(index, P.fieldOfActivity),\n deweyClasses: deweyClasses(index, P.fieldOfActivity),\n sameAs,\n catalogueUrl,\n recordCreated: firstValue(index, P.created),\n recordModified: firstValue(index, P.modified),\n depictions,\n types,\n truncated: results.results.bindings.length >= MOST_TRIPLES,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * How settled a record is.\n *\n * The record states it in words, and the address states it in its shape: a\n * provisional work is addressed under `temp-work` rather than by an ARK. The\n * statement governs when there is one, since a cataloguer can settle a record\n * before it is re-addressed.\n */\nexport function readStatus(statement: string | null, iri: string): \"established\" | \"provisional\" {\n if (statement !== null) {\n const said = statement.toLowerCase();\n if (said.includes(\"provisional\")) {\n return \"provisional\";\n }\n if (said.includes(\"established\")) {\n return \"established\";\n }\n }\n return iri.includes(\"/temp-work/\") ? \"provisional\" : \"established\";\n}\n\n/** Rows of a work search, gathering the creators of each work. */\nexport function toWorkSummaries(results: SparqlResults, limit: number): Page<WorkSummary> {\n const order: string[] = [];\n const byId = new Map<string, WorkSummary>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n if (isOccupancyRow(row, \"work\")) {\n continue;\n }\n const iri = text(row.work);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let work = byId.get(id);\n if (!work) {\n work = {\n id,\n title: text(row.title),\n date: text(row.date),\n creators: [],\n status: readStatus(text(row.status), iri),\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, work);\n order.push(id);\n }\n\n const creatorIri = text(row.creator);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (creatorId && !work.creators.some((c) => c.id === creatorId)) {\n work.creators.push({ id: creatorId, name: text(row.creatorName) });\n }\n }\n\n return {\n ...page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n ),\n indexWindowFull: readWindowOccupancy(results),\n };\n}\n\n/**\n * The works one person is credited with, gathered from rows that repeat.\n *\n * A work pointing at two form terms arrives as two rows differing in one\n * column. Counting rows would report two works where the catalogue links one,\n * so the address of the work is what a work is counted by.\n */\nexport function toAuthoredWorks(results: SparqlResults, limit: number): Page<AuthoredWork> {\n const order: string[] = [];\n const byId = new Map<string, AuthoredWork>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.work);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let work = byId.get(id);\n if (!work) {\n work = {\n id,\n title: text(row.title),\n date: text(row.date),\n year: integer(row.year),\n forms: [],\n status: readStatus(text(row.status), iri),\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, work);\n order.push(id);\n }\n\n const form = text(row.form);\n if (form?.startsWith(\"http\")) {\n const term = vocabularyTerm(form);\n if (!work.forms.includes(term)) {\n work.forms.push(term);\n }\n }\n }\n\n return page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n );\n}\n\n/** One work, assembled from its record. */\nexport function toWorkDetail(\n results: SparqlResults,\n id: string,\n iri: string,\n pageUrl: string,\n): WorkDetail {\n const index = byPredicate(results);\n const types = allValues(index, P.type);\n const truncated = results.results.bindings.length >= MOST_TRIPLES;\n\n const creators: Array<{ id: string; name: string | null }> = [];\n for (const row of index.get(P.creator) ?? []) {\n const creatorIri = text(row.o);\n const creatorId = creatorIri ? idFromIri(creatorIri) : null;\n if (!creatorId || creators.some((c) => c.id === creatorId)) {\n continue;\n }\n creators.push({ id: creatorId, name: text(row.name) });\n }\n\n const depictions: DigitisedLink[] = [];\n for (const address of allValues(index, P.depiction)) {\n const link = toDigitisedLink(address, \"depiction\", id, null);\n if (link) {\n depictions.push(link);\n }\n }\n\n const statusStatement = firstValue(index, P.statusOfIdentification);\n const catalogueUrl =\n allValues(index, P.seeAlso).find((address) => address.includes(\"catalogue.bnf.fr\")) ?? null;\n\n return {\n id,\n title: firstValue(index, P.title),\n label: firstValue(index, P.label),\n date: firstValue(index, P.date),\n firstYear: integer(index.get(P.firstYear)?.[0]?.o),\n creators,\n languages: allTerms(index, P.language),\n forms: allTerms(index, P.formOfWork),\n subjects: allWords(index, P.subjectLabel),\n deweyClasses: deweyClasses(index, P.subject),\n status: readStatus(statusStatement, iri),\n statusStatement,\n // A cut record makes this the ceiling rather than the count, so it is\n // withheld rather than published as a number the record does not support.\n expressionCount: truncated ? null : allValues(index, P.expressionOfWork).length,\n sameAs: alignments(index, id),\n catalogueUrl,\n depictions,\n types,\n truncated,\n sourceUrl: pageUrl,\n };\n}\n\n/**\n * Editions, gathered from rows that repeat.\n *\n * One manifestation carrying two digitised copies arrives as two rows differing\n * in one column. Counting rows would report two editions where the record\n * describes one, so the address of the manifestation is what an edition is\n * counted by.\n */\nexport function toEditions(results: SparqlResults, limit: number): Page<Edition> {\n const order: string[] = [];\n const byId = new Map<string, Edition>();\n\n let skipped = 0;\n for (const row of results.results.bindings) {\n const iri = text(row.edition);\n const id = iri === null ? null : idFromIri(iri);\n if (iri === null || id === null) {\n // A row naming no record, or naming one at an address this client cannot\n // read, is a row that was dropped. Counting it is what keeps a short page\n // from reading as a small catalogue.\n skipped += 1;\n continue;\n }\n\n let edition = byId.get(id);\n if (!edition) {\n edition = {\n id,\n title: text(row.title),\n date: text(row.date),\n year: integer(row.year),\n publisher: text(row.publisher),\n place: text(row.place),\n editionStatement: text(row.editionStatement),\n extent: text(row.extent),\n isbn: text(row.isbn),\n note: text(row.note),\n catalogueUrl: text(row.catalogue),\n digitised: [],\n sourceUrl: publicPageFor(iri),\n };\n byId.set(id, edition);\n order.push(id);\n }\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, id, edition.title);\n if (link && !edition.digitised.some((existing) => existing.url === link.url)) {\n edition.digitised.push(link);\n }\n }\n }\n\n return page(\n order.map((id) => byId.get(id)).filter((one) => one !== undefined),\n limit,\n skipped,\n );\n}\n\n/**\n * Every digitised document a set of rows points at, kept in one list.\n *\n * One document can be reached by several paths, so the same address arrives\n * more than once and is kept once. That makes the length of the list a poor\n * signal of whether more exist: twenty-one rows holding seventeen addresses\n * would otherwise be reported as a page with room to spare. The endpoint was\n * asked for one row beyond the page, so the count of rows is what answers that.\n */\nexport function toDigitisedLinks(results: SparqlResults, limit: number): Page<DigitisedLink> {\n const links: DigitisedLink[] = [];\n const seen = new Set<string>();\n const rowCount = results.results.bindings.length;\n\n for (const row of results.results.bindings) {\n const fromIri = text(row.edition);\n const fromId = fromIri ? (idFromIri(fromIri) ?? fromIri) : \"\";\n const title = text(row.title);\n\n for (const [column, role] of [\n [\"reproduction\", \"reproduction\"],\n [\"ocr\", \"ocr\"],\n [\"depiction\", \"depiction\"],\n ] as const) {\n const link = toDigitisedLink(text(row[column]), role, fromId, title);\n if (!link || seen.has(link.url)) {\n continue;\n }\n seen.add(link.url);\n links.push(link);\n }\n }\n\n // `rowCount` counts what the endpoint sent, including rows whose address was\n // not a Gallica one and rows naming a document already listed. Raising the\n // limit after such a page returns the same list, so the claim is confined to\n // the case where the links themselves fill the page.\n return { rows: links.slice(0, limit), hasMore: links.length > limit || rowCount > limit };\n}\n\n/** The classes an address is typed with, reduced to their vocabulary terms. */\nexport function toTypes(results: SparqlResults): string[] {\n const types = new Set<string>();\n for (const row of results.results.bindings) {\n const iri = text(row.type);\n if (iri !== null) {\n types.add(iri);\n }\n }\n return [...types];\n}\n","/**\n * Keeps this client to one request at a time, spaced out.\n *\n * data.bnf.fr is a query service a public institution runs at its own cost, and\n * a SPARQL endpoint answers an expensive question per request rather than\n * serving a file. Serialising also means a burst of tool calls cannot turn into\n * a burst of concurrent queries on one planner.\n *\n * The spacing widens when the service pushes back and narrows again on a run of\n * quiet successes, so a slow patch does not become the permanent speed.\n */\n\nexport interface RateLimiterOptions {\n /** Spacing between requests when nothing has gone wrong. */\n intervalMs: number;\n /** The widest the spacing may become under push-back. */\n maxIntervalMs?: number;\n}\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n private lastStartedAt = 0;\n private queue: Promise<void> = Promise.resolve();\n private calmStreak = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = options.intervalMs;\n this.maxIntervalMs = options.maxIntervalMs ?? options.intervalMs * 8;\n this.intervalMs = options.intervalMs;\n }\n\n /** The spacing in force, which callers report rather than guess. */\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /**\n * Run `task` with nothing else in flight.\n *\n * This only serialises. Spacing is claimed per attempt through\n * `beforeRequest`, because a task that retries makes several requests and\n * each of them owes the service the same gap.\n */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const result = this.queue.then(task, task);\n this.queue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n\n /** Wait until enough time has passed since the previous request began. */\n async beforeRequest(): Promise<void> {\n const elapsed = Date.now() - this.lastStartedAt;\n // Capped at one whole interval, so a clock that moved backwards cannot\n // produce a wait longer than the spacing itself.\n const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (wait > 0) {\n await sleep(wait);\n }\n this.lastStartedAt = Date.now();\n }\n\n /** Called when the service asks for room: double the gap, up to the ceiling. */\n pushBack(): void {\n this.calmStreak = 0;\n this.intervalMs = Math.min(this.maxIntervalMs, this.intervalMs * 2);\n }\n\n /**\n * Called on a clean answer. Recovery takes several in a row, so one lucky\n * response after a rough patch does not undo the caution that earned it.\n */\n succeeded(): void {\n if (this.intervalMs === this.baseIntervalMs) {\n return;\n }\n this.calmStreak += 1;\n if (this.calmStreak < 3) {\n return;\n }\n this.calmStreak = 0;\n this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * The one place that talks to data.bnf.fr.\n *\n * It holds a single rate limiter and a single cache, so pacing applies to the\n * server as a whole rather than to whichever tool happens to be running. It\n * imports nothing from the MCP layer and is published on its own, so the same\n * code serves a plain script.\n *\n * Every read runs a query, parses it and only then stores: an answer nobody\n * could parse must not be served back for the rest of the cache's lifetime.\n *\n * Every read also carries the moment the metadata came off the endpoint. The\n * licence asks for the date of retrieval alongside the source, so that date is\n * a value the layer produces rather than something a caller is trusted to add.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n MAX_ALLOWED_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { notFound } from \"../errors.js\";\nimport type {\n AuthorDetail,\n AuthorSummary,\n AuthoredWork,\n DigitisedLink,\n Edition,\n Page,\n WorkDetail,\n WorkSummary,\n} from \"../types.js\";\nimport { REPO_URL } from \"../version.js\";\nimport { Cache } from \"./cache.js\";\nimport type { SparqlResults } from \"./http.js\";\nimport { runQuery } from \"./http.js\";\nimport {\n authorQuery,\n digitisedForPersonQuery,\n digitisedForWorkQuery,\n editionsQuery,\n kindQuery,\n searchAuthorsQuery,\n searchWorksQuery,\n workQuery,\n worksByCreatorQuery,\n} from \"./queries.js\";\nimport {\n toAuthorDetail,\n toAuthorSummaries,\n toAuthoredWorks,\n toDigitisedLinks,\n toEditions,\n toTypes,\n toWorkDetail,\n toWorkSummaries,\n} from \"./parse.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport type { EntityId } from \"./sparql.js\";\nimport { toSearchWords } from \"./sparql.js\";\n\n/** The class data.bnf.fr types a person with. */\nconst PERSON_CLASS = \"http://xmlns.com/foaf/0.1/Person\";\n\n/**\n * The classes data.bnf.fr types a work with.\n *\n * Two vocabularies say the same thing here, and a record carries both. Either\n * one is enough, because a record carrying one has always carried the other.\n */\nconst WORK_CLASSES = new Set([\n \"http://rdvocab.info/uri/schema/FRBRentitiesRDA/Work\",\n \"http://rdaregistry.info/Elements/c/#C10001\",\n]);\n\nexport interface ClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/** Every read reports where the value came from and when it was retrieved. */\nexport interface Read<T> {\n data: T;\n cached: boolean;\n /**\n * When the metadata came off data.bnf.fr, as an ISO 8601 instant.\n *\n * A cached answer keeps the moment it was first read. Reporting the moment it\n * was served would state a date of retrieval that never happened, which is\n * exactly the claim the licence asks to be accurate.\n */\n retrievedAt: string;\n /** Rows the endpoint sent that could not be read, which paging still counts. */\n skipped?: number;\n}\n\n/**\n * What this server owes the BnF, applied to whatever it is handed.\n *\n * A configuration object assembled by a caller has not been through\n * `loadConfig`, so it can carry a missing value, a value of the wrong shape, or\n * a User-Agent that names somebody else. Every bound the environment path\n * enforces is enforced here too: the published `./client` entry point is the\n * one a program uses, and it would be a strange guarantee that held only for\n * the executable.\n *\n * Three of these bounds are about what one call can cost the service. A retry\n * budget of two hundred turns a single lookup into two hundred requests against\n * an endpoint that is already failing. A deadline of an hour holds the one\n * request slot for an hour. A cache of zero entries evicts each value as it is\n * written, which switches off the caching that the three-second floor is set\n * against.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\n\n /**\n * A setting that is absent or unreadable falls back, and one outside its\n * range is brought inside it. Refusing outright is what the environment path\n * does, because a person reads stderr; a program handed an object does not.\n */\n const bounded = (value: unknown, fallback: number, min: number, max: number): number => {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n return fallback;\n }\n return Math.min(max, Math.max(min, parsed));\n };\n\n const claimed = typeof config.userAgent === \"string\" ? config.userAgent.trim() : \"\";\n const identifier = defaults.userAgent;\n\n return {\n ...config,\n // A caller may say who they are. Appending rather than replacing means the\n // BnF can always tell which software is calling, and reach someone.\n userAgent:\n claimed === \"\" || claimed.includes(REPO_URL) ? identifier : `${claimed} ${identifier}`,\n minIntervalMs: bounded(\n config.minIntervalMs,\n defaults.minIntervalMs,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: bounded(config.timeoutMs, defaults.timeoutMs, 1000, 300_000),\n maxRetries: bounded(config.maxRetries, defaults.maxRetries, 0, 8),\n cacheTtlMs: bounded(config.cacheTtlMs, defaults.cacheTtlMs, 0, 86_400_000),\n cacheMaxEntries: bounded(config.cacheMaxEntries, defaults.cacheMaxEntries, 1, 5000),\n };\n}\n\nexport class BnfClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: Cache<unknown>;\n /** Questions already on the wire, so an identical one waits instead of asking. */\n private readonly inFlight = new Map<string, Promise<Read<unknown>>>();\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: ClientOptions = {}) {\n const base = { ...loadConfig(), ...options.config };\n this.config = withGuarantees(base);\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ intervalMs: this.config.minIntervalMs });\n this.cache = new Cache(this.config.cacheTtlMs, this.config.cacheMaxEntries);\n this.fetchImpl = options.fetchImpl;\n }\n\n /** The pacing in force, which widens when the endpoint pushes back. */\n get intervalMs(): number {\n return this.limiter.currentIntervalMs;\n }\n\n /** What the BnF sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n /** The deadline one query is given, which callers report rather than guess. */\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n /** The one address this client sends anything to. */\n /**\n * `cacheKey` is stated rather than taken from the query, because two reads of\n * one query can produce different values: a list cut to five rows is not the\n * same value as the same list cut to twenty, and serving one for the other\n * would answer a size the caller never asked for.\n */\n private read<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const hit = this.cache.get(cacheKey) as { value: T; retrievedAt: number } | undefined;\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${cacheKey}`);\n return Promise.resolve({\n data: hit.value,\n cached: true,\n retrievedAt: new Date(hit.retrievedAt).toISOString(),\n });\n }\n\n // Two callers asking one question at the same moment both miss the cache,\n // because the first has not answered yet. Sharing the flight means the BnF\n // is asked once, which is the point of the cache in the first place.\n const flying = this.inFlight.get(cacheKey);\n if (flying !== undefined) {\n this.logger.debug(`joining a request already in flight for ${cacheKey}`);\n return flying as Promise<Read<T>>;\n }\n\n const flight = this.fetchAndParse(cacheKey, query, parse);\n this.inFlight.set(cacheKey, flight as Promise<Read<unknown>>);\n // A failure is never remembered, so the next caller asks again rather than\n // being handed the failure of somebody else's request.\n return flight.finally(() => this.inFlight.delete(cacheKey));\n }\n\n private async fetchAndParse<T>(\n cacheKey: string,\n query: string,\n parse: (results: SparqlResults) => T,\n ): Promise<Read<T>> {\n const results = await this.limiter.schedule(() =>\n runQuery({\n query,\n userAgent: this.config.userAgent,\n timeoutMs: this.config.timeoutMs,\n maxRetries: this.config.maxRetries,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n }),\n );\n\n // The moment is taken once the answer is in hand, so it names when the\n // metadata was read rather than when the request left.\n const retrievedAt = Date.now();\n const data = parse(results);\n this.cache.set(cacheKey, data, retrievedAt);\n return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };\n }\n\n /** Read an identifier a caller wrote, refusing anything this dataset cannot address. */\n /** The words a search will actually look for, which a tool reports back. */\n searchAuthors(name: string, limit: number, offset: number): Promise<Read<Page<AuthorSummary>>> {\n const words = toSearchWords(name);\n const query = searchAuthorsQuery(words, limit, offset);\n return this.read(`authors:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toAuthorSummaries(results, limit),\n );\n }\n\n async getAuthor(id: EntityId): Promise<Read<AuthorDetail>> {\n const result = await this.read(`author:${id.id}`, authorQuery(id), (results) =>\n toAuthorDetail(results, id.id, id.pageUrl),\n );\n\n // The record decides what it is, and a name is not the test: a subject\n // heading and a corporate body both carry one. Answering with a person\n // record for either would put a date of birth beside something that was\n // never born, and report its absence as a fact about a life.\n if (!result.data.types.includes(PERSON_CLASS)) {\n await this.refuse(id, \"a person\", \"get_work reads a work.\");\n }\n return result;\n }\n\n /**\n * Say what an address is, when it is not the kind of thing that was asked for.\n *\n * A record the catalogue does not describe at all and a record describing\n * something else are two different answers, and only one of them means the\n * caller should go looking elsewhere.\n */\n private async refuse(id: EntityId, wanted: string, hint: string): Promise<never> {\n // A detail query gathers the classes of everything it walks through, so the\n // list it leaves behind is longer on one tool than on another and one\n // record would be described differently depending on which refused it. What\n // the address itself answers is one list, and it is the one every refusal\n // states, whatever the caller was reading at the time.\n const known = (await this.read(`types:${id.id}`, kindQuery(id), toTypes)).data;\n\n if (known.length === 0) {\n throw notFound(`data.bnf.fr describes nothing at \"${id.id}\".`, {\n hint: \"Find the identifier with search_authors or search_works rather than writing one.\",\n url: id.pageUrl,\n });\n }\n throw notFound(\n `\"${id.id}\" is described by data.bnf.fr, and it is not ${wanted}: it is typed ${known.join(\", \")}.`,\n { hint, url: id.pageUrl },\n );\n }\n\n searchWorks(title: string, limit: number, offset: number): Promise<Read<Page<WorkSummary>>> {\n const words = toSearchWords(title);\n const query = searchWorksQuery(words, limit, offset);\n return this.read(`works:${words.join(\"+\")}:${limit}:${offset}`, query, (results) =>\n toWorkSummaries(results, limit),\n );\n }\n\n async getWork(id: EntityId): Promise<Read<WorkDetail>> {\n const result = await this.read(`work:${id.id}`, workQuery(id), (results) =>\n toWorkDetail(results, id.id, id.iri, id.pageUrl),\n );\n\n // An edition carries a title too, and its ARK names a manifestation rather\n // than a work. Accepting one here would hand back an identifier described\n // as the work's, which is the identifier a citation would then carry.\n if (!result.data.types.some((type) => WORK_CLASSES.has(type))) {\n await this.refuse(\n id,\n \"a work\",\n \"get_author reads a person, and list_editions reads the editions of a work.\",\n );\n }\n return result;\n }\n\n /**\n * The works one person is named the creator of.\n *\n * The record is not checked here. A person the catalogue credits with nothing\n * and an address that names no person both answer with no rows, and telling\n * them apart costs a second query that only an empty answer needs.\n */\n worksByAuthor(id: EntityId, limit: number, offset: number): Promise<Read<Page<AuthoredWork>>> {\n return this.read(\n `author-works:${id.id}:${limit}:${offset}`,\n worksByCreatorQuery(id, limit, offset),\n (results) => toAuthoredWorks(results, limit),\n );\n }\n\n listEditions(id: EntityId, limit: number, offset: number): Promise<Read<Page<Edition>>> {\n const query = editionsQuery(id, limit, offset);\n return this.read(`editions:${id.id}:${limit}:${offset}`, query, (results) =>\n toEditions(results, limit),\n );\n }\n\n /** Digitised documents attached to a work and to its editions. */\n digitisedForWork(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-work:${id.id}:${limit}`,\n digitisedForWorkQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** Digitised documents attached to a person and to the editions of their works. */\n digitisedForPerson(id: EntityId, limit: number): Promise<Read<Page<DigitisedLink>>> {\n return this.read(\n `digitised-person:${id.id}:${limit}`,\n digitisedForPersonQuery(id, limit),\n (results) => toDigitisedLinks(results, limit),\n );\n }\n\n /** What an address is typed as, which tells a person from a work. */\n types(id: EntityId): Promise<Read<string[]>> {\n return this.read(`types:${id.id}`, kindQuery(id), toTypes);\n }\n}\n\n/**\n * What a caller needs beside the client itself.\n *\n * Every route that reads one record takes an `EntityId` rather than a string,\n * because an identifier built from a guess sends the call to a record that does\n * not exist and the answer reads as an absence. A program importing this entry\n * point therefore has to be able to build one, so the reading of an address is\n * published alongside the client that consumes it.\n */\nexport { parseEntityId } from \"./sparql.js\";\nexport type { EntityId, EntityKind } from \"./sparql.js\";\n"],"mappings":";AASA,OAAO,aAAa;;;ACLb,IAAM,cAAc;AACpB,IAAM,WAAW;;;ADOjB,IAAM,aAAa,CAAC,UAAU,SAAS,QAAQ,OAAO;AAkBtD,IAAM,0BAA0B;AAEhC,IAAM,0BAA0B;AAEhC,IAAM,sBAAsB;AAW5B,IAAM,qBAAqB;AAY3B,IAAM,qBAAqB,eAAe,WAAW,MAAM,QAAQ;AASnE,SAAS,aAAa,OAAyB;AACpD,QAAM,OAAO,WAAW,QAAQ,KAAK;AACrC,QAAM,QAAQ,CAAC,IAAc,YAAoB;AAC/C,QAAI,SAAS,KAAK,WAAW,QAAQ,EAAE,IAAI,MAAM;AAC/C;AAAA,IACF;AACA,YAAQ,OAAO,MAAM,iBAAiB,EAAE,KAAK,OAAO;AAAA,CAAI;AAAA,EAC1D;AACA,SAAO;AAAA,IACL,OAAO,CAAC,MAAM,MAAM,SAAS,CAAC;AAAA,IAC9B,MAAM,CAAC,MAAM,MAAM,QAAQ,CAAC;AAAA;AAAA;AAAA,IAG5B,MAAM,CAAC,MAAM,MAAM,SAAS,CAAC;AAAA,IAC7B,OAAO,CAAC,MAAM,MAAM,SAAS,CAAC;AAAA,EAChC;AACF;AAEA,SAAS,YACP,KACA,MACA,UACA,KACA,KACQ;AACR,QAAM,MAAM,IAAI,IAAI;AACpB,MAAI,QAAQ,UAAa,IAAI,KAAK,MAAM,IAAI;AAC1C,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,OAAO,GAAG;AACxB,MAAI,EAAE,OAAO,SAAS,KAAK,KAAK,OAAO,UAAU,KAAK,IAAI;AACxD,YAAQ,OAAO;AAAA,MACb,wBAAwB,IAAI,KAAK,GAAG,kCAAkC,QAAQ;AAAA;AAAA,IAChF;AACA,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,OAAO,QAAQ,KAAK;AAG9B,YAAQ,OAAO;AAAA,MACb,wBAAwB,IAAI,IAAI,KAAK,eAAe,GAAG,KAAK,GAAG,WAAW,QAAQ;AAAA;AAAA,IACpF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,WAAW,MAAyB,QAAQ,KAAa;AACvE,QAAM,QAAQ,IAAI;AAClB,QAAM,WAAW,SAAS,WAAW,SAAS,KAAK,IAAI,QAAQ;AAC/D,MAAI,SAAS,CAAC,WAAW,SAAS,KAAK,GAAG;AACxC,YAAQ,OAAO;AAAA,MACb,uCAAuC,KAAK,mBAAmB,WAAW,KAAK,IAAI,CAAC;AAAA;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,gBAAgB,KAAK;AAExC,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,WAAW,SAAS,GAAG,MAAM,IAAI,kBAAkB,KAAK;AAAA,IACxD,eAAe;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,WAAW,YAAY,KAAK,kBAAkB,oBAAoB,KAAM,GAAO;AAAA,IAC/E,YAAY,YAAY,KAAK,mBAAmB,GAAG,GAAG,CAAC;AAAA,IACvD,YAAY,YAAY,KAAK,oBAAoB,KAAS,GAAG,KAAU;AAAA,IACvE,iBAAiB,YAAY,KAAK,yBAAyB,KAAK,GAAG,GAAI;AAAA,IACvE;AAAA,EACF;AACF;;;AE9GO,IAAM,WAAN,cAAuB,MAAM;AAAA,EACzB;AAAA,EACA;AAAA,EAET,YAAY,MAAiB,SAAiB,UAAwB,CAAC,GAAG;AACxE,UAAM,SAAS,QAAQ,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;AACjF,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;AAEO,IAAM,WAAW,CAAC,SAAiB,YACxC,IAAI,SAAS,aAAa,SAAS,OAAO;AAErC,IAAM,eAAe,CAAC,SAAiB,SAC5C,IAAI,SAAS,iBAAiB,SAAS,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC;AAEtD,IAAM,cAAc,CAAC,SAAiB,YAC3C,IAAI,SAAS,gBAAgB,SAAS;AAAA,EACpC,MAAM;AAAA,EACN,GAAG;AACL,CAAC;AAEI,IAAM,eAAe,CAAC,SAAiB,YAC5C,IAAI,SAAS,iBAAiB,SAAS;AAAA,EACrC,MAAM;AAAA,EACN,GAAG;AACL,CAAC;AAEI,IAAM,eAAe,CAAC,SAAiB,YAC5C,IAAI,SAAS,iBAAiB,SAAS,OAAO;AAEzC,IAAM,UAAU,CAAC,SAAiB,YACvC,IAAI,SAAS,WAAW,SAAS,OAAO;;;ACtCnC,IAAM,QAAN,MAAe;AAAA,EAGpB,YACmB,OACA,YACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAJF,UAAU,oBAAI,IAAsB;AAAA,EAOrD,IAAI,KAAiC;AACnC,UAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;AAClC,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AACA,QAAI,MAAM,aAAa,KAAK,IAAI,GAAG;AACjC,WAAK,QAAQ,OAAO,GAAG;AACvB,aAAO;AAAA,IACT;AAGA,SAAK,QAAQ,OAAO,GAAG;AACvB,SAAK,QAAQ,IAAI,KAAK,KAAK;AAC3B,WAAO,EAAE,OAAO,MAAM,OAAO,aAAa,MAAM,YAAY;AAAA,EAC9D;AAAA,EAEA,IAAI,KAAa,OAAU,aAA2B;AAGpD,QAAI,KAAK,SAAS,GAAG;AACnB;AAAA,IACF;AACA,SAAK,QAAQ,OAAO,GAAG;AACvB,SAAK,QAAQ,IAAI,KAAK,EAAE,OAAO,WAAW,KAAK,IAAI,IAAI,KAAK,OAAO,YAAY,CAAC;AAChF,WAAO,KAAK,QAAQ,OAAO,KAAK,YAAY;AAC1C,YAAM,SAAS,KAAK,QAAQ,KAAK,EAAE,KAAK;AACxC,UAAI,OAAO,MAAM;AACf;AAAA,MACF;AACA,WAAK,QAAQ,OAAO,OAAO,KAAK;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AACF;;;AC3DA,IAAM,eAAe;AACrB,IAAM,WAAW;AACjB,IAAM,cAAc;AAGb,IAAM,kBAAkB;AAGxB,IAAM,eAAe;AAiBrB,IAAM,eAAe;AASrB,SAAS,kBAAkB,KAAmB;AACnD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,SAAS,OAAO;AACd,UAAM,QAAQ,mCAAmC,GAAG,4BAA4B,KAAK;AAAA,EACvF;AACA,QAAM,OAAO,OAAO,SAAS,YAAY,EAAE,QAAQ,cAAc,EAAE;AACnE,MAAI,SAAS,cAAc;AACzB,UAAM;AAAA,MACJ,wBAAwB,YAAY,uBAAuB,IAAI;AAAA,IAEjE;AAAA,EACF;AACF;AASA,IAAM,UAAU,CAAC,SAAiB,UAChC,IAAI,SAAS,iBAAiB,SAAS;AAAA,EACrC,MAAM;AAAA,EACN;AACF,CAAC;AAGI,SAAS,iBAAiB,SAA0B;AACzD,MAAI;AAIF,UAAM,OAAO,IAAI,IAAI,OAAO,EAAE,SAAS,YAAY,EAAE,QAAQ,cAAc,EAAE;AAC7E,WAAO,SAAS,gBAAgB,KAAK,SAAS,IAAI,YAAY,EAAE;AAAA,EAClE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAEN,OAAO;AAAA;AAAA,EAEP,MAAM;AAAA;AAAA,EAEN,QAAQ;AAAA;AAAA,EAER,SAAS;AACX;AAEO,IAAM,eAAe,OAAO,QAAQ,QAAQ,EAChD,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,UAAU,IAAI,MAAM,GAAG,GAAG,EAC/C,KAAK,IAAI;AAOL,IAAM,gBAAgB,CAAC,cAC5B,UAAU,QAAQ,UAAU,EAAE,EAAE,QAAQ,aAAa,QAAQ;;;ACjG/D,IAAM,iBAAiB;AA4BvB,IAAM,YAAY,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AAQ7D,IAAM,YAAY,oBAAI,IAAI,CAAC,KAAK,GAAG,CAAC;AAUpC,IAAM,kBAAkB;AASxB,IAAM,wBAAwB;AAS9B,IAAM,gBAAgB;AAMf,SAAS,gBAAgB,OAAsB,MAAM,KAAK,IAAI,GAAkB;AACrF,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,UAAU,MAAM,KAAK;AAE3B,QAAM,UAAU,OAAO,OAAO;AAC9B,MAAI,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC5C,WAAO,KAAK,MAAM,UAAU,GAAI;AAAA,EAClC;AAEA,QAAM,KAAK,KAAK,MAAM,OAAO;AAC7B,MAAI,OAAO,MAAM,EAAE,GAAG;AACpB,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,KAAK,GAAG;AAC7B;AAGA,SAAS,UAAU,SAAyB;AAC1C,QAAM,OAAO,KAAK,IAAI,MAAQ,MAAO,KAAK,OAAO;AACjD,SAAO,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAC9C;AAWO,SAAS,gBACd,MACsD;AACtD,QAAM,QAAQ,eAAe,KAAK,KAAK,KAAK,CAAC;AAC7C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,MAAM,CAAC,KAAK;AAC1B,QAAMA,SAAQ,MAAM,CAAC,KAAK,IAAI,MAAM,IAAI,EAAE,CAAC,GAAG,KAAK,KAAK;AAExD,SAAO,EAAE,MAAM,UAAU,UAAU,YAAY,WAAW,MAAAA,MAAK;AACjE;AAEA,eAAe,SACb,SACA,QAC0E;AAC1E,oBAAkB,eAAe;AACjC,QAAM,UAAU,QAAQ,aAAa;AAErC,QAAM,OAAO,IAAI,gBAAgB;AACjC,OAAK,IAAI,SAAS,QAAQ,KAAK;AAG/B,OAAK,IAAI,UAAU,iCAAiC;AAMpD,MAAI,SAAS;AACb,WAAS,MAAM,GAAG,OAAO,eAAe,OAAO,GAAG;AAChD,UAAM,WAAW,MAAM,QAAQ,QAAQ;AAAA,MACrC,QAAQ;AAAA,MACR;AAAA,MACA,UAAU;AAAA,MACV,SAAS;AAAA,QACP,cAAc,QAAQ;AAAA,QACtB,QAAQ;AAAA,QACR,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,SAAS;AAAA,IACtB,CAAC;AAED,UAAM,WAAW,SAAS,QAAQ,IAAI,UAAU;AAChD,QAAI,SAAS,UAAU,OAAO,SAAS,SAAS,OAAO,aAAa,MAAM;AACxE,YAAM,OAAO,IAAI,IAAI,UAAU,MAAM,EAAE,SAAS;AAGhD,wBAAkB,IAAI;AACtB,eAAS;AACT;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ,SAAS;AAAA,MACjB,SAAS,SAAS;AAAA,MAClB,MAAM,MAAM,SAAS,KAAK;AAAA,MAC1B,IAAI,SAAS;AAAA,IACf;AAAA,EACF;AAEA,QAAM,aAAa,iDAAiD,aAAa,WAAW;AAAA,IAC1F,KAAK;AAAA,EACP,CAAC;AACH;AAgBA,SAAS,YACP,UACA,SACA,YACS;AACT,QAAM,WAAW,UAAU,IAAI,SAAS,MAAM;AAE9C,MAAI,UAAU,IAAI,SAAS,MAAM,GAAG;AAClC,UAAM,QAAQ,gBAAgB,SAAS,QAAQ,IAAI,aAAa,CAAC;AAEjE,QAAI,UAAU,QAAQ,QAAQ,iBAAiB;AAC7C,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA,OAAO;AAAA,UACL,yCAAyC,KAAK,MAAM,QAAQ,GAAI,CAAC,kBAAkB,SAAS,MAAM;AAAA,UAClG,EAAE,KAAK,iBAAiB,QAAQ,SAAS,OAAO;AAAA,QAClD;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,YAAY;AACzB,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA,OAAO;AAAA,UACL,oDAAoD,SAAS,MAAM;AAAA,UACnE;AAAA,YACE,KAAK;AAAA,YACL,QAAQ,SAAS;AAAA,UACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,WAAO,EAAE,MAAM,SAAS,UAAU,QAAQ,SAAS,UAAU,OAAO,EAAE;AAAA,EACxE;AAEA,MAAI,UAAU,IAAI,SAAS,MAAM,KAAK,UAAU,YAAY;AAC1D,WAAO,EAAE,MAAM,SAAS,UAAU,QAAQ,UAAU,OAAO,EAAE;AAAA,EAC/D;AAEA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,UAAM,SAAS,gBAAgB,SAAS,IAAI;AAC5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,OAAO;AAAA,QACL,uCAAuC,QAAQ,OAAO,KAAK,OAAO,IAAI,KAAK,GAAG;AAAA,QAC9E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,OAAO,aAAa,6BAA6B,SAAS,MAAM,KAAK;AAAA,MACnE,KAAK;AAAA,MACL,QAAQ,SAAS;AAAA,IACnB,CAAC;AAAA,EACH;AACF;AAUA,SAAS,YACP,OACA,UACO;AACP,QAAM,EAAE,SAAS,YAAY,UAAU,IAAI;AAE3C,MAAI,iBAAiB,SAAS,MAAM,SAAS,YAAY;AACvD,UAAM;AAAA,EACR;AAEA,MAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,QAAI,WAAW,KAAK,IAAI,YAAY,qBAAqB,GAAG;AAC1D,YAAM;AAAA,QACJ,qCAAqC,SAAS;AAAA,QAC9C,EAAE,KAAK,gBAAgB;AAAA,MACzB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACxE,MAAI,WAAW,YAAY;AACzB,UAAM,aAAa,gCAAgC,QAAQ,OAAO,IAAI,EAAE,KAAK,gBAAgB,CAAC;AAAA,EAChG;AACA,SAAO;AACT;AAGA,eAAsB,SAAS,SAA+C;AAC5E,QAAM,EAAE,WAAW,YAAY,SAAS,OAAO,IAAI;AAEnD,MAAI,YAA0B;AAE9B,MAAI,cAAc;AAElB,WAAS,UAAU,GAAG,WAAW,YAAY,WAAW,GAAG;AACzD,QAAI,cAAc,GAAG;AACnB,aAAO,MAAM,WAAW,WAAW,cAAc;AAGjD,YAAM,QAAQ;AACd,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AACzD,oBAAc;AAAA,IAChB;AACA,UAAM,QAAQ,cAAc;AAE5B,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,WAAW,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAE/D,QAAI;AACF,aAAO,MAAM,QAAQ,eAAe,KAAK,QAAQ,MAAM,MAAM,cAAc;AAC3E,YAAM,WAAW,MAAM,SAAS,SAAS,WAAW,MAAM;AAE1D,UAAI,SAAS,IAAI;AAKf,YAAI;AACJ,YAAI;AACF,mBAAS,aAAa,SAAS,IAAI;AAAA,QACrC,SAAS,OAAO;AACd,kBAAQ,SAAS;AACjB,gBAAM;AAAA,QACR;AACA,gBAAQ,UAAU;AAClB,eAAO;AAAA,MACT;AAEA,YAAM,UAAU,YAAY,UAAU,SAAS,UAAU;AACzD,UAAI,QAAQ,UAAU;AACpB,gBAAQ,SAAS;AAAA,MACnB;AACA,UAAI,QAAQ,SAAS,WAAW;AAC9B,cAAM,QAAQ;AAAA,MAChB;AACA,oBAAc,QAAQ;AACtB,kBAAY,IAAI,MAAM,QAAQ,SAAS,MAAM,EAAE;AAAA,IACjD,SAAS,OAAO;AACd,mBAAa,QAAQ;AAErB,kBAAY,YAAY,OAAO,EAAE,SAAS,YAAY,UAAU,CAAC;AACjE,oBAAc,UAAU,OAAO;AAAA,IACjC,UAAE;AACA,mBAAa,QAAQ;AAAA,IACvB;AAAA,EACF;AAEA,QAAM;AAAA,IACJ,gCAAgC,WAAW,WAAW,qBAAqB;AAAA,IAC3E;AAAA,MACE,KAAK;AAAA,IACP;AAAA,EACF;AACF;AAWO,SAAS,aAAa,MAA6B;AACxD,MAAI,KAAK,KAAK,MAAM,IAAI;AACtB,UAAM;AAAA,MACJ;AAAA,MACA,EAAE,KAAK,gBAAgB;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,SAAS,gBAAgB,IAAI;AACnC,MAAI,QAAQ;AACV,QAAI,OAAO,SAAS,WAAW;AAC7B,YAAM;AAAA,QACJ,0CAA0C,OAAO,IAAI;AAAA,QACrD;AAAA,MACF;AAAA,IACF;AACA,UAAM,QAAa,kDAAkD,OAAO,IAAI,IAAI;AAAA,MAClF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,IAAI;AAAA,EAC3B,SAAS,OAAO;AACd,UAAM,aAAa,yDAAyD;AAAA,MAC1E,KAAK;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MACE,OAAO,YAAY,YACnB,YAAY,QACZ,EAAE,aAAa,YACf,OAAQ,QAA0B,YAAY,YAC9C,CAAC,MAAM,QAAS,QAA0B,SAAS,QAAQ,GAC3D;AACA,UAAM,aAAa,mEAAmE;AAAA,MACpF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;ACjZA,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AAEzB,IAAM,eAAe;AACrB,IAAM,eAAe;AACrB,IAAM,oBAAoB;AAC1B,IAAM,qBAAqB;AAC3B,IAAM,cAAc;AAWb,SAAS,cAAc,OAAuB;AACnD,SACE,MACG,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK,EAKpB,QAAQ,oDAAoD,EAAE;AAErE;AAGO,IAAM,UAAU,CAAC,UAA0B,IAAI,cAAc,KAAK,CAAC;AAc1E,IAAM,YACJ;AAUK,SAAS,kBAAkB,OAAuB;AACvD,SAAO,MAAM,QAAQ,WAAW,EAAE;AACpC;AA2EO,SAAS,cAAc,OAAyB;AACrD,QAAM,UAAU,kBAAkB,KAAK,EACpC,UAAU,KAAK,EAIf,QAAQ,WAAW,GAAG,EACtB,QAAQ,sBAAsB,GAAG;AAEpC,QAAM,QAAkB,CAAC;AACzB,aAAW,OAAO,QAAQ,MAAM,GAAG,GAAG;AAEpC,UAAM,OAAO,IAAI,QAAQ,iBAAiB,EAAE,EAAE,QAAQ,kBAAkB,EAAE;AAC1E,QAAI,SAAS,IAAI;AACf;AAAA,IACF;AAGA,UAAM,KAAK,KAAK,SAAS,KAAK,KAAK,MAAM,GAAG,EAAE,IAAI,IAAI;AACtD,QAAI,MAAM,UAAU,IAAI;AACtB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAyCO,SAAS,iBAAiB,OAAkC;AACjE,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,MAAM,IAAI,CAAC,SAAS,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,OAAO;AAChF,SAAO,QAAQ,MAAM;AACvB;AAUA,IAAM,SAAS;AACf,IAAM,eAAe;AAuBd,SAAS,cAAc,OAAyB;AACrD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,IAAI;AAClB,UAAM,aAAa,4BAA4B;AAAA,EACjD;AAEA,QAAM,WAAW,aAAa,KAAK,OAAO;AAC1C,QAAM,OAAO,WACT,SAAS,CAAC,IACV,QAAQ,QAAQ,cAAc,EAAE,EAAE,MAAM,iBAAiB,EAAE,CAAC;AAChE,QAAM,aAAa,QAAQ,IAAI,YAAY;AAE3C,MAAI,OAAO,KAAK,SAAS,GAAG;AAC1B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,KAAK,iCAAiC,SAAS;AAAA,MAC/C,SAAS,kCAAkC,SAAS;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,YAAY,mBAAmB,KAAK,OAAO;AACjD,QAAM,UAAU,YAAY,CAAC,KAAK,WAAW,YAAY;AACzD,MAAI,aAAa,KAAK,MAAM,GAAG;AAC7B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,IAAI,aAAa,MAAM;AAAA,MACvB,KAAK,gCAAgC,MAAM;AAAA,MAC3C,SAAS,iCAAiC,MAAM;AAAA,IAClD;AAAA,EACF;AAEA,QAAM,aAAa,IAAI,KAAK,sCAAsC,QAAQ,OAAO,CAAC;AACpF;AAUA,IAAM,mBAAmB;AAEzB,IAAM,eAAe;AAErB,IAAM,eACJ;AAEF,SAAS,QAAQ,OAAuB;AACtC,QAAMC,QAAO,aAAa,KAAK,KAAK;AACpC,MAAIA,OAAM;AACR,UAAM,QAAQA,MAAK,CAAC,KAAK,IAAI,QAAQ,MAAM,GAAG;AAC9C,WACE,2DAA2DA,MAAK,CAAC,CAAC,gGACnD,IAAI;AAAA,EAEvB;AAEA,QAAM,aAAa,YAAY,KAAK,KAAK,IAAI,CAAC,KAAK;AACnD,MAAI,iBAAiB,KAAK,UAAU,GAAG;AACrC,WACE;AAAA,EAGJ;AAEA,SAAO;AACT;AAGO,IAAM,QAAQ,CAAC,OAAyB,IAAI,GAAG,GAAG;AAUlD,SAAS,eAAe,OAAe,KAAa,KAAa,MAAsB;AAC5F,MAAI,EAAE,OAAO,SAAS,KAAK,KAAK,OAAO,UAAU,KAAK,IAAI;AACxD,UAAM,aAAa,IAAI,IAAI,6BAA6B;AAAA,EAC1D;AACA,MAAI,QAAQ,OAAO,QAAQ,KAAK;AAC9B,UAAM,aAAa,IAAI,IAAI,uBAAuB,GAAG,QAAQ,GAAG,GAAG;AAAA,EACrE;AACA,SAAO;AACT;;;ACjTO,IAAM,cAAc;AAE3B,IAAM,WAAW,CAAC,SAAyB,GAAG,YAAY;AAAA,EAAK,KAAK,KAAK,CAAC;AAAA;AAgB1E,SAAS,gBAAgB,WAAmB,OAAkC;AAC5E,SAAO;AAAA;AAAA;AAAA;AAAA,wBAIe,SAAS;AAAA,mCACE,iBAAiB,KAAK,CAAC;AAAA;AAAA,cAE5C,WAAW;AAAA;AAAA;AAGzB;AAaO,SAAS,mBACd,OACA,OACA,QACQ;AACR,QAAM,OAAO,eAAe,OAAO,GAAG,IAAI,OAAO,IAAI;AACrD,QAAM,OAAO,eAAe,QAAQ,GAAG,KAAM,QAAQ;AAUrD,SAAO,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAQe,iBAAiB,KAAK,CAAC;AAAA;AAAA,kBAEtC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,cAKf,IAAI,WAAW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAQxB,gBAAgB,aAAa,KAAK,CAAC;AAAA;AAAA,iBAE3B;AACjB;AASO,IAAM,eAAe;AASrB,SAAS,YAAY,IAAsB;AAChD,QAAM,SAAS,MAAM,EAAE;AACvB,SAAO,SAAS;AAAA;AAAA;AAAA,MAGZ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,yBAKa,MAAM;AAAA;AAAA;AAAA;AAAA,QAIvB,YAAY,EAAE;AACtB;AAUO,SAAS,iBAAiB,OAA0B,OAAe,QAAwB;AAChG,QAAM,OAAO,eAAe,OAAO,GAAG,IAAI,OAAO,IAAI;AACrD,QAAM,OAAO,eAAe,QAAQ,GAAG,KAAM,QAAQ;AAKrD,SAAO,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kCAQgB,iBAAiB,KAAK,CAAC;AAAA;AAAA,kBAEvC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,cAKf,IAAI,WAAW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAUxB,gBAAgB,iBAAiB,KAAK,CAAC;AAAA;AAAA,eAEjC;AACf;AAgBO,SAAS,oBAAoB,IAAc,OAAe,QAAwB;AACvF,QAAM,SAAS,MAAM,EAAE;AACvB,QAAM,OAAO,eAAe,OAAO,GAAG,IAAI,OAAO,IAAI;AACrD,QAAM,OAAO,eAAe,QAAQ,GAAG,KAAM,QAAQ;AAErD,SAAO,SAAS;AAAA;AAAA;AAAA;AAAA,8BAIY,MAAM;AAAA;AAAA;AAAA,YAGxB,IAAI,WAAW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAQhB;AACf;AAGO,SAAS,UAAU,IAAsB;AAC9C,QAAM,OAAO,MAAM,EAAE;AACrB,SAAO,SAAS;AAAA;AAAA,IAEd,IAAI;AAAA;AAAA;AAAA;AAAA,QAIA,YAAY,EAAE;AACtB;AASO,SAAS,cAAc,IAAc,OAAe,QAAwB;AACjF,QAAM,OAAO,MAAM,EAAE;AACrB,QAAM,OAAO,eAAe,OAAO,GAAG,IAAI,OAAO,IAAI;AACrD,QAAM,OAAO,eAAe,QAAQ,GAAG,KAAM,QAAQ;AAErD,SAAO,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,uCAKqB,IAAI;AAAA;AAAA;AAAA,YAG/B,IAAI,WAAW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAeb;AAClB;AAQO,SAAS,sBAAsB,IAAc,OAAuB;AACzE,QAAM,OAAO,MAAM,EAAE;AACrB,QAAM,OAAO,eAAe,OAAO,GAAG,KAAK,OAAO,IAAI;AAEtD,SAAO,SAAS;AAAA;AAAA;AAAA,qCAGmB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASnC,IAAI;AAAA,WACC,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,QAKP,IAAI,EAAE;AACd;AAcO,SAAS,wBAAwB,IAAc,OAAuB;AAC3E,QAAM,SAAS,MAAM,EAAE;AACvB,QAAM,OAAO,eAAe,OAAO,GAAG,KAAK,OAAO,IAAI;AAEtD,SAAO,SAAS;AAAA;AAAA;AAAA,4BAGU,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAU5B,MAAM;AAAA,WACD,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,QAKT,IAAI,EAAE;AACd;AAQO,SAAS,UAAU,IAAsB;AAC9C,SAAO,SAAS;AAAA;AAAA,IAEd,MAAM,EAAE,CAAC;AAAA;AAAA,SAEJ;AACT;;;AC9WA,IAAM,aAAa;AACnB,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAC3B,IAAM,iBAAiB;AACvB,IAAM,cAAc;AACpB,IAAM,cAAc;AACpB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AAGzB,IAAM,IAAI;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AAAA,EACN,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,OAAO;AAAA,EACP,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,cAAc;AAAA,EACd,MAAM;AAAA,EACN,cAAc;AAAA,EACd,cAAc;AAAA,EACd,yBAAyB;AAAA,EACzB,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,wBAAwB;AAAA;AAAA,EAExB,YAAY;AAAA;AAAA,EAEZ,kBAAkB;AACpB;AAGA,IAAM,kBAAyD;AAAA,EAC7D,EAAE,MAAM,YAAY,MAAM,OAAO;AAAA,EACjC,EAAE,MAAM,YAAY,MAAM,QAAQ;AAAA,EAClC,EAAE,MAAM,eAAe,MAAM,UAAU;AAAA,EACvC,EAAE,MAAM,gBAAgB,MAAM,WAAW;AAAA,EACzC,EAAE,MAAM,YAAY,MAAM,OAAO;AAAA,EACjC,EAAE,MAAM,aAAa,MAAM,MAAM;AAAA,EACjC,EAAE,MAAM,cAAc,MAAM,MAAM;AAAA,EAClC,EAAE,MAAM,gBAAgB,MAAM,MAAM;AAAA,EACpC,EAAE,MAAM,iBAAiB,MAAM,YAAY;AAAA,EAC3C,EAAE,MAAM,mBAAmB,MAAM,cAAc;AAAA,EAC/C,EAAE,MAAM,0BAA0B,MAAM,iBAAiB;AAAA,EACzD,EAAE,MAAM,aAAa,MAAM,QAAQ;AAAA,EACnC,EAAE,MAAM,oBAAoB,MAAM,YAAY;AAChD;AAEA,IAAM,OAAO,CAAC,SAAgD;AAC5D,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,SAAO,UAAU,KAAK,OAAO;AAC/B;AAEA,IAAM,UAAU,CAAC,SAAgD;AAC/D,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,QAAQ,MAAM;AAChB,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,OAAO,GAAG;AACxB,SAAO,OAAO,UAAU,KAAK,IAAI,QAAQ;AAC3C;AAGO,SAAS,UAAU,KAA4B;AACpD,QAAM,MAAM,WAAW,KAAK,GAAG;AAC/B,MAAI,MAAM,CAAC,GAAG;AACZ,WAAO,IAAI,CAAC,EAAE,YAAY;AAAA,EAC5B;AACA,QAAM,OAAO,iBAAiB,KAAK,GAAG;AACtC,MAAI,OAAO,CAAC,GAAG;AACb,WAAO,aAAa,KAAK,CAAC,EAAE,YAAY,CAAC;AAAA,EAC3C;AACA,SAAO;AACT;AAGA,IAAM,iBAAiB,CAAC,QACtB,mBAAmB,IAAI,QAAQ,oBAAoB,EAAE,EAAE,MAAM,cAAc,EAAE,IAAI,KAAK,GAAG;AASpF,SAAS,gBACd,SACA,MACA,QACA,WACsB;AACtB,MAAI,EAAE,WAAW,iBAAiB,OAAO,IAAI;AAC3C,WAAO;AAAA,EACT;AACA,QAAM,UAAU,YAAY,KAAK,OAAO,IAAI,CAAC,KAAK;AAClD,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAMA,QAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK;AACrC,MAAI,QAAQ,IAAI;AACd,WAAO;AAAA,EACT;AACA,SAAO,EAAE,KAAK,KAAK,SAAS,WAAW,YAAY,SAAS,GAAG,GAAG,MAAM,QAAQ,UAAU;AAC5F;AAWA,SAAS,YAAY,SAAiB,KAA4B;AAChE,QAAM,OAAO,QAAQ,MAAM,WAAW,EAAE,CAAC,KAAK;AAC9C,QAAM,QAAQ,KAAK,MAAM,KAAK,QAAQ,cAAc,GAAG,EAAE,IAAI,cAAc,GAAG,GAAG,MAAM;AACvF,QAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,EAAE,QAAQ,kBAAkB,EAAE;AAC9E,SAAO,cAAc,KAAK,OAAO;AACnC;AASA,SAAS,KAAQ,MAAW,OAAe,SAA0B;AACnE,QAAM,OAAO,KAAK,SAAS;AAC3B,SAAO,UAAU,IACb,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,MAAM,QAAQ,IACrD,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,KAAK;AAClD;AAYA,SAAS,oBAAoB,SAAwC;AACnE,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,MAAM,KAAK,IAAI,UAAU;AAC/B,QAAI,QAAQ,MAAM;AAChB;AAAA,IACF;AACA,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,OAAO,UAAU,KAAK,GAAG;AAC3B,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AASA,IAAM,iBAAiB,CAAC,KAAgB,WACtC,IAAI,eAAe,UAAa,IAAI,MAAM,MAAM;AAG3C,SAAS,YAAY,SAAkD;AAC5E,QAAM,QAAQ,oBAAI,IAAyB;AAC3C,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,YAAY,KAAK,IAAI,CAAC;AAC5B,QAAI,cAAc,MAAM;AACtB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,IAAI,SAAS;AAClC,QAAI,QAAQ;AACV,aAAO,KAAK,GAAG;AAAA,IACjB,OAAO;AACL,YAAM,IAAI,WAAW,CAAC,GAAG,CAAC;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,aAAa,CAAC,OAAiC,cACnD,KAAK,MAAM,IAAI,SAAS,IAAI,CAAC,GAAG,CAAC;AAEnC,IAAM,YAAY,CAAC,OAAiC,cAAgC;AAClF,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,OAAO,MAAM,IAAI,SAAS,KAAK,CAAC,GAAG;AAC5C,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,UAAU,MAAM;AAClB,WAAK,IAAI,KAAK;AAAA,IAChB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,IAAI;AACjB;AAGA,IAAM,WAAW,CAAC,OAAiC,cACjD,UAAU,OAAO,SAAS,EACvB,OAAO,CAAC,UAAU,MAAM,WAAW,MAAM,CAAC,EAC1C,IAAI,cAAc;AASvB,IAAM,eAAe,CAAC,OAAiC,cACrD,UAAU,OAAO,SAAS,EACvB,OAAO,CAAC,UAAU,MAAM,SAAS,mBAAmB,CAAC,EACrD,IAAI,cAAc;AAGvB,IAAM,WAAW,CAAC,OAAiC,cACjD,UAAU,OAAO,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,CAAC;AAYzE,SAAS,WACP,OACA,QAC0B;AAC1B,QAAM,QAAqC,CAAC;AAC5C,aAAW,aAAa,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,GAAG;AACzE,eAAW,WAAW,UAAU,OAAO,SAAS,GAAG;AACjD,UAAI,CAAC,QAAQ,WAAW,MAAM,GAAG;AAC/B;AAAA,MACF;AAGA,UAAI,UAAU,QAAQ,SAAS,MAAM,GAAG;AACtC;AAAA,MACF;AACA,UAAI;AACJ,UAAI;AACF,eAAO,IAAI,IAAI,OAAO,EAAE,SAAS,YAAY;AAAA,MAC/C,QAAQ;AACN;AAAA,MACF;AACA,YAAM,QAAQ,gBAAgB;AAAA,QAC5B,CAAC,UAAU,SAAS,MAAM,QAAQ,KAAK,SAAS,IAAI,MAAM,IAAI,EAAE;AAAA,MAClE;AACA,UAAI,CAAC,OAAO;AACV;AAAA,MACF;AACA,YAAM,YAAY,MAAM,MAAM,IAAI,KAAK,oBAAI,IAAY;AACvD,YAAM,MAAM,IAAI,IAAI;AACpB,gBAAU,IAAI,OAAO;AAAA,IACvB;AAAA,EACF;AACA,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AACxF;AAGA,SAAS,cAAc,OAA2C;AAChE,SAAO,UAAU,OAAO,EAAE,IAAI,EAAE;AAAA,IAC9B,CAAC,UAAU,yBAAyB,MAAM,QAAQ,OAAO,EAAE,CAAC;AAAA,EAC9D;AACF;AAGO,SAAS,kBAAkB,SAAwB,OAAoC;AAC5F,QAAM,OAAwB,CAAC;AAC/B,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,UAAU;AAEd,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,QAAI,eAAe,KAAK,QAAQ,GAAG;AACjC;AAAA,IACF;AACA,UAAM,MAAM,KAAK,IAAI,MAAM;AAC3B,UAAM,KAAK,QAAQ,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,QAAQ,QAAQ,OAAO,MAAM;AAI/B,iBAAW;AACX;AAAA,IACF;AACA,QAAI,KAAK,IAAI,EAAE,GAAG;AAChB;AAAA,IACF;AACA,SAAK,IAAI,EAAE;AACX,SAAK,KAAK;AAAA,MACR;AAAA,MACA,MAAM,KAAK,IAAI,IAAI;AAAA,MACnB,OAAO,KAAK,IAAI,KAAK;AAAA,MACrB,WAAW,QAAQ,IAAI,SAAS;AAAA,MAChC,WAAW,QAAQ,IAAI,SAAS;AAAA,MAChC,MAAM,KAAK,IAAI,IAAI;AAAA,MACnB,WAAW,cAAc,GAAG;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,GAAG,KAAK,MAAM,OAAO,OAAO,GAAG,iBAAiB,oBAAoB,OAAO,EAAE;AACxF;AAGO,SAAS,eAAe,SAAwB,IAAY,SAA+B;AAChG,QAAM,QAAQ,YAAY,OAAO;AACjC,QAAM,QAAQ,UAAU,OAAO,EAAE,IAAI;AAErC,QAAM,aAAgE,CAAC;AACvE,QAAM,aAAa,oBAAI,IAAY;AACnC,aAAW,OAAO,MAAM,IAAI,EAAE,QAAQ,KAAK,CAAC,GAAG;AAC7C,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,UAAU,QAAQ,WAAW,IAAI,KAAK,GAAG;AAC3C;AAAA,IACF;AACA,eAAW,IAAI,KAAK;AACpB,eAAW,KAAK,EAAE,OAAO,UAAU,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA,EACrD;AAEA,QAAM,SAAS,WAAW,OAAO,EAAE;AACnC,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,KAAK,SAAS,GAAG;AACnB,WAAO,OAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAI,OAAO,QAAQ,CAAC,GAAI,GAAG,IAAI,CAAC,CAAC;AAAA,EAC9D;AAEA,QAAM,eACJ,UAAU,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,QAAQ,SAAS,kBAAkB,CAAC,KAAK;AAEzF,QAAM,aAA8B,CAAC;AACrC,aAAW,WAAW,UAAU,OAAO,EAAE,SAAS,GAAG;AACnD,UAAM,OAAO,gBAAgB,SAAS,aAAa,IAAI,IAAI;AAC3D,QAAI,MAAM;AACR,iBAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,WAAW,OAAO,EAAE,IAAI;AAAA,IAC9B,OAAO,WAAW,OAAO,EAAE,SAAS;AAAA,IACpC,WAAW,WAAW,OAAO,EAAE,SAAS;AAAA,IACxC,YAAY,WAAW,OAAO,EAAE,UAAU;AAAA,IAC1C;AAAA,IACA,WAAW,WAAW,OAAO,EAAE,KAAK;AAAA,IACpC,WAAW,WAAW,OAAO,EAAE,KAAK;AAAA,IACpC,WAAW,QAAQ,MAAM,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,CAAC;AAAA,IACjD,WAAW,QAAQ,MAAM,IAAI,EAAE,QAAQ,IAAI,CAAC,GAAG,CAAC;AAAA,IAChD,YAAY,WAAW,OAAO,EAAE,YAAY;AAAA,IAC5C,YAAY,WAAW,OAAO,EAAE,YAAY;AAAA,IAC5C,yBAAyB,WAAW,OAAO,EAAE,uBAAuB;AAAA,IACpE,YAAY,WAAW,OAAO,EAAE,UAAU;AAAA,IAC1C,WAAW,SAAS,OAAO,EAAE,mBAAmB;AAAA,IAChD,WAAW,SAAS,OAAO,EAAE,iBAAiB;AAAA,IAC9C,QAAQ,SAAS,OAAO,EAAE,eAAe;AAAA,IACzC,cAAc,aAAa,OAAO,EAAE,eAAe;AAAA,IACnD;AAAA,IACA;AAAA,IACA,eAAe,WAAW,OAAO,EAAE,OAAO;AAAA,IAC1C,gBAAgB,WAAW,OAAO,EAAE,QAAQ;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,WAAW,QAAQ,QAAQ,SAAS,UAAU;AAAA,IAC9C,WAAW;AAAA,EACb;AACF;AAUO,SAAS,WAAW,WAA0B,KAA4C;AAC/F,MAAI,cAAc,MAAM;AACtB,UAAM,OAAO,UAAU,YAAY;AACnC,QAAI,KAAK,SAAS,aAAa,GAAG;AAChC,aAAO;AAAA,IACT;AACA,QAAI,KAAK,SAAS,aAAa,GAAG;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,IAAI,SAAS,aAAa,IAAI,gBAAgB;AACvD;AAGO,SAAS,gBAAgB,SAAwB,OAAkC;AACxF,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,oBAAI,IAAyB;AAE1C,MAAI,UAAU;AACd,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,QAAI,eAAe,KAAK,MAAM,GAAG;AAC/B;AAAA,IACF;AACA,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,UAAM,KAAK,QAAQ,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,QAAQ,QAAQ,OAAO,MAAM;AAI/B,iBAAW;AACX;AAAA,IACF;AAEA,QAAI,OAAO,KAAK,IAAI,EAAE;AACtB,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,QACL;AAAA,QACA,OAAO,KAAK,IAAI,KAAK;AAAA,QACrB,MAAM,KAAK,IAAI,IAAI;AAAA,QACnB,UAAU,CAAC;AAAA,QACX,QAAQ,WAAW,KAAK,IAAI,MAAM,GAAG,GAAG;AAAA,QACxC,WAAW,cAAc,GAAG;AAAA,MAC9B;AACA,WAAK,IAAI,IAAI,IAAI;AACjB,YAAM,KAAK,EAAE;AAAA,IACf;AAEA,UAAM,aAAa,KAAK,IAAI,OAAO;AACnC,UAAM,YAAY,aAAa,UAAU,UAAU,IAAI;AACvD,QAAI,aAAa,CAAC,KAAK,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS,GAAG;AAC/D,WAAK,SAAS,KAAK,EAAE,IAAI,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;AAAA,IACnE;AAAA,EACF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,MACD,MAAM,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,QAAQ,QAAQ,MAAS;AAAA,MACjE;AAAA,MACA;AAAA,IACF;AAAA,IACA,iBAAiB,oBAAoB,OAAO;AAAA,EAC9C;AACF;AASO,SAAS,gBAAgB,SAAwB,OAAmC;AACzF,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,oBAAI,IAA0B;AAE3C,MAAI,UAAU;AACd,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,UAAM,KAAK,QAAQ,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,QAAQ,QAAQ,OAAO,MAAM;AAI/B,iBAAW;AACX;AAAA,IACF;AAEA,QAAI,OAAO,KAAK,IAAI,EAAE;AACtB,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,QACL;AAAA,QACA,OAAO,KAAK,IAAI,KAAK;AAAA,QACrB,MAAM,KAAK,IAAI,IAAI;AAAA,QACnB,MAAM,QAAQ,IAAI,IAAI;AAAA,QACtB,OAAO,CAAC;AAAA,QACR,QAAQ,WAAW,KAAK,IAAI,MAAM,GAAG,GAAG;AAAA,QACxC,WAAW,cAAc,GAAG;AAAA,MAC9B;AACA,WAAK,IAAI,IAAI,IAAI;AACjB,YAAM,KAAK,EAAE;AAAA,IACf;AAEA,UAAM,OAAO,KAAK,IAAI,IAAI;AAC1B,QAAI,MAAM,WAAW,MAAM,GAAG;AAC5B,YAAM,OAAO,eAAe,IAAI;AAChC,UAAI,CAAC,KAAK,MAAM,SAAS,IAAI,GAAG;AAC9B,aAAK,MAAM,KAAK,IAAI;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,QAAQ,QAAQ,MAAS;AAAA,IACjE;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,aACd,SACA,IACA,KACA,SACY;AACZ,QAAM,QAAQ,YAAY,OAAO;AACjC,QAAM,QAAQ,UAAU,OAAO,EAAE,IAAI;AACrC,QAAM,YAAY,QAAQ,QAAQ,SAAS,UAAU;AAErD,QAAM,WAAuD,CAAC;AAC9D,aAAW,OAAO,MAAM,IAAI,EAAE,OAAO,KAAK,CAAC,GAAG;AAC5C,UAAM,aAAa,KAAK,IAAI,CAAC;AAC7B,UAAM,YAAY,aAAa,UAAU,UAAU,IAAI;AACvD,QAAI,CAAC,aAAa,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS,GAAG;AAC1D;AAAA,IACF;AACA,aAAS,KAAK,EAAE,IAAI,WAAW,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;AAAA,EACvD;AAEA,QAAM,aAA8B,CAAC;AACrC,aAAW,WAAW,UAAU,OAAO,EAAE,SAAS,GAAG;AACnD,UAAM,OAAO,gBAAgB,SAAS,aAAa,IAAI,IAAI;AAC3D,QAAI,MAAM;AACR,iBAAW,KAAK,IAAI;AAAA,IACtB;AAAA,EACF;AAEA,QAAM,kBAAkB,WAAW,OAAO,EAAE,sBAAsB;AAClE,QAAM,eACJ,UAAU,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,YAAY,QAAQ,SAAS,kBAAkB,CAAC,KAAK;AAEzF,SAAO;AAAA,IACL;AAAA,IACA,OAAO,WAAW,OAAO,EAAE,KAAK;AAAA,IAChC,OAAO,WAAW,OAAO,EAAE,KAAK;AAAA,IAChC,MAAM,WAAW,OAAO,EAAE,IAAI;AAAA,IAC9B,WAAW,QAAQ,MAAM,IAAI,EAAE,SAAS,IAAI,CAAC,GAAG,CAAC;AAAA,IACjD;AAAA,IACA,WAAW,SAAS,OAAO,EAAE,QAAQ;AAAA,IACrC,OAAO,SAAS,OAAO,EAAE,UAAU;AAAA,IACnC,UAAU,SAAS,OAAO,EAAE,YAAY;AAAA,IACxC,cAAc,aAAa,OAAO,EAAE,OAAO;AAAA,IAC3C,QAAQ,WAAW,iBAAiB,GAAG;AAAA,IACvC;AAAA;AAAA;AAAA,IAGA,iBAAiB,YAAY,OAAO,UAAU,OAAO,EAAE,gBAAgB,EAAE;AAAA,IACzE,QAAQ,WAAW,OAAO,EAAE;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AACF;AAUO,SAAS,WAAW,SAAwB,OAA8B;AAC/E,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,oBAAI,IAAqB;AAEtC,MAAI,UAAU;AACd,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,MAAM,KAAK,IAAI,OAAO;AAC5B,UAAM,KAAK,QAAQ,OAAO,OAAO,UAAU,GAAG;AAC9C,QAAI,QAAQ,QAAQ,OAAO,MAAM;AAI/B,iBAAW;AACX;AAAA,IACF;AAEA,QAAI,UAAU,KAAK,IAAI,EAAE;AACzB,QAAI,CAAC,SAAS;AACZ,gBAAU;AAAA,QACR;AAAA,QACA,OAAO,KAAK,IAAI,KAAK;AAAA,QACrB,MAAM,KAAK,IAAI,IAAI;AAAA,QACnB,MAAM,QAAQ,IAAI,IAAI;AAAA,QACtB,WAAW,KAAK,IAAI,SAAS;AAAA,QAC7B,OAAO,KAAK,IAAI,KAAK;AAAA,QACrB,kBAAkB,KAAK,IAAI,gBAAgB;AAAA,QAC3C,QAAQ,KAAK,IAAI,MAAM;AAAA,QACvB,MAAM,KAAK,IAAI,IAAI;AAAA,QACnB,MAAM,KAAK,IAAI,IAAI;AAAA,QACnB,cAAc,KAAK,IAAI,SAAS;AAAA,QAChC,WAAW,CAAC;AAAA,QACZ,WAAW,cAAc,GAAG;AAAA,MAC9B;AACA,WAAK,IAAI,IAAI,OAAO;AACpB,YAAM,KAAK,EAAE;AAAA,IACf;AAEA,eAAW,CAAC,QAAQ,IAAI,KAAK;AAAA,MAC3B,CAAC,gBAAgB,cAAc;AAAA,MAC/B,CAAC,OAAO,KAAK;AAAA,IACf,GAAY;AACV,YAAM,OAAO,gBAAgB,KAAK,IAAI,MAAM,CAAC,GAAG,MAAM,IAAI,QAAQ,KAAK;AACvE,UAAI,QAAQ,CAAC,QAAQ,UAAU,KAAK,CAAC,aAAa,SAAS,QAAQ,KAAK,GAAG,GAAG;AAC5E,gBAAQ,UAAU,KAAK,IAAI;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,QAAQ,QAAQ,MAAS;AAAA,IACjE;AAAA,IACA;AAAA,EACF;AACF;AAWO,SAAS,iBAAiB,SAAwB,OAAoC;AAC3F,QAAM,QAAyB,CAAC;AAChC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,WAAW,QAAQ,QAAQ,SAAS;AAE1C,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,UAAU,KAAK,IAAI,OAAO;AAChC,UAAM,SAAS,UAAW,UAAU,OAAO,KAAK,UAAW;AAC3D,UAAM,QAAQ,KAAK,IAAI,KAAK;AAE5B,eAAW,CAAC,QAAQ,IAAI,KAAK;AAAA,MAC3B,CAAC,gBAAgB,cAAc;AAAA,MAC/B,CAAC,OAAO,KAAK;AAAA,MACb,CAAC,aAAa,WAAW;AAAA,IAC3B,GAAY;AACV,YAAM,OAAO,gBAAgB,KAAK,IAAI,MAAM,CAAC,GAAG,MAAM,QAAQ,KAAK;AACnE,UAAI,CAAC,QAAQ,KAAK,IAAI,KAAK,GAAG,GAAG;AAC/B;AAAA,MACF;AACA,WAAK,IAAI,KAAK,GAAG;AACjB,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF;AAMA,SAAO,EAAE,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,SAAS,MAAM,SAAS,SAAS,WAAW,MAAM;AAC1F;AAGO,SAAS,QAAQ,SAAkC;AACxD,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,OAAO,QAAQ,QAAQ,UAAU;AAC1C,UAAM,MAAM,KAAK,IAAI,IAAI;AACzB,QAAI,QAAQ,MAAM;AAChB,YAAM,IAAI,GAAG;AAAA,IACf;AAAA,EACF;AACA,SAAO,CAAC,GAAG,KAAK;AAClB;;;AChsBO,IAAM,cAAN,MAAkB;AAAA,EACN;AAAA,EACA;AAAA,EACT;AAAA,EACA,gBAAgB;AAAA,EAChB,QAAuB,QAAQ,QAAQ;AAAA,EACvC,aAAa;AAAA,EAErB,YAAY,SAA6B;AACvC,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,gBAAgB,QAAQ,iBAAiB,QAAQ,aAAa;AACnE,SAAK,aAAa,QAAQ;AAAA,EAC5B;AAAA;AAAA,EAGA,IAAI,oBAA4B;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,SAAY,MAAoC;AAC9C,UAAM,SAAS,KAAK,MAAM,KAAK,MAAM,IAAI;AACzC,SAAK,QAAQ,OAAO;AAAA,MAClB,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,gBAA+B;AACnC,UAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAGlC,UAAM,OAAO,KAAK,IAAI,KAAK,YAAY,KAAK,aAAa,OAAO;AAChE,QAAI,OAAO,GAAG;AACZ,YAAM,MAAM,IAAI;AAAA,IAClB;AACA,SAAK,gBAAgB,KAAK,IAAI;AAAA,EAChC;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK,aAAa;AAClB,SAAK,aAAa,KAAK,IAAI,KAAK,eAAe,KAAK,aAAa,CAAC;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAkB;AAChB,QAAI,KAAK,eAAe,KAAK,gBAAgB;AAC3C;AAAA,IACF;AACA,SAAK,cAAc;AACnB,QAAI,KAAK,aAAa,GAAG;AACvB;AAAA,IACF;AACA,SAAK,aAAa;AAClB,SAAK,aAAa,KAAK,IAAI,KAAK,gBAAgB,KAAK,MAAM,KAAK,aAAa,CAAC,CAAC;AAAA,EACjF;AACF;AAEO,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;;AC3BA,IAAM,eAAe;AAQrB,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AACF,CAAC;AAyCD,SAAS,eAAe,QAAwB;AAC9C,QAAM,WAAW,WAAW,CAAC,CAAC;AAO9B,QAAM,UAAU,CAAC,OAAgB,UAAkB,KAAa,QAAwB;AACtF,UAAM,SAAS,OAAO,KAAK;AAC3B,QAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AAAA,EAC5C;AAEA,QAAM,UAAU,OAAO,OAAO,cAAc,WAAW,OAAO,UAAU,KAAK,IAAI;AACjF,QAAM,aAAa,SAAS;AAE5B,SAAO;AAAA,IACL,GAAG;AAAA;AAAA;AAAA,IAGH,WACE,YAAY,MAAM,QAAQ,SAAS,QAAQ,IAAI,aAAa,GAAG,OAAO,IAAI,UAAU;AAAA,IACtF,eAAe;AAAA,MACb,OAAO;AAAA,MACP,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAAA,IACA,WAAW,QAAQ,OAAO,WAAW,SAAS,WAAW,KAAM,GAAO;AAAA,IACtE,YAAY,QAAQ,OAAO,YAAY,SAAS,YAAY,GAAG,CAAC;AAAA,IAChE,YAAY,QAAQ,OAAO,YAAY,SAAS,YAAY,GAAG,KAAU;AAAA,IACzE,iBAAiB,QAAQ,OAAO,iBAAiB,SAAS,iBAAiB,GAAG,GAAI;AAAA,EACpF;AACF;AAEO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA,WAAW,oBAAI,IAAoC;AAAA,EACnD;AAAA,EAEjB,YAAY,UAAyB,CAAC,GAAG;AACvC,UAAM,OAAO,EAAE,GAAG,WAAW,GAAG,GAAG,QAAQ,OAAO;AAClD,SAAK,SAAS,eAAe,IAAI;AACjC,SAAK,SAAS,QAAQ,UAAU,aAAa,KAAK,OAAO,QAAQ;AACjE,SAAK,UAAU,IAAI,YAAY,EAAE,YAAY,KAAK,OAAO,cAAc,CAAC;AACxE,SAAK,QAAQ,IAAI,MAAM,KAAK,OAAO,YAAY,KAAK,OAAO,eAAe;AAC1E,SAAK,YAAY,QAAQ;AAAA,EAC3B;AAAA;AAAA,EAGA,IAAI,aAAqB;AACvB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA,EAGA,IAAI,YAAoB;AACtB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA,EAGA,IAAI,YAAoB;AACtB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,KACN,UACA,OACA,OACkB;AAClB,UAAM,MAAM,KAAK,MAAM,IAAI,QAAQ;AACnC,QAAI,QAAQ,QAAW;AACrB,WAAK,OAAO,MAAM,aAAa,QAAQ,EAAE;AACzC,aAAO,QAAQ,QAAQ;AAAA,QACrB,MAAM,IAAI;AAAA,QACV,QAAQ;AAAA,QACR,aAAa,IAAI,KAAK,IAAI,WAAW,EAAE,YAAY;AAAA,MACrD,CAAC;AAAA,IACH;AAKA,UAAM,SAAS,KAAK,SAAS,IAAI,QAAQ;AACzC,QAAI,WAAW,QAAW;AACxB,WAAK,OAAO,MAAM,2CAA2C,QAAQ,EAAE;AACvE,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,KAAK,cAAc,UAAU,OAAO,KAAK;AACxD,SAAK,SAAS,IAAI,UAAU,MAAgC;AAG5D,WAAO,OAAO,QAAQ,MAAM,KAAK,SAAS,OAAO,QAAQ,CAAC;AAAA,EAC5D;AAAA,EAEA,MAAc,cACZ,UACA,OACA,OACkB;AAClB,UAAM,UAAU,MAAM,KAAK,QAAQ;AAAA,MAAS,MAC1C,SAAS;AAAA,QACP;AAAA,QACA,WAAW,KAAK,OAAO;AAAA,QACvB,WAAW,KAAK,OAAO;AAAA,QACvB,YAAY,KAAK,OAAO;AAAA,QACxB,SAAS,KAAK;AAAA,QACd,QAAQ,KAAK;AAAA,QACb,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,MACxD,CAAC;AAAA,IACH;AAIA,UAAM,cAAc,KAAK,IAAI;AAC7B,UAAM,OAAO,MAAM,OAAO;AAC1B,SAAK,MAAM,IAAI,UAAU,MAAM,WAAW;AAC1C,WAAO,EAAE,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK,WAAW,EAAE,YAAY,EAAE;AAAA,EACjF;AAAA;AAAA;AAAA,EAIA,cAAc,MAAc,OAAe,QAAoD;AAC7F,UAAM,QAAQ,cAAc,IAAI;AAChC,UAAM,QAAQ,mBAAmB,OAAO,OAAO,MAAM;AACrD,WAAO,KAAK;AAAA,MAAK,WAAW,MAAM,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,MAAM;AAAA,MAAI;AAAA,MAAO,CAAC,YACxE,kBAAkB,SAAS,KAAK;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,IAA2C;AACzD,UAAM,SAAS,MAAM,KAAK;AAAA,MAAK,UAAU,GAAG,EAAE;AAAA,MAAI,YAAY,EAAE;AAAA,MAAG,CAAC,YAClE,eAAe,SAAS,GAAG,IAAI,GAAG,OAAO;AAAA,IAC3C;AAMA,QAAI,CAAC,OAAO,KAAK,MAAM,SAAS,YAAY,GAAG;AAC7C,YAAM,KAAK,OAAO,IAAI,YAAY,wBAAwB;AAAA,IAC5D;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,OAAO,IAAc,QAAgB,MAA8B;AAM/E,UAAM,SAAS,MAAM,KAAK,KAAK,SAAS,GAAG,EAAE,IAAI,UAAU,EAAE,GAAG,OAAO,GAAG;AAE1E,QAAI,MAAM,WAAW,GAAG;AACtB,YAAM,SAAS,qCAAqC,GAAG,EAAE,MAAM;AAAA,QAC7D,MAAM;AAAA,QACN,KAAK,GAAG;AAAA,MACV,CAAC;AAAA,IACH;AACA,UAAM;AAAA,MACJ,IAAI,GAAG,EAAE,gDAAgD,MAAM,iBAAiB,MAAM,KAAK,IAAI,CAAC;AAAA,MAChG,EAAE,MAAM,KAAK,GAAG,QAAQ;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,YAAY,OAAe,OAAe,QAAkD;AAC1F,UAAM,QAAQ,cAAc,KAAK;AACjC,UAAM,QAAQ,iBAAiB,OAAO,OAAO,MAAM;AACnD,WAAO,KAAK;AAAA,MAAK,SAAS,MAAM,KAAK,GAAG,CAAC,IAAI,KAAK,IAAI,MAAM;AAAA,MAAI;AAAA,MAAO,CAAC,YACtE,gBAAgB,SAAS,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,IAAyC;AACrD,UAAM,SAAS,MAAM,KAAK;AAAA,MAAK,QAAQ,GAAG,EAAE;AAAA,MAAI,UAAU,EAAE;AAAA,MAAG,CAAC,YAC9D,aAAa,SAAS,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO;AAAA,IACjD;AAKA,QAAI,CAAC,OAAO,KAAK,MAAM,KAAK,CAAC,SAAS,aAAa,IAAI,IAAI,CAAC,GAAG;AAC7D,YAAM,KAAK;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,cAAc,IAAc,OAAe,QAAmD;AAC5F,WAAO,KAAK;AAAA,MACV,gBAAgB,GAAG,EAAE,IAAI,KAAK,IAAI,MAAM;AAAA,MACxC,oBAAoB,IAAI,OAAO,MAAM;AAAA,MACrC,CAAC,YAAY,gBAAgB,SAAS,KAAK;AAAA,IAC7C;AAAA,EACF;AAAA,EAEA,aAAa,IAAc,OAAe,QAA8C;AACtF,UAAM,QAAQ,cAAc,IAAI,OAAO,MAAM;AAC7C,WAAO,KAAK;AAAA,MAAK,YAAY,GAAG,EAAE,IAAI,KAAK,IAAI,MAAM;AAAA,MAAI;AAAA,MAAO,CAAC,YAC/D,WAAW,SAAS,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAGA,iBAAiB,IAAc,OAAmD;AAChF,WAAO,KAAK;AAAA,MACV,kBAAkB,GAAG,EAAE,IAAI,KAAK;AAAA,MAChC,sBAAsB,IAAI,KAAK;AAAA,MAC/B,CAAC,YAAY,iBAAiB,SAAS,KAAK;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA,EAGA,mBAAmB,IAAc,OAAmD;AAClF,WAAO,KAAK;AAAA,MACV,oBAAoB,GAAG,EAAE,IAAI,KAAK;AAAA,MAClC,wBAAwB,IAAI,KAAK;AAAA,MACjC,CAAC,YAAY,iBAAiB,SAAS,KAAK;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,IAAuC;AAC3C,WAAO,KAAK,KAAK,SAAS,GAAG,EAAE,IAAI,UAAU,EAAE,GAAG,OAAO;AAAA,EAC3D;AACF;","names":["text","page"]}
|