mcp-recipes 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/config.ts","../src/version.ts","../src/errors.ts","../src/sources/ids.ts","../src/sources/registry.ts","../src/sources/adapter.ts","../src/sources/cookbook.ts","../src/sources/marmiton.ts","../src/sources/wordings.ts","../src/sources/client.ts","../src/tools/compareRecipes.ts","../src/tools/recipeView.ts","../src/recipe/language.ts","../src/recipe/units.ts","../src/recipe/quantity.ts","../src/recipe/scale.ts","../src/recipe/sections.ts","../src/tools/shared.ts","../src/tools/arguments.ts","../src/tools/getRecipe.ts","../src/tools/scaleIngredients.ts","../src/tools/searchRecipes.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point.\n *\n * stdout carries the protocol and nothing else: every diagnostic goes to\n * stderr, because a stray line on stdout corrupts the session for the client.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = async () => {\n await server.close().catch(() => undefined);\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[mcp-recipes] fatal: ${message}\\n`);\n process.exit(1);\n});\n","/**\n * Wiring: one client over the registered sources, four tools, and the guidance\n * a model reads before using any of them.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { RecipesClient } from \"./sources/client.js\";\nimport {\n compareRecipesDescription,\n compareRecipesInput,\n compareRecipesOutput,\n runCompareRecipes,\n} from \"./tools/compareRecipes.js\";\nimport type { CompareRecipesArgs } from \"./tools/compareRecipes.js\";\nimport {\n getRecipeDescription,\n getRecipeInput,\n getRecipeOutput,\n runGetRecipe,\n} from \"./tools/getRecipe.js\";\nimport type { GetRecipeArgs } from \"./tools/getRecipe.js\";\nimport {\n runScaleIngredients,\n scaleIngredientsDescription,\n scaleIngredientsInput,\n scaleIngredientsOutput,\n} from \"./tools/scaleIngredients.js\";\nimport type { ScaleIngredientsArgs } from \"./tools/scaleIngredients.js\";\nimport {\n runSearchRecipes,\n searchRecipesDescription,\n searchRecipesInput,\n searchRecipesOutput,\n} from \"./tools/searchRecipes.js\";\nimport type { SearchRecipesArgs } from \"./tools/searchRecipes.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n client?: RecipesClient;\n}\n\n/** Nothing here writes, uploads or deletes; every tool only reads. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\n/**\n * The guidance a model reads before choosing a tool.\n *\n * The sources are named from the registry rather than written into the prose,\n * so the sentence stays true when the registry grows. What it says about them\n * holds for any corpus this server reads.\n */\nexport function buildInstructions(sources: Array<{ name: string; language: string }>): string {\n const named = sources.map((source) => `${source.name} (${source.language})`).join(\", \");\n\n return [\n `Tools for recipes, reading ${sources.length} sources: ${named}. No API key and no account are needed.`,\n \"search_recipes asks every source at once and returns one list; every id it hands back names the source it came from, and get_recipe routes on that.\",\n \"Use compare_recipes when the question is how a dish differs between traditions: it reads each source's closest version and can rescale them all to the same number of servings so the lists stand comparison.\",\n \"Do not rescale quantities yourself. get_recipe and scale_ingredients keep an egg whole, halve what pours or cuts, move a small measurement to a smaller unit before rounding it, and flag what cannot be scaled, which is what stops answers like '2.4 eggs'.\",\n \"scale_ingredients works offline on a list from anywhere and reads French and English lines in one call, each in its own language.\",\n \"A source that fails is named as a source that failed. An answer holding rows from some sources is never evidence about what the others hold, and 'per_source' says which was which.\",\n \"The sources share no scale. Their result counts count different things and are never added; some carry reader ratings and some have no author and no rating by nature, so rows are interleaved rather than ranked.\",\n \"Nothing is converted between measuring systems, and a yield is repeated in the source's own words, because '4 à 6 personnes' and '4 personnes' are different claims. A page stating no yield cannot be rescaled and says so.\",\n \"Every result carries a url. Credit the source you took a recipe from and link it; some publish under a licence that requires it, and one that states no terms at all has not granted permission.\",\n ].join(\" \");\n}\n\n/** The guidance as it reads for the sources this build registers. */\nexport const INSTRUCTIONS = buildInstructions([\n { name: \"Marmiton\", language: \"fr\" },\n { name: \"Wikibooks Cookbook\", language: \"en\" },\n]);\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = options.client ?? new RecipesClient({ config, logger });\n\n const server = new McpServer(\n { name: \"mcp-recipes\", version: PKG_VERSION },\n { instructions: buildInstructions(client.profiles) },\n );\n\n server.registerTool(\n \"search_recipes\",\n {\n title: \"Search every recipe source at once\",\n description: searchRecipesDescription,\n inputSchema: searchRecipesInput,\n outputSchema: searchRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runSearchRecipes(client, args as SearchRecipesArgs),\n );\n\n server.registerTool(\n \"get_recipe\",\n {\n title: \"Read one recipe, optionally rescaled\",\n description: getRecipeDescription,\n inputSchema: getRecipeInput,\n outputSchema: getRecipeOutput,\n annotations: READ_ONLY,\n },\n async (args) => runGetRecipe(client, args as GetRecipeArgs),\n );\n\n server.registerTool(\n \"scale_ingredients\",\n {\n title: \"Scale an ingredient list, in either language\",\n description: scaleIngredientsDescription,\n inputSchema: scaleIngredientsInput,\n outputSchema: scaleIngredientsOutput,\n annotations: {\n ...READ_ONLY,\n // Nothing is fetched, so this tool reaches nothing outside itself.\n openWorldHint: false,\n },\n },\n async (args) => runScaleIngredients(args as ScaleIngredientsArgs),\n );\n\n server.registerTool(\n \"compare_recipes\",\n {\n title: \"One dish, as each source writes it\",\n description: compareRecipesDescription,\n inputSchema: compareRecipesInput,\n outputSchema: compareRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runCompareRecipes(client, args as CompareRecipesArgs),\n );\n\n logger.info(\n `ready: ${client.profiles.length} sources (${client.profiles.map((source) => source.id).join(\", \")}), user-agent=\"${client.userAgent}\", ${client.intervalMs}ms between requests to one source`,\n );\n\n return server;\n}\n","/**\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 * One setting governs every source. Each is read through its own client, and\n * each of those keeps a pacing floor of its own that nothing here can lower, so\n * a value set here can only make this server more patient than it already is.\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 * Spacing between two requests to the same site.\n *\n * No source publishes a ceiling for a client like this one, so the number is\n * self-imposed politeness: a second between requests is far below what a person\n * browsing one of these sites generates, and the answers are cached, so a\n * conversation that circles back to a recipe costs no traffic at all.\n */\nexport const DEFAULT_INTERVAL_MS = 1000;\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 entry point.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_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-recipes/${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-recipes] ${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 a site was left out of an answer.\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-recipes] 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-recipes] 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.RECIPES_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-recipes] error: RECIPES_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.RECIPES_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: a site 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 \"RECIPES_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"RECIPES_TIMEOUT_MS\", 20_000, 1000, 120_000),\n maxRetries: readInteger(env, \"RECIPES_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"RECIPES_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"RECIPES_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 sites this server reads can tell which build is calling from their own\n * logs.\n */\nexport const PKG_VERSION = \"2.0.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-recipes\";\nexport const ISSUES_URL = \"https://github.com/smeet666/mcp-recipes/issues\";\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"no site holds this\" and \"the\n * question could not be asked\". Collapsing the two lets a model report an\n * absence it never established, which is a false statement about the world\n * rather than a missing feature.\n */\n\nimport { ISSUES_URL } from \"./version.js\";\n\nexport type ErrorCode =\n /** A site answered, and holds no such recipe. */\n | \"not_found\"\n /** The arguments cannot produce a request. */\n | \"invalid_input\"\n /** A site asked this client to slow down. */\n | \"rate_limited\"\n /** A response 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 const ERROR_CODES: readonly ErrorCode[] = [\n \"not_found\",\n \"invalid_input\",\n \"rate_limited\",\n \"parse_failure\",\n \"network_error\",\n \"timeout\",\n];\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 RecipesError 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 = \"RecipesError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"not_found\", message, details ?? {});\n\nexport const invalidInput = (message: string, hint?: string, cause?: unknown) =>\n new RecipesError(\"invalid_input\", message, { ...(hint ? { hint } : {}), cause });\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the recipe exists.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"parse_failure\", message, {\n hint: `A site may have changed how it answers. Please report this at ${ISSUES_URL} with the arguments you used.`,\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"network_error\", message, details ?? {});\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"timeout\", message, details ?? {});\n\n/**\n * Read the code off a failure raised by the library that reads a source.\n *\n * Those libraries throw their own error classes carrying the six codes above,\n * and the classes are not part of the interface this server imports. Reading\n * the field keeps the taxonomy intact without depending on a class identity a\n * library is free to change, and anything unrecognisable is reported as a\n * network failure, which is the reading that claims least.\n */\nexport function toRecipesError(error: unknown): RecipesError {\n if (error instanceof RecipesError) {\n return error;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n const raw = error as { code?: unknown; details?: unknown } | null;\n const code = typeof raw?.code === \"string\" ? raw.code : \"\";\n const known = ERROR_CODES.find((candidate) => candidate === code);\n\n const details =\n raw && typeof raw.details === \"object\" && raw.details !== null\n ? (raw.details as ErrorDetails)\n : {};\n\n return new RecipesError(known ?? \"network_error\", message, details);\n}\n\n/**\n * Adopt a failure raised while reading a source, in this server's own words.\n *\n * The code is kept, since that is the taxonomy every layer branches on. The\n * sentence is not: what arrives with such a failure is written by whoever the\n * reader was talking to, in their vocabulary and about their internals, and\n * serving it as this answer's own sentence states as this server's finding\n * something it never established. It is quoted and credited to the source\n * instead, on one line, so the diagnostic survives without borrowing a voice\n * or a shape it has no claim to. The advice that came with it goes, because\n * advice addressed to somebody else's reader sends a caller somewhere this\n * server cannot answer for.\n */\nexport function fromSource(error: unknown, sourceName: string): RecipesError {\n if (error instanceof RecipesError) {\n return error;\n }\n\n const known = toRecipesError(error);\n const reported = known.message.replace(/\\s+/g, \" \").trim();\n const opening =\n known.code === \"not_found\"\n ? `${sourceName} holds nothing under this name.`\n : `${sourceName} could not be read.`;\n const message = reported === \"\" ? opening : `${opening} It reports: \"${reported}\"`;\n const details: ErrorDetails = {};\n if (known.details.url !== undefined) {\n details.url = known.details.url;\n }\n if (known.details.status !== undefined) {\n details.status = known.details.status;\n }\n\n switch (known.code) {\n case \"not_found\":\n return notFound(message, details);\n case \"invalid_input\":\n return invalidInput(message);\n case \"rate_limited\":\n return rateLimited(message, details);\n case \"parse_failure\":\n return parseFailure(message, details);\n case \"timeout\":\n return timeout(message, details);\n default:\n return networkError(message, details);\n }\n}\n","/**\n * Identifiers that name the source they came from.\n *\n * Every source hands back an opaque string, and a caller holding a merged list\n * has no way to tell one source's string from another's. Every identifier this\n * server returns therefore carries a prefix, and `get_recipe` routes on it\n * rather than trying each source in turn.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\n\nconst SEPARATOR = \":\";\n\nexport interface ResolvedId {\n source: SourceAdapter;\n /** The string that source's own reader takes. */\n reference: string;\n /** Set when the shape was read rather than stated, so the answer can say so. */\n inferred: string | null;\n}\n\n/**\n * Work out which source an identifier belongs to.\n *\n * A prefixed identifier is routed on what it says. A raw one is offered to each\n * source in turn, and a source claims only the shapes it mints: digits for one,\n * a namespaced page key for another. Exactly one claim routes the read and the\n * answer says which reading it used.\n *\n * More than one claim is refused rather than resolved, because picking a winner\n * would send the read somewhere a caller did not intend and answer with a\n * confident recipe for the wrong dish. No claim at all is refused too: a bare\n * dish name is a search, not an identifier, and guessing a page name from it\n * returns whatever that guess happens to hit.\n */\nexport function resolveId(rawId: string, sources: readonly SourceAdapter[]): ResolvedId {\n const trimmed = rawId.trim();\n if (trimmed === \"\") {\n throw invalidInput(\n \"A recipe identifier is required.\",\n `Use one of the ids search_recipes returned, such as ${example(sources)}.`,\n );\n }\n\n for (const source of sources) {\n const prefix = `${source.id}${SEPARATOR}`;\n if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase())) {\n continue;\n }\n const reference = trimmed.slice(prefix.length).trim();\n if (reference === \"\") {\n throw invalidInput(\n `\"${trimmed}\" names a source and no recipe.`,\n \"Put the source's own identifier after the colon.\",\n );\n }\n return { source, reference, inferred: null };\n }\n\n const claimed = sources\n .map((source) => ({ source, claim: source.claims(trimmed) }))\n .filter((entry): entry is { source: SourceAdapter; claim: NonNullable<typeof entry.claim> } =>\n Boolean(entry.claim),\n );\n\n if (claimed.length > 1) {\n throw invalidInput(\n `\"${trimmed}\" could be an identifier on ${names(claimed.map((entry) => entry.source))}, so it names no one recipe.`,\n `Spell it with its source, as in ${claimed.map((entry) => `${entry.source.id}:${trimmed}`).join(\" or \")}.`,\n );\n }\n\n const only = claimed[0];\n if (!only) {\n throw invalidInput(\n `\"${trimmed}\" is not an identifier any of the sources this server reads would mint.`,\n `Call search_recipes with \"${trimmed}\" as the query and pass an id from a row, ` +\n `which reads like ${example(sources)}.`,\n );\n }\n\n return {\n source: only.source,\n reference: only.claim.reference,\n inferred: only.claim.guess\n ? `${only.claim.why}, which is a guess: check the recipe that comes back is the one you meant`\n : only.claim.why,\n };\n}\n\n/** Sources named the way a sentence names them. */\nfunction names(sources: readonly { name: string }[]): string {\n const all = sources.map((source) => source.name);\n if (all.length <= 1) {\n return all.join(\"\");\n }\n return `${all.slice(0, -1).join(\", \")} and ${all.at(-1)}`;\n}\n\n/** An identifier of the shape this server hands out, for a hint. */\nfunction example(sources: readonly SourceAdapter[]): string {\n const first = sources[0];\n return first ? `${first.id}${SEPARATOR}<the source's own id>` : \"<source>:<id>\";\n}\n\n/** Which source an identifier names, without resolving what it points at. */\nexport function sourceOf(rawId: string, sources: readonly SourceAdapter[]): SourceId | null {\n const prefix = rawId.split(SEPARATOR)[0]?.toLowerCase();\n return sources.find((source) => source.id.toLowerCase() === prefix)?.id ?? null;\n}\n","/**\n * The sources this server reads.\n *\n * One list, and everything above it is written for however long the list is.\n * Adding a corpus means writing an adapter and adding it here; no tool, no\n * merge and no error path has a branch per source.\n */\n\nimport { MarmitonClient } from \"mcp-marmiton/client\";\nimport type { MarmitonClientOptions } from \"mcp-marmiton/client\";\nimport { CookbookClient } from \"mcp-wikibooks-cookbook/client\";\nimport type { ClientOptions as CookbookClientOptions } from \"mcp-wikibooks-cookbook/client\";\n\nimport type { Config } from \"../config.js\";\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { COOKBOOK_PROFILE, cookbookAdapter } from \"./cookbook.js\";\nimport type { CookbookReader } from \"./cookbook.js\";\nimport { MARMITON_PROFILE, marmitonAdapter } from \"./marmiton.js\";\nimport type { MarmitonReader } from \"./marmiton.js\";\n\n/**\n * A reader may be supplied in place of the one that talks to a site, so a\n * program embedding this server can put its own cache in front of a corpus, and\n * a test can drive it from fixed answers.\n */\nexport interface Readers {\n marmiton?: MarmitonReader;\n cookbook?: CookbookReader;\n}\n\n/** The sources a caller can name, in the order an answer takes them. */\nexport const SOURCE_IDS: readonly SourceId[] = [MARMITON_PROFILE.id, COOKBOOK_PROFILE.id];\n\nexport function buildSources(config: Config, readers: Readers = {}): SourceAdapter[] {\n const shared = {\n userAgent: config.userAgent,\n minIntervalMs: config.minIntervalMs,\n timeoutMs: config.timeoutMs,\n maxRetries: config.maxRetries,\n cacheTtlMs: config.cacheTtlMs,\n cacheMaxEntries: config.cacheMaxEntries,\n logLevel: config.logLevel,\n };\n\n const marmitonOptions: MarmitonClientOptions = { config: shared };\n const cookbookOptions: CookbookClientOptions = { config: shared };\n\n return [\n marmitonAdapter(readers.marmiton ?? new MarmitonClient(marmitonOptions)),\n cookbookAdapter(readers.cookbook ?? new CookbookClient(cookbookOptions)),\n ];\n}\n\n/** The sources a caller asked for, in the registry's own order. */\nexport function selectSources(\n sources: SourceAdapter[],\n wanted: readonly SourceId[] | undefined,\n): SourceAdapter[] {\n if (!wanted) {\n return sources;\n }\n\n const unknown = wanted.filter((id) => !sources.some((source) => source.id === id));\n if (unknown.length > 0) {\n throw invalidInput(\n `This server reads no source called ${unknown.map((id) => `\"${id}\"`).join(\", \")}.`,\n `It reads ${sources.map((source) => source.id).join(\", \")}.`,\n );\n }\n\n const chosen = sources.filter((source) => wanted.includes(source.id));\n if (chosen.length === 0) {\n throw invalidInput(\n \"A search needs at least one source.\",\n `Name one of ${sources.map((source) => source.id).join(\", \")}, or leave the argument out to ask them all.`,\n );\n }\n return chosen;\n}\n","/**\n * What a source has to provide to be one.\n *\n * Everything above this file is written for however many sources the registry\n * holds. A source knows four things nothing else does: what it is called, how\n * to recognise one of its own identifiers, how to search it, and how to read one\n * recipe off it. Adding a corpus is writing those four and registering it.\n *\n * The helpers below are the ones every adapter needs, because no source answers\n * with a contract: a field can arrive missing, null, or holding something other\n * than the type it usually holds.\n */\n\nimport { parseFailure } from \"../errors.js\";\nimport type { Language } from \"../recipe/language.js\";\nimport type { RecipeDetail, RecipeRow, SourceId, SourceProfile } from \"../types.js\";\n\nconst NUMBER_AND_REST = /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/;\nconst RANGE_AND_REST =\n /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(?:à|a|to|-|–|—|ou|or)\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/i;\n\n/** Rows that could be read, and how many could not. */\nexport interface ReadRows {\n rows: RecipeRow[];\n /** Rows the source sent in a shape this server could not read. */\n skipped: number;\n /** Rows the source said it saw, in the terms that source counts in. */\n reportedTotal: number | null;\n /** What `reportedTotal` counts on this source, in words. Null when it states none. */\n reportedTotalMeans: string | null;\n cached: boolean;\n}\n\nexport interface ReadRecipe {\n recipe: RecipeDetail;\n cached: boolean;\n}\n\n/** How a source recognised a raw identifier as one of its own. */\nexport interface Claim {\n /** The string this source's own reader takes. */\n reference: string;\n /** Why the shape was read this way, for an answer that has to say so. */\n why: string;\n /**\n * Whether reading it this way is a guess. A shape only this source mints is\n * not; a shape it would merely accept is, and the answer says so.\n */\n guess: boolean;\n}\n\nexport interface SourceAdapter extends SourceProfile {\n /**\n * Recognise a raw identifier, or decline it.\n *\n * A source claims a shape it mints and declines everything else, so a string\n * no source claims is refused, instead of being sent somewhere that would\n * answer it with a confident absence.\n */\n claims: (raw: string) => Claim | null;\n search: (query: string, limit: number) => Promise<ReadRows>;\n getRecipe: (reference: string) => Promise<ReadRecipe>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Reading a value a source sent, without trusting its shape */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The distinction these draw is between a row and a record. One row this server\n * cannot read is dropped and counted, because the rest of the list is still a\n * good answer. A record it cannot read is `parse_failure`, because there is\n * nothing left to return and a caller told \"network_error\" would retry a source\n * that has actually changed.\n */\nexport function text(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() !== \"\" ? value : null;\n}\n\nexport function textList(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n}\n\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nexport function required(value: unknown, field: string, source: SourceProfile): string {\n const found = text(value);\n if (found === null) {\n throw parseFailure(\n `${source.name} answered without a readable \"${field}\", so there is no recipe to return.`,\n );\n }\n return found;\n}\n\nexport function rowsOf<T>(value: unknown, source: SourceProfile): T[] {\n if (!Array.isArray(value)) {\n throw parseFailure(\n `${source.name} answered in a shape this server could not read: the list of results was missing.`,\n );\n }\n return value as T[];\n}\n\n/** Build the identifier this server hands out for a row from a source. */\nexport function namespacedId(source: SourceId, reference: string): string {\n return `${source}:${reference}`;\n}\n\n/**\n * Keep the entries a page actually published.\n *\n * A nutrition panel arrives with a slot for every figure the source knows how\n * to print, and the slots a page left empty are null. Passing those through\n * would put \"protein: null\" in front of a reader as though the page had said\n * something about protein.\n *\n * The panel is walked rather than typed, because each source names its slots\n * differently and a figure is repeated as the page wrote it either way.\n */\nexport function publishedFigures(panel: unknown): Record<string, string> | null {\n if (typeof panel !== \"object\" || panel === null) {\n return null;\n }\n const kept: Record<string, string> = {};\n for (const [key, value] of Object.entries(panel)) {\n if (typeof value === \"string\" && value.trim() !== \"\") {\n kept[key] = value.trim();\n }\n }\n return Object.keys(kept).length > 0 ? kept : null;\n}\n\n/**\n * Read a yield that names a span rather than a number.\n *\n * \"4 à 6 personnes\" and \"4 personnes\" are different claims, and a source that\n * reports the first as a plain count of four hands this server a factor that is\n * too large for anyone at the upper end. The span is read back off the published\n * wording so the answer can say which end it scaled from.\n */\nexport function readYieldSpan(published: string | null): {\n count: number | null;\n max: number | null;\n unit: string | null;\n} {\n if (published === null) {\n return { count: null, max: null, unit: null };\n }\n\n const span = RANGE_AND_REST.exec(published);\n if (span) {\n const [lowText = \"\", highText = \"\"] = span.slice(1);\n const low = Number(lowText.replace(\",\", \".\"));\n const high = Number(highText.replace(\",\", \".\"));\n if (Number.isFinite(low) && Number.isFinite(high) && high > low) {\n return { count: low, max: high, unit: text(span[3]) };\n }\n }\n\n const single = NUMBER_AND_REST.exec(published);\n if (single) {\n const [figure = \"\"] = single.slice(1);\n const value = Number(figure.replace(\",\", \".\"));\n if (Number.isFinite(value)) {\n return { count: value, max: null, unit: text(single[2]) };\n }\n }\n\n return { count: null, max: null, unit: null };\n}\n\n/** Make the language a source publishes in available to whatever reads its lines. */\nexport function detailBase(source: SourceProfile): {\n source: SourceId;\n sourceName: string;\n language: Language;\n attribution: string;\n} {\n return {\n source: source.id,\n sourceName: source.name,\n language: source.language,\n attribution: source.attribution,\n };\n}\n","/**\n * The Cookbook on the English Wikibooks, as a source.\n *\n * A wiki written by whoever edits it, published under a licence that requires\n * attribution, with no author and no reader rating by nature. Its identifiers\n * are page keys carrying the namespace every Cookbook page lives in.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\n/** A search row, as the Cookbook's own reader publishes one. */\nexport interface CookbookSummary {\n key: string;\n title: string;\n description: string | null;\n excerpt: string | null;\n imageUrl: string | null;\n sourceUrl: string;\n}\n\n/** A page, as the Cookbook's own reader publishes one. */\nexport interface CookbookRecipe {\n key: string;\n title: string;\n sourceUrl: string;\n license: { title: string; url: string } | null;\n description: string | null;\n category: string | null;\n servings: number | null;\n yieldText: string | null;\n yieldUnit: string | null;\n totalMinutes: number | null;\n ingredients: string[];\n equipment: string[];\n steps: string[];\n tips: string[];\n nutrition: unknown;\n sectionTitles: string[];\n}\n\n/** The part of the Cookbook's client this server uses. */\nexport interface CookbookReader {\n search: (\n query: string,\n limit: number,\n ) => Promise<{ data: { results: CookbookSummary[] }; cached: boolean }>;\n getRecipe: (reference: string) => Promise<{ data: CookbookRecipe; cached: boolean }>;\n}\n\nexport const COOKBOOK_PROFILE = {\n id: \"cookbook\",\n name: \"Wikibooks Cookbook\",\n homeUrl: \"https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents\",\n language: \"en\" as const,\n attribution: \"Source: Wikibooks Cookbook\",\n // The namespace holds pages about salt and about a dish made with it alike.\n mixesReferencePages: true,\n};\n\nconst SITE_URL = /^https?:\\/\\/en\\.wikibooks\\.org\\/wiki\\//i;\n/** The namespace every page in the Cookbook lives under. */\nconst NAMESPACE = /^cookbook\\s*:/i;\n\n/**\n * Every page in the Cookbook is filed under one namespace, so its title arrives\n * as \"Cookbook:Spaghetti alla Carbonara\". The prefix is where the page lives on\n * the wiki rather than what the dish is called, and repeating it on every row of\n * a list says nothing and crowds out the names. The key keeps it, because that\n * is the string the site resolves.\n */\nfunction dishName(title: string): string {\n return title.replace(NAMESPACE, \"\").trim() || title;\n}\n\nexport function cookbookAdapter(reader: CookbookReader): SourceAdapter {\n return {\n ...COOKBOOK_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n let key: string;\n try {\n key = decodeURIComponent(raw.replace(SITE_URL, \"\")).replace(/_/g, \" \");\n } catch (cause) {\n // A percent sign that opens no escape is common in a wiki title, and\n // it is the caller's string rather than a site that failed.\n throw invalidInput(\n `\"${raw}\" carries a percent sign that is not a valid escape, so the page name cannot be read.`,\n \"Pass the id search_recipes returned instead of the address.\",\n cause,\n );\n }\n return {\n reference: key,\n why: \"the address is a page on the English Wikibooks\",\n guess: false,\n };\n }\n\n if (NAMESPACE.test(raw)) {\n return {\n reference: raw,\n why: \"the name opens with the namespace every Cookbook page lives under\",\n guess: false,\n };\n }\n\n return null;\n },\n\n async search(query: string, limit: number): Promise<ReadRows> {\n const outcome = await reader.search(query, limit);\n const list = rowsOf<Partial<CookbookSummary>>(\n (outcome.data as { results?: unknown })?.results,\n COOKBOOK_PROFILE,\n );\n const rows: RecipeRow[] = [];\n let skipped = 0;\n\n for (const summary of list) {\n const key = text(summary?.key);\n const title = text(summary?.title) ?? key;\n const url = text(summary?.sourceUrl);\n if (!(key && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(COOKBOOK_PROFILE.id, key),\n source: COOKBOOK_PROFILE.id,\n sourceName: COOKBOOK_PROFILE.name,\n title: dishName(title),\n url,\n imageUrl: text(summary?.imageUrl),\n excerpt: text(summary?.excerpt) ?? text(summary?.description),\n });\n }\n\n return {\n rows,\n skipped,\n // The gateway states no total and offers no second page, so there is no\n // number to report. Counting the rows returned and calling it a total\n // would invent one.\n reportedTotal: null,\n reportedTotalMeans: null,\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: cookbookDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function cookbookDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<CookbookRecipe>;\n const license = recipe.license as Partial<NonNullable<CookbookRecipe[\"license\"]>> | null;\n const licenseTitle = text(license?.title);\n const licenseUrl = text(license?.url);\n\n const published = text(recipe.yieldText);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(COOKBOOK_PROFILE),\n id: namespacedId(COOKBOOK_PROFILE.id, required(recipe.key, \"key\", COOKBOOK_PROFILE)),\n title: dishName(required(recipe.title ?? recipe.key, \"title\", COOKBOOK_PROFILE)),\n url: required(recipe.sourceUrl, \"url\", COOKBOOK_PROFILE),\n // The page source carries no picture this server can address.\n imageUrl: null,\n yieldCount: count(recipe.servings) ?? span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: text(recipe.yieldUnit) ?? span.unit,\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n // The page's own headings, which the reader reports. An answer holding no\n // ingredient line uses them to say whether the page announced a list.\n publishedSections: Array.isArray(recipe.sectionTitles) ? textList(recipe.sectionTitles) : null,\n // The Cookbook publishes one time for a recipe rather than splitting it.\n prepMinutes: null,\n cookMinutes: null,\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n // The page is written by everyone who edited it, so it credits nobody in\n // particular and carries no reader rating.\n author: null,\n rating: null,\n nutrition: publishedFigures(recipe.nutrition),\n equipment: textList(recipe.equipment),\n tips: textList(recipe.tips),\n // A licence is only stated when both halves of it arrived: a name with no\n // address, or an address with no name, is not terms anyone can follow.\n license: licenseTitle && licenseUrl ? { title: licenseTitle, url: licenseUrl } : null,\n };\n}\n","/**\n * Marmiton, as a source.\n *\n * A French recipe site with reader ratings, whose identifiers are digits and\n * whose search serves a single page it disallows paging past.\n */\n\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\n\n/** A search row, as Marmiton's own reader publishes one. */\nexport interface MarmitonSummary {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n}\n\n/** A recipe, as Marmiton's own reader publishes one. */\nexport interface MarmitonRecipe {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n ingredients: string[];\n steps: string[];\n recipeYield: { count: number | null; unit: string | null; text: string };\n prepMinutes: number | null;\n cookMinutes: number | null;\n totalMinutes: number | null;\n category: string | null;\n author: string | null;\n rating: { value: number; count: number | null; best: number | null } | null;\n nutrition: unknown;\n}\n\n/** The part of Marmiton's client this server uses. */\nexport interface MarmitonReader {\n search: (query: string) => Promise<{ data: MarmitonSummary[]; cached: boolean }>;\n getRecipe: (ref: {\n id?: string;\n url?: string;\n }) => Promise<{ data: MarmitonRecipe; cached: boolean }>;\n}\n\nexport const MARMITON_PROFILE = {\n id: \"marmiton\",\n name: \"Marmiton\",\n homeUrl: \"https://www.marmiton.org\",\n language: \"fr\" as const,\n attribution: \"Source: Marmiton\",\n // Every page the search returns is a recipe.\n mixesReferencePages: false,\n};\n\n/** Recipe pages, whose address ends in the numeric identifier. */\nconst SITE_URL = /^https?:\\/\\/(?:www\\.)?marmiton\\.org\\//i;\n/** The shape Marmiton mints: digits and nothing else. */\nconst REFERENCE = /^\\d+$/;\n\n/**\n * Ask Marmiton, and read the answer it gives to a wording it matches nothing\n * for.\n *\n * Marmiton serves a 404 on the results page when no recipe matches, which is\n * the site stating an absence. Handed on as a failure it would put \"Marmiton\n * did not answer\" in front of a reader for a search Marmiton answered, and\n * leave the corpus unspoken for. Every other failure stays a failure, so a site\n * that is unreachable is never read as a site that holds nothing.\n */\nasync function readSearch(\n reader: MarmitonReader,\n query: string,\n): Promise<{ data: MarmitonSummary[]; cached: boolean }> {\n try {\n return await reader.search(query);\n } catch (error) {\n const code = (error as { code?: string } | null)?.code;\n if (code === \"not_found\") {\n return { data: [], cached: false };\n }\n throw error;\n }\n}\n\nexport function marmitonAdapter(reader: MarmitonReader): SourceAdapter {\n return {\n ...MARMITON_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n return { reference: raw, why: \"the address is a Marmiton page\", guess: false };\n }\n if (REFERENCE.test(raw)) {\n return {\n reference: raw,\n why: \"a bare number is the shape Marmiton mints\",\n guess: false,\n };\n }\n return null;\n },\n\n async search(query: string): Promise<ReadRows> {\n const outcome = await readSearch(reader, query);\n const list = rowsOf<Partial<MarmitonSummary>>(outcome.data, MARMITON_PROFILE);\n const rows: RecipeRow[] = [];\n let skipped = 0;\n\n for (const summary of list) {\n // A row needs something to call it, somewhere to send a reader, and an\n // identifier that reads back. A row missing any of them is dropped\n // rather than returned with a hole in it.\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!(id && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(MARMITON_PROFILE.id, id),\n source: MARMITON_PROFILE.id,\n sourceName: MARMITON_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.imageUrl),\n // The result list carries a title and a picture and no prose, so\n // there is nothing to quote here.\n excerpt: null,\n });\n }\n\n return {\n rows,\n skipped,\n // Rows on the page, including the ones this server could not read: the\n // site saw them, and reporting only the readable ones would understate\n // what it holds.\n reportedTotal: rows.length + skipped,\n reportedTotalMeans:\n \"rows on the single page of results Marmiton serves; the site disallows paging, so this is not a catalogue count\",\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n // The reader takes an identifier or an address and tells them apart.\n const ref = ABSOLUTE_URL.test(reference) ? { url: reference } : { id: reference };\n const outcome = await reader.getRecipe(ref);\n return { recipe: marmitonDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function marmitonDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<MarmitonRecipe>;\n const yields = (recipe.recipeYield ?? {}) as Partial<MarmitonRecipe[\"recipeYield\"]>;\n const rating = recipe.rating as Partial<NonNullable<MarmitonRecipe[\"rating\"]>> | null | undefined;\n const value = count(rating?.value);\n\n // The reader splits the published yield at the first number, which leaves the\n // rest of a span sitting where the word being counted belongs: \"4 à 6\n // personnes\" arrives as a count of four and a unit of \"à 6 personnes\". The\n // wording is read again here so both ends of the span are known.\n const published = text(yields.text);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(MARMITON_PROFILE),\n id: namespacedId(MARMITON_PROFILE.id, required(recipe.id, \"id\", MARMITON_PROFILE)),\n title: required(recipe.title, \"title\", MARMITON_PROFILE),\n url: required(recipe.url, \"url\", MARMITON_PROFILE),\n imageUrl: text(recipe.imageUrl),\n yieldCount: span.count ?? count(yields.count),\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: span.unit ?? text(yields.unit),\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n // The reader hands over the parts of a page it recognised and not the\n // headings the page carries, so there is nothing here to say how it is\n // laid out.\n publishedSections: null,\n prepMinutes: count(recipe.prepMinutes),\n cookMinutes: count(recipe.cookMinutes),\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n author: text(recipe.author),\n rating:\n value === null ? null : { value, count: count(rating?.count), max: count(rating?.best) },\n nutrition: publishedFigures(recipe.nutrition),\n equipment: [],\n tips: [],\n // Marmiton states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * Several wordings from one question.\n *\n * A person asks for a recipe in a sentence, and a recipe index reads none of\n * it. One of the indexes behind this server requires every word given to appear\n * on the same page, so a sentence returns nothing where the corpus holds several\n * of the dish. The other ranks pages on the words it was handed, so the words\n * framing the question pull up whatever happens to share them and push the dish\n * down the list or out of it. Both come back to a reader looking like a\n * statement about a corpus when they are statements about a wording. Deriving\n * the shorter wordings and asking for the union is what keeps the two apart.\n *\n * Every wording is built from the words the question carried, with no\n * dictionary, no corpus statistics and no translation. That has two\n * consequences worth stating. A derived wording is one a reader can retype by\n * hand, so an answer built from several of them stays checkable. And the\n * derivations that would need a lexicon are not made: a question asked in one\n * language is never turned into the other, because a source that publishes in\n * the other language and holds nothing under the words written is an honest\n * absence, while a guessed translation is a word nobody wrote.\n *\n * The order runs from the wording closest to what was asked to the widest.\n *\n * A question also states what the recipe must not hold, and that part is read\n * by the role its words play rather than by the turn of phrase they are written\n * in: a negation, a sentence about how a food affects the person, or the name\n * of a diet. All three put a food out of the dish, so all three are set aside\n * from the derived wordings and named back to the caller. Reading them off a\n * list of phrasings is what lets a declared allergen become a word to search\n * for, and there is no phrasing whose absence from such a list is safe.\n */\n\nconst DIGITS_ONLY = /^\\d+$/;\nconst WORD_SEPARATORS = /[^\\p{L}\\p{N}-]+/u;\n\n/** One wording, and how it was arrived at. */\nexport interface Wording {\n query: string;\n /** How this wording was derived from the question, in words. */\n derivation: string;\n}\n\n/**\n * The most searches one source receives for one question.\n *\n * A ceiling per source rather than a budget shared between them, because the\n * sources are asked in parallel and each is paced on its own, so the worst case\n * is three of one source's intervals rather than six. Three is also the whole\n * ladder: the question as asked, the dish words, and the word naming the dish.\n */\nexport const MAX_WORDINGS_PER_SOURCE = 3;\n\n/* -------------------------------------------------------------------------- */\n/* The words a question is built out of, as opposed to the words naming a dish */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Words that frame a question rather than name what is cooked.\n *\n * Both languages are applied to every question. Telling them apart would need a\n * detector, and a detector that guesses wrong strips the wrong words; the two\n * lists barely overlap, and where they do the word frames a question in both.\n *\n * A word is left out of these lists wherever it is also something a person\n * eats. French \"son\" is a possessive and is also bran, so it stays. The same\n * caution governs anything added here: the cost of keeping a framing word is a\n * diluted ranking, and the cost of dropping an ingredient is the dish.\n */\nconst FRENCH_FRAME = new Set([\n \"a\",\n \"ai\",\n \"aimerais\",\n \"au\",\n \"aurais\",\n \"auriez\",\n \"aux\",\n \"avec\",\n \"avons\",\n \"bcp\",\n \"beaucoup\",\n \"bien\",\n \"bonjour\",\n \"bonne\",\n \"bonnes\",\n \"cherche\",\n \"chercher\",\n \"cherchez\",\n \"connais\",\n \"connaissez\",\n \"coucou\",\n \"ce\",\n \"ces\",\n \"cette\",\n \"comment\",\n \"d\",\n \"dans\",\n \"de\",\n \"des\",\n \"du\",\n \"en\",\n \"est\",\n \"et\",\n \"facile\",\n \"faire\",\n \"fais\",\n \"fait\",\n \"idee\",\n \"idees\",\n \"idée\",\n \"idées\",\n \"j\",\n \"je\",\n \"l\",\n \"la\",\n \"le\",\n \"les\",\n \"ma\",\n \"meilleur\",\n \"meilleure\",\n \"merci\",\n \"mes\",\n \"mon\",\n \"ou\",\n \"par\",\n \"peux\",\n \"plait\",\n \"plaît\",\n \"plus\",\n \"pour\",\n \"qu\",\n \"que\",\n \"quel\",\n \"quelle\",\n \"quelles\",\n \"quels\",\n \"qui\",\n \"rapide\",\n \"recette\",\n \"recettes\",\n \"recherche\",\n \"s\",\n \"salut\",\n \"se\",\n \"si\",\n \"simple\",\n \"sommes\",\n \"souhaite\",\n \"stp\",\n \"suis\",\n \"sur\",\n \"svp\",\n \"tres\",\n \"très\",\n \"un\",\n \"une\",\n \"veux\",\n \"voudrais\",\n \"vous\",\n \"à\",\n]);\n\nconst ENGLISH_FRAME = new Set([\n \"a\",\n \"about\",\n \"am\",\n \"an\",\n \"and\",\n \"any\",\n \"anything\",\n \"are\",\n \"at\",\n \"be\",\n \"best\",\n \"by\",\n \"can\",\n \"could\",\n \"do\",\n \"does\",\n \"easy\",\n \"find\",\n \"for\",\n \"from\",\n \"get\",\n \"good\",\n \"hello\",\n \"hi\",\n \"how\",\n \"i\",\n \"in\",\n \"into\",\n \"is\",\n \"it\",\n \"like\",\n \"looking\",\n \"make\",\n \"making\",\n \"me\",\n \"my\",\n \"need\",\n \"of\",\n \"on\",\n \"or\",\n \"please\",\n \"quick\",\n \"recipe\",\n \"recipes\",\n \"should\",\n \"simple\",\n \"some\",\n \"something\",\n \"suggest\",\n \"thank\",\n \"thanks\",\n \"that\",\n \"the\",\n \"less\",\n \"over\",\n \"than\",\n \"to\",\n \"under\",\n \"want\",\n \"what\",\n \"which\",\n \"will\",\n \"with\",\n \"would\",\n \"you\",\n \"your\",\n]);\n\n/**\n * What a condition is measured in.\n *\n * \"under 300 calories\" and \"in 20 minutes\" set a bound, and the word naming the\n * dimension is left in the sentence once the number has gone. No page is named\n * for it, so carrying it into a derived wording ranks pages that merely mention\n * counting calories.\n */\nconst CONDITION_UNITS = new Set([\n \"calorie\",\n \"calories\",\n \"couvert\",\n \"couverts\",\n \"heure\",\n \"heures\",\n \"kcal\",\n \"min\",\n \"mins\",\n \"minute\",\n \"minutes\",\n \"part\",\n \"parts\",\n \"people\",\n \"person\",\n \"persons\",\n \"personne\",\n \"personnes\",\n \"portion\",\n \"portions\",\n \"serving\",\n \"servings\",\n]);\n\n/**\n * Words whose whole job is to take something out of the dish.\n *\n * A negation is a function word, and function words are a closed class: the\n * ones a language has are the ones it has, and covering the class covers every\n * phrasing built on it, whether the question writes \"sans beurre\", \"without\n * butter\", \"no butter\" or \"pas de beurre\". That is what makes this a rule\n * rather than a collection of turns of phrase, and it is why the verbs that do\n * the same work, \"avoid\" and \"éviter\", sit here beside them.\n *\n * The price is a question such as \"no bake cookies\", where the negation is part\n * of a dish's name. It costs a caveat the reader can discard. Missing an\n * exclusion costs the reader the thing they wrote the question to avoid, so the\n * two errors are not weighed the same.\n */\nconst NEGATORS = new Set([\n \"aucun\",\n \"aucune\",\n \"avoid\",\n \"avoiding\",\n \"eviter\",\n \"evite\",\n \"evitez\",\n \"exclude\",\n \"excluding\",\n \"exclure\",\n \"except\",\n \"ni\",\n \"no\",\n \"non\",\n \"not\",\n \"pas\",\n \"sans\",\n \"sauf\",\n \"without\",\n \"zero\",\n]);\n\n/**\n * Words saying that a food does something to the person rather than to the\n * dish.\n *\n * A sentence about the eater names what the recipe must not hold just as\n * plainly as a negation does, and it is the one condition whose failure is a\n * matter of health, so it is read as its own kind and reported as one.\n */\nconst HEALTH_MARKERS = new Set([\n \"allergene\",\n \"allergenes\",\n \"allergen\",\n \"allergens\",\n \"allergic\",\n \"allergie\",\n \"allergies\",\n \"allergique\",\n \"allergiques\",\n \"allergy\",\n \"celiac\",\n \"coeliaque\",\n \"intolerance\",\n \"intolerances\",\n \"intolerant\",\n \"intolerante\",\n \"intolerantes\",\n \"intolerants\",\n]);\n\n/**\n * Diets, each of which names a class of recipes by what it leaves out.\n *\n * Naming one states an exclusion without writing a negation anywhere, which is\n * why a reading built on negations alone lets it through and hands the word\n * back to the index as though it named a dish.\n */\nconst DIETS = new Set([\n \"casher\",\n \"cetogene\",\n \"halal\",\n \"kascher\",\n \"keto\",\n \"ketogenic\",\n \"kosher\",\n \"paleo\",\n \"pescatarian\",\n \"pescetarien\",\n \"pescetarienne\",\n \"vegan\",\n \"vegane\",\n \"veganes\",\n \"vegans\",\n \"vegetalien\",\n \"vegetalienne\",\n \"vegetaliennes\",\n \"vegetaliens\",\n \"vegetarian\",\n \"vegetarians\",\n \"vegetarien\",\n \"vegetarienne\",\n \"vegetariennes\",\n \"vegetariens\",\n \"veggie\",\n]);\n\n/**\n * Where a condition ends.\n *\n * \"sans farine de blé et sans oeufs\" names two things to leave out, and the\n * conjunction is where the first one stops.\n */\nconst CONDITION_ENDS = new Set([\n \"et\",\n \"and\",\n \"ou\",\n \"or\",\n \"pour\",\n \"for\",\n \"avec\",\n \"with\",\n \"mais\",\n \"but\",\n \"dans\",\n \"sur\",\n]);\n\n/**\n * Words joining a marker to what it names, which name nothing themselves.\n *\n * \"pas de beurre\" and \"allergique aux noix\" put one of these between the word\n * carrying the condition and the food it is about. One of them standing right\n * after a marker is what says the food comes after it, which is the only sign\n * in the sentence of which side the marker reaches towards.\n */\nconst CONDITION_JOINERS = new Set([\n \"a\",\n \"au\",\n \"aux\",\n \"d\",\n \"de\",\n \"des\",\n \"du\",\n \"from\",\n \"l\",\n \"la\",\n \"le\",\n \"les\",\n \"my\",\n \"of\",\n \"the\",\n \"to\",\n]);\n\n/**\n * Words that carry a food's name on into a second noun.\n *\n * \"farine de blé\" and \"noix de coco\" are one food written in three words, and\n * the middle one is what says so. A food's name stops wherever no such word\n * carries it further, which is what keeps \"sans gluten gateau au chocolat\" from\n * reading as a condition on chocolate cake.\n */\nconst FOOD_LINKERS = new Set([\"d\", \"de\", \"des\", \"du\", \"of\"]);\n\n/**\n * How many words a condition names before the sentence resumes.\n *\n * What a recipe has to leave out is a food, so it is a short noun phrase:\n * \"beurre\", \"farine de blé\", \"noix de coco\". Past that the words belong to the\n * question again, and a scope with no end turns a negation used inside a dish's\n * name into a condition swallowing the rest of the sentence.\n */\nconst CONDITION_WORDS = 3;\n\n/* -------------------------------------------------------------------------- */\n/* Conditions */\n/* -------------------------------------------------------------------------- */\n\n/** How a question put a condition, which decides how it is reported back. */\nexport type ConditionKind =\n /** The question wrote the recipe must not hold this. */\n | \"excluded\"\n /** The question said the person reacts to this food. */\n | \"allergy\"\n /** The question named a diet, which is a class defined by what it leaves out. */\n | \"diet\";\n\n/** One thing a question asks the recipe not to hold. */\nexport interface Condition {\n /**\n * The words the question used for the food, or null where the question states\n * a condition and the sentence does not say plainly which food it covers.\n *\n * A word named here is a word the recipe must not hold and a word no wording\n * looks for, so naming the wrong one both hides a dish and leaves the food\n * being avoided in the search. Where the reading is not certain, the caller is\n * told a condition was stated and that its food was not read, which is a\n * smaller claim than either.\n */\n named: string | null;\n kind: ConditionKind;\n}\n\n/** What a question asks the dish to avoid, and how many it has to serve. */\nexport interface Conditions {\n /** What the dish must not hold, in the words the question used. */\n conditions: Condition[];\n /** How many the dish has to serve, when the question said. */\n servings: number | null;\n}\n\n/** How many the question asks the dish to serve, written either way round. */\nconst SERVING_PHRASES = [\n /\\bpour\\s+(\\d{1,3})\\s*(?:personnes?|parts?|couverts?)\\b/iu,\n /\\bfor\\s+(\\d{1,3})\\s*(?:people|persons?|servings?|portions?)\\b/iu,\n /\\bserves?\\s+(\\d{1,3})\\b/iu,\n /\\b(\\d{1,3})\\s*(?:personnes?|people|servings?)\\b/iu,\n];\n\n/** A food and the marker written as a single word: \"gluten-free\", \"sugarfree\". */\nconst FREE_OF = /\\b([\\p{L}]{3,})[\\s-]?free\\b/iu;\n\n/** What one reading of a question found in it. */\ninterface ReadQuestion {\n /** The words that name what is cooked, in the order the question wrote them. */\n dish: string[];\n conditions: Condition[];\n}\n\n/** Lowercase and strip diacritics, so \"végétarien\" and \"vegetarien\" read alike. */\nfunction foldWord(word: string): string {\n return word\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/œ/gu, \"oe\");\n}\n\n/**\n * Whether a word left over from a question names the dish.\n *\n * The framing of a question is not the dish it asks for, a number counts eaters\n * or minutes rather than naming anything, and what an apostrophe leaves behind\n * once it has split an elision is not a word at all.\n */\nfunction namesTheDish(word: string, key: string): boolean {\n if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) {\n return false;\n }\n if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) {\n return false;\n }\n if (DIGITS_ONLY.test(word)) {\n return false;\n }\n if (word.length < 2) {\n return false;\n }\n return !CONDITION_UNITS.has(word);\n}\n\n/**\n * Read a question once, deciding for every word whether it names the dish or\n * states a condition on it.\n *\n * One pass rather than two, because the two answers have to agree: a word read\n * as a condition here and as a dish word there is a condition reported to the\n * caller and searched for at the same time, which is the shape of the worst\n * thing this file can do. A single reading cannot disagree with itself.\n */\nfunction readQuestion(question: string): ReadQuestion {\n const words = tokenise(question);\n const conditions: Condition[] = [];\n const dish: string[] = [];\n\n const state = (named: string | null, kind: ConditionKind): void => {\n const trimmed = named === null ? null : named.trim();\n if (trimmed === \"\") {\n return;\n }\n if (conditions.some((held) => held.named === trimmed && held.kind === kind)) {\n return;\n }\n conditions.push({ named: trimmed, kind });\n };\n\n for (let index = 0; index < words.length; index += 1) {\n const word = words[index] ?? \"\";\n const key = foldWord(word);\n\n // A hyphen joins everywhere else, so \"sans-gluten\" and \"no-sugar\" arrive as\n // one word with the negation inside it.\n const hyphen = word.indexOf(\"-\");\n if (hyphen > 0 && NEGATORS.has(foldWord(word.slice(0, hyphen)))) {\n state(word.slice(hyphen + 1), \"excluded\");\n continue;\n }\n\n // \"gluten-free\" writes the food and the marker as one word.\n const free = FREE_OF.exec(word);\n if (free) {\n state(free[1] ?? \"\", \"excluded\");\n continue;\n }\n\n // \"free\" is also a price and a way of keeping hens, so it states a\n // condition only where the sentence puts a food on one side of it.\n if (key === \"free\") {\n const scope = scopeOf(words, index, dish);\n if (scope.named !== null) {\n state(scope.named, \"excluded\");\n index = scope.next - 1;\n }\n continue;\n }\n\n // A negation stands before what it negates, in both languages: \"sans\n // beurre\", \"without butter\", \"pas de beurre\".\n if (NEGATORS.has(key)) {\n const forward = takeFood(words, index + 1);\n state(forward.named === \"\" ? null : forward.named, \"excluded\");\n // The marker itself never names a dish, so it leaves whether or not it\n // turned out to scope over anything.\n index = forward.next - 1;\n continue;\n }\n\n if (HEALTH_MARKERS.has(key)) {\n const scope = scopeOf(words, index, dish);\n state(scope.named, \"allergy\");\n index = scope.next - 1;\n continue;\n }\n\n if (DIETS.has(key)) {\n state(word, \"diet\");\n continue;\n }\n\n if (namesTheDish(word, key) && !dish.includes(word)) {\n dish.push(word);\n }\n }\n\n return { dish, conditions };\n}\n\n/**\n * The food a marker covers, on whichever side of it the sentence put one.\n *\n * A joining word straight after the marker is the sentence saying its food\n * follows: \"allergique aux noix\", \"allergic to peanuts\", \"free of dairy\". With\n * no such word the food is the one the marker was written onto the end of:\n * \"peanut allergy\", \"lactose intolerant\", \"gluten free\". The two orders belong\n * to the two languages these sources publish in, so reading only one of them\n * leaves every question written in the other unread.\n *\n * Read the wrong way round, a marker takes a word out of the dish and leaves\n * the food being avoided in the search, which is both halves of the harm at\n * once. So a marker with a food on neither side covers none, and says so.\n *\n * A food found before the marker was already read as a word of the dish, and\n * leaves it here: the same word cannot be a thing to avoid and a thing to look\n * for.\n */\nfunction scopeOf(\n words: string[],\n marker: number,\n dish: string[],\n): { named: string | null; next: number } {\n if (CONDITION_JOINERS.has(foldWord(words[marker + 1] ?? \"\"))) {\n const forward = takeFood(words, marker + 1);\n return { named: forward.named === \"\" ? null : forward.named, next: forward.next };\n }\n\n const before = words[marker - 1];\n if (before !== undefined) {\n const held = dish.lastIndexOf(before);\n if (held !== -1) {\n dish.splice(held, 1);\n return { named: before, next: marker + 1 };\n }\n }\n\n return { named: null, next: marker + 1 };\n}\n\n/**\n * The food named from a point in a sentence onwards, and where the sentence\n * picks up again.\n *\n * A food is a short noun phrase, and it ends where nothing carries it further.\n * Taking a fixed number of words instead lets a condition run on into the dish,\n * so \"sans gluten gateau au chocolat\" comes back as a condition on the cake.\n */\nfunction takeFood(words: string[], from: number): { named: string; next: number } {\n let index = from;\n // An article or a quantifier can stand between the marker and the food:\n // \"pas de beurre\", \"without any butter\".\n while (index < words.length && isFiller(words[index] ?? \"\")) {\n index += 1;\n }\n\n const named: string[] = [];\n while (index < words.length && named.length < CONDITION_WORDS) {\n const word = words[index] ?? \"\";\n if (!namesFood(word)) {\n break;\n }\n named.push(word);\n index += 1;\n\n const beyond = words[index + 1];\n if (\n named.length + 2 <= CONDITION_WORDS &&\n FOOD_LINKERS.has(foldWord(words[index] ?? \"\")) &&\n beyond !== undefined &&\n namesFood(beyond)\n ) {\n named.push(words[index] ?? \"\");\n index += 1;\n continue;\n }\n break;\n }\n\n return { named: named.join(\" \"), next: index };\n}\n\n/** A word standing between a marker and its food, naming no food itself. */\nfunction isFiller(word: string): boolean {\n const key = foldWord(word);\n return CONDITION_JOINERS.has(key) || FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key);\n}\n\n/** Whether a word can be part of the name of something a recipe holds. */\nfunction namesFood(word: string): boolean {\n const key = foldWord(word);\n if (key.length < 2) {\n return false;\n }\n if (DIGITS_ONLY.test(key)) {\n return false;\n }\n if (key === \"free\") {\n return false;\n }\n if (CONDITION_ENDS.has(key)) {\n return false;\n }\n if (NEGATORS.has(key) || HEALTH_MARKERS.has(key)) {\n return false;\n }\n if (CONDITION_UNITS.has(key)) {\n return false;\n }\n return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));\n}\n\n/**\n * Read what the question asks the recipe to avoid or to serve.\n *\n * These are the parts of a question no recipe index answers. A page is not\n * named for what it leaves out, and the number of people at a table is not\n * printed in a title at all, so both are read here and handled as what they\n * are rather than as words to look for.\n */\nexport function readConditions(question: string): Conditions {\n const { conditions } = readQuestion(question);\n\n let servings: number | null = null;\n for (const phrase of SERVING_PHRASES) {\n const found = phrase.exec(question);\n if (found) {\n const value = Number(found[1]);\n if (Number.isFinite(value) && value > 0) {\n servings = value;\n break;\n }\n }\n }\n\n return { conditions, servings };\n}\n\n/* -------------------------------------------------------------------------- */\n/* The ladder */\n/* -------------------------------------------------------------------------- */\n\nexport function deriveWordings(question: string): Wording[] {\n const asked = tidy(question);\n const wordings: Wording[] = [];\n const seen = new Set<string>();\n\n const offer = (candidate: string, derivation: string): void => {\n const clean = tidy(candidate);\n if (clean === \"\") {\n return;\n }\n // Two wordings differing only in case or spacing are one wording to these\n // indexes, and sending both spends an interval to learn nothing.\n const key = clean.toLowerCase();\n if (seen.has(key)) {\n return;\n }\n seen.add(key);\n wordings.push({ query: clean, derivation });\n };\n\n offer(asked, \"the words as asked, condition words included\");\n\n const dish = dishWords(asked);\n offer(\n dish.join(\" \"),\n \"the words naming the dish, with the words framing the question and the conditions set aside, because a page is named for what it holds\",\n );\n\n offer(\n dish[0] ?? \"\",\n \"the leading dish word on its own, because an index that requires every word given returns nothing for a phrase and a great deal for the word naming the dish\",\n );\n\n // The ladder is three rungs by construction; the ceiling holds whatever a\n // future rung would add to it.\n return wordings.slice(0, MAX_WORDINGS_PER_SOURCE);\n}\n\n/**\n * Whether a row's title carries a word naming the dish that was asked for.\n *\n * This decides whether a wording answered, and nothing else: no row is dropped\n * and no row is moved for failing it. A source that ranks on the words it was\n * handed answers a sentence with a page of recipes that share its framing\n * words, and counting those as an answer would stop the search on the page that\n * proves it has not found the dish.\n *\n * Diacritics and punctuation fold away, so \"crêpes\" reads the same as \"crepes\"\n * and a namespace in front of a page name settles nothing. A question that\n * names no dish has nothing to check against, and every row passes.\n */\nexport function namesDish(title: string, question: string): boolean {\n const words = namingWords(question);\n if (words.length === 0) {\n return true;\n }\n return dishWordsMissing(title, question).length < words.length;\n}\n\n/**\n * The words naming the dish that a title does not carry.\n *\n * Sharing one word is what makes a row worth offering, and it is not what makes\n * a row the dish. \"Biscuits and gravy\" and a tin of Christmas biscuits share\n * the biscuit and nothing else, so a caller weighing one against the other has\n * to be told which of the words asked for went unanswered.\n *\n * Empty for a title carrying all of them, and empty for a question that names\n * no dish, since there is then nothing to go unanswered.\n */\nexport function dishWordsMissing(title: string, question: string): string[] {\n const haystack = ` ${fold(title)} `;\n return namingWords(question).filter((word) => !haystack.includes(` ${fold(word)}`));\n}\n\n/** The words of a question long enough to name what is cooked. */\nfunction namingWords(question: string): string[] {\n return dishWords(question).filter((word) => word.length > 2);\n}\n\nfunction fold(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/œ/gu, \"oe\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \");\n}\n\n/**\n * The words of a question that name what is cooked.\n *\n * The framing words go because they rank pages that merely share them. The\n * condition words go for a different reason, and it is worth stating: a search\n * for \"sans gluten\" ranks pages about gluten, and a search for \"6 personnes\"\n * ranks nothing at all, so carrying them into a derived wording buries the dish\n * without excluding anything. Dropping them silently would be the worse move,\n * since the recipe found may hold exactly what the person was avoiding, and\n * that is why the question as asked is sent first and every condition set aside\n * is named back to the caller.\n */\nfunction dishWords(question: string): string[] {\n return readQuestion(question).dish;\n}\n\n/**\n * The words of a question, lowercased.\n *\n * An apostrophe separates: \"l'origan\" is the article and the herb, and the herb\n * is the word an index holds. A hyphen joins, because \"pot-au-feu\" and\n * \"gluten-free\" are each one word wherever they are written.\n */\nfunction tokenise(value: string): string[] {\n return value\n .toLowerCase()\n .replace(/['’]/gu, \" \")\n .split(WORD_SEPARATORS)\n .map((word) => word.replace(/^-+|-+$/g, \"\"))\n .filter((word) => word !== \"\");\n}\n\nfunction tidy(value: string): string {\n return value.replace(/\\s+/g, \" \").trim();\n}\n","/**\n * The one place that talks to the sources.\n *\n * It asks them all at once and merges what comes back, so a question is\n * answered by whichever of them holds something. It imports nothing from the\n * MCP layer and is published on its own, so the same code serves a plain\n * script.\n *\n * A source that fails is reported as a source that failed. It is never folded\n * into the answer as an absence: \"Marmiton was unreachable\" and \"Marmiton holds\n * no such recipe\" are different statements about the world, and a caller that\n * cannot tell them apart will say the second when only the first is true.\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 { fromSource, invalidInput, timeout as timedOut } from \"../errors.js\";\nimport type {\n MergedSearch,\n RecipeDetail,\n RecipeRow,\n SourceId,\n SourceReport,\n WordingAttempt,\n} from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { resolveId } from \"./ids.js\";\nimport type { ResolvedId } from \"./ids.js\";\nimport { buildSources, selectSources } from \"./registry.js\";\nimport type { Readers } from \"./registry.js\";\nimport { MAX_WORDINGS_PER_SOURCE, deriveWordings, namesDish } from \"./wordings.js\";\nimport type { Wording } from \"./wordings.js\";\n\nexport type { SourceAdapter, ReadRecipe, ReadRows } from \"./adapter.js\";\nexport type { CookbookReader } from \"./cookbook.js\";\nexport type { MarmitonReader } from \"./marmiton.js\";\nexport type { Readers } from \"./registry.js\";\nexport type {\n MergedSearch,\n RecipeDetail,\n RecipeRow,\n SourceId,\n SourceProfile,\n SourceReport,\n WordingAttempt,\n} from \"../types.js\";\n\nconst LETTER_OR_DIGIT = /[\\p{L}\\p{N}]/u;\n\nexport interface RecipesClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n /** Stands in for one or more of the site readers. */\n readers?: Readers;\n /** Replaces the registry outright, for a program bringing its own corpora. */\n sources?: SourceAdapter[];\n}\n\n/** The largest number of rows any one source is asked for in a single call. */\nexport const MAX_LIMIT_PER_SOURCE = 25;\n\nexport interface SearchOptions {\n /**\n * Whether a question may be sent in wordings derived from it as well as in\n * the words it was written in.\n *\n * On unless a caller turns it off. A recipe index answers the words it is\n * handed, so a question written as a sentence comes back empty from a corpus\n * holding several of the dish, and an empty answer is read as a corpus that\n * holds nothing. Off, a caller gets exactly the wording they typed and the\n * answer names the wordings that were not sent.\n */\n fanOut?: boolean;\n}\n\n/**\n * The pacing this server owes each source, 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 setting is held to the same range\n * the environment parser enforces: `timeoutMs: 0` is the usual way of writing\n * \"no deadline\", and a retry count of a hundred thousand aimed at a site paced\n * in milliseconds is hours of traffic from a single call.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\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 userAgent:\n claimed === \"\" || claimed.includes(identifier) ? 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, 120_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 logLevel: config.logLevel ?? defaults.logLevel,\n };\n}\n\n/**\n * A wall clock over one source, as a backstop under the deadline that source's\n * own reader keeps.\n *\n * That reader times out its own requests, so this only matters when it cannot:\n * a socket that stalls without erroring, a stand-in reader, a bug in a\n * dependency free to change under a caret range. Without it, one source that\n * never answers holds the whole call open and the rows the others had ready in\n * milliseconds are never returned.\n *\n * The allowance covers every attempt a reader is entitled to make, plus the\n * pacing between them, so this never fires before the reader's own deadline has.\n */\nfunction withDeadline<T>(work: Promise<T>, ms: number, source: SourceAdapter): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const alarm = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () =>\n reject(\n timedOut(`${source.name} did not answer within ${ms}ms and was left out of this answer.`),\n ),\n ms,\n );\n // A pending timer must not hold the process open once the answer is out.\n timer.unref?.();\n });\n return Promise.race([work, alarm]).finally(() => clearTimeout(timer));\n}\n\n/** What one source produced for one question, whether or not it produced anything. */\ninterface Attempt {\n source: SourceAdapter;\n rows: RecipeRow[];\n /** Whether rows naming the dish were put in front of rows that do not. */\n preferredByName: boolean;\n cached: boolean;\n reportedTotal: number | null;\n reportedTotalMeans: string | null;\n skipped: number;\n error: { code: string; message: string; hint?: string } | null;\n wordings: WordingAttempt[];\n}\n\nfunction reportOf(attempt: Attempt, count: number): SourceReport {\n return {\n source: attempt.source.id,\n name: attempt.source.name,\n status: attempt.error ? \"failed\" : \"answered\",\n count,\n reportedTotal: attempt.reportedTotal,\n reportedTotalMeans: attempt.reportedTotalMeans,\n skipped: attempt.skipped,\n mixesReferencePages: attempt.source.mixesReferencePages,\n cached: attempt.cached,\n error: attempt.error,\n wordings: attempt.wordings,\n preferredByName: attempt.preferredByName,\n };\n}\n\n/** A wording that was never sent, and the reason it was not. */\nfunction heldBack(wording: Wording, because: string): WordingAttempt {\n return {\n query: wording.query,\n derivation: wording.derivation,\n ran: false,\n count: null,\n added: null,\n notRunBecause: because,\n error: null,\n };\n}\n\n/**\n * Keep the rows a wording brought back that no earlier wording already did, and\n * count how many of them name the dish that was asked for.\n *\n * The identifier names its source, so two sources minting the same reference\n * stay two rows while one recipe reached twice stays one.\n */\nfunction keepNewRows<T extends { id: string; title: string }>(\n read: { rows: readonly T[] },\n seen: Set<string>,\n rows: T[],\n question: string,\n): { added: number; onTopic: number } {\n let added = 0;\n let onTopic = 0;\n\n for (const row of read.rows) {\n if (seen.has(row.id)) {\n continue;\n }\n seen.add(row.id);\n rows.push(row);\n added += 1;\n if (namesDish(row.title, question)) {\n onTopic += 1;\n }\n }\n\n return { added, onTopic };\n}\n\n/**\n * Interleave rows so no source opens the list twice before another has opened\n * it once.\n *\n * Ranking them against each other would need a score every source carries, and\n * there is none: one has reader ratings, another has no author and no rating by\n * nature, and a single ordering would rank on a field most rows cannot have.\n * Taking one from each in turn is an order a caller can describe exactly, which\n * is what the answer does.\n */\nexport function interleave(groups: RecipeRow[][]): RecipeRow[] {\n const merged: RecipeRow[] = [];\n const longest = Math.max(0, ...groups.map((group) => group.length));\n for (let index = 0; index < longest; index += 1) {\n for (const group of groups) {\n const row = group[index];\n if (row) {\n merged.push(row);\n }\n }\n }\n return merged;\n}\n\nexport class RecipesClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly sources: SourceAdapter[];\n\n constructor(options: RecipesClientOptions = {}) {\n this.config = withGuarantees({ ...loadConfig(), ...options.config });\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.sources = options.sources ?? buildSources(this.config, options.readers ?? {});\n }\n\n /** What every source sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n get intervalMs(): number {\n return this.config.minIntervalMs;\n }\n\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n get maxRetries(): number {\n return this.config.maxRetries;\n }\n\n /** The sources this server reads, in the order an answer takes them. */\n get profiles(): Array<{ id: SourceId; name: string; language: string; homeUrl: string }> {\n return this.sources.map((source) => ({\n id: source.id,\n name: source.name,\n language: source.language,\n homeUrl: source.homeUrl,\n }));\n }\n\n /**\n * The backstop deadline over one source, covering every attempt it is\n * entitled to make and the pacing between them.\n */\n private get deadlineMs(): number {\n const attempts = this.config.maxRetries + 1;\n return this.config.timeoutMs * attempts + this.config.minIntervalMs * this.config.maxRetries;\n }\n\n /**\n * Ask every source the same question and merge the rows.\n *\n * The calls go out together, so the answer takes as long as the slowest\n * source rather than as long as all of them. Whichever of them fail, the rest\n * still come back, and the report says which was which.\n */\n async searchRecipes(\n query: string,\n limitPerSource: number,\n wanted?: readonly SourceId[],\n options: SearchOptions = {},\n ): Promise<MergedSearch> {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"A search needs something to look for.\", \"Name a dish or an ingredient.\");\n }\n // A recipe index handed nothing it can match on answers with the page it\n // shows by default, and those rows would come back as this question's\n // recipes. One character is a word in the scripts that write one that way,\n // so what is asked for is a letter or a digit rather than a length.\n if (!LETTER_OR_DIGIT.test(trimmed)) {\n throw invalidInput(\n `\"${trimmed}\" carries no letter and no digit, so there is no word to search for.`,\n \"Name a dish or an ingredient.\",\n );\n }\n\n const chosen = selectSources(this.sources, wanted);\n const limit = Math.max(1, Math.min(MAX_LIMIT_PER_SOURCE, Math.trunc(limitPerSource)));\n const fanOut = options.fanOut ?? true;\n\n const attempts = await Promise.all(\n chosen.map((source) => this.searchOne(source, trimmed, limit, fanOut)),\n );\n\n const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));\n return {\n rows: interleave(answered.map((one) => one.rows)),\n reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows.length)),\n };\n }\n\n /**\n * Ask one source the question in several wordings, and union what comes back.\n *\n * The wordings go out one after another rather than together, because each\n * source is paced and a burst would spend the whole ladder's politeness at\n * once. They stop early: a wording is only sent when the ones before it left\n * the source short of what was asked for, so the ordinary case where the\n * question names a dish costs one request.\n *\n * Each wording is a different search rather than a further page of the same\n * one. One of these sites serves a single page of results and disallows\n * paging past it, so a second page is never asked for, here or anywhere else.\n */\n private async searchOne(\n source: SourceAdapter,\n question: string,\n limit: number,\n fanOut: boolean,\n ): Promise<Attempt> {\n const derived = deriveWordings(question);\n const wordings: WordingAttempt[] = [];\n const rows: RecipeRow[] = [];\n const seen = new Set<string>();\n\n let cached = false;\n let skipped = 0;\n let reportedTotal: number | null = null;\n let reportedTotalMeans: string | null = null;\n let answered = false;\n let failure: { code: string; message: string; hint?: string } | null = null;\n /**\n * Rows whose title carries a word naming the dish.\n *\n * This is what \"enough\" counts, rather than rows. A source that ranks on\n * the words it was handed fills a page with recipes sharing a question's\n * framing words, and stopping on the size of that page would stop on the\n * evidence that the dish has not been found. Every row is kept either way.\n */\n let onTopic = 0;\n\n for (const [index, wording] of derived.entries()) {\n if (index > 0 && !fanOut) {\n wordings.push(\n heldBack(wording, \"fan_out was turned off, so only the words as asked were sent\"),\n );\n continue;\n }\n if (index >= MAX_WORDINGS_PER_SOURCE) {\n wordings.push(\n heldBack(\n wording,\n `the ceiling of ${MAX_WORDINGS_PER_SOURCE} searches of one source was reached`,\n ),\n );\n continue;\n }\n if (failure !== null) {\n wordings.push(\n heldBack(wording, `${source.name} did not answer the wording before this one`),\n );\n continue;\n }\n if (onTopic >= limit) {\n wordings.push(\n heldBack(\n wording,\n \"the wordings already sent returned as many rows naming the dish as were asked for\",\n ),\n );\n continue;\n }\n\n try {\n const read = await withDeadline(\n source.search(wording.query, limit),\n this.deadlineMs,\n source,\n );\n answered = true;\n cached = cached || read.cached;\n skipped += read.skipped;\n if (reportedTotal === null && reportedTotalMeans === null) {\n reportedTotal = read.reportedTotal;\n reportedTotalMeans = read.reportedTotalMeans;\n }\n if (read.skipped > 0) {\n this.logger.warn(\n `${source.name} sent ${read.skipped} row(s) this server could not read; they were left out.`,\n );\n }\n\n const kept = keepNewRows(read, seen, rows, question);\n const added = kept.added;\n onTopic += kept.onTopic;\n\n wordings.push({\n query: wording.query,\n derivation: wording.derivation,\n ran: true,\n count: read.rows.length,\n added,\n notRunBecause: null,\n error: null,\n });\n } catch (error) {\n const known = fromSource(error, source.name);\n this.logger.warn(\n `${source.name} did not answer \"${wording.query}\": [${known.code}] ${known.message}`,\n );\n const reason = {\n code: known.code,\n message: known.message,\n ...(known.details.hint ? { hint: known.details.hint } : {}),\n };\n wordings.push({\n query: wording.query,\n derivation: wording.derivation,\n ran: true,\n count: null,\n added: null,\n notRunBecause: null,\n error: reason,\n });\n failure = reason;\n }\n }\n\n // Where several wordings contributed, the rows naming the dish are put in\n // front of the rows that do not, each group keeping the order it arrived\n // in. Without this, a first wording answering with a page of near-misses\n // fills the limit and cuts away the rows a later wording found. A single\n // wording is left in the order its source returned it, since there is\n // nothing to rescue it from.\n const contributing = wordings.filter((attempt) => (attempt.added ?? 0) > 0).length;\n const ordered =\n contributing > 1\n ? [\n ...rows.filter((row) => namesDish(row.title, question)),\n ...rows.filter((row) => !namesDish(row.title, question)),\n ]\n : rows;\n\n // A source that answered one wording answered. Reporting it as failed\n // because a later wording timed out would hide the rows it did return\n // behind a claim that it said nothing.\n return {\n source,\n rows: ordered,\n preferredByName: contributing > 1,\n cached,\n reportedTotal,\n reportedTotalMeans,\n skipped,\n error: answered ? null : failure,\n wordings,\n };\n }\n\n /**\n * Read one recipe, from whichever source the identifier names.\n *\n * Only that source is called. Trying another after a miss would turn \"this\n * source has no page by this name\" into somebody else's recipe with a\n * different title, which is a worse answer than the absence it replaced.\n */\n async getRecipe(\n id: string,\n ): Promise<{ recipe: RecipeDetail; cached: boolean; read: ResolvedId }> {\n const read = resolveId(id, this.sources);\n try {\n const outcome = await withDeadline(\n read.source.getRecipe(read.reference),\n this.deadlineMs,\n read.source,\n );\n return { recipe: outcome.recipe, cached: outcome.cached, read };\n } catch (error) {\n throw fromSource(error, read.source.name);\n }\n }\n}\n","/**\n * compare_recipes: the same dish, as each source writes it.\n *\n * This is the one tool that exists because there is more than one source. A\n * French kitchen and an English one write the same dish differently, and the\n * difference is the answer: what each asks for, what each measures in, how many\n * it serves, and what each source publishes about a recipe at all.\n *\n * Nothing is ranked. The sources share no score, and declaring a winner would\n * mean inventing one.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { dishWordsMissing } from \"../sources/wordings.js\";\nimport type { RecipeDetail, SourceId } from \"../types.js\";\nimport { buildRecipeView, recipeSchema, renderYield, SECTIONS } from \"./recipeView.js\";\nimport type { RecipePayload, Section } from \"./recipeView.js\";\nimport type { RecipeView } from \"./recipeView.js\";\nimport {\n creditLine,\n fitLines,\n mustKeep,\n omittedLinesLine,\n noteTexts,\n ok,\n quoteForeign,\n roomForBody,\n reportNotes,\n reportSchema,\n toReportPayload,\n toToolError,\n withRead,\n} from \"./shared.js\";\nimport type { Note } from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/**\n * Why nothing was compared.\n *\n * A version that was offered and could not be read is a different statement\n * from no source offering one at all: the first says the dish is out there.\n */\nfunction nothingWasCompared(someWereUnread: boolean, dish: string): string {\n if (someWereUnread) {\n return `Every version of \"${quoteForeign(dish)}\" that was offered could not be read, so nothing was compared.`;\n }\n return `No source offered a recipe for \"${quoteForeign(dish)}\".`;\n}\n\nexport const compareRecipesDescription = [\n \"Take a dish and show how each source writes it, side by side.\",\n \"Each source's closest match is read in full, and all of them can be rescaled to the same number of servings so the ingredient lists stand comparison.\",\n \"The answer states what differs and leaves it there: the quantities each asks for, what each measures in, what each yields, and which fields each source publishes at all.\",\n \"No version is ranked above another. Some sources carry reader ratings and some have no author and no rating by nature, so there is no score they share.\",\n \"When only one source answers, the answer says so and shows that one rather than presenting part of a comparison as the whole of it.\",\n].join(\" \");\n\nexport const compareRecipesInput = strictInput({\n dish: z\n .string()\n .min(1)\n .max(200)\n .describe(\"The dish to compare, in any of the languages the sources publish in.\"),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale every version to this many, which is what makes the lists comparable.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\"])\n .describe(\"Which parts of each version to return. Several full recipes is a lot of text.\"),\n max_steps: z.number().int().min(1).max(100).default(10).describe(\"Steps to return per version.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n});\n\nexport const compareRecipesOutput = z.object({\n dish: z.string(),\n versions: z.array(recipeSchema),\n differences: z\n .array(z.string())\n .describe(\"What differs between the versions, stated as fact. No version is ranked.\"),\n per_source: z.array(reportSchema),\n notes: z.array(z.string()),\n});\n\nexport type CompareRecipesArgs = z.infer<typeof compareRecipesInput>;\n\n/** Ingredient lines each version shows in the text block. */\nconst TEXT_INGREDIENT_LINES = 8;\n\n/** Differences the text block states before pointing at the rest. */\nconst TEXT_DIFFERENCE_LINES = 4;\n\n/** Sources named the way a sentence names them. */\nfunction listNames(names: string[]): string {\n if (names.length <= 1) {\n return names.join(\"\");\n }\n return `${names.slice(0, -1).join(\", \")} and ${names.at(-1)}`;\n}\n\n/**\n * Which versions actually answer to the dish that was asked for.\n *\n * A title sharing one word with the question is not the dish: \"biscuits and\n * gravy\" and a tin of Christmas biscuits share the biscuit, and setting them\n * side by side would present a search that missed as a difference between two\n * traditions.\n */\nfunction weighTitlesAgainstTheDish(\n payloads: readonly RecipeView[\"payload\"][],\n dish: string,\n notes: RecipeView[\"notes\"][number][],\n): Array<{ payload: RecipeView[\"payload\"]; missing: readonly string[] }> {\n const weighed = payloads.map((payload) => ({\n payload,\n missing: dishWordsMissing(payload.title, dish),\n }));\n const carriesDish = weighed.some((one) => one.missing.length === 0);\n\n for (const { payload, missing } of weighed) {\n if (missing.length === 0) {\n continue;\n }\n notes.push(\n mustKeep(\n `${payload.source_name}'s closest row is ${quoteForeign(payload.title)}, whose title ` +\n `carries no ${missing.map((word) => `\"${quoteForeign(word)}\"`).join(\" and no \")}. ` +\n \"Read it as a candidate to check rather than as that dish.\",\n ),\n );\n }\n\n if (payloads.length > 0 && !carriesDish) {\n notes.push(\n `No version here carries the whole of \"${quoteForeign(dish)}\" in its title. These are ` +\n \"the closest rows each source returned for that spelling, so read them as candidates to \" +\n \"check rather than as that dish.\",\n );\n }\n\n if (payloads.length === 1 && carriesDish) {\n notes.push(\n mustKeep(\n \"This is one version rather than a comparison. Read it as what that one source publishes.\",\n ),\n );\n }\n\n // A difference is a statement about one dish written two ways. Setting a\n // row that answers to another name beside it turns a search that missed\n // into a claim about two traditions.\n\n return weighed;\n}\n\n/**\n * Say what the versions do differently, and say nothing where they agree.\n *\n * A field one source has and another does not is the most useful thing a\n * comparison can report, and the least tempting to turn into a ranking: a wiki\n * has no author because everyone edited the page, which says nothing about the\n * recipe. Where every version says the same thing there is no difference to\n * report, and reporting one anyway costs a reader's trust in the rest.\n *\n * A difference is only stated about a part of the recipe the answer carries.\n * The sections a call leaves out come back empty, and the answer says so, so a\n * sentence comparing one of them describes something the reader was told is not\n * there and cannot check.\n */\nfunction describeDifferences(\n recipes: RecipeDetail[],\n payloads: RecipePayload[],\n sections: readonly Section[],\n): string[] {\n if (recipes.length < 2) {\n return [];\n }\n const differences: string[] = [];\n const nameOf = (recipe: RecipeDetail) => recipe.sourceName;\n\n const stated = payloads.map((payload) => ({\n name: payload.source_name,\n text: payload.yield.original_text ?? \"no stated amount\",\n }));\n if (new Set(stated.map((one) => one.text)).size > 1) {\n differences.push(\n `${stated.map((one) => `${one.name} yields ${quoteForeign(one.text)}`).join(\"; \")}.`,\n );\n }\n\n const counts = payloads.map((payload) => payload.ingredients.length);\n if (new Set(counts).size > 1 && counts.every((value) => value > 0)) {\n differences.push(\n `${payloads\n .map(\n (payload) =>\n `${payload.source_name} lists ${payload.ingredients.length} ingredient lines`,\n )\n .join(\"; \")}.`,\n );\n }\n\n if (sections.includes(\"times\")) {\n const timed = recipes.filter((recipe) => recipe.totalMinutes !== null);\n const untimed = recipes.filter((recipe) => recipe.totalMinutes === null);\n if (timed.length > 0 && new Set(timed.map((recipe) => recipe.totalMinutes)).size > 1) {\n differences.push(\n `${timed.map((recipe) => `${nameOf(recipe)} states ${recipe.totalMinutes} minutes`).join(\"; \")}.`,\n );\n }\n if (timed.length > 0 && untimed.length > 0) {\n differences.push(\n `${listNames(untimed.map(nameOf))} states no time for this recipe, so there is nothing there to compare against.`,\n );\n }\n }\n\n const rated = recipes.filter((recipe) => recipe.rating !== null);\n if (rated.length > 0 && rated.length < recipes.length) {\n differences.push(\n `Only ${listNames(rated.map(nameOf))} carries a reader rating, so the versions cannot be compared on one.`,\n );\n }\n\n const credited = recipes.filter((recipe) => recipe.author !== null);\n if (credited.length > 0 && credited.length < recipes.length) {\n differences.push(\n `Only ${listNames(credited.map(nameOf))} credits an author; the rest are written by whoever edited the page.`,\n );\n }\n\n for (const recipe of recipes) {\n const license = recipe.license;\n if (license === null) {\n continue;\n }\n differences.push(\n `${nameOf(recipe)} publishes under ${quoteForeign(license.title)}, which asks for attribution.`,\n );\n }\n const silent = recipes.filter((recipe) => recipe.license === null);\n if (silent.length > 0 && silent.length < recipes.length) {\n differences.push(\n `${listNames(silent.map(nameOf))} states no terms of use. Silence is not permission.`,\n );\n }\n\n return differences;\n}\n\nexport async function runCompareRecipes(\n client: RecipesClient,\n args: CompareRecipesArgs,\n): Promise<ToolResult> {\n try {\n // A few rows from each source is enough: the point is the traditions side\n // by side rather than a long list from any of them.\n const merged = await client.searchRecipes(args.dish, 3);\n\n const best = new Map<SourceId, string>();\n for (const row of merged.rows) {\n if (!best.has(row.source)) {\n best.set(row.source, row.id);\n }\n }\n\n const offered = [...best.entries()];\n const reads = await Promise.allSettled(offered.map(([, id]) => client.getRecipe(id)));\n\n const recipes: RecipeDetail[] = [];\n /** Sources whose row was found and whose page then could not be read. */\n const unread = new Map<SourceId, { code: string; message: string }>();\n /** Sources whose row was opened, whichever way that went. */\n const opened = new Set<SourceId>();\n\n reads.forEach((read, index) => {\n const source = offered[index]?.[0];\n if (source === undefined) {\n return;\n }\n opened.add(source);\n if (read.status === \"fulfilled\") {\n recipes.push(read.value.recipe);\n return;\n }\n const reason = read.reason as { code?: string; message?: string } | undefined;\n unread.set(source, {\n code: reason?.code ?? \"network_error\",\n message: reason?.message ?? \"unknown\",\n });\n });\n\n const views = recipes.map((recipe) =>\n buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n label: recipe.sourceName,\n // The choice of sections belongs to the call rather than to a version,\n // so it is said once below instead of once per recipe.\n announceSections: false,\n }),\n );\n const payloads = views.map((view) => view.payload);\n\n const notes: Note[] = reportNotes(merged.reports);\n for (const view of views) {\n notes.push(...view.notes);\n }\n\n const omitted = SECTIONS.filter((section) => !args.sections.includes(section));\n if (omitted.length > 0) {\n notes.push(\n `Not requested, so not returned on any version: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n // Three reasons a source can be missing from a comparison, and they are\n // three different statements about the world. Reading a fetch failure as\n // \"that source offered nothing\" is the one that turns a bad minute into a\n // claim about what a corpus holds.\n for (const report of merged.reports) {\n if (recipes.some((recipe) => recipe.source === report.source)) {\n continue;\n }\n const failedRead = unread.get(report.source);\n if (report.status === \"failed\") {\n notes.push(\n mustKeep(\n `${report.name} is missing from this comparison because its search did not answer (${quoteForeign(\n `${report.error?.code ?? \"unknown\"}: ${report.error?.message ?? \"unknown\"}`,\n )}). Nothing here is evidence about what it holds.`,\n ),\n );\n } else if (failedRead) {\n notes.push(\n mustKeep(\n `${report.name}'s search answered and offered a version, and that version could not be read (${quoteForeign(\n `${failedRead.code}: ${failedRead.message}`,\n )}). The failure is in the reading, so nothing here says whether ${report.name} holds this dish.`,\n ),\n );\n } else {\n notes.push(\n mustKeep(\n `${report.name} answered and offered nothing close enough to \"${quoteForeign(args.dish)}\" to compare.`,\n ),\n );\n }\n }\n\n // One of these indexes answers almost any spelling with its closest row, so\n // a dish nobody publishes still comes back as a recipe. Presenting that row\n // as a source's version of the dish states as fact the one thing the search\n // did not establish, and sharing one word of a name is not carrying it:\n // \"biscuits and gravy\" and a tin of Christmas biscuits share the biscuit.\n const weighed = weighTitlesAgainstTheDish(payloads, args.dish, notes);\n const comparable = recipes.filter((recipe) =>\n weighed.some(\n ({ payload, missing }) =>\n missing.length === 0 && payload.id === recipe.id && payload.source === recipe.source,\n ),\n );\n const differences =\n comparable.length === payloads.length\n ? describeDifferences(recipes, payloads, args.sections as readonly Section[])\n : [];\n if (payloads.length > 1 && differences.length === 0 && comparable.length < payloads.length) {\n notes.push(\n mustKeep(\n \"Nothing is set side by side here: not every version's title carries the dish that was \" +\n \"asked for, so what separates them may be that they are different dishes.\",\n ),\n );\n }\n if (payloads.length > 1) {\n notes.push(\n \"Quantities are shown in the units each source published. Nothing was converted between \" +\n \"measuring systems, so a gram in one version and a cup in another are two ways of \" +\n \"writing a recipe rather than one quantity restated.\",\n );\n }\n\n const credit = creditLine(\n payloads.map((payload) => ({ attribution: payload.attribution, url: payload.url })),\n );\n // The differences a reader most needs are the ones about the recipes\n // themselves, and they come first. The rest stay in the structured output\n // rather than taking the room the versions need.\n const shownDifferences = differences.slice(0, TEXT_DIFFERENCE_LINES);\n const moreDifferences = differences.length - shownDifferences.length;\n const differencesBlock =\n differences.length > 0\n ? `What differs:\\n${shownDifferences.map((line) => `- ${line}`).join(\"\\n\")}${\n moreDifferences > 0\n ? `\\n- … and ${moreDifferences} more, in full in the structured output`\n : \"\"\n }\\n\\n`\n : \"\";\n\n // Every version has to fit, so the room left after the differences and the\n // trailer is shared out equally: each version shows an opening rather than\n // the first one filling the block and the rest being cut away.\n const share =\n payloads.length === 0\n ? 0\n : (roomForBody({ notes, credit }) - differencesBlock.length) / payloads.length;\n\n const versionBlocks = payloads.map((payload) => {\n const head = [\n `${quoteForeign(payload.source_name)}: ${quoteForeign(payload.title)}`,\n ` ${quoteForeign(payload.url)}`,\n // The yield is the page's, and the list under it is the one the factor\n // produced. Stating the first alone above the second reads as a list\n // for that many people.\n ` ${renderYield(payload)}`,\n ];\n const listed = payload.ingredients\n .slice(0, TEXT_INGREDIENT_LINES)\n .map((entry) => ` - ${quoteForeign(entry.text)}`);\n const fitted = fitLines(listed, share - head.join(\"\\n\").length);\n\n const lines = [...head, ...fitted.lines];\n const hidden = payload.ingredients.length - fitted.lines.length;\n if (hidden > 0) {\n lines.push(\n ` ${omittedLinesLine(fitted.lines.length, payload.ingredients.length, \"ingredient lines\")}`,\n );\n }\n return lines.join(\"\\n\");\n });\n\n const body =\n payloads.length === 0\n ? nothingWasCompared(unread.size > 0, args.dish)\n : versionBlocks.join(\"\\n\\n\");\n\n // What differs comes first. It is the answer to the question that was\n // asked, and it is short, so it survives a text block cut to fit while\n // several full ingredient lists never could.\n const summary = `${differencesBlock}${body}`;\n\n return ok(\n {\n dish: args.dish,\n versions: payloads,\n differences,\n // Two moments per source, told apart: whether the search answered, and\n // what became of the row it offered.\n per_source: merged.reports.map((report) => {\n const payload = toReportPayload(report);\n if (!opened.has(report.source)) {\n return payload;\n }\n const failedRead = unread.get(report.source);\n return withRead(\n payload,\n failedRead\n ? { status: \"failed\", error: { code: failedRead.code, message: failedRead.message } }\n : { status: \"read\", error: null },\n );\n }),\n notes: noteTexts(notes),\n },\n summary,\n { notes, credit },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n\nexport type { SourceReport } from \"../types.js\";\n","/**\n * Turning one recipe into the payload a tool returns.\n *\n * Both `get_recipe` and `compare_recipes` need the same thing: a yield stated\n * as published and as asked for, an ingredient list scaled to match, and the\n * sentences that qualify both. Sharing it means the two tools cannot drift into\n * describing the same recipe differently.\n */\n\nimport { z } from \"zod\";\nimport { passthroughIngredients, scaleIngredients } from \"../recipe/scale.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport { headingFor } from \"../recipe/sections.js\";\nimport type { PagePart } from \"../recipe/sections.js\";\nimport type { RecipeDetail } from \"../types.js\";\nimport {\n ingredientSchema,\n labelNote,\n mustKeep,\n quoteForeign,\n toIngredientPayload,\n truncate,\n} from \"./shared.js\";\nimport type { Note } from \"./shared.js\";\n\nconst SERVING_WORD = /personne|people|serving|portion|part\\b/i;\n\n/**\n * What the other half of the recipe holds, when this half is being read alone.\n *\n * A page whose ingredients were read and whose method was not is a page that\n * has one, and saying so is what stops a caller concluding the recipe is a list\n * with no procedure.\n */\nfunction whatTheOtherPartHolds(\n part: PagePart,\n recipe: { steps: readonly unknown[]; ingredients: readonly unknown[] },\n): string | null {\n if (part === \"ingredients\") {\n return recipe.steps.length > 0\n ? `${recipe.steps.length} step(s) of method were read from it`\n : null;\n }\n return recipe.ingredients.length > 0\n ? `${recipe.ingredients.length} ingredient line(s) were read from it`\n : null;\n}\n\nexport const yieldSchema = z.object({\n original_count: z\n .number()\n .nullable()\n .describe(\"Servings the page states as a number. Null when it states none.\"),\n original_max: z\n .number()\n .nullable()\n .describe(\"The upper end when the page states a span, as in '4 à 6 personnes'.\"),\n original_text: z\n .string()\n .nullable()\n .describe(\n \"The yield in the page's own words. '4 à 6 personnes' and '4 personnes' are different claims, so the wording is kept.\",\n ),\n requested: z.number().int().nullable().describe(\"Servings asked for, when any were.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"What the yield counts when it counts something other than people, such as 'balls'.\"),\n factor: z\n .number()\n .nullable()\n .describe(\"What the quantities were multiplied by. Null when nothing was rescaled.\"),\n});\n\nexport const ratingSchema = z.object({\n value: z.number(),\n count: z.number().int().nullable().describe(\"How many readers rated it, when the source says.\"),\n max: z\n .number()\n .nullable()\n .describe(\"The top of the scale. A rating read without its scale means nothing.\"),\n});\n\nexport const recipeSchema = z.object({\n id: z.string(),\n source: z.string(),\n source_name: z.string(),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language this recipe is written in.\"),\n title: z.string(),\n url: z.string(),\n image_url: z.string().nullable(),\n yield: yieldSchema,\n ingredients: z.array(ingredientSchema),\n steps: z.array(z.string()),\n prep_minutes: z.number().int().nullable(),\n cook_minutes: z.number().int().nullable(),\n total_minutes: z.number().int().nullable(),\n category: z.string().nullable(),\n author: z\n .string()\n .nullable()\n .describe(\"Null on a source written by everyone who edited the page.\"),\n rating: ratingSchema.nullable().describe(\"Null on a source that carries no reader rating.\"),\n nutrition: z\n .record(z.string(), z.string())\n .nullable()\n .describe(\"The panel as published, key by key. Figures the page omits are absent, never zero.\"),\n equipment: z.array(z.string()),\n tips: z.array(z.string()),\n license: z\n .object({ title: z.string(), url: z.string() })\n .nullable()\n .describe(\"Terms the page is published under, when the source states them.\"),\n attribution: z.string().describe(\"Show this, with the url, when repeating anything from here.\"),\n sections_returned: z\n .array(z.string())\n .describe(\"The sections this answer carries. Anything outside this list was not looked at.\"),\n sections_omitted: z\n .array(z.string())\n .describe(\n \"Sections nobody asked for. A field belonging to one of these is empty because it was not requested, never because the page states nothing.\",\n ),\n scaling_summary: z.object({\n scaled_count: z.number().int(),\n rounded_count: z.number().int(),\n unscaled_count: z.number().int(),\n }),\n});\n\nexport type RecipePayload = z.infer<typeof recipeSchema>;\n\n/** The sections a caller can ask for, since a full recipe is a lot of text. */\nexport const SECTIONS = [\n \"ingredients\",\n \"steps\",\n \"times\",\n \"nutrition\",\n \"tips\",\n \"equipment\",\n] as const;\nexport type Section = (typeof SECTIONS)[number];\n\nexport interface RecipeView {\n payload: RecipePayload;\n notes: Note[];\n ingredients: ScaledIngredient[];\n}\n\n/**\n * Work out what to multiply by, and say when nothing can be.\n *\n * A page that states no number of servings cannot be rescaled: dividing by a\n * yield nobody wrote would answer with quantities for a number of people the\n * page never claimed. Such a recipe comes back as published, and the answer\n * says why.\n */\nfunction resolveFactor(\n recipe: RecipeDetail,\n servings: number | null,\n): { factor: number | null; notes: Note[] } {\n if (servings === null) {\n return { factor: null, notes: [] };\n }\n\n if (recipe.yieldCount === null || recipe.yieldCount <= 0) {\n return {\n factor: null,\n notes: [\n mustKeep(\n `${recipe.sourceName} states no number of servings for this recipe, so the quantities are ` +\n \"as published. Use scale_ingredients with a factor you choose if you know what it serves.\",\n ),\n ],\n };\n }\n\n const factor = servings / recipe.yieldCount;\n const notes: Note[] = [];\n\n if (factor === 1) {\n return {\n factor: 1,\n notes: [`The page already yields ${recipe.yieldCount}, so nothing was multiplied.`],\n };\n }\n\n // A page that says \"4 à 6 personnes\" has named a span, and there is no one\n // factor that serves both ends of it. Scaling from the lower end is the\n // choice that leaves nobody short, and saying so is what stops the number\n // being read as the page's own arithmetic.\n if (recipe.yieldMax !== null && recipe.yieldMax > recipe.yieldCount) {\n notes.push(\n `The page states a span rather than a number: ${quoteForeign(recipe.yieldText ?? \"\")}. ` +\n `The factor was taken from the lower end, so ${servings} is what this makes at that end. ` +\n `Taken from ${recipe.yieldMax} it would be ${round(servings / recipe.yieldMax)} instead.`,\n );\n }\n\n const unit = recipe.yieldUnit;\n if (unit && !SERVING_WORD.test(unit)) {\n notes.push(\n `This page states its yield in ${quoteForeign(unit)} rather than in servings, so asking for ` +\n `${servings} multiplied the quantities by ${round(factor)} to give ${servings} ${quoteForeign(unit)}.`,\n );\n }\n\n return { factor, notes };\n}\n\nfunction round(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\n/** One part of a recipe, in the words an answer needs to talk about it. */\ninterface PartWords {\n part: PagePart;\n /** A single entry of it, as a sentence names one. */\n entry: string;\n /** The whole of it, as a page publishes it. */\n whole: string;\n /** The reading of an empty one that has to be shut off. */\n caution: string;\n}\n\nconst INGREDIENTS_PART: PartWords = {\n part: \"ingredients\",\n entry: \"ingredient line\",\n whole: \"ingredient list\",\n caution: \"An empty list here is never evidence that an ingredient is absent from the dish.\",\n};\n\nconst METHOD_PART: PartWords = {\n part: \"method\",\n entry: \"step of method\",\n whole: \"method\",\n caution: \"An empty method here is never evidence that the dish is made without one.\",\n};\n\n/**\n * What a part of a recipe that came back empty is allowed to say.\n *\n * Reading nothing off a page is a fact about this server, and stating it as a\n * fact about the page turns a failure of this reader into a claim the page\n * never made. Someone looking for a dish without butter, told that a page\n * publishes no ingredients, concludes there is no butter in it; the page can\n * open on butter.\n *\n * Two things the page itself shows settle it. A heading announcing the part\n * says the page published one. Another part of the recipe that was read says\n * the page is a recipe. Either of them makes an empty result this server's\n * failure, and the answer says so. Where the page shows neither, the answer\n * says the two cannot be told apart from here rather than picking whichever\n * reads more fluently.\n */\nfunction emptyPartNote(recipe: RecipeDetail, words: PartWords): Note {\n const heading = headingFor(words.part, recipe.publishedSections);\n const counterpart = whatTheOtherPartHolds(words.part, recipe);\n\n const shown = [\n heading === null ? null : `the page heads a section \"${quoteForeign(heading)}\"`,\n counterpart,\n ].filter((clause): clause is string => clause !== null);\n\n if (shown.length > 0) {\n return mustKeep(\n `No ${words.entry} was read from this page, yet ${shown.join(\" and \")}. This server failed ` +\n `to read the ${words.whole} the page carries; read it at the url. ${words.caution}`,\n );\n }\n\n // Naming the headings as evidence requires having seen them, and a source\n // that reports none has shown nothing either way.\n const looked = recipe.publishedSections === null ? \"\" : \", which heads no section announcing one\";\n return mustKeep(\n `No ${words.entry} was read from this page${looked}. A page publishing no ${words.whole} and ` +\n \"one written in a layout this server cannot follow look the same from here, so read the url \" +\n `before settling which this is. ${words.caution}`,\n );\n}\n\nexport interface BuildOptions {\n servings: number | null;\n sections: readonly Section[];\n /** Steps to return, since a long method is the bulk of a recipe. */\n maxSteps?: number;\n /** Characters kept per step. */\n maxStepChars?: number;\n /**\n * Names this recipe at the front of each of its notes. An answer carrying\n * several recipes needs to say which one a note is about; an answer carrying\n * one does not, and the name would only repeat what the answer already says.\n */\n label?: string;\n /**\n * Whether the choice of sections is said here. An answer built from several\n * recipes says it once for the call rather than once per recipe.\n */\n announceSections?: boolean;\n}\n\nexport function buildRecipeView(recipe: RecipeDetail, options: BuildOptions): RecipeView {\n const { factor, notes: yieldNotes } = resolveFactor(recipe, options.servings);\n const wants = (section: Section) => options.sections.includes(section);\n\n const ingredients =\n factor === null || factor === 1\n ? passthroughIngredients(recipe.ingredients, recipe.language)\n : scaleIngredients(recipe.ingredients, { factor, language: recipe.language });\n\n const notes: Note[] = [...yieldNotes];\n\n // Returning an empty list in silence reads as a recipe that needs no\n // ingredients, and an empty method as a dish that needs no cooking.\n if (wants(\"ingredients\") && recipe.ingredients.length === 0) {\n notes.push(emptyPartNote(recipe, INGREDIENTS_PART));\n }\n if (wants(\"steps\") && recipe.steps.length === 0) {\n notes.push(emptyPartNote(recipe, METHOD_PART));\n }\n\n const rounded = ingredients.filter((entry) => entry.scaling === \"rounded\");\n const unscaled = ingredients.filter((entry) => entry.scaling === \"unscaled\");\n\n if (rounded.length > 0) {\n notes.push(\n `${rounded.length} line(s) did not land on the exact product; each says in its own note ` +\n \"what happened to it.\",\n );\n }\n if (unscaled.length > 0 && factor !== null && factor !== 1) {\n notes.push(\n `${unscaled.length} line(s) carry no quantity to multiply and are repeated as published.`,\n );\n }\n if (recipe.license) {\n notes.push(\n mustKeep(\n `Published under ${quoteForeign(recipe.license.title)}: ${quoteForeign(recipe.license.url)}`,\n ),\n );\n }\n\n const omitted = SECTIONS.filter((section) => !wants(section));\n if (options.announceSections !== false && omitted.length > 0) {\n notes.push(\n `Not requested, so not returned: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n const steps = wants(\"steps\") ? recipe.steps : [];\n const shownSteps = steps\n .slice(0, options.maxSteps ?? steps.length)\n .map((step) => truncate(step, options.maxStepChars ?? step.length));\n if (steps.length > shownSteps.length) {\n notes.push(\n `The method runs to ${steps.length} steps and ${shownSteps.length} are here. Raise ` +\n \"'max_steps' for the rest.\",\n );\n }\n\n const payload: RecipePayload = {\n id: recipe.id,\n source: recipe.source,\n source_name: recipe.sourceName,\n language: recipe.language,\n title: recipe.title,\n url: recipe.url,\n image_url: recipe.imageUrl,\n yield: {\n original_count: recipe.yieldCount,\n original_max: recipe.yieldMax,\n original_text: recipe.yieldText,\n requested: options.servings,\n unit: recipe.yieldUnit,\n factor: factor === null ? null : round(factor),\n },\n ingredients: wants(\"ingredients\") ? ingredients.map(toIngredientPayload) : [],\n steps: shownSteps,\n prep_minutes: wants(\"times\") ? recipe.prepMinutes : null,\n cook_minutes: wants(\"times\") ? recipe.cookMinutes : null,\n total_minutes: wants(\"times\") ? recipe.totalMinutes : null,\n category: recipe.category,\n author: recipe.author,\n rating: recipe.rating,\n nutrition: wants(\"nutrition\") ? recipe.nutrition : null,\n equipment: wants(\"equipment\") ? recipe.equipment : [],\n tips: wants(\"tips\") ? recipe.tips : [],\n license: recipe.license,\n attribution: recipe.attribution,\n sections_returned: [...options.sections],\n sections_omitted: omitted,\n scaling_summary: {\n scaled_count: ingredients.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: rounded.length,\n unscaled_count: unscaled.length,\n },\n };\n\n if (options.label) {\n const label = quoteForeign(options.label);\n return { payload, notes: notes.map((line) => labelNote(label, line)), ingredients };\n }\n return { payload, notes, ingredients };\n}\n\n/** The yield as a reader would say it, with what was asked for beside it. */\nexport function renderYield(payload: RecipePayload): string {\n const published = payload.yield.original_text ?? \"an amount the page does not state\";\n if (payload.yield.factor === null) {\n return `Yields ${quoteForeign(published)} (as published).`;\n }\n return `Yields ${quoteForeign(published)} as published, scaled by ${payload.yield.factor} for ${payload.yield.requested}.`;\n}\n","/**\n * Which language an ingredient line is written in.\n *\n * A list can hold French lines and English lines at once, because a cook\n * comparing two versions of a dish keeps both in front of them. The language\n * decides how a number is written, how a noun agrees with it, and which\n * vocabulary of measures applies, so it is settled per line rather than once\n * for a whole list.\n */\n\nexport type Language = \"fr\" | \"en\";\n\n/** What a caller may ask for: a language, or a reading taken per line. */\nexport type LanguageChoice = Language | \"auto\";\n\n/**\n * Structural words, which are the reliable signal.\n *\n * A line names its ingredient in whatever language the page is written in, and\n * an ingredient name travels: a French recipe asks for \"Mountain Dew\" and an\n * English one for \"crème fraîche\". The small words that hold a line together do\n * not travel, so \"de\", \"à\" and \"of\" say more about the line than the name of\n * the food does.\n */\nconst FRENCH_WORDS =\n /(?:^|[\\s(])(?:de|du|des|à|au|aux|la|le|les|un|une|quelques|dans|avec|sans|pour|selon|environ)(?=[\\s),.]|$)|\\bd'|\\bl'|\\bqu'/iu;\n\n/**\n * The bare article \"a\" is left out on purpose. French recipes abbreviate\n * \"cuillère à soupe\" as \"c a s\", dropping the accent, and counting that \"a\" as\n * English evidence would send a French line through the English vocabulary.\n */\nconst ENGLISH_WORDS =\n /(?:^|[\\s(])(?:of|an|the|and|with|without|for|into|about|each|per|plus|freshly|finely|roughly|coarsely|optional)(?=[\\s),.]|$)/iu;\n\n/** Letters English does not write, which no ingredient name borrows either. */\nconst FRENCH_LETTERS = /[àâäçéèêëîïôöùûüœ]/iu;\n\n/**\n * Foods each language names its own way.\n *\n * Weak evidence, and deliberately so: a name is the part of a line most likely\n * to be borrowed, and a structural word outranks any number of these. It is\n * enough to settle a line that carries nothing else, such as \"6 oeufs\", where\n * every other signal is silent.\n */\nconst FRENCH_FOODS =\n /\\b(?:oeufs?|œufs?|sel|poivre|sucres?|farine|beurre|lait|crème|creme|huile|ail|oignons?|fromage|pain|poulet|boeuf|bœuf|riz|pâtes|pates|jambon|citrons?|pommes?|vin|eau|persil|thym|miel|jaunes?|blancs?)\\b/iu;\n\nconst ENGLISH_FOODS =\n /\\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|garlic|onions?|cheese|bread|chicken|beef|rice|pasta|ham|lemons?|apples?|wine|water|parsley|thyme|honey|yolks?|whites?)\\b/iu;\n\n/** Weight of one structural signal, which outranks a vocabulary match. */\nconst STRUCTURAL = 3;\n/** Weight of a measure only one of the two vocabularies carries. */\nconst VOCABULARY = 2;\n/** Weight of a food name, which is the signal a line borrows most readily. */\nconst NAME = 1;\n\nexport interface LanguageEvidence {\n language: Language;\n french: number;\n english: number;\n}\n\n/**\n * Read the language off one line.\n *\n * `frenchUnit` and `englishUnit` say whether each vocabulary recognised a\n * measure in the line. They count for less than a structural word, because a\n * French line can name an English measure in passing: \"1 dose (cup) de Mountain\n * Dew\" is French with an English gloss inside it, and the \"de\" is what settles\n * it.\n *\n * English is the answer when nothing points either way, which is what an\n * ingredient with no small words and no measure gets.\n */\nexport function readLanguage(\n line: string,\n evidence: { frenchUnit: boolean; englishUnit: boolean } = {\n frenchUnit: false,\n englishUnit: false,\n },\n): LanguageEvidence {\n let french = 0;\n let english = 0;\n\n if (FRENCH_WORDS.test(line)) {\n french += STRUCTURAL;\n }\n if (FRENCH_LETTERS.test(line)) {\n french += STRUCTURAL;\n }\n if (ENGLISH_WORDS.test(line)) {\n english += STRUCTURAL;\n }\n if (evidence.frenchUnit) {\n french += VOCABULARY;\n }\n if (evidence.englishUnit) {\n english += VOCABULARY;\n }\n if (FRENCH_FOODS.test(line)) {\n french += NAME;\n }\n if (ENGLISH_FOODS.test(line)) {\n english += NAME;\n }\n\n return { language: french > english ? \"fr\" : \"en\", french, english };\n}\n\n/** The language a list is written in, when a caller wants one answer for all of it. */\nexport function readListLanguage(lines: string[]): Language {\n let french = 0;\n let english = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n }\n return french > english ? \"fr\" : \"en\";\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst FUL_MEASURE = /^[a-z]{3,}fuls?$/;\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst HOUSEHOLD_MEASURE =\n /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in both languages: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French line and an English line alike,\n * and they take no plural mark in either.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n // A page glossing a metric weight names the pound, which French writes as a\n // livre: \"450 g (1 livre) de spaghetti\".\n livre: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n // A dish of the book standing as an ingredient of another: \"1 recipe Flaky\n // Pie Crust\". The count is what the word measures, so the plural mark belongs\n // on it and not on the name of the dish.\n recipe: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n recipes: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return (\n text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n};\n\nexport function unitKeys(language: Language): string[] {\n return UNIT_KEYS[language];\n}\n\nexport function lookupUnit(text: string, language: Language): UnitInfo | null {\n return VOCABULARY[language][normalizeUnitKey(text)] ?? null;\n}\n\n/**\n * Matches a number followed by a measure anywhere in a piece of text.\n *\n * Used to spot a quantity the parser did not take, such as the second half of\n * \"1 Tbsp vanilla sugar OR 1 tsp vanilla extract\", which would otherwise sit in\n * a scaled line still saying what the original said.\n */\nconst EMBEDDED: Record<Language, RegExp> = {\n fr: buildEmbedded(\"fr\"),\n en: buildEmbedded(\"en\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // A quantity keeps the same shape whether the measure stands against the\n // figure or behind the word introducing it: \"3/4 tasse\" and \"3/4 de tasse\"\n // are one quantity written twice. Reading only the first spelling leaves the\n // second sitting on a scaled line, still saying what the page said.\n const joiner = \"(?:(?:de|du|des|d|of|a|an|the)\\\\s+)?\";\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*${joiner}(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\n/**\n * Nouns naming what a food is sold in.\n *\n * A measure written in front of one of them gives what that container holds:\n * \"12 oz can\" is the size of the tin rather than an amount to weigh out, and\n * doubling the recipe means opening a second tin rather than finding a bigger\n * one.\n *\n * Written in either language, because what a word names has nothing to do with\n * which language names it.\n */\nexport const CONTAINER_NOUN =\n /^(?:cans?|tins?|jars?|packets?|packages?|packs?|box|boxes|bottles?|cartons?|bags?|tubs?|sachets?|blocks?|blocs?|boites?|pots?|briques?|bocaux|bocals?|barquettes?)\\b/i;\n\n/**\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a French line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(normalizeUnitKey(text));\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a French measure named with a container or a gesture the vocabulary has\n * no entry for.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it: a bouchon, a poignée, a ramequin hold what they hold, and the recipe's\n * proportion lives in how many are asked for. French marks that grammatically,\n * by placing the noun between the amount and the partitive that introduces the\n * thing measured: \"un bouchon de rhum\", \"2 bouquets de persil\". A noun in that\n * position measures whatever follows it, so a container nobody thought to list\n * is read by the same rule as the ones that are, and the vocabulary only has to\n * carry the words whose plural or spelling the rule would get wrong.\n *\n * The amount has to come first. A line opening on the noun, as in \"beurre\n * pommade\", carries no quantity, and inventing one from the grammar would put a\n * number where the recipe wrote none.\n */\nexport function readPartitiveMeasure(text: string): { unit: UnitInfo; rest: string } | null {\n const match = HEAD_BEFORE_DE.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n if (word.length < 3) {\n return null;\n }\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {\n return null;\n }\n if (lookupUnit(word, \"fr\")) {\n return null;\n }\n\n const canonical = frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!FUL_MEASURE.test(key)) {\n return null;\n }\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (EAUX_ENDING.test(word)) {\n return word.slice(0, -1);\n }\n if (AUX_ENDING.test(word)) {\n return `${word.slice(0, -3)}al`;\n }\n if (VOWEL_THEN_S.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/** The plural French writes for a noun, or the noun itself when it takes no mark. */\nfunction frenchPlural(word: string): string {\n if (SIBILANT_LETTER.test(word)) {\n return word;\n }\n if (EAU_ENDING.test(word)) {\n return `${word}x`;\n }\n if (AL_ENDING.test(word)) {\n return `${word.slice(0, -2)}aux`;\n }\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n};\n\n/** The everyday equivalence for an approximate measure, when there is a settled one. */\nexport function approximateEquivalent(unit: UnitInfo): string | null {\n return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;\n}\n\n/**\n * Ladders, used to keep a scaled amount at a human size.\n *\n * Multiplying a recipe by thirty is arithmetically fine and practically poor:\n * \"8335 g of sugar\" is correct, and nobody weighs eight thousand grams. Each\n * measured unit therefore knows the unit above and below it, so a large amount\n * climbs the ladder and a small one comes back down. Each system keeps its own\n * ladder, because converting between them changes what the recipe said.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n /** Which vocabulary the step's spelling comes from. */\n language: Language;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"kg\", per: 1000, language: \"en\" },\n ml: { to: \"l\", per: 1000, language: \"en\" },\n cl: { to: \"l\", per: 100, language: \"en\" },\n dl: { to: \"l\", per: 10, language: \"en\" },\n oz: { to: \"lb\", per: 16, language: \"en\" },\n ounce: { to: \"pound\", per: 16, language: \"en\" },\n \"fl oz\": { to: \"pint\", per: 20, language: \"en\" },\n pint: { to: \"quart\", per: 2, language: \"en\" },\n quart: { to: \"gallon\", per: 4, language: \"en\" },\n};\n\nconst DEMOTIONS: Record<string, UnitStep> = {\n // Spoons and cups hold a fixed volume, so a share of one is stated in the\n // smaller spoon rather than as a fraction no measuring set carries. The\n // spelling of the step matches the spelling of the unit it comes from, so a\n // line written in abbreviations stays in abbreviations, and a French line\n // stays in French.\n cup: { to: \"tablespoon\", per: 16, language: \"en\" },\n tablespoon: { to: \"teaspoon\", per: 3, language: \"en\" },\n tbsp: { to: \"tsp\", per: 3, language: \"en\" },\n \"cuillere a soupe\": { to: \"cuillere a cafe\", per: 3, language: \"fr\" },\n tasse: { to: \"cuillere a soupe\", per: 16, language: \"fr\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A gesture keeps its own answer. A pincée, a poignée, a pinch is the amount a\n * hand produces in one go, and there is no half of a hand: the size of one is\n * the cook's and the count is the whole of what the measure can say, so the\n * count lands on a whole and the line reports that it moved.\n *\n * A doubtful word takes the half. A cook reading \"1/2 sachet\" knows what to do\n * with it, so a measure answers `half` apart from the few named below, and the\n * one thing that does not divide is decided from the item's own name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"approximate\") {\n return \"whole\";\n }\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A tranche is already cut off something larger, and the board that produced\n * one takes a corner off it in the same gesture: a quarter of a tranche de pain\n * is a crouton.\n *\n * Each measure is listed in either language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n *\n * Both directions ask whether the unit can hold the figure. A kitchen reads two\n * decimals and no more, so 2468 g written in kilos is 2.47 and the eight grams\n * are gone. A quantity the bigger unit cannot state stays where the page wrote\n * it, and a quantity the page's own unit cannot state walks down to the one that\n * can, so the same mass comes out the same however the page spelled it: \"1234 g\"\n * and \"1 kg 234\" are one quantity and answer with one figure.\n */\nexport function chooseReadableUnit(unit: UnitInfo, amount: number): ChosenUnit {\n if (unit.kind !== \"measured\" || !Number.isFinite(amount) || amount <= 0) {\n return { unit, ratio: 1 };\n }\n\n let current = unit;\n let ratio = 1;\n\n while (amount * ratio < 1) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per && writesExactly((amount * ratio) / up.per)) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n while (!writesExactly(amount * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/** Whether a figure survives being written with the two decimals a kitchen reads. */\nfunction writesExactly(value: number): boolean {\n return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) {\n return unit.canonical;\n }\n return unit.plural ?? `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice, LanguageEvidence } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n CONTAINER_NOUN,\n demoteUnit,\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nconst BRACKETED_TAIL = /\\s*(\\([^()]*\\))\\s*$/;\nconst LEADING_ARTICLE = /^(?:of\\s+)?an?\\s+/i;\nconst LEADING_ARTICLE_FRENCH = /^(?:une|un)\\s+/i;\nconst LEADING_INDEFINITE_WITH_SPACE = /^an?\\s+/i;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst OF_OPENING = /^of\\s+/i;\nconst OPENING_ARTICLE_FRENCH = /^\\s*(un|une)\\b\\s*/i;\nconst REPEATED_COMMA_GROUPS = /^\\s*\\d+,\\d+,\\d/;\nconst SLASH_OUTSIDE_A_FRACTION = /(?<!\\d)\\/|\\/(?!\\d)/;\n\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst DIGIT = /\\d/;\nconst ENGLISH_RANGE_JOINER = /^\\s+(to|or)\\s+/i;\nconst FRACTION_WORD =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i;\nconst FRENCH_RANGE_JOINER = /^\\s+(à|a|ou)\\s+/i;\nconst GROUPED_DECIMAL = /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/;\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_DASH = /^\\s*(–|—|-)\\s*/;\nconst LEADING_INDEFINITE = /^an?\\s/i;\nconst LEADING_OF = /^\\s*of\\s+/i;\nconst LEADING_QUANTITY = /^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst PARTITIVE_BEFORE_BRACKET = /^(?:de\\s+|du\\s+|des\\s+|d'|of\\s+)(?=\\()/i;\nconst PARTITIVE_OPENING = /^(?:de\\s|d'|du\\s|des\\s|,)/i;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst TRAILING_S = /s$/;\nconst VAGUE_ARTICLE = /^(?:un|une|quelques|of\\s+an?|an?)\\s+/iu;\nconst WHITESPACE = /\\s+/;\n\n/**\n * The measure the line leads with: the one standing straight after the amount,\n * or the one a size word stands in front of.\n */\nfunction leadingMeasure<T extends { unit: unknown }>(direct: T, behind: T | null): T {\n if (direct.unit) {\n return direct;\n }\n return behind?.unit ? behind : direct;\n}\n\n/**\n * How a line writes the equivalents beside its measure, when it writes any.\n *\n * Three shapes and three renderings: a slash beside the amount, a trailing\n * restatement, or a bracketed group. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(\n count: number,\n slashed: boolean,\n trailing: boolean,\n): \"slash\" | \"trailing\" | \"bracket\" | null {\n if (count === 0) {\n return null;\n }\n if (slashed) {\n return \"slash\";\n }\n if (trailing) {\n return \"trailing\";\n }\n return \"bracket\";\n}\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Named HTML entities a page writes where the character itself would do.\n *\n * The fractions are the ones that matter: a line reading \"3&frac12; cups\" holds\n * three and a half cups, and a reader that does not decode it sees three and\n * carries the rest into the item name, where doubling the line loses half a cup\n * without saying so.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n frac12: \"\\u00bd\",\n frac13: \"\\u2153\",\n frac23: \"\\u2154\",\n frac14: \"\\u00bc\",\n frac34: \"\\u00be\",\n frac15: \"\\u2155\",\n frac18: \"\\u215b\",\n nbsp: \" \",\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n deg: \"\\u00b0\",\n times: \"\\u00d7\",\n minus: \"\\u2212\",\n ndash: \"\\u2013\",\n mdash: \"\\u2014\",\n};\n\n/**\n * Turn HTML entities back into the characters they stand for.\n *\n * A numeric entity names a code point directly, and the fraction glyphs a\n * recipe uses live well inside the range a page can write that way, so\n * \"&#8532;\" is two thirds and reads as such once decoded.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]+);/gi, (whole, hex: string) =>\n codePoint(Number.parseInt(hex, 16), whole),\n )\n .replace(/&#(\\d+);/g, (whole, digits: string) => codePoint(Number(digits), whole))\n .replace(\n /&([a-z][a-z0-9]*);/gi,\n (whole, name: string) => NAMED_ENTITIES[name.toLowerCase()] ?? whole,\n );\n}\n\n/** A code point a page named, or the entity as published when it names none. */\nfunction codePoint(value: number, published: string): string {\n if (!Number.isInteger(value) || value < 32 || value > 0x10ffff) {\n return published;\n }\n try {\n return String.fromCodePoint(value);\n } catch {\n return published;\n }\n}\n\n/**\n * Empty brackets, which a page leaves behind when it carried a conversion the\n * source did not render. They say nothing and belong in no answer.\n */\nconst EMPTY_BRACKETS = /\\(\\s*\\)/g;\n\n/**\n * Prepare a published line for reading: entities decoded, empty brackets\n * dropped, runs of spaces squeezed.\n */\nfunction readable(text: string): string {\n return decodeEntities(text).replace(EMPTY_BRACKETS, \" \").replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Signs and words a page puts before a number to say it is not exact.\n *\n * The number behind one of them is still a number, and reading none at all\n * hands back the line with a note saying it carries no quantity, which is\n * untrue. It is read, scaled and given back with the mark the page put on it,\n * so the answer stays as loose as the page was.\n */\nconst APPROXIMATION_PREFIX: Record<Language, RegExp> = {\n en: /^(?:~|\\u2248|about|approx\\.?|approximately|around|roughly)\\s*/i,\n fr: /^(?:~|\\u2248|environ|approximativement|\\u00e0 peu pr\\u00e8s|a peu pres)\\s*/i,\n};\n\n/**\n * Words that introduce what a container holds.\n *\n * \"1 pot de 500 g de miel\" counts pots and says how much honey is in one, so\n * the count is the recipe's to multiply. The partitive standing after the\n * measure is what marks the line as a container and its contents.\n */\nconst CONTAINER_CONTENTS = /^\\s*(?:de\\s|d'|du\\s|des\\s)/i;\n\n/**\n * Whether the measure standing behind an item gives the size of one of them,\n * as in \"1 dinde de 3 kg\".\n *\n * The count and the measure answer different questions: how many birds, and how\n * heavy one bird is. A cook serving half again as many people takes a heavier\n * bird, so the count belongs to the page rather than to the factor.\n *\n * Read on the item alone, which is what the line counts once its own measure\n * has been taken off it, so \"450 g (1 livre) de spaghetti\" never reaches here:\n * that line counts grams, and its bracket restates the same quantity.\n */\nfunction statesItemSize(item: string): boolean {\n const attached = ATTACHED_DE.exec(item);\n if (!attached) {\n return false;\n }\n\n const named = item.slice(0, attached.index).trim();\n if (!named || DIGIT.test(named)) {\n return false;\n }\n\n return isStatedSize(item.slice(attached.index + attached[0].length));\n}\n\n/** A mass or a volume standing on its own, with nothing it is the amount of. */\nfunction isStatedSize(text: string): boolean {\n const size = parseLeadingQuantity(text, \"fr\");\n if (!size) {\n return false;\n }\n\n const measure = takeUnit(text.slice(size.length).trimStart(), \"fr\");\n if (measure.unit?.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\n\n/**\n * A second measure a line writes straight after the first, both of them naming\n * one quantity: \"1 lb 4 oz beef\", \"2 kg 300 g de farine\", and the French\n * \"1 kg 500\", which leaves the smaller unit for the reader to supply.\n *\n * What joins the two members is the ladder: the second is written in the unit\n * the first steps down to, and it holds less than one of the first. A measure\n * from another ladder restates the quantity instead of continuing it, and a\n * number followed by a food names a second ingredient; neither is folded in\n * here.\n *\n * The share the second member adds is returned rather than the number itself,\n * so the quantity comes out as one amount in the unit the line opened with:\n * 1 lb 4 oz is 1.25 lb, and doubling it gives the 2.5 lb a butcher weighs.\n * Reading only the first member leaves the rest of the quantity sitting in the\n * item name, where doubling the line loses it.\n *\n * The two members can be joined by a sign or a word saying they add up, as in\n * \"2 c. à s. + 1 c. à c.\". The joiner is read wherever the same ladder relation\n * holds, since it states outright what juxtaposition leaves implicit.\n */\nfunction takeCompoundMember(\n text: string,\n unit: UnitInfo | null,\n language: Language,\n): { adds: number; rest: string } | null {\n if (!unit) {\n return null;\n }\n const step = demoteUnit(unit);\n if (!step) {\n return null;\n }\n\n const joiner = ADDS_UP.exec(text);\n const behindTheJoiner = joiner ? text.slice(joiner[0].length) : text;\n\n const second = parseLeadingQuantity(behindTheJoiner, language);\n if (!second || second.amount <= 0 || second.amount >= step.per) {\n return null;\n }\n\n const after = behindTheJoiner.slice(second.length);\n const measure = takeUnit(after.trimStart(), language);\n if (measure.unit) {\n if (measure.unit.canonical !== step.unit.canonical) {\n return null;\n }\n return { adds: second.amount / step.per, rest: measure.rest.trimStart() };\n }\n\n // The unwritten form: \"1 kg 500\" is a kilo and five hundred grams. It is read\n // only where a mass or a volume opened the line, and only where nothing but\n // the partitive follows the figure, so \"1 cup 2 eggs\" stays two eggs.\n if (unit.kind !== \"measured\") {\n return null;\n }\n if (!Number.isInteger(second.amount)) {\n return null;\n }\n const trailing = after.trimStart();\n if (trailing !== \"\" && !PARTITIVE_OPENING.test(trailing)) {\n return null;\n }\n\n return { adds: second.amount / step.per, rest: trailing };\n}\n\n/** How a line joins two measures that add up to one quantity. */\nconst ADDS_UP = /^\\s*(?:\\+|et|and|plus)\\s+/i;\n\n/**\n * A mark a page opens an ingredient line with, in front of the quantity.\n *\n * A bullet, a dash or a picture of the food is decoration: it says nothing\n * about how much, and a reader stopping at it answers that the line carries no\n * quantity, so a doubled recipe keeps the published amount of whatever the mark\n * stood in front of. It is set aside for reading and put back for writing.\n *\n * The signs that say an amount is loose are excluded, because those do carry a\n * claim about the figure behind them, and the mark has to be followed by space\n * so that a bracket or a decimal point opening a line is left where it is.\n */\nconst LEADING_DECORATION = /^(?:(?!~|≈|\\()[^\\p{L}\\p{N}\\s])+(?=\\s)/u;\n\n/** The mark a line opens with, or null when it opens on the quantity. */\nfunction takeDecoration(text: string): string | null {\n const match = LEADING_DECORATION.exec(text);\n return match ? match[0] : null;\n}\n\n/** Whether a word names something a food is sold in. */\nfunction namesContainer(word: string | undefined): boolean {\n return word !== undefined && CONTAINER_NOUN.test(normalizeUnitKey(word));\n}\n\n/**\n * Measures of time, in either language.\n *\n * An ingredient list carries lines that state a length rather than an amount:\n * a rest, a proof, a marinade, a bake. The factor says how much of the dish to\n * make, and how long a dough takes to rise is no part of that.\n */\nconst TIME_UNIT: Record<Language, RegExp> = {\n en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\\b/i,\n fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\\b/i,\n};\n\n/**\n * The letters a language glues to a figure to make it a rank: the \"er\" of\n * \"1er choix\", the \"e\" of \"2e couche\", the \"st\" of \"1st choice\".\n *\n * A rank names a position rather than an amount, so the line carries nothing to\n * multiply. The letters have to sit against the figure; a line that puts a\n * space between them wrote a number and then a word.\n */\nconst ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\\b/i;\n\n/**\n * A line that states its amount for one eater.\n *\n * The factor already says how many people the recipe is being made for, so\n * multiplying an amount that is per person applies it twice and asks for twice\n * as much on every plate.\n */\nconst PER_PERSON: Record<Language, RegExp> = {\n en: /\\bper\\s+(?:person|head|serving|guest|diner)\\b/i,\n fr: /\\bpar\\s+(?:personne|convive|part|t\\u00eate)\\b/i,\n};\n\n/**\n * Size and preparation words a recipe puts between the number and the measure.\n *\n * \"1 small handful\" counts handfuls and says how full one was. Reading the\n * adjective as the thing being counted loses the measure, and with it the fact\n * that a handful is held to no better than the hand: the line comes back as an\n * exact count of something the page never named.\n */\nconst MEASURE_ADJECTIVES: Record<Language, Set<string>> = {\n en: new Set([\n \"big\",\n \"generous\",\n \"good\",\n \"heaped\",\n \"heaping\",\n \"large\",\n \"level\",\n \"medium\",\n \"scant\",\n \"small\",\n ]),\n fr: new Set([\n \"beau\",\n \"belle\",\n \"bon\",\n \"bonne\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"petit\",\n \"petite\",\n ]),\n};\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(\n text: string,\n language: Language,\n): { adjective: string | null; rest: string } {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return { adjective: null, rest: text };\n }\n\n const [adjective = \"\"] = match.slice(1);\n const folded = normalizeUnitKey(adjective);\n // The word can be written in the plural where the count is, as in \"2 grosses\n // cuillères\", and the list carries the singular.\n const listed =\n MEASURE_ADJECTIVES[language].has(folded) ||\n MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, \"\"));\n if (!listed) {\n return { adjective: null, rest: text };\n }\n\n return { adjective, rest: text.slice(match[0].length) };\n}\n\n/**\n * A comma neither reading can account for.\n *\n * English groups thousands in threes and marks the decimal with a point;\n * French marks the decimal with a comma and never groups with one. A comma\n * followed by anything other than three digits is therefore not an English\n * number, and a second comma group is not a French one. Where the line gives no\n * sign which language it is in, \"1,500\" is fifteen hundred under one reading\n * and one and a half under the other, and choosing wrong is wrong by a factor\n * of a thousand. Neither is safe, so the line goes back as published and says\n * why.\n */\nconst COMMA_GROUPED = /^\\s*\\d{1,3}(?:,\\d{3})+(?!\\d)/;\nconst COMMA_DECIMAL = /^\\s*\\d+,\\d+/;\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2] ?? \"\"] ?? Number.NaN;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) {\n return null;\n }\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (language === \"fr\" ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);\n if (decimal) {\n const [written = \"\"] = decimal.slice(1);\n const amount = Number(\n language === \"fr\" ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n if (language === \"en\") {\n const written = parseWrittenFraction(trimmed);\n if (written) {\n return { amount: written.amount, length: offset + written.length };\n }\n }\n\n return null;\n}\n\n/** How many of the part a line names: \"two thirds\", \"a quarter\", \"half\". */\nconst WRITTEN_NUMERATORS: Record<string, number> = { a: 1, an: 1, one: 1, two: 2, three: 3 };\n\n/** What the part is a part of. */\nconst WRITTEN_DENOMINATORS: Record<string, number> = {\n half: 2,\n halves: 2,\n third: 3,\n thirds: 3,\n quarter: 4,\n quarters: 4,\n fourth: 4,\n fourths: 4,\n};\n\n/**\n * Read a fraction an English line spells out, as in \"half a bottle\" or \"two\n * thirds of a cup\".\n *\n * Recipes write the small fractions in words as readily as in figures, and a\n * line opening on one of them carries an amount like any other. The numerator\n * is optional because \"half\" alone is the common form.\n *\n * What follows decides whether the words are a quantity at all. A share of one\n * thing, \"half a lemon\" or \"two thirds of a cup\", states an amount of its own.\n * A share of a definite thing, \"half of the dough\", points back at an amount\n * stated elsewhere, and multiplying it would answer with a number that belongs\n * to another line.\n */\nfunction parseWrittenFraction(text: string): ParsedQuantity | null {\n const match = FRACTION_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[(match[2] ?? \"\").toLowerCase()];\n if (!(numerator && denominator)) {\n return null;\n }\n\n const rest = text.slice(match[0].length).replace(LEADING_OF, \"\").trimStart();\n if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, \"en\").unit)) {\n return null;\n }\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = language === \"fr\" ? FRENCH_RANGE_JOINER : ENGLISH_RANGE_JOINER;\n const separator = written.exec(after) ?? LEADING_DASH.exec(after);\n if (!separator) {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) {\n return null;\n }\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1] ?? \"\",\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\n/**\n * Why a line that shows a figure is still not the factor's to multiply.\n *\n * Each of these is a reading the parser can make and a scaling it must not do,\n * and they are kept apart from a line with no figure at all so the answer can\n * say which of the two it is looking at.\n */\nexport type HeldBack =\n /** \"4 to 5-pound roast\": the figures give the size of one, not how many. */\n | \"sizeQualifier\"\n /** \"1 dinde de 3 kg\": the measure behind the item weighs one of them. */\n | \"itemSize\"\n /** \"12 oz can tomatoes\": the measure gives what the tin holds, and no count is written. */\n | \"containerSize\"\n /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"2 h de repos\": the figure measures a length of time rather than an amount. */\n | \"duration\"\n /** \"1,500 g\" with nothing to say whether the comma groups or divides. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n /**\n * Why the figure on this line must not be multiplied, when there is such a\n * reason. Null for the ordinary line, whose amount is the factor's to scale.\n */\n heldBack: HeldBack | null;\n /**\n * The mark the page opened the line with, in front of the quantity, such as a\n * bullet or a picture of the food. It goes back where the page had it.\n */\n decoration: string | null;\n /**\n * The sign or word the page put before the amount to say it is loose, as in\n * the \"~\" of \"~1 cup water\". Null when the page stated the amount plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"small\"\n * of \"1 small handful\". It goes back in front of the measure so the answer\n * reads the way the page did.\n */\n measureAdjective: string | null;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets beside the amount, as\n * in \"450 g (1 pound)\", after a slash, as in \"500 g / 1.1 lb\", or in a\n * bracket closing the line, as in \"1 cup milk (240 ml)\". The rewrite puts\n * them back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | \"trailing\" | null;\n /**\n * The word the bracket opened on, as in the \"soit\" of \"(soit 3/4 de tasse)\".\n * Null when the line stated the equivalent without one.\n */\n alternateIntro: string | null;\n /**\n * A bracket giving what one container holds, as in \"1 (14 oz) can\", exactly\n * as the page wrote it. The count is the factor's to multiply and this figure\n * is not, so it goes back untouched.\n */\n capacity: string | null;\n /** What the amount and the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/**\n * Articles a French line writes where a digit would go, and what they count as.\n *\n * \"un\" and \"une\" are the number one written out, so a line using one has stated\n * a count. A word that says merely that there are several, \"quelques\" as much\n * as \"plusieurs\" or the English \"a few\", has stated none: any figure put behind\n * it is this server's reading rather than the page's quantity, and a reading\n * multiplied is a number nobody wrote.\n */\nconst FRENCH_ARTICLES: Record<string, number> = { un: 1, une: 1 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key, language);\n if (!unit) {\n continue;\n }\n const rest = afterKey(text, key);\n if (rest === null) {\n continue;\n }\n return { unit, rest };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(WHITESPACE);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) {\n return { unit: load, rest: words.slice(1).join(\" \") };\n }\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * What stands after the measure a key names, or null when the key does not line\n * up with the words the line is written in.\n *\n * How many words a key spells and how many words the line spends on it are\n * different numbers: normalising turns the abbreviation \"c.à.s\" into the three\n * words \"c a s\", and the line writes it as one. Consuming the key's own word\n * count would take two words of the ingredient with it, so the words the line\n * wrote are consumed one at a time until they normalise to the key.\n */\nfunction afterKey(text: string, key: string): string | null {\n const words = text.trim().split(WHITESPACE);\n for (let count = 1; count <= words.length; count += 1) {\n if (normalizeUnitKey(words.slice(0, count).join(\" \")) !== key) {\n continue;\n }\n return words.slice(count).join(\" \");\n }\n return null;\n}\n\n/**\n * A measure inside brackets can be stated in the other language's vocabulary: a\n * French page glosses grams in ounces, and an English page glosses cups in\n * millilitres. The line's own language is tried first, so a word both\n * vocabularies carry keeps the spelling and the plural of the line it sits in.\n */\nfunction takeUnitEitherLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) {\n return first;\n }\n return takeUnit(text, language === \"fr\" ? \"en\" : \"fr\");\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n return readEvidence(line).language;\n}\n\n/** The language a line reads as, with the weight each side gathered. */\nfunction readEvidence(line: string): LanguageEvidence {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(LEADING_QUANTITY, \"\").trimStart();\n const afterArticle = afterFigures.replace(VAGUE_ARTICLE, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit });\n}\n\n/**\n * The measure standing after the amount, and the size word in front of it.\n *\n * The adjective is only an adjective when a measure stands behind it. In\n * \"1 cleaned leek green\" the words that follow name the food itself, and taking\n * one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n rest: string,\n language: Language,\n fromArticle: boolean,\n): {\n leading: ReturnType<typeof takeLeadingUnit>;\n described: { adjective: string | null; rest: string };\n behind: ReturnType<typeof takeLeadingUnit> | null;\n} {\n const direct = takeLeadingUnit(rest, language, fromArticle);\n const described = direct.unit ? { adjective: null, rest } : takeMeasureAdjective(rest, language);\n const behind = described.adjective\n ? takeLeadingUnit(described.rest, language, fromArticle)\n : null;\n\n return { leading: leadingMeasure(direct, behind), described, behind };\n}\n\n/**\n * Whether the figure the line opens with counts anything, and what it is\n * instead when it does not.\n *\n * Three ways a figure is not a quantity: it is joined to a word by a hyphen and\n * describes one thing, it names a rank in an order, or it measures a length of\n * time, which belongs to the method rather than to the proportions. `false`\n * means the figure counts.\n */\nfunction whatTheFigureIsNot(\n behindFigure: string,\n couldBeARank: boolean,\n language: Language,\n): HeldBack | null | false {\n if (HYPHENATED_TAIL.test(behindFigure)) {\n return \"sizeQualifier\";\n }\n if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {\n return null;\n }\n if (TIME_UNIT[language].test(behindFigure.trimStart())) {\n return \"duration\";\n }\n return false;\n}\n\n/**\n * The equivalents a line writes beside its measure, in whichever of the three\n * shapes it wrote them.\n *\n * A bracketed group, a slash beside the amount, or a restatement trailing the\n * item: only one of them is read, because a line offering two would be stating\n * the same quantity three times.\n */\nfunction takeEquivalents(\n text: string,\n language: Language,\n): {\n rest: string;\n bracketed: ReturnType<typeof takeAlternates>;\n slashed: ReturnType<typeof takeSlashAlternates>;\n item: string;\n trailing: ReturnType<typeof takeTrailingAlternates>;\n group:\n | ReturnType<typeof takeTrailingAlternates>\n | Omit<ReturnType<typeof takeAlternates>, \"rest\">\n | null;\n} {\n const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text);\n const bracketed = takeAlternates(\n introducedBracket ? text.slice(introducedBracket[0].length) : text,\n language,\n );\n let rest = bracketed.measures.length > 0 ? bracketed.rest : text;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) {\n rest = slashed.rest;\n }\n\n let item = stripItemLead(rest, language);\n\n const trailing =\n bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;\n if (trailing) {\n item = trailing.item;\n }\n\n const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);\n // A measure inside a bracket beside a container gives what one of them holds,\n // as in \"1 (14 oz) can\". The recipe's proportion lives in how many tins are\n // opened, and a tin of twice the size is a tin no shop sells, so the figure\n // goes back exactly as the page wrote it.\n\n return { rest, item, bracketed, slashed, trailing, group };\n}\n\n/**\n * Whether the figures on the line give the size of something rather than how\n * many of it the recipe wants, and of what.\n *\n * Three ways a line says it. Without a measure, the words behind the count\n * carry the mass. With a partitive, \"une dinde de 3 kg\" writes the noun where a\n * measure stands and the partitive takes it for one — a noun the vocabulary\n * lists as a measure keeps counting, since a pot is a thing to buy more of,\n * while a noun read as a measure only for standing there names the food itself.\n * And \"12 oz can tomatoes\" qualifies the tin, leaving how many tins unwritten.\n */\nfunction whatTheFiguresSize(\n leading: { unit: UnitInfo | null; partitive: boolean },\n item: string,\n language: Language,\n): HeldBack | null {\n if (language === \"fr\" && !leading.unit && statesItemSize(item)) {\n return \"itemSize\";\n }\n\n // \"une dinde de 3 kg\" writes the noun where a measure stands, and the\n // partitive takes it for one. A noun the vocabulary lists as a measure keeps\n // counting, since a pot or a boîte is a thing to buy more of; a noun read as\n // a measure only for standing there names the food itself, and a mass behind\n // it is the size of one of them.\n if (language === \"fr\" && leading.partitive && isStatedSize(item)) {\n return \"itemSize\";\n }\n\n // \"12 oz can tomatoes\": the measure qualifies the tin, and how many tins the\n // recipe wants is not written anywhere on the line.\n if (leading.unit?.kind === \"measured\" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {\n return \"containerSize\";\n }\n\n return null;\n}\n\n/**\n * What the words behind the figure say the figure counts.\n *\n * \"two thirds of a cup\" names a share of one cup, and the measure stands behind\n * the preposition and the article that introduce it. \"2 dozen mushrooms\" counts\n * mushrooms, twelve to the dozen, so the multiplier is folded into the amount\n * and the line goes on to be read as the count of a thing it now is.\n */\nfunction whatTheCountIsOf(\n behindFigure: string,\n language: Language,\n): { rest: string; multiplier: ReturnType<typeof readCountMultiplier> } {\n let rest = behindFigure.trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") {\n rest = rest.replace(LEADING_ARTICLE, \"\");\n }\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n\n return { rest, multiplier };\n}\n\n/**\n * What a container holds, when the bracket beside it states a capacity rather\n * than an equivalent.\n *\n * \"2 boîtes (400 g)\" names two tins of that size, and the figure is the page's\n * own rather than the factor's, so it goes back between the count and the\n * container exactly as published. A bracket that restates the quantity in\n * another unit is a different thing and stays an equivalent.\n */\nfunction capacityOfTheContainer(\n group: { measures: ReadonlyArray<{ unit: UnitInfo | null }>; published: string } | null,\n measureName: string | undefined,\n item: string,\n): string | null {\n if (!group?.measures.every((measure) => measure.unit?.kind === \"measured\")) {\n return null;\n }\n if (!(namesContainer(measureName) || namesContainer(item))) {\n return null;\n }\n return group.published;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const marked = readable(line);\n const decoration = takeDecoration(marked);\n const text = decoration ? marked.slice(decoration.length).trimStart() : marked;\n const evidence = readEvidence(text);\n const language = choice === \"auto\" ? evidence.language : choice;\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n language,\n heldBack,\n decoration,\n approximation: null,\n measureAdjective: null,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n alternateIntro: null,\n capacity: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (unreadableComma(text, language, evidence)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX[language].exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated, language);\n const article = range ? null : readArticle(stated, language);\n const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;\n if (!quantity) {\n return empty(null);\n }\n\n // A figure joined to a word by a hyphen describes one thing rather than\n // counting things: \"4 to 5-pound roast\" is one roast that weighs that much.\n const notAnAmount = whatTheFigureIsNot(\n stated.slice(quantity.length),\n quantity !== article,\n language,\n );\n if (notAnAmount !== false) {\n return empty(notAnAmount);\n }\n\n const counted = whatTheCountIsOf(stated.slice(quantity.length), language);\n let rest = counted.rest;\n const multiplier = counted.multiplier;\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, language, fromArticle);\n const { leading, described, behind } = measure;\n rest = leading.rest;\n\n // A range gives two amounts, and a second member behind it would belong to\n // one of them without saying which.\n const compound = range ? null : takeCompoundMember(rest, leading.unit, language);\n if (compound) {\n rest = compound.rest;\n }\n const whole = quantity.amount + (compound?.adds ?? 0);\n\n // The partitive can stand between the measure and the bracket restating it,\n // as in \"150 g de (3/4 de tasse) de sucre\". It introduces the equivalent\n // rather than the food, so it is stepped over and the bracket read behind it.\n const equivalents = takeEquivalents(rest, language);\n const { slashed, trailing, group } = equivalents;\n rest = equivalents.rest;\n const item = equivalents.item;\n const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);\n\n // A counted thing whose size the line states: the number the line opens with\n // is one bird, and the measure behind it is what that bird weighs.\n const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);\n if (sizedRatherThanCounted !== null) {\n return empty(sizedRatherThanCounted);\n }\n\n const alternates = capacity === null ? (slashed?.measures ?? group?.measures ?? []) : [];\n\n return {\n original,\n language,\n heldBack: PER_PERSON[language].test(text) ? \"perPerson\" : null,\n decoration,\n approximation: loose ? loose[0] : null,\n measureAdjective: behind?.unit ? described.adjective : null,\n amount: whole * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates,\n alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),\n alternateIntro: capacity === null ? (group?.intro ?? null) : null,\n capacity,\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Whether the comma in a leading number is one this line cannot settle.\n *\n * A group of exactly three digits reads as thousands in English and as a\n * decimal in French, so it needs the line to say which language it is in; where\n * nothing does, both readings stand and they differ by a factor of a thousand.\n * A group of any other size is not an English number at all, and a second group\n * is not a French one.\n */\nfunction unreadableComma(text: string, language: Language, evidence: LanguageEvidence): boolean {\n if (COMMA_GROUPED.test(text)) {\n const settled = evidence.french !== evidence.english;\n return !settled;\n }\n if (language === \"en\") {\n return COMMA_DECIMAL.test(text);\n }\n return REPEATED_COMMA_GROUPS.test(text);\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (language === \"fr\") {\n const match = OPENING_ARTICLE_FRENCH.exec(text);\n if (!match) {\n return null;\n }\n if (!counts(text.slice(match[0].length))) {\n return null;\n }\n const [word = \"\"] = match.slice(1);\n const amount = FRENCH_ARTICLES[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n return { amount, length: match[0].length, word };\n }\n\n const article = LEADING_INDEFINITE_WITH_SPACE.exec(text);\n if (!article) {\n return null;\n }\n if (!counts(text.slice(article[0].length))) {\n return null;\n }\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];\n if (times === undefined) {\n return null;\n }\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string; partitive: boolean } {\n const taken = takeUnit(text, language);\n if (taken.unit) {\n return { ...taken, partitive: false };\n }\n\n if (language === \"fr\" && fromArticle) {\n const measure = readPartitiveMeasure(text);\n // A noun the vocabulary never listed, read as a measure for standing\n // between the article and the partitive.\n if (measure) {\n return { unit: measure.unit, rest: measure.rest, partitive: true };\n }\n }\n\n return { ...taken, partitive: false };\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): string {\n if (language === \"fr\") {\n return text.replace(LEADING_PARTITIVE, \"\").replace(LEADING_ARTICLE_FRENCH, \"\").trim();\n }\n return text.replace(OF_OPENING, \"\").replace(LEADING_INDEFINITE_WITH_SPACE, \"\").trim();\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): BracketedMeasures {\n const none: BracketedMeasures = { measures: [], intro: null, published: \"\", rest: text };\n if (!text.startsWith(\"(\")) {\n return none;\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return none;\n }\n\n const read = readBracket(text.slice(0, close + 1), language);\n if (!read) {\n return none;\n }\n return { ...read, rest: text.slice(close + 1).trimStart() };\n}\n\ninterface BracketedMeasures {\n measures: Measure[];\n /** What introduced the restatement, as the line wrote it. */\n intro: string | null;\n /** The bracket exactly as published, for a figure that must not be scaled. */\n published: string;\n rest: string;\n}\n\n/**\n * Words a page puts inside a bracket to announce that what follows restates the\n * quantity beside it rather than adding to it.\n */\nconst EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\\.?|approximately|or|ou|=)\\s*/i;\n\n/**\n * The partitive a line can put between a figure and the measure it names.\n *\n * \"3/4 de tasse\" and \"3/4 tasse\" are one quantity written two ways, and a\n * reader that only knows the second leaves the first sitting in prose beside an\n * amount that moved.\n */\nconst MEASURE_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+|of\\s+)/i;\n\n/** The measure a figure names, allowing for the partitive introducing it. */\nfunction takeMeasureAfterQuantity(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const direct = takeUnitEitherLanguage(text, language);\n if (direct.unit) {\n return direct;\n }\n\n const partitive = MEASURE_PARTITIVE.exec(text);\n if (!partitive) {\n return direct;\n }\n return takeUnitEitherLanguage(text.slice(partitive[0].length), language);\n}\n\n/**\n * Read a whole bracket as a group of equivalent measures, or nothing.\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", is prose, and scaling prose is not something a reader can check.\n */\nfunction readBracket(bracket: string, language: Language): Omit<BracketedMeasures, \"rest\"> | null {\n const inside = bracket.slice(1, -1);\n const intro = EQUIVALENT_INTRODUCER.exec(inside);\n const body = intro ? inside.slice(intro[0].length) : inside;\n // A slash separates two ways of stating the quantity, and it also writes a\n // fraction. One sitting between two digits belongs to the number, so\n // \"(3/4 de tasse)\" is one measure rather than a three and a four.\n const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) {\n return null;\n }\n\n const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") {\n return null;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: after.unit,\n });\n }\n\n if (measures.length === 0) {\n return null;\n }\n return { measures, intro: intro ? intro[0].trim() : null, published: bracket };\n}\n\n/**\n * Read a bracket a line closes on, as in \"150 g de sucre (soit 3/4 de tasse)\".\n *\n * A page states an equivalence wherever it reads best, and after the name of\n * the food is as common as beside the figure. Both say the same thing about the\n * same quantity, so both move with it: a doubled line whose closing bracket\n * still reads as published tells the cook that 300 g is three quarters of a\n * cup.\n */\nfunction takeTrailingAlternates(\n item: string,\n language: Language,\n): (Omit<BracketedMeasures, \"rest\"> & { item: string }) | null {\n const closing = BRACKETED_TAIL.exec(item);\n if (!closing) {\n return null;\n }\n\n const read = readBracket(closing[1] ?? \"\", language);\n if (!read) {\n return null;\n }\n\n const head = item.slice(0, closing.index).trim();\n // A bracket standing on its own is the quantity itself rather than a\n // restatement of one, and there would be nothing left for it to qualify.\n if (head === \"\") {\n return null;\n }\n\n return { ...read, item: head };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) {\n break;\n }\n\n const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) {\n break;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return language === \"fr\" ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) {\n return \"\";\n }\n if (Number.isInteger(amount)) {\n return String(amount);\n }\n if (options.fractions === false) {\n return decimal(amount);\n }\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: [number, string][] = [\n [0.25, \"1/4\"],\n [1 / 3, \"1/3\"],\n [0.5, \"1/2\"],\n [2 / 3, \"2/3\"],\n [0.75, \"3/4\"],\n ];\n for (const [value, label] of known) {\n if (Math.abs(rest - value) < 0.02) {\n return whole > 0 ? `${whole} ${label}` : label;\n }\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French and in English.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act\n * on. Multiplying every number by the factor is arithmetically correct and\n * practically useless: it produces \"2,4 oeufs\" and \"0.67 pinch of salt\" with\n * the same confidence as \"267 g flour\". Each line is therefore classified by\n * what its measure allows, and the classification travels with the result so\n * the caller can see what was computed and what was left alone.\n *\n * Leaving a line alone is a decision of the same weight. A quantity a recipe\n * states loosely still holds a share of the dish, and a leavening agent left at\n * one pinch for twenty-five servings is a recipe that does not rise.\n *\n * The two languages are read by one set of rules. A list holding French lines\n * and English lines comes back scaled by the same arithmetic, with each line\n * written the way its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst CLOVE = /\\bcloves?\\b/i;\nconst CONSONANT_THEN_Y = /[^aeiou]y$/i;\nconst DOUBLE_S_ENDING = /ss$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst F_ENDING = /(?:[^f]f|fe)$/i;\nconst F_OR_FE_ENDING = /fe?$/i;\nconst GARLIC = /\\bgarlic\\b/i;\nconst IES_ENDING = /ies$/i;\nconst LETTER = /\\p{L}/u;\nconst LETTERS_ONLY = /^[A-Za-z]+$/;\nconst OF_JOINER = / of /i;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\nconst X_ENDING = /x$/i;\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n *\n * A whole number is already what a scale shows, so it is left where the\n * arithmetic put it: 1234 g doubled is 2468 g, and a step of five grams would\n * report 2470 for a product that needed no rounding at all.\n */\nfunction roundMeasured(value: number): number {\n if (Number.isInteger(value)) {\n return value;\n }\n if (value >= 100) {\n return roundTo(value, 5);\n }\n if (value >= 10) {\n return roundTo(value, 1);\n }\n if (value >= 1) {\n return roundTo(value, 0.1);\n }\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) {\n return { value: floor, clamped: true };\n }\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) {\n return { value: floor, clamped: true };\n }\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0] ?? floor;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n if (value < 1) {\n const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(\n (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION),\n );\n let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) {\n return false;\n }\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\n/**\n * Whether a bound came out as the exact product, judged in the finer of the two\n * units involved.\n *\n * An absolute tolerance only means what it says at one scale. Read in kilos, a\n * hundredth is ten grams, so a quantity moved from 2468 g to 2470 g passes as\n * exact for being 0.002 kg away; read in grams, the same gap is two grams and\n * the answer is that the value moved.\n */\nfunction boundLandedExactly(bound: ScaledBound): boolean {\n const fine = Math.max(bound.ratio, 1);\n return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n /** How many of the unit that came back fit in one of the unit the recipe wrote. */\n ratio: number;\n}\n\ninterface ScaledMeasure {\n /**\n * One bound for a single measure, two for a range, in that order. The shape\n * says so, because every reader takes the first without checking: a measure\n * that scaled to nothing is not a thing this can produce.\n */\n bounds: [ScaledBound, ...ScaledBound[]];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n /** What the page published at one end of the measure, and what it scales to. */\n interface End {\n published: number;\n raw: number;\n }\n\n const ends: [End, ...End[]] =\n high === null\n ? [{ published: low, raw: low * factor }]\n : [\n { published: low, raw: low * factor },\n { published: high, raw: high * factor },\n ];\n\n /** Read both ends, keeping the shape every caller of the bounds relies on. */\n const eachEnd = <T>(read: (end: End) => T): [T, ...T[]] => [\n read(ends[0]),\n ...ends.slice(1).map(read),\n ];\n\n const raws = ends.map((end) => end.raw);\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : low * factor;\n\n /** Both bounds share one unit, and each keeps the precision that unit affords. */\n const inUnit = (target: UnitInfo, ratio: number): ScaledMeasure => ({\n bounds: eachEnd(({ published, raw }) => {\n const exact = raw * ratio;\n // The rounding happens in the smaller of the two units, so moving to a\n // bigger one never throws away precision the page wrote: 1500 g rounded\n // as kilos is 2, and rounded as grams it is the 1.5 kg a scale shows.\n const rounded =\n ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);\n // At the bottom of a ladder, keep what precision is left rather than\n // deleting the ingredient.\n const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;\n // Rounding to a step of five grams above a hundred can round upwards, and\n // a recipe being made smaller must never come out asking for more than\n // the page published.\n const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n ratio,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) {\n return inUnit(stepped.unit, stepped.ratio);\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(raw, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, divisibility, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n}\n\n/**\n * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in either language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genievres?|genevriers?|badianes?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n \")\\\\b\",\n \"iu\",\n);\n\n/**\n * A juice, the one counted thing whose division stops at the half.\n *\n * Half the juice of a lemon is taken by squeezing half the fruit, which is a\n * step a recipe writes. A quarter of one has to be poured out and measured\n * back, and no recipe asks for that.\n *\n * It reads before the fruit, which a knife divides further on its own.\n */\nconst HALVED_ITEM = /\\b(?:jus|juices?)\\b/iu;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An egg comes out of its shell whole, and so does the yolk or the white a\n * recipe asks for on its own: half of one means beating it and weighing the\n * result, which no recipe asks for, and there is no way to keep the other half.\n * A count of them therefore lands on a whole number, whichever side of the half\n * the arithmetic fell on.\n *\n * This list is short on purpose, and a word joins it only when half of the\n * thing is genuinely not something a cook can measure out. Anything that is\n * poured, weighed or cut halves, and takes the half.\n *\n * Two more belong here for reasons the criterion cannot reach on its own:\n *\n * - a clou de girofle is a dried flower bud, dropped into the pot and fished\n * back out of it. Nothing about it is measured, so there is no half of one to\n * take;\n * - a zeste is what comes off one fruit in one go. A line asking for the zeste\n * of a citron is asking for all of it, and a share of a zeste names no amount\n * a cook stops at.\n */\nconst WHOLE_ITEM =\n /\\b(?:eggs?|yolks?|egg\\s+whites?|oeufs?|œufs?|jaunes?\\s+d['e]|clous?|zestes?|zests?)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and the share of\n * one a recipe asks for is the half a knife makes of it. A clove on its own is\n * the dried flower bud, the clou de girofle, dropped into the pot and fished\n * back out of it: nothing about it is measured, so there is no share of one to\n * take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : CLOVE.test(item);\n if (!counted) {\n return null;\n }\n return GARLIC.test(item) ? \"half\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) {\n return null;\n }\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\n/**\n * Accents removed and the ligature spelled out, so \"échalote\" and \"echalote\"\n * hit one entry.\n *\n * A word boundary sits between an ASCII letter and a non-letter and nowhere\n * else, so a pattern opening on \"é\" never matches at the start of a word:\n * folding the item is what lets the lists below be written once, in plain\n * letters.\n */\nfunction foldItem(item: string): string {\n return item\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\u0153/g, \"oe\")\n .replace(/\\u0152/g, \"OE\");\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const key = foldItem(item);\n const clove = cloveDivisibility(unit, key);\n if (clove) {\n return clove;\n }\n if (unit && !countsBarePieces(unit)) {\n return unitDivisibility(unit);\n }\n const blanc = blancDivisibility(key);\n if (blanc) {\n return blanc;\n }\n if (WHOLE_ITEM.test(key)) {\n return \"whole\";\n }\n if (PORTION_SIZED_ITEM.test(key)) {\n return \"whole\";\n }\n if (HALVED_ITEM.test(key)) {\n return \"half\";\n }\n if (HALVED_CUT.test(key)) {\n return \"half\";\n }\n if (QUARTERED_MEASURE.test(key)) {\n return \"quarter\";\n }\n return QUARTERED_ITEM.test(key) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/**\n * Keep the capitalisation the line used while looking the word up in lower\n * case.\n *\n * A page writes its ingredients in whichever case it likes, and the shape of a\n * word is the page's rather than this server's: a list set in capitals reads as\n * a list somebody typed over when one line of it comes back in lower case.\n * Three shapes carry: all capitals, one leading capital, and everything else,\n * which keeps the replacement as the vocabulary spells it.\n */\nfunction matchCase(source: string, replacement: string): string {\n const hasLetter = LETTER.test(source);\n if (hasLetter && source === source.toUpperCase()) {\n return replacement.toUpperCase();\n }\n const first = source.slice(0, 1);\n if (\n first !== \"\" &&\n first === first.toUpperCase() &&\n source.slice(1) === source.slice(1).toLowerCase()\n ) {\n return (replacement[0] ?? \"\").toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (SIBILANT_ENDING.test(word)) {\n return `${word}es`;\n }\n if (CONSONANT_THEN_Y.test(word)) {\n return `${word.slice(0, -1)}ies`;\n }\n if (F_ENDING.test(word)) {\n return `${word.replace(F_OR_FE_ENDING, \"\")}ves`;\n }\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (IES_ENDING.test(word) && word.length > 4) {\n return `${word.slice(0, -3)}y`;\n }\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (SIBILANT_PLURAL.test(word)) {\n return word.slice(0, -2);\n }\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (DOUBLE_S_ENDING.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word)) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = OF_JOINER.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words.at(-1) ?? \"\";\n if (!LETTERS_ONLY.test(last) || last.length <= 2) {\n return item;\n }\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) {\n words[words.length - 1] = plural;\n } else if (!wantsPlural && isPlural) {\n words[words.length - 1] = singular;\n } else {\n return item;\n }\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\n/**\n * French nouns in -ou whose plural takes an -x.\n *\n * The ending settles nothing on its own: \"clou\" and \"trou\" take the ordinary\n * -s, and only these few take the -x. A cabbage is the one a recipe counts, and\n * the others are here because the list is the rule.\n */\nconst FRENCH_OU_PLURAL_IN_X = new Set([\n \"bijou\",\n \"caillou\",\n \"chou\",\n \"genou\",\n \"hibou\",\n \"joujou\",\n \"pou\",\n]);\n\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"mur\",\n \"mure\",\n \"vert\",\n \"verte\",\n \"rouge\",\n \"jaune\",\n \"noir\",\n \"noire\",\n \"blanc\",\n \"blanche\",\n \"rond\",\n \"ronde\",\n \"hache\",\n \"hachee\",\n \"coupe\",\n \"coupee\",\n \"rape\",\n \"rapee\",\n \"pele\",\n \"pelee\",\n \"epluche\",\n \"epluchee\",\n \"denoyaute\",\n \"denoyautee\",\n \"emince\",\n \"emincee\",\n]);\n\n/** Lowercase and strip accents, so \"entière\" and \"entiere\" hit the same entry. */\nfunction foldWord(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The trailing adjective agreed with the count, or null when it is left alone. */\nfunction agreeTrailingAdjective(word: string, wantsPlural: boolean): string | null {\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\");\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return null;\n }\n if (wantsPlural === isPlural) {\n return null;\n }\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * The head of a French noun phrase, put in the number its count asks for.\n *\n * The marks are not uniform: \"morceau\" and \"chou\" take -x where an ordinary\n * noun takes -s, \"bocal\" takes -aux, a word ending in -s, -x or -z takes no\n * mark at all, and \"ananas\" or \"couscous\" carry their -s in the singular. A\n * head already in the number wanted is left as written.\n */\nfunction agreeFrenchHead(\n head: string,\n number: { wantsPlural: boolean; isPlural: boolean; ouPlural: boolean },\n): string {\n const { wantsPlural, isPlural, ouPlural } = number;\n\n if (wantsPlural && !isPlural) {\n if (SIBILANT_LETTER.test(head)) {\n return head;\n }\n if (EAU_ENDING.test(head)) {\n return `${head}x`;\n }\n if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) {\n return `${head}x`;\n }\n if (AL_ENDING.test(head)) {\n return `${head.slice(0, -2)}aux`;\n }\n return `${head}s`;\n }\n\n if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n return head.slice(0, -1);\n }\n if (AUX_ENDING.test(head)) {\n return `${head.slice(0, -3)}al`;\n }\n if (ouPlural) {\n return head.slice(0, -1);\n }\n if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n return head;\n }\n return head.slice(0, -1);\n }\n\n return head;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) {\n return item;\n }\n\n const wantsPlural = amount >= 2;\n // A final -x marks the plural of the few nouns in -ou that take one, and it\n // belongs to the singular of everything else (\"prix\", \"houx\").\n const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));\n const isPlural = PLURAL_ENDING.test(head) || ouPlural;\n\n words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });\n\n const last = words.length - 1;\n const trailing = last > 0 ? words[last] : undefined;\n if (trailing !== undefined) {\n const adjective = agreeTrailingAdjective(trailing, wantsPlural);\n if (adjective) {\n words[last] = adjective;\n }\n }\n\n return words.join(\" \");\n}\n\n/**\n * Agree the adjective a line put in front of its measure, as in the \"grosse\" of\n * \"1 grosse pincée\".\n *\n * The word qualifies the measure, so in French it takes the number the measure\n * takes: \"2 grosses pincées\". English adjectives do not decline, and a French\n * word outside the declinable list stays as the recipe wrote it, for the same\n * reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number, language: Language): string {\n if (language === \"en\") {\n return word;\n }\n\n const wantsPlural = amount >= 2;\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\") && !AGREEABLE_ADJECTIVES.has(folded);\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return word;\n }\n if (wantsPlural === isPlural) {\n return word;\n }\n return wantsPlural ? `${singular}s` : singular;\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n return language === \"fr\" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * Agree the container a line counts, which is the word the item opens with.\n *\n * \"2 (14 oz) cans tomatoes\" counts tins and names what is in them, so the\n * number belongs to the tin. English otherwise agrees the last word before a\n * comma, which would put the plural mark on the tomatoes and leave the tin\n * reading as one.\n */\nfunction agreeCountedContainer(item: string, amount: number, language: Language): string {\n if (language === \"fr\") {\n return agreeInFrench(item, amount);\n }\n const space = item.indexOf(\" \");\n if (space < 0) {\n return agreeInEnglish(item, amount);\n }\n return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language): string {\n if (!item) {\n return \"\";\n }\n if (language === \"en\") {\n return ` ${item}`;\n }\n const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in either language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) {\n return null;\n }\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) {\n continue;\n }\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) {\n continue;\n }\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) {\n return passthroughIngredient(line, options.language);\n }\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) {\n return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\n }\n\n return scaleSingleLine(line, options);\n}\n\n/**\n * Scale a line that offers a choice, one branch at a time.\n *\n * A cook follows one branch and ignores the other, so both have to carry the\n * same share of the recipe: a doubled line whose second branch still reads as\n * published hands whoever takes it half the ingredient. The two branches name\n * different things, and how far one stands for the other is the page's claim\n * rather than arithmetic, so such a line is never reported as exact.\n */\nfunction scaleBranchedLine(line: string, branch: Branch, options: ScaleOptions): ScaledIngredient {\n const head = scaleSingleLine(branch.head, options);\n if (head.scaling === \"unscaled\") {\n return { ...head, text: line.trim(), original: line };\n }\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) {\n return { text: published, rewritten: false };\n }\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) {\n return { text: published, rewritten: false };\n }\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\n/**\n * The equivalents beside a measure, written the way the line wrote them.\n *\n * A trailing restatement is printed after the item rather than beside the\n * amount, so it carries no label here.\n */\nfunction alternateLabel(\n count: number,\n style: string | null,\n texts: readonly string[],\n bracketed: string,\n): string {\n if (count === 0) {\n return \"\";\n }\n if (style === \"slash\") {\n return ` / ${texts.join(\" / \")}`;\n }\n if (style === \"trailing\") {\n return \"\";\n }\n return bracketed;\n}\n\n/**\n * What follows the amount: the partitive French puts between a measure and what\n * it measures, or the counted item itself, which stands straight after its\n * number in both languages and agrees with it.\n */\nfunction itemLabelFor(\n named: UnitInfo | null,\n item: string,\n counted: string,\n language: Language,\n): string {\n if (named) {\n return joinItem(item, language);\n }\n if (counted) {\n return ` ${counted}`;\n }\n return \"\";\n}\n\n/** What the answer says about a line beyond the figure it came to. */\ninterface LineOutcome {\n parsed: ParsedIngredient;\n bounds: readonly ScaledBound[];\n unit: UnitInfo | null;\n low: ScaledBound;\n clamped: ScaledBound | null;\n movedPrimary: boolean;\n movedAlternate: boolean;\n restated: boolean;\n collapsed: boolean;\n language: Language;\n carriesMore: boolean;\n}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * The reasons stack: a line can be clamped, carry a second quantity, collapse\n * its range and state an approximation, and each has to be said without\n * cancelling the others.\n */\nfunction noteForScaledLine(outcome: LineOutcome): string | undefined {\n const {\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n } = outcome;\n let note: string | undefined;\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));\n sentences.push(\n moved\n .map(\n (bound) =>\n `Rounded ${bound.amount > bound.exact ? \"up\" : \"down\"} from ` +\n `${asPublished(bound.raw, parsed.unit)}.`,\n )\n .join(\" \"),\n );\n } else if (movedAlternate) {\n // The amount itself came out exact, and only the equivalent beside it had\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (carriesMore) {\n sentences.push(\n \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\",\n );\n }\n\n if (parsed.capacity) {\n sentences.push(\n `${parsed.capacity} is what one container holds, so it stays as published and the count is ` +\n \"what the factor moved. Scaling this line is a matter of opening that many containers of \" +\n \"the size named.\",\n );\n }\n\n if (collapsed) {\n sentences.push(\"The page gave a range, and at this size both ends come to the same amount.\");\n }\n\n // Below what any scale shows, the arithmetic is right and the kitchen cannot\n // follow it. Saying so is the difference between an answer and a number.\n if (unit?.kind === \"measured\" && low.amount > 0 && low.amount < 0.05) {\n sentences.push(\n \"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye.\",\n );\n }\n\n // The page put the amount forward as loose, and multiplying it keeps it that\n // way: the answer is as approximate as the figure it came from.\n if (parsed.approximation) {\n sentences.push(\n \"The page gave this amount as an approximation, and the scaled figure is no firmer.\",\n );\n }\n\n if (sentences.length > 0) {\n note = sentences.join(\" \");\n }\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n note = withApproximateNote(parsed.unit, note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\n}\n\n/**\n * The line as it reads once scaled: the amount, its measure, what the container\n * holds, the equivalents beside it, and the item they belong to.\n *\n * The size word the page put in front of its measure goes back in front of it,\n * and the equivalents go back the way the line offered them, since a slash and\n * a bracket are two different ways of restating one quantity.\n */\nfunction renderScaledLine(\n parsed: ParsedIngredient,\n alternates: ReadonlyArray<{ text: string }>,\n unit: UnitInfo | null,\n shown: number,\n language: Language,\n): {\n named: UnitInfo | null;\n unitLabel: string;\n capacityLabel: string;\n altLabel: string;\n trailingLabel: string;\n itemLabel: string;\n} {\n const named = unit && !countsBarePieces(unit) ? unit : null;\n // The size word the page put in front of its measure goes back in front of\n // it: the page asked for a grosse pincée, and a pincée is not the same ask.\n const adjective =\n named && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}`\n : \"\";\n const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : \"\";\n const alternateTexts = alternates.map((entry) => entry.text);\n const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : \"\";\n const bracketed = alternates.length === 0 ? \"\" : ` (${introduced}${alternateTexts.join(\" / \")})`;\n // Equivalents go back where the line offered them: inside brackets beside the\n // amount, after a slash, or in the bracket the line closes on.\n const altLabel = alternateLabel(\n alternates.length,\n parsed.alternateStyle,\n alternateTexts,\n bracketed,\n );\n const trailingLabel = parsed.alternateStyle === \"trailing\" ? bracketed : \"\";\n // What a container holds is the page's figure and not the factor's, so it\n // goes back between the count and the container, exactly as published.\n const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : \"\";\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = parsed.capacity\n ? agreeCountedContainer(parsed.item, shown, language)\n : agreeWithAmount(parsed.item, shown, language);\n const itemLabel = itemLabelFor(named, parsed.item, counted, language);\n\n return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };\n}\n\n/** Why a line showing a figure came back as the page published it. */\nconst HELD_BACK_NOTE: Record<HeldBack, string> = {\n sizeQualifier:\n \"The figures here give the size of one item rather than how many, so the line is \" +\n \"left as published.\",\n itemSize:\n \"The measure standing behind the item gives the size of one of them rather than how many, \" +\n \"so the line is left as published. Serving more people is a matter of taking a bigger one, \" +\n \"and serving fewer a smaller one.\",\n containerSize:\n \"The measure here gives what one container holds rather than how many of them the recipe \" +\n \"uses, and the line states no count, so it is left as published. Scaling it is a matter of \" +\n \"opening that many containers of the size named.\",\n perPerson:\n \"This line already states an amount for one person, and the factor is what changes \" +\n \"how many people the recipe serves, so the line is left as published.\",\n duration:\n \"This line states a length of time rather than an amount of an ingredient, so it is left as \" +\n \"published. A rest, a proof or a bake takes as long for a large batch as for a small one, \" +\n \"and the factor has nothing to say about it.\",\n ambiguousDecimal:\n \"The comma in this number marks thousands in one convention and the decimal point in \" +\n \"another, and the line gives no sign which was meant, so it is left as published.\",\n};\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n note: parsed.heldBack\n ? HELD_BACK_NOTE[parsed.heldBack]\n : \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((bound) => !boundLandedExactly(bound));\n const movedAlternate = alternateBounds.some((bound) => !boundLandedExactly(bound));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote\n // them, and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\n const carriesMore = hasEmbeddedMeasure(parsed.item, language);\n // A further quantity beside a mass or a volume restates or qualifies it, and\n // it did not move, so the line as a whole is not the exact product of what\n // the page published. Beside a counted container the same figure gives the\n // size of one of them, which is a thing the factor must leave alone: \"2\n // boîtes de 400 g\" is exactly twice \"1 boîte de 400 g\".\n const leftover = carriesMore && parsed.unit?.kind === \"measured\";\n\n const low = primaryBounds[0];\n const high = primaryBounds[1] ?? null;\n const unit = primary.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(\n parsed,\n alternates,\n unit,\n shown,\n language,\n );\n const result: ScaledIngredient = {\n text: `${parsed.decoration ? `${parsed.decoration} ` : \"\"}${parsed.approximation ?? \"\"}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated || leftover ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n };\n\n const note = noteForScaledLine({\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n });\n if (note !== undefined) {\n result.note = note;\n }\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0];\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) {\n return low;\n }\n return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n const parsed = parseIngredient(line, language);\n\n const held = parsed.amount === null || parsed.heldBack !== null;\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: held ? \"unscaled\" : \"scaled\",\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n language: parsed.language,\n };\n if (parsed.heldBack) {\n result.note = HELD_BACK_NOTE[parsed.heldBack];\n } else if (parsed.amount === null) {\n result.note = \"No quantity given; adjust to taste.\";\n } else if (parsed.unit?.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, undefined);\n }\n return result;\n}\n\n/** An ingredient list returned unchanged, for when no scaling was requested. */\nexport function passthroughIngredients(\n lines: string[],\n language: LanguageChoice = \"auto\",\n): ScaledIngredient[] {\n return lines.map((line) => passthroughIngredient(line, language));\n}\n","/**\n * Reading what a page's own headings announce.\n *\n * A heading is the one statement a page makes about its own layout, and it is\n * the only evidence there is that a part of a recipe was published when nothing\n * was read out of that part. That matters because reading nothing is a fact\n * about this server, and only the page can say whether there was anything to\n * read.\n *\n * Both languages the pages are written in are recognised here, since a heading\n * belongs to a page rather than to the corpus it sits in.\n */\n\n/** The parts of a recipe a page announces with a heading of its own. */\nexport type PagePart = \"ingredients\" | \"method\";\n\nconst HEADINGS: Record<PagePart, RegExp> = {\n ingredients: /^\\s*(?:ingr[ée]dients?|ingredients?|garniture)\\b/i,\n method:\n /^\\s*(?:procedure|proc[ée]d[ée]|directions?|instructions?|method|preparation|pr[ée]paration|r[ée]alisation|steps?|[ée]tapes?)\\b/i,\n};\n\n/**\n * The first heading on the page announcing this part, when the page carries\n * one. Null covers two different things on purpose, and the caller separates\n * them: a page heading nothing of the kind, and a source that does not report\n * a page's headings at all.\n */\nexport function headingFor(part: PagePart, headings: readonly string[] | null): string | null {\n if (headings === null) {\n return null;\n }\n return headings.find((heading) => HEADINGS[part].test(heading)) ?? null;\n}\n","/** Schemas, error mapping and rendering shared by the tools. */\n\nimport { z } from \"zod\";\nimport { RecipesError } from \"../errors.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport type { RecipeRow, SourceReport } from \"../types.js\";\n\n/**\n * The text block is what many clients render, and some render nothing else, so\n * it has to answer on its own. This ceiling is what keeps a recipe with forty\n * ingredients from arriving as a wall of text.\n */\nexport const MAX_TEXT_CHARS = 2200;\n\nexport interface ToolResult {\n // The SDK's result type carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/** One search row, carrying what it takes to pick a recipe out of a list. */\nexport const rowSchema = z.object({\n id: z\n .string()\n .describe(\"Pass this to get_recipe. It names its source, so nothing has to be guessed.\"),\n source: z.string().describe(\"Which source published this row.\"),\n source_name: z.string(),\n title: z.string(),\n url: z.string().describe(\"The recipe's own page. Show this when citing it.\"),\n image_url: z.string().nullable(),\n excerpt: z\n .string()\n .nullable()\n .describe(\"The gloss or matching passage the source offered, when it offered one.\"),\n});\n\n/**\n * What one source answered, or why it did not.\n *\n * Present for every source asked, including the ones that failed, because an\n * answer that quietly drops a source reads as a complete answer that found\n * less.\n */\nexport const reportSchema = z.object({\n source: z.string(),\n name: z.string(),\n status: z.enum([\"answered\", \"failed\"]).describe(\"Whether this source replied at all.\"),\n count: z.number().int().describe(\"Rows this source contributed to the answer.\"),\n reported_total: z\n .number()\n .int()\n .nullable()\n .describe(\"What the source said it saw. Null when it states no number at all.\"),\n reported_total_means: z\n .string()\n .nullable()\n .describe(\n \"What 'reported_total' counts on this source. Counts are never summed across sources.\",\n ),\n skipped: z\n .number()\n .int()\n .describe(\"Rows this source sent in a shape the server could not read, and left out.\"),\n cached: z.boolean().describe(\"Served from this server's short-lived in-memory cache.\"),\n preferred_by_name: z\n .boolean()\n .describe(\n \"Whether this source's rows were arranged so the ones whose title names the dish come first. An order over one source's own rows, never a score against another source.\",\n ),\n wordings: z\n .array(\n z.object({\n query: z.string().describe(\"What was sent to this source, or would have been.\"),\n derivation: z.string().describe(\"How this wording was derived from the question.\"),\n ran: z.boolean(),\n count: z\n .number()\n .int()\n .nullable()\n .describe(\"Rows this wording returned. Null when it did not run.\"),\n added: z\n .number()\n .int()\n .nullable()\n .describe(\"Rows it returned that no earlier wording had.\"),\n not_run_because: z.string().nullable().describe(\"Why it was held back. Null when it ran.\"),\n error: z\n .object({ code: z.string(), message: z.string(), hint: z.string().optional() })\n .nullable(),\n }),\n )\n .describe(\n \"Every wording this source was sent or held back from, in the order tried. A question written as a sentence is also sent in wordings derived from it, because these indexes answer the words they are handed. Counts here belong to one wording each and are never added.\",\n ),\n error: z\n .object({\n code: z.string(),\n message: z.string(),\n hint: z.string().optional(),\n })\n .nullable()\n .describe(\"Why this source did not answer. Null when it did.\"),\n read: z\n .object({\n status: z.enum([\"read\", \"failed\"]),\n error: z\n .object({ code: z.string(), message: z.string(), hint: z.string().optional() })\n .nullable(),\n })\n .nullable()\n .describe(\n \"What became of the row this source offered, once it was opened. Null when no row was opened, which is what a search that did not answer and a search holding nothing both come to. A search and a read are two moments, and this is the second one.\",\n ),\n});\n\n/** One ingredient line, scaled, in the shape every source is read into. */\nexport const ingredientSchema = z.object({\n text: z.string().describe(\"The line as it now reads.\"),\n original: z.string().describe(\"The line as published.\"),\n scaling: z\n .enum([\"scaled\", \"rounded\", \"unscaled\"])\n .describe(\n \"'scaled' means the arithmetic landed on the exact product, 'rounded' that something had to move for the line to stay usable or that the line carries a second quantity whose match is the page's claim, 'unscaled' that there is nothing to multiply.\",\n ),\n amount: z\n .number()\n .nullable()\n .describe(\"The scaled quantity, in 'unit'. Read the two together, never the number alone.\"),\n amount_max: z.number().nullable().describe(\"Upper bound when the line gives a range.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"The unit 'amount' is in, which may differ from the page's.\"),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language the line was read and rewritten in.\"),\n note: z.string().optional().describe(\"What happened to this line, when anything did.\"),\n});\n\nexport function toIngredientPayload(entry: ScaledIngredient): z.infer<typeof ingredientSchema> {\n return {\n text: entry.text,\n original: entry.original,\n scaling: entry.scaling,\n amount: entry.amount,\n amount_max: entry.amountMax,\n unit: entry.unit,\n language: entry.language,\n ...(entry.note ? { note: entry.note } : {}),\n };\n}\n\nexport function toRowPayload(row: RecipeRow): z.infer<typeof rowSchema> {\n return {\n id: row.id,\n source: row.source,\n source_name: row.sourceName,\n title: row.title,\n url: row.url,\n image_url: row.imageUrl,\n // An excerpt is a source's own prose, and a long one crowds out the rows it\n // was meant to help choose between.\n excerpt: row.excerpt === null ? null : truncate(row.excerpt, MAX_EXCERPT_CHARS),\n };\n}\n\n/** How much of a source's own prose a search row carries. */\nconst MAX_EXCERPT_CHARS = 200;\n\nexport function toReportPayload(report: SourceReport): z.infer<typeof reportSchema> {\n return {\n source: report.source,\n name: report.name,\n status: report.status,\n count: report.count,\n reported_total: report.reportedTotal,\n reported_total_means: report.reportedTotalMeans,\n skipped: report.skipped,\n cached: report.cached,\n preferred_by_name: report.preferredByName,\n wordings: report.wordings.map((attempt) => ({\n query: attempt.query,\n derivation: attempt.derivation,\n ran: attempt.ran,\n count: attempt.count,\n added: attempt.added,\n not_run_because: attempt.notRunBecause,\n error: attempt.error,\n })),\n error: report.error ?? null,\n // A search alone opens nothing, so only a tool that reads a row fills this\n // in, through `withRead`.\n read: null,\n };\n}\n\n/** What became of the row a source offered, added to that source's report. */\nexport function withRead(\n payload: z.infer<typeof reportSchema>,\n read: z.infer<typeof reportSchema>[\"read\"],\n): z.infer<typeof reportSchema> {\n return { ...payload, read };\n}\n\n/**\n * A sentence qualifying an answer.\n *\n * The text block has room for a limited number of them, so something gives when\n * an answer carries more, and a note the answer would mislead without says so\n * for itself. Whoever writes a note knows what it is for; deciding it later\n * from the words the note happens to use makes every rewording of a warning a\n * warning that can be dropped, and the notes most often reworded are the ones\n * written to correct a specific misreading.\n */\nexport type Note = string | EssentialNote;\n\nexport interface EssentialNote {\n text: string;\n /** The answer misleads without this sentence. */\n essential: true;\n}\n\n/** Mark a note the answer cannot be read safely without. */\nexport function mustKeep(text: string): EssentialNote {\n return { text, essential: true };\n}\n\nexport function noteText(note: Note): string {\n return typeof note === \"string\" ? note : note.text;\n}\n\nexport function noteTexts(notes: readonly Note[]): string[] {\n return notes.map(noteText);\n}\n\n/** Name the recipe a note is about, keeping what the note is worth. */\nexport function labelNote(label: string, note: Note): Note {\n if (typeof note === \"string\") {\n const plain: string = note;\n return `${label}: ${plain}`;\n }\n const said: string = note.text;\n return mustKeep(`${label}: ${said}`);\n}\n\n/**\n * Turn the per-source reports into sentences a reader can act on.\n *\n * A failed source is named with the reason, so an answer holding part of what\n * was asked for never reads as the whole of what exists.\n */\nexport function reportNotes(reports: SourceReport[]): Note[] {\n const notes: Note[] = [];\n\n const answered = reports.filter((report) => report.status === \"answered\");\n const failed = reports.filter((report) => report.status === \"failed\");\n\n for (const report of failed) {\n // What the rest of the answer holds depends on whether anything else\n // answered. Promising a caller that another source made up the difference\n // when none did is the failure this whole shape exists to prevent.\n const consolation =\n answered.length > 0\n ? `This answer holds what the other sources found, and says nothing about what ${report.name} holds.`\n : \"Nothing here is evidence about what it holds.\";\n notes.push(\n mustKeep(\n `${report.name} did not answer (${report.error?.code}): ${report.error?.message} ${consolation}`,\n ),\n );\n }\n\n for (const report of answered) {\n const sent = report.wordings.filter((attempt) => attempt.ran && attempt.error === null);\n\n if (report.count === 0) {\n notes.push(\n mustKeep(\n sent.length > 1\n ? `${report.name} answered and offered no row for any of the ${sent.length} wordings it was ` +\n `sent (${sent.map((attempt) => `\"${attempt.query}\"`).join(\", \")}). Each of those is a ` +\n \"statement about a wording; together they are the closest this server comes to saying \" +\n \"the corpus holds nothing. Try a main ingredient, or the dish's name in the language \" +\n \"that source publishes in.\"\n : `${report.name} answered and offered no row for this wording. That is a statement about ` +\n \"the wording as much as about the corpus: try the dish's name in another language, or a \" +\n \"main ingredient.\",\n ),\n );\n }\n if (sent.length > 1) {\n notes.push(\n `${report.name} was asked ${sent.length} wordings and these rows are their union: ${sent\n .map((attempt) => `\"${attempt.query}\" (${attempt.count ?? 0})`)\n .join(\", \")}. Those counts are per wording and are never added.`,\n );\n }\n if (report.preferredByName) {\n notes.push(\n `${report.name}'s rows are arranged with the ones naming the dish first, so a wording that ` +\n \"returned near-misses cannot crowd out what another wording found. That is an order over \" +\n \"one source's own rows and not a score against any other source.\",\n );\n }\n if (report.skipped > 0) {\n notes.push(\n mustKeep(\n `${report.name} sent ${report.skipped} row(s) in a shape this server could not read, and they were left out.`,\n ),\n );\n }\n if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {\n const forWording =\n sent.length > 1 ? ` That number belongs to \"${sent[0]?.query}\" alone.` : \"\";\n notes.push(\n `${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.${forWording}`,\n );\n } else {\n notes.push(\n `${report.name} states no total and offers no second page, so a short list here is not evidence that little exists.`,\n );\n }\n }\n\n if (answered.some((report) => report.mixesReferencePages && report.count > 0)) {\n const mixing = answered.filter((report) => report.mixesReferencePages && report.count > 0);\n notes.push(\n `${mixing.map((report) => report.name).join(\" and \")} files pages about an ingredient beside ` +\n \"recipes using it, so a row here can be a page about an ingredient rather than a recipe. \" +\n \"get_recipe says what it read off the page.\",\n );\n }\n\n if (answered.length > 1) {\n notes.push(\n \"Each count above measures something different, and they are never added together into one total.\",\n );\n }\n\n if (reports.some((report) => report.cached)) {\n notes.push(\"Part of this answer came from this server's short-lived in-memory cache.\");\n }\n\n return notes;\n}\n\n/**\n * Words this server writes at the start of one of its own lines.\n *\n * A caller has no way to tell one of these from the same words inside a title,\n * an ingredient or a step written by whoever published it, so a value opening\n * with one is indented before it is rendered. Spacing and case are allowed for,\n * because a forged line only has to look like one of these to a reader.\n */\nconst SERVER_MARKERS = /^(\\s*)(Note|Sources?|Hint|Ingredients|Steps|What differs)(\\s*:)/gim;\n\n/**\n * Keep text somebody else wrote out of the shape this server's own lines take.\n *\n * Applied to the foreign value rather than to the assembled block, because the\n * server writes those same words itself: indenting the whole block would indent\n * its own headings and leave the answer looking like quoted text.\n *\n * The structured output still carries the text exactly as it was published;\n * this is the rendered block only.\n */\nexport function indentMarkerLines(value: string): string {\n return value.replace(SERVER_MARKERS, \" $1$2$3\");\n}\n\n/**\n * Put a value somebody else wrote onto one of this server's own lines.\n *\n * Two things make that safe. A line terminator is turned into a space, because\n * a title or a licence carrying a newline can otherwise close the server's\n * sentence and open a whole section of its own: a forged \"Steps:\" block, a\n * forged search row with an address of its own, or a forged credit line. And an\n * image is defused, because most clients render this block as markdown and an\n * image tag fetches its address the moment it is drawn.\n *\n * Everything a source or a caller wrote passes through here on its way into the\n * text block, including into the notes and the credit, which are the lines a\n * reader trusts most.\n */\nexport function quoteForeign(value: string): string {\n return (\n value\n // Every line terminator Unicode recognises, so a value can occupy one line\n // and no more.\n .replace(/[\\r\\n\\u2028\\u2029\\u0085]+/g, \" \")\n // Control characters, which render as nothing and hide what follows them.\n .replace(/[\\u0000-\\u0008\\u000b-\\u001f\\u007f]/g, \"\")\n // An image tag fetches its address on render; escaping the mark leaves the\n // text readable and the request unmade.\n .replace(/!\\[/g, \"!\\\\[\")\n .replace(/\\s{2,}/g, \" \")\n .trim()\n // Now that it occupies one line, a value opening on one of the server's\n // own markers is the last way it could pass for a line the server wrote.\n .replace(SERVER_MARKERS, \" $1$2$3\")\n );\n}\n\n/**\n * How much of the block the notes may take.\n *\n * Enough for the sentences that qualify an answer, while leaving the answer\n * they qualify room to be one. Whatever is dropped is still in the structured\n * output, where a caller reading that instead loses nothing.\n */\nconst NOTE_BUDGET = Math.round(MAX_TEXT_CHARS * 0.55);\n\nexport interface OkOptions {\n notes?: readonly Note[];\n /** The line that credits whoever published what the answer holds. */\n credit?: string;\n}\n\n/**\n * The notes the block will carry, and the trailer they make with the credit.\n *\n * A long run of notes must not crowd out the answer it qualifies, so the last\n * note the answer reads correctly without goes first and the ones it misleads\n * without are still there when the room runs out. Whatever is dropped stays in\n * the structured output.\n */\nfunction buildTrailer(options: OkOptions): string {\n const kept: Note[] = [];\n const held = new Set<string>();\n for (const note of options.notes ?? []) {\n const text = noteText(note);\n if (held.has(text)) {\n continue;\n }\n held.add(text);\n kept.push(note);\n }\n\n const length = (): number => kept.map(noteText).join(\"\\n\").length;\n\n while (kept.length > 0 && length() > NOTE_BUDGET) {\n let victim = -1;\n for (let index = kept.length - 1; index >= 0; index -= 1) {\n if (typeof kept[index] === \"string\") {\n victim = index;\n break;\n }\n }\n // Room runs out with nothing but the sentences the answer misleads without.\n // Dropping one of those buys space by removing the warning the answer most\n // needed, so the body gives up its own room instead.\n if (victim === -1) {\n break;\n }\n kept.splice(victim, 1);\n }\n\n const credit = options.credit ?? \"Source: this server called none.\";\n return [...kept.map((note) => `Note: ${noteText(note)}`), credit].join(\"\\n\");\n}\n\n/**\n * Build a result whose text block ends with its notes and its credit.\n *\n * The body is cut to fit around the trailer rather than the whole block being\n * cut afterwards. Appending the credit and then truncating loses exactly the\n * credit, which is the one line that must survive.\n *\n * Notes qualify an answer: that a source failed, that a quantity was rounded,\n * that a count means one thing here and another there. A client that shows only\n * the text would otherwise present an unqualified answer, so they travel with\n * the credit.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: OkOptions = {},\n): ToolResult {\n const trailer = buildTrailer(options);\n const cut = \"\\n\\n[shortened; the full result is in the structured output]\";\n const room = MAX_TEXT_CHARS - `\\n\\n${trailer}`.length;\n const text =\n body.length <= room\n ? `${body}\\n\\n${trailer}`\n : `${truncate(body, Math.max(0, room - cut.length))}${cut}\\n\\n${trailer}`;\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\n}\n\n/**\n * Errors carry no structured payload: the SDK checks it against the tool's\n * declared output schema, and a failure does not fit that shape.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof RecipesError\n ? error\n : new RecipesError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${quoteForeign(known.message)}`];\n if (known.details.hint) {\n lines.push(`Hint: ${quoteForeign(known.details.hint)}`);\n }\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\n/**\n * How much room a body has once its notes and its credit are set aside.\n *\n * A tool rendering several things at once shares this out between them, so each\n * gets an opening rather than the first one filling the block and the rest\n * being cut away.\n */\nexport function roomForBody(options: OkOptions = {}): number {\n return Math.max(200, MAX_TEXT_CHARS - buildTrailer(options).length - 60);\n}\n\n/**\n * The opening of a list, and how much of it is not here.\n *\n * The sentences qualifying an answer keep their room, which leaves the result\n * lines as the thing that gives way when a block runs short. Giving way to\n * nothing at all, in silence, is what turns a hundred ingredients into a recipe\n * that appears to need none, so a few lines are kept whatever the room allows\n * and the caller is told how many are missing.\n */\nexport interface FittedLines {\n lines: string[];\n hidden: number;\n}\n\n/** Lines that were always going to be shown, however tight the block is. */\nconst ALWAYS_SHOWN = 3;\n\nexport function fitLines(lines: readonly string[], room: number): FittedLines {\n const kept: string[] = [];\n let used = 0;\n\n for (const line of lines) {\n if (kept.length >= ALWAYS_SHOWN && used + line.length > room) {\n break;\n }\n kept.push(line);\n used += line.length + 1;\n }\n\n return { lines: kept, hidden: lines.length - kept.length };\n}\n\n/** What a block says about the rows it had no room for. */\nexport function omittedLinesLine(shown: number, total: number, what: string): string {\n return `… ${shown} of ${total} ${what} shown here; all ${total} are in the structured output`;\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;\n}\n\n/** A compact listing, carrying what it takes to pick one recipe out of many. */\nexport function renderRows(rows: z.infer<typeof rowSchema>[]): string {\n return rows\n .map((row, index) => {\n const head = `${index + 1}. ${quoteForeign(row.title)} · ${quoteForeign(row.source_name)} · id: ${row.id}`;\n // The address goes on its own line: a client that renders only text has\n // nothing else to cite from, and a model with an identifier and no link\n // will build one.\n return `${head}\\n ${quoteForeign(row.url)}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * The credit line, naming the sources that actually contributed to this answer.\n *\n * A source that failed, or that was never asked, has published nothing here and\n * crediting it would say it had.\n */\nexport function creditLine(contributors: Array<{ attribution: string; url?: string }>): string {\n if (contributors.length === 0) {\n return \"No source contributed to this answer.\";\n }\n const names = contributors.map((entry) =>\n entry.url ? `${entry.attribution} — ${quoteForeign(entry.url)}` : entry.attribution,\n );\n return names.join(\"\\n\");\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n *\n * A caller branches on the code a refusal opens with, and arguments are refused\n * along two paths: the code of a tool writes its own refusals, and the schema\n * refuses on its own before that code runs. Both open with the same code here,\n * so a caller reading the vocabulary finds it whichever path refused.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/** Zod's own wording. The error code is written in front of it. */\nconst defaultMessage = z.core.locales.en().localeError;\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n const refuse: z.core.$ZodErrorMap = (issue) =>\n issue.code === \"unrecognized_keys\"\n ? unknownArgumentMessage(issue.keys, declared)\n : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;\n\n for (const argument of Object.values(shape)) {\n carryTheCode(argument, refuse);\n }\n\n return z.strictObject(shape, { error: refuse });\n}\n\n/**\n * Route every refusal a declared argument can raise through one error map.\n *\n * A bound such as `min` raises its issue from the check that holds it, and the\n * issue names that check as the instance whose error map applies, so the map\n * reaches it only by being written onto the check itself. Zod offers no copy\n * that keeps a check's identity, hence the assignment. It is confined to\n * schemas built for a declaration and handed to this function, which is the\n * whole of what a tool module does with them.\n */\nfunction carryTheCode(schema: z.core.$ZodType, refuse: z.core.$ZodErrorMap): void {\n const definition = schema._zod.def as {\n error?: unknown;\n checks?: Array<{ _zod: { def: { error?: unknown } } }>;\n innerType?: z.core.$ZodType;\n };\n\n definition.error = refuse;\n for (const check of definition.checks ?? []) {\n check._zod.def.error = refuse;\n }\n\n // A default, an optional or any other wrapper answers for its own shape and\n // leaves the bounds to the type it wraps.\n if (definition.innerType) {\n carryTheCode(definition.innerType, refuse);\n }\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * get_recipe: read one recipe, from the source its identifier names.\n *\n * Only that source is called. Trying another after a miss would answer a\n * question about one corpus with somebody else's recipe of a different name,\n * which is a worse answer than the absence it replaced.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { buildRecipeView, recipeSchema, renderYield, SECTIONS } from \"./recipeView.js\";\nimport type { Section } from \"./recipeView.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n creditLine,\n fitLines,\n noteTexts,\n ok,\n omittedLinesLine,\n quoteForeign,\n roomForBody,\n toToolError,\n} from \"./shared.js\";\nimport type { Note, ToolResult } from \"./shared.js\";\n\nexport const getRecipeDescription = [\n \"Read one recipe in full: its ingredients, its steps, what it yields, and whatever times, rating and nutrition its source publishes.\",\n \"'id' must come from search_recipes. It names the source, so this reads the right one without guessing; an identifier no source would have minted is refused, because sending it anywhere would answer about the wrong dish.\",\n \"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable is flagged rather than scaled.\",\n \"A page that states no number of servings comes back as published and says so, because dividing by a yield nobody wrote would answer for a number of people the page never claimed.\",\n \"A part this answer holds nothing for says which of two things happened: the page showed no sign of it, or this server failed to read what the page carries. An empty ingredient list is never evidence that an ingredient is absent from the dish.\",\n \"'sections' decides what comes back, and 'sections_omitted' names what was left out: a field belonging to an omitted section is empty because nobody asked for it, never because the page states nothing.\",\n \"A field a source does not publish is null, never zero. Credit the source and link the url when you repeat any of it.\",\n].join(\" \");\n\nexport const getRecipeInput = strictInput({\n id: z\n .string()\n .min(1)\n .max(500)\n .describe(\"From search_recipes, such as 'marmiton:44078' or 'cookbook:Cookbook:Carbonara'.\"),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale to this many. Left out, the quantities come back as published.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\", \"steps\"])\n .describe(\"Which parts to return. A full recipe is a lot of text, so this defaults to two.\"),\n max_steps: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Steps to return. The answer says how many more there are.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n});\n\nexport const getRecipeOutput = z.object({\n recipe: recipeSchema,\n id_read_as: z\n .string()\n .nullable()\n .describe(\"How a raw identifier was routed, when it was not spelled with its source.\"),\n notes: z.array(z.string()),\n});\n\nexport type GetRecipeArgs = z.infer<typeof getRecipeInput>;\n\nexport async function runGetRecipe(\n client: RecipesClient,\n args: GetRecipeArgs,\n): Promise<ToolResult> {\n try {\n const { recipe, cached, read } = await client.getRecipe(args.id);\n const view = buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n });\n\n const notes: Note[] = [...view.notes];\n if (read.inferred) {\n notes.unshift(\n `\"${quoteForeign(args.id)}\" was read as ${read.source.name}'s, because ${read.inferred}. ` +\n \"Spell an id with its source to leave nothing to infer.\",\n );\n }\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const payload = view.payload;\n const credit = creditLine([{ attribution: payload.attribution, url: payload.url }]);\n const head = [\n `${quoteForeign(payload.title)} · ${quoteForeign(payload.source_name)}`,\n quoteForeign(payload.url),\n renderYield(payload),\n ];\n\n const ingredientLines = payload.ingredients.map((entry) =>\n entry.scaling === \"unscaled\"\n ? `- ${quoteForeign(entry.text)} (no quantity)`\n : `- ${quoteForeign(entry.text)}`,\n );\n const stepLines = payload.steps.map((step, index) => `${index + 1}. ${quoteForeign(step)}`);\n\n // A long method and a long ingredient list share what the notes leave, so\n // neither of them fills the block and cuts the other away.\n const room = roomForBody({ notes, credit }) - head.join(\"\\n\").length;\n const parts = [ingredientLines, stepLines].filter((part) => part.length > 0);\n const share = parts.length === 0 ? 0 : room / parts.length;\n\n const lines = [...head];\n const ingredients = fitLines(ingredientLines, share);\n if (ingredientLines.length > 0) {\n lines.push(\"\", \"Ingredients:\", ...ingredients.lines);\n if (ingredients.hidden > 0) {\n lines.push(\n omittedLinesLine(ingredients.lines.length, ingredientLines.length, \"ingredient lines\"),\n );\n }\n }\n const steps = fitLines(stepLines, share);\n if (stepLines.length > 0) {\n lines.push(\"\", \"Steps:\", ...steps.lines);\n if (steps.hidden > 0) {\n lines.push(omittedLinesLine(steps.lines.length, stepLines.length, \"steps\"));\n }\n }\n\n return ok(\n { recipe: payload, id_read_as: read.inferred, notes: noteTexts(notes) },\n lines.join(\"\\n\"),\n { notes, credit },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * scale_ingredients: multiply a list a caller already holds.\n *\n * Nothing here touches the network. The list can come from any of the sources,\n * from a cookbook on a shelf or from a photograph of a card, and it can mix\n * French and English lines: each line is read in the language it is written in\n * and comes back written the same way.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport { scaleIngredients } from \"../recipe/scale.js\";\nimport type { LanguageChoice } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ingredientSchema, ok, quoteForeign, toIngredientPayload, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const scaleIngredientsDescription = [\n \"Multiply a list of ingredient lines, in French, in English, or in a list holding both.\",\n \"Give either 'factor', or 'from_servings' and 'to_servings' and the factor is worked out from them.\",\n \"Quantities land where a kitchen can follow them: an egg stays whole because half of one is not something a cook takes out of the shell, while anything that pours, weighs or cuts can halve, a spoonful shrinks into the smaller spoon before it is rounded, and a pinch keeps whatever size a hand gives it while its count is multiplied.\",\n \"Every line comes back with 'scaling': 'scaled' when the arithmetic landed on the exact product, 'rounded' when something had to move for the line to stay usable, 'unscaled' when the line carries nothing to multiply. A rounded line says what it was rounded from and in which direction.\",\n \"No quantity is converted between measuring systems: grams stay grams and cups stay cups, because a conversion changes what the recipe said.\",\n].join(\" \");\n\nexport const scaleIngredientsInput = strictInput({\n ingredients: z\n .array(z.string().max(500))\n .min(1)\n .max(200)\n .describe(\n \"The lines as written, one ingredient each, such as '200 g de farine' or '3 eggs'. One line, not a whole recipe.\",\n ),\n factor: z\n .number()\n .gt(0)\n .max(1000)\n .optional()\n .describe(\n \"What to multiply by. Give exactly one of: 'factor', or both 'from_servings' and 'to_servings'.\",\n ),\n from_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What the list serves now. Give it with 'to_servings', and without 'factor'.\"),\n to_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What it should serve. Give it with 'from_servings', and without 'factor'.\"),\n language: z\n .enum([\"auto\", \"fr\", \"en\"])\n .default(\"auto\")\n .describe(\n \"'auto' reads each line on its own, which is what a list holding both languages needs. Name a language to read every line that way.\",\n ),\n});\n\nexport const scaleIngredientsOutput = z.object({\n factor: z.number().describe(\"What every quantity was multiplied by.\"),\n language: z.string().describe(\"How the lines were read.\"),\n ingredients: z.array(ingredientSchema),\n scaled_count: z.number().int().describe(\"Lines whose arithmetic came out exact.\"),\n rounded_count: z.number().int().describe(\"Lines whose value moved to stay usable.\"),\n unscaled_count: z.number().int().describe(\"Lines carrying nothing that can be multiplied.\"),\n notes: z.array(z.string()),\n});\n\nexport type ScaleIngredientsArgs = z.infer<typeof scaleIngredientsInput>;\n\n/**\n * Read the factor off whichever pair of arguments the caller gave.\n *\n * The two ways of saying it are kept apart rather than merged, because a caller\n * who gives both and means different things by them has made a mistake worth\n * hearing about.\n */\nexport function resolveFactor(args: ScaleIngredientsArgs): number {\n const hasPair = args.from_servings !== undefined && args.to_servings !== undefined;\n\n if (args.factor !== undefined && hasPair) {\n throw invalidInput(\n \"Give either 'factor' or the pair of serving counts, not both.\",\n `factor=${args.factor} and ${args.from_servings}→${args.to_servings} would be two different multipliers.`,\n );\n }\n\n if (args.factor !== undefined) {\n return args.factor;\n }\n const { from_servings: from, to_servings: to } = args;\n if (from !== undefined && to !== undefined) {\n return to / from;\n }\n\n if (args.from_servings !== undefined || args.to_servings !== undefined) {\n throw invalidInput(\n \"Serving counts come in pairs: give both 'from_servings' and 'to_servings'.\",\n \"Or give 'factor' on its own.\",\n );\n }\n\n throw invalidInput(\n \"Nothing said how much to multiply by.\",\n \"Give 'factor', or both 'from_servings' and 'to_servings'.\",\n );\n}\n\n/**\n * The factor as a reader can check the quantities against.\n *\n * A pair of serving counts divides into a repeating decimal, so what is stated\n * is shortened. Shortening it to three places puts a small factor at zero, and\n * \"Scaled by 0\" beside quantities that are not zero contradicts the answer it\n * introduces; below that, the significant digits are what says what happened.\n */\nfunction statedFactor(factor: number): number {\n const shortened = Math.round(factor * 1000) / 1000;\n return shortened === 0 ? Number(factor.toPrecision(3)) : shortened;\n}\n\nexport function runScaleIngredients(args: ScaleIngredientsArgs): ToolResult {\n try {\n const factor = resolveFactor(args);\n const scaled = scaleIngredients(args.ingredients, {\n factor,\n language: args.language as LanguageChoice,\n });\n\n const counts = {\n scaled_count: scaled.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: scaled.filter((entry) => entry.scaling === \"rounded\").length,\n unscaled_count: scaled.filter((entry) => entry.scaling === \"unscaled\").length,\n };\n\n const notes: string[] = [];\n if (counts.rounded_count > 0) {\n notes.push(\n `${counts.rounded_count} line(s) did not land on the exact product; each says in its own ` +\n \"note what happened to it.\",\n );\n }\n if (counts.unscaled_count > 0) {\n notes.push(\n `${counts.unscaled_count} line(s) carry no quantity to multiply and are repeated as given.`,\n );\n }\n if (args.language === \"auto\") {\n const languages = new Set(scaled.map((entry) => entry.language));\n if (languages.size > 1) {\n notes.push(\n \"This list holds lines in both languages, and each was read and rewritten in its own.\",\n );\n }\n }\n if (scaled.some((entry) => entry.note?.includes(\"approximate measure\"))) {\n notes.push(\n \"A pinch, a handful and their kind were multiplied by their count. The size of one stays \" +\n \"whatever the cook's hand gives it, and none of them was turned into grams or spoons.\",\n );\n }\n\n const body = scaled\n .map((entry, index) => `${index + 1}. ${quoteForeign(entry.text)}`)\n .join(\"\\n\");\n\n return ok(\n {\n factor: statedFactor(factor),\n language: args.language,\n ingredients: scaled.map(toIngredientPayload),\n ...counts,\n notes,\n },\n `Scaled by ${statedFactor(factor)}:\\n${body}`,\n { notes, credit: \"Source: the list you supplied. Scaled offline; nothing was fetched.\" },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_recipes: one question, every source, one list.\n *\n * The calls go out together, so asking them all costs about what asking one\n * costs. What each source said about its own results is reported beside the\n * rows, because those numbers count different things and adding them would\n * invent a figure no source published.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { namesDish, readConditions } from \"../sources/wordings.js\";\nimport type { ConditionKind } from \"../sources/wordings.js\";\nimport type { SourceId } from \"../types.js\";\nimport {\n creditLine,\n mustKeep,\n noteTexts,\n ok,\n quoteForeign,\n renderRows,\n reportNotes,\n reportSchema,\n rowSchema,\n toReportPayload,\n toRowPayload,\n toToolError,\n} from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nconst WHITESPACE = /\\s+/;\n\n/**\n * The order the rows are in, and what no order could be.\n *\n * No score orders the sources against each other, so the only order this server\n * can describe is the one it imposed.\n */\nfunction howTheRowsAreOrdered(contributed: ReadonlyArray<{ name: string }>): string {\n if (contributed.length > 1) {\n return \"One row from each source in turn, in the order each source returned them. No score orders them against each other.\";\n }\n if (contributed.length === 1) {\n return `Every row came from ${contributed[0]?.name}, in the order it returned them.`;\n }\n return \"No source contributed a row.\";\n}\n\n/**\n * What an empty answer amounts to.\n *\n * No source answering and every source holding nothing are different statements\n * about the world, and a caller cannot tell them apart from an empty list.\n */\nfunction nothingCameBack(answeredCount: number, query: string, alsoSent: string): string {\n if (answeredCount === 0) {\n return `No source answered for \"${query}\", so nothing here says whether such a recipe exists.`;\n }\n return `Nothing came back for \"${query}\".${alsoSent}`;\n}\n\nexport const searchRecipesDescription = [\n \"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.\",\n \"Each row carries the id get_recipe takes, and that id names the source it came from, so nothing has to be guessed afterwards.\",\n \"The sources are written in different languages and count their own results differently, so 'per_source' says what each one answered, what its own number means, and names any that failed. A short list is never evidence of what exists.\",\n \"Some sources keep recipes and reference pages together, so a row can be a page about an ingredient rather than a recipe using it. Only get_recipe can tell them apart, and it says what it read off the page.\",\n \"The query goes to each source's own search as free text. There is no filtering: a word naming a diet, a time or a calorie count matches only where that source's index happens to carry it.\",\n \"Ask in a whole sentence if that is the question. These indexes answer the words they are handed, so a sentence is also sent as the words naming the dish and as the dish word alone, and the rows are the union; 'per_source' lists every wording and what it returned.\",\n \"What the question says the recipe must not hold is set aside from those shorter wordings rather than searched for, and named back in the notes: a negation, an allergy stated as one, a diet named in one word, and the number of people at the table. No source filters on any of it, so open a row with get_recipe and read the ingredient list before calling it suitable.\",\n \"A condition is read with the food on whichever side of it the sentence put one, so 'allergique aux noix' and 'peanut allergy' both name the nut. Where a sentence puts a food on neither side, the notes say a condition was stated and that its food was not read, because naming the wrong word would hide a dish and search for the food being avoided at once.\",\n \"Rows are interleaved one source at a time rather than ranked, because the sources share no score that could order them against each other.\",\n].join(\" \");\n\nexport const searchRecipesInput = strictInput({\n query: z\n .string()\n .min(1)\n .max(200)\n .describe(\"A dish or an ingredient, in any of the languages the sources publish in.\"),\n limit_per_source: z\n .number()\n .int()\n .min(1)\n .max(25)\n .default(5)\n .describe(\"Rows to take from each source, so one source cannot fill the whole list.\"),\n sources: z\n .array(z.string())\n .optional()\n .describe(\n \"Sources to ask, by id. Left out, they are all asked, which is the point of this tool. The ids are the ones 'per_source' reports.\",\n ),\n fan_out: z\n .boolean()\n .default(true)\n .describe(\n \"Whether a question may also be sent in shorter wordings derived from it. On, because these indexes answer the words they are handed: a question written as a sentence comes back empty from a corpus holding several of the dish. Turn it off to send exactly the words typed; 'per_source' then names the wordings that were withheld.\",\n ),\n});\n\nexport const searchRecipesOutput = z.object({\n query: z.string(),\n results: z.array(rowSchema),\n result_count: z.number().int().describe(\"Rows in this answer, across every source.\"),\n per_source: z.array(reportSchema),\n order: z.string().describe(\"How the list was built, in words.\"),\n notes: z.array(z.string()),\n});\n\nexport type SearchRecipesArgs = z.infer<typeof searchRecipesInput>;\n\n/**\n * Beyond this many words a question is asking for conditions rather than naming\n * a dish, so the answer says that none of them were applied as conditions.\n */\nconst WORDS_BEFORE_SAYING_SO = 4;\n\nexport async function runSearchRecipes(\n client: RecipesClient,\n args: SearchRecipesArgs,\n): Promise<ToolResult> {\n try {\n const merged = await client.searchRecipes(\n args.query,\n args.limit_per_source,\n args.sources as readonly SourceId[] | undefined,\n { fanOut: args.fan_out },\n );\n\n const results = merged.rows.map(toRowPayload);\n const notes = reportNotes(merged.reports);\n\n const answered = merged.reports.filter((report) => report.status === \"answered\");\n const contributed = merged.reports.filter((report) => report.count > 0);\n\n // How the order was built depends on how many sources are in it. Describing\n // a merge that did not happen tells a caller the list means something it\n // does not.\n const order = howTheRowsAreOrdered(contributed);\n\n // What a question asks the dish to avoid is the part of it no source can\n // answer, and the part a reader is most harmed by getting wrong. A recipe\n // returned here may hold exactly the ingredient the person was avoiding, so\n // the conditions are named back rather than left to be assumed met.\n const conditions = readConditions(args.query);\n const named = (kind: ConditionKind): string =>\n conditions.conditions\n .filter((condition) => condition.kind === kind && condition.named !== null)\n .map((condition) => `\"${quoteForeign(condition.named as string)}\"`)\n .join(\" and \");\n // A condition whose food could not be read is still a condition the person\n // set, and the one thing an answer must not do with it is stay quiet.\n const unread = (kind: ConditionKind): boolean =>\n conditions.conditions.some(\n (condition) => condition.kind === kind && condition.named === null,\n );\n\n // An allergy is named as one. It is the same fact as an exclusion and not\n // the same stake: a row that turns out to carry the ingredient is a dish\n // somebody cannot eat.\n const allergies = named(\"allergy\");\n if (allergies !== \"\") {\n notes.push(\n mustKeep(\n `This question states an allergy to ${allergies}. No source filtered on it, and none can: ` +\n \"these are searches over what a page holds rather than over what it leaves out, so a row \" +\n \"here may be a recipe built on that ingredient. Open every row with get_recipe and read \" +\n \"the whole ingredient list before cooking from it.\",\n ),\n );\n }\n if (unread(\"allergy\")) {\n notes.push(\n mustKeep(\n \"This question states an allergy, and the words around it do not say plainly which food \" +\n \"it names, so no food was read from it and none is named here. Nothing was set aside \" +\n \"and no source filters on an allergy in any case: open every row with get_recipe and \" +\n \"read the whole ingredient list before cooking from it.\",\n ),\n );\n }\n\n const excluded = named(\"excluded\");\n if (excluded !== \"\") {\n notes.push(\n mustKeep(\n `This question asks for a recipe free of ${excluded}. No source filtered on that, and none ` +\n \"can: these are searches over what a page holds rather than over what it leaves out. \" +\n \"Open a row with get_recipe and check the ingredient list before calling it suitable.\",\n ),\n );\n }\n if (unread(\"excluded\")) {\n notes.push(\n mustKeep(\n \"This question says the recipe must leave something out, and the words around it do not \" +\n \"say plainly which food it names, so nothing was read from it and nothing is named \" +\n \"here. Open a row with get_recipe and read the ingredient list before calling it \" +\n \"suitable.\",\n ),\n );\n }\n\n const diets = named(\"diet\");\n if (diets !== \"\") {\n notes.push(\n mustKeep(\n `This question names a diet: ${diets}. A diet is a rule about what a recipe leaves out, and ` +\n \"no source filters on one, so the word was sent as text and matches only where a source's \" +\n \"index happens to carry it. Open a row with get_recipe and read the ingredient list \" +\n \"before calling it suitable.\",\n ),\n );\n }\n if (conditions.servings !== null) {\n notes.push(\n mustKeep(\n `This question asks for ${conditions.servings} servings. That is not something a search ` +\n \"matches, so it was not looked for. Pass 'servings' to get_recipe or compare_recipes to \" +\n \"rescale a recipe once one is chosen.\",\n ),\n );\n }\n\n // A long question reads like a request for filtering, and none is applied:\n // the words go to each source's own search as they were typed. A caller\n // told nothing would report \"desserts under 300 calories\" as a filtered\n // answer, when the number was matched as text or matched nothing at all.\n // This is said even where a condition was read by name, because the ones\n // read by name are the ones this server can recognise and a question can\n // carry others.\n if (args.query.trim().split(WHITESPACE).length > WORDS_BEFORE_SAYING_SO) {\n notes.push(\n mustKeep(\n \"This query went to each source as free text and no filter was applied to it. Anything the \" +\n \"question said the recipe must not contain went out as words like the rest of it, so read \" +\n \"the rows rather than assuming they meet it.\",\n ),\n );\n }\n\n // A source ranking a title on the letters it opens with answers \"chameau\"\n // with a chapeau and three châteaux. The rows are what the sources offered,\n // and a reader shown them without a word about it takes them for the dish.\n if (results.length > 0 && !results.some((row) => namesDish(row.title, args.query))) {\n notes.push(\n `No title here carries a word of \"${args.query}\". These rows are what the sources ranked ` +\n \"for that spelling, so read them as candidates to check rather than as recipes for the dish.\",\n );\n }\n\n if (results.length === 0 && answered.length === merged.reports.length) {\n notes.push(\n \"Every source answered and none holds anything under this wording. Try the dish's name in another language, or name a main ingredient instead.\",\n );\n }\n\n // A reader who can see what was actually sent can redo the search by hand,\n // and can tell a wording that found nothing from a corpus that holds\n // nothing. The words as asked are left out of this line: they are already\n // at the head of the answer.\n const derived = [\n ...new Set(\n merged.reports.flatMap((report) =>\n report.wordings\n .filter((attempt) => attempt.ran && attempt.query !== args.query.trim())\n .map((attempt) => attempt.query),\n ),\n ),\n ];\n const alsoSent =\n derived.length > 0\n ? `\\nAlso searched for: ${derived.map((wording) => `\"${quoteForeign(wording)}\"`).join(\", \")}.`\n : \"\";\n\n const body =\n results.length > 0\n ? `${results.length} recipes for \"${args.query}\":\\n${renderRows(results)}${alsoSent}`\n : nothingCameBack(answered.length, args.query, alsoSent);\n\n return ok(\n {\n query: args.query,\n results,\n result_count: results.length,\n per_source: merged.reports.map(toReportPayload),\n order,\n notes: noteTexts(notes),\n },\n body,\n {\n notes,\n credit: creditLine(\n contributed.map((report) => ({ attribution: `Source: ${report.name}` })),\n ),\n },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":";;;AAQA,OAAOA,cAAa;AACpB,SAAS,4BAA4B;;;ACJrC,SAAS,iBAAiB;;;ACQ1B,OAAO,aAAa;;;ACRb,IAAM,cAAc;AACpB,IAAM,WAAW;AACjB,IAAM,aAAa;;;ADSnB,IAAM,aAAa,CAAC,UAAU,SAAS,QAAQ,OAAO;AAWtD,IAAM,sBAAsB;AAM5B,IAAM,0BAA0B;AAEhC,IAAM,0BAA0B;AAYhC,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,2CAA2C,KAAK,mBAAmB,WAAW,KAAK,IAAI,CAAC;AAAA;AAAA,IAC1F;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,oBAAoB,KAAK;AAE5C,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,sBAAsB,KAAQ,KAAM,IAAO;AAAA,IACvE,YAAY,YAAY,KAAK,uBAAuB,GAAG,GAAG,CAAC;AAAA,IAC3D,YAAY,YAAY,KAAK,wBAAwB,KAAS,GAAG,KAAU;AAAA,IAC3E,iBAAiB,YAAY,KAAK,6BAA6B,KAAK,GAAG,GAAI;AAAA,IAC3E;AAAA,EACF;AACF;;;AE5GO,IAAM,cAAoC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAC7B;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,aAAa,aAAa,SAAS,WAAW,CAAC,CAAC;AAE/C,IAAM,eAAe,CAAC,SAAiB,MAAe,UAC3D,IAAI,aAAa,iBAAiB,SAAS,EAAE,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC,GAAI,MAAM,CAAC;AAE1E,IAAM,cAAc,CAAC,SAAiB,YAC3C,IAAI,aAAa,gBAAgB,SAAS;AAAA,EACxC,MAAM;AAAA,EACN,GAAG;AACL,CAAC;AAEI,IAAM,eAAe,CAAC,SAAiB,YAC5C,IAAI,aAAa,iBAAiB,SAAS;AAAA,EACzC,MAAM,iEAAiE,UAAU;AAAA,EACjF,GAAG;AACL,CAAC;AAEI,IAAM,eAAe,CAAC,SAAiB,YAC5C,IAAI,aAAa,iBAAiB,SAAS,WAAW,CAAC,CAAC;AAEnD,IAAM,UAAU,CAAC,SAAiB,YACvC,IAAI,aAAa,WAAW,SAAS,WAAW,CAAC,CAAC;AAW7C,SAAS,eAAe,OAA8B;AAC3D,MAAI,iBAAiB,cAAc;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,QAAM,MAAM;AACZ,QAAM,OAAO,OAAO,KAAK,SAAS,WAAW,IAAI,OAAO;AACxD,QAAM,QAAQ,YAAY,KAAK,CAAC,cAAc,cAAc,IAAI;AAEhE,QAAM,UACJ,OAAO,OAAO,IAAI,YAAY,YAAY,IAAI,YAAY,OACrD,IAAI,UACL,CAAC;AAEP,SAAO,IAAI,aAAa,SAAS,iBAAiB,SAAS,OAAO;AACpE;AAeO,SAAS,WAAW,OAAgB,YAAkC;AAC3E,MAAI,iBAAiB,cAAc;AACjC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,eAAe,KAAK;AAClC,QAAM,WAAW,MAAM,QAAQ,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACzD,QAAM,UACJ,MAAM,SAAS,cACX,GAAG,UAAU,oCACb,GAAG,UAAU;AACnB,QAAM,UAAU,aAAa,KAAK,UAAU,GAAG,OAAO,iBAAiB,QAAQ;AAC/E,QAAM,UAAwB,CAAC;AAC/B,MAAI,MAAM,QAAQ,QAAQ,QAAW;AACnC,YAAQ,MAAM,MAAM,QAAQ;AAAA,EAC9B;AACA,MAAI,MAAM,QAAQ,WAAW,QAAW;AACtC,YAAQ,SAAS,MAAM,QAAQ;AAAA,EACjC;AAEA,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,SAAS,SAAS,OAAO;AAAA,IAClC,KAAK;AACH,aAAO,aAAa,OAAO;AAAA,IAC7B,KAAK;AACH,aAAO,YAAY,SAAS,OAAO;AAAA,IACrC,KAAK;AACH,aAAO,aAAa,SAAS,OAAO;AAAA,IACtC,KAAK;AACH,aAAO,QAAQ,SAAS,OAAO;AAAA,IACjC;AACE,aAAO,aAAa,SAAS,OAAO;AAAA,EACxC;AACF;;;AC7IA,IAAM,YAAY;AAwBX,SAAS,UAAU,OAAe,SAA+C;AACtF,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,IAAI;AAClB,UAAM;AAAA,MACJ;AAAA,MACA,uDAAuD,QAAQ,OAAO,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,aAAW,UAAU,SAAS;AAC5B,UAAM,SAAS,GAAG,OAAO,EAAE,GAAG,SAAS;AACvC,QAAI,CAAC,QAAQ,YAAY,EAAE,WAAW,OAAO,YAAY,CAAC,GAAG;AAC3D;AAAA,IACF;AACA,UAAM,YAAY,QAAQ,MAAM,OAAO,MAAM,EAAE,KAAK;AACpD,QAAI,cAAc,IAAI;AACpB,YAAM;AAAA,QACJ,IAAI,OAAO;AAAA,QACX;AAAA,MACF;AAAA,IACF;AACA,WAAO,EAAE,QAAQ,WAAW,UAAU,KAAK;AAAA,EAC7C;AAEA,QAAM,UAAU,QACb,IAAI,CAAC,YAAY,EAAE,QAAQ,OAAO,OAAO,OAAO,OAAO,EAAE,EAAE,EAC3D;AAAA,IAAO,CAAC,UACP,QAAQ,MAAM,KAAK;AAAA,EACrB;AAEF,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM;AAAA,MACJ,IAAI,OAAO,+BAA+B,MAAM,QAAQ,IAAI,CAAC,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,MACrF,mCAAmC,QAAQ,IAAI,CAAC,UAAU,GAAG,MAAM,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,KAAK,MAAM,CAAC;AAAA,IACzG;AAAA,EACF;AAEA,QAAM,OAAO,QAAQ,CAAC;AACtB,MAAI,CAAC,MAAM;AACT,UAAM;AAAA,MACJ,IAAI,OAAO;AAAA,MACX,6BAA6B,OAAO,8DACd,QAAQ,OAAO,CAAC;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,KAAK;AAAA,IACb,WAAW,KAAK,MAAM;AAAA,IACtB,UAAU,KAAK,MAAM,QACjB,GAAG,KAAK,MAAM,GAAG,8EACjB,KAAK,MAAM;AAAA,EACjB;AACF;AAGA,SAAS,MAAM,SAA8C;AAC3D,QAAM,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI;AAC/C,MAAI,IAAI,UAAU,GAAG;AACnB,WAAO,IAAI,KAAK,EAAE;AAAA,EACpB;AACA,SAAO,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,EAAE,CAAC;AACzD;AAGA,SAAS,QAAQ,SAA2C;AAC1D,QAAM,QAAQ,QAAQ,CAAC;AACvB,SAAO,QAAQ,GAAG,MAAM,EAAE,GAAG,SAAS,0BAA0B;AAClE;;;ACjGA,SAAS,sBAAsB;AAE/B,SAAS,sBAAsB;;;ACO/B,IAAM,kBAAkB;AACxB,IAAM,iBACJ;AAwDK,SAAS,KAAK,OAA+B;AAClD,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,KAAK,QAAQ;AACpE;AAEO,SAAS,SAAS,OAA0B;AACjD,SAAO,MAAM,QAAQ,KAAK,IACtB,MAAM,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,IAClE,CAAC;AACP;AAEO,SAAS,MAAM,OAA+B;AACnD,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE;AAEO,SAAS,SAAS,OAAgB,OAAe,QAA+B;AACrF,QAAM,QAAQ,KAAK,KAAK;AACxB,MAAI,UAAU,MAAM;AAClB,UAAM;AAAA,MACJ,GAAG,OAAO,IAAI,iCAAiC,KAAK;AAAA,IACtD;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,OAAU,OAAgB,QAA4B;AACpE,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM;AAAA,MACJ,GAAG,OAAO,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,aAAa,QAAkB,WAA2B;AACxE,SAAO,GAAG,MAAM,IAAI,SAAS;AAC/B;AAaO,SAAS,iBAAiB,OAA+C;AAC9E,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,OAA+B,CAAC;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI;AACpD,WAAK,GAAG,IAAI,MAAM,KAAK;AAAA,IACzB;AAAA,EACF;AACA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,IAAI,OAAO;AAC/C;AAUO,SAAS,cAAc,WAI5B;AACA,MAAI,cAAc,MAAM;AACtB,WAAO,EAAE,OAAO,MAAM,KAAK,MAAM,MAAM,KAAK;AAAA,EAC9C;AAEA,QAAM,OAAO,eAAe,KAAK,SAAS;AAC1C,MAAI,MAAM;AACR,UAAM,CAAC,UAAU,IAAI,WAAW,EAAE,IAAI,KAAK,MAAM,CAAC;AAClD,UAAM,MAAM,OAAO,QAAQ,QAAQ,KAAK,GAAG,CAAC;AAC5C,UAAM,OAAO,OAAO,SAAS,QAAQ,KAAK,GAAG,CAAC;AAC9C,QAAI,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,IAAI,KAAK,OAAO,KAAK;AAC/D,aAAO,EAAE,OAAO,KAAK,KAAK,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC,EAAE;AAAA,IACtD;AAAA,EACF;AAEA,QAAM,SAAS,gBAAgB,KAAK,SAAS;AAC7C,MAAI,QAAQ;AACV,UAAM,CAAC,SAAS,EAAE,IAAI,OAAO,MAAM,CAAC;AACpC,UAAM,QAAQ,OAAO,OAAO,QAAQ,KAAK,GAAG,CAAC;AAC7C,QAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,aAAO,EAAE,OAAO,OAAO,KAAK,MAAM,MAAM,KAAK,OAAO,CAAC,CAAC,EAAE;AAAA,IAC1D;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM,KAAK,MAAM,MAAM,KAAK;AAC9C;AAGO,SAAS,WAAW,QAKzB;AACA,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,YAAY,OAAO;AAAA,IACnB,UAAU,OAAO;AAAA,IACjB,aAAa,OAAO;AAAA,EACtB;AACF;;;AC/HO,IAAM,mBAAmB;AAAA,EAC9B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA;AAAA,EAEb,qBAAqB;AACvB;AAEA,IAAM,WAAW;AAEjB,IAAM,YAAY;AASlB,SAAS,SAAS,OAAuB;AACvC,SAAO,MAAM,QAAQ,WAAW,EAAE,EAAE,KAAK,KAAK;AAChD;AAEO,SAAS,gBAAgB,QAAuC;AACrE,SAAO;AAAA,IACL,GAAG;AAAA,IAEH,OAAO,KAA2B;AAChC,UAAI,SAAS,KAAK,GAAG,GAAG;AACtB,YAAI;AACJ,YAAI;AACF,gBAAM,mBAAmB,IAAI,QAAQ,UAAU,EAAE,CAAC,EAAE,QAAQ,MAAM,GAAG;AAAA,QACvE,SAAS,OAAO;AAGd,gBAAM;AAAA,YACJ,IAAI,GAAG;AAAA,YACP;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,eAAO;AAAA,UACL,WAAW;AAAA,UACX,KAAK;AAAA,UACL,OAAO;AAAA,QACT;AAAA,MACF;AAEA,UAAI,UAAU,KAAK,GAAG,GAAG;AACvB,eAAO;AAAA,UACL,WAAW;AAAA,UACX,KAAK;AAAA,UACL,OAAO;AAAA,QACT;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,OAAe,OAAkC;AAC5D,YAAM,UAAU,MAAM,OAAO,OAAO,OAAO,KAAK;AAChD,YAAM,OAAO;AAAA,QACV,QAAQ,MAAgC;AAAA,QACzC;AAAA,MACF;AACA,YAAM,OAAoB,CAAC;AAC3B,UAAI,UAAU;AAEd,iBAAW,WAAW,MAAM;AAC1B,cAAM,MAAM,KAAK,SAAS,GAAG;AAC7B,cAAM,QAAQ,KAAK,SAAS,KAAK,KAAK;AACtC,cAAM,MAAM,KAAK,SAAS,SAAS;AACnC,YAAI,EAAE,OAAO,SAAS,MAAM;AAC1B,qBAAW;AACX;AAAA,QACF;AACA,aAAK,KAAK;AAAA,UACR,IAAI,aAAa,iBAAiB,IAAI,GAAG;AAAA,UACzC,QAAQ,iBAAiB;AAAA,UACzB,YAAY,iBAAiB;AAAA,UAC7B,OAAO,SAAS,KAAK;AAAA,UACrB;AAAA,UACA,UAAU,KAAK,SAAS,QAAQ;AAAA,UAChC,SAAS,KAAK,SAAS,OAAO,KAAK,KAAK,SAAS,WAAW;AAAA,QAC9D,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,QACL;AAAA,QACA;AAAA;AAAA;AAAA;AAAA,QAIA,eAAe;AAAA,QACf,oBAAoB;AAAA,QACpB,QAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;AAAA,IAEA,MAAM,UAAU,WAAwC;AACtD,YAAM,UAAU,MAAM,OAAO,UAAU,SAAS;AAChD,aAAO,EAAE,QAAQ,eAAe,QAAQ,IAAI,GAAG,QAAQ,QAAQ,OAAO;AAAA,IACxE;AAAA,EACF;AACF;AAEO,SAAS,eAAe,SAAgC;AAC7D,QAAM,SAAU,WAAW,CAAC;AAC5B,QAAM,UAAU,OAAO;AACvB,QAAM,eAAe,KAAK,SAAS,KAAK;AACxC,QAAM,aAAa,KAAK,SAAS,GAAG;AAEpC,QAAM,YAAY,KAAK,OAAO,SAAS;AACvC,QAAM,OAAO,cAAc,SAAS;AAEpC,SAAO;AAAA,IACL,GAAG,WAAW,gBAAgB;AAAA,IAC9B,IAAI,aAAa,iBAAiB,IAAI,SAAS,OAAO,KAAK,OAAO,gBAAgB,CAAC;AAAA,IACnF,OAAO,SAAS,SAAS,OAAO,SAAS,OAAO,KAAK,SAAS,gBAAgB,CAAC;AAAA,IAC/E,KAAK,SAAS,OAAO,WAAW,OAAO,gBAAgB;AAAA;AAAA,IAEvD,UAAU;AAAA,IACV,YAAY,MAAM,OAAO,QAAQ,KAAK,KAAK;AAAA,IAC3C,UAAU,KAAK;AAAA,IACf,WAAW;AAAA,IACX,WAAW,KAAK,OAAO,SAAS,KAAK,KAAK;AAAA,IAC1C,aAAa,SAAS,OAAO,WAAW;AAAA,IACxC,OAAO,SAAS,OAAO,KAAK;AAAA;AAAA;AAAA,IAG5B,mBAAmB,MAAM,QAAQ,OAAO,aAAa,IAAI,SAAS,OAAO,aAAa,IAAI;AAAA;AAAA,IAE1F,aAAa;AAAA,IACb,aAAa;AAAA,IACb,cAAc,MAAM,OAAO,YAAY;AAAA,IACvC,UAAU,KAAK,OAAO,QAAQ;AAAA;AAAA;AAAA,IAG9B,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,WAAW,iBAAiB,OAAO,SAAS;AAAA,IAC5C,WAAW,SAAS,OAAO,SAAS;AAAA,IACpC,MAAM,SAAS,OAAO,IAAI;AAAA;AAAA;AAAA,IAG1B,SAAS,gBAAgB,aAAa,EAAE,OAAO,cAAc,KAAK,WAAW,IAAI;AAAA,EACnF;AACF;;;AC9LA,IAAM,eAAe;AAqCd,IAAM,mBAAmB;AAAA,EAC9B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA;AAAA,EAEb,qBAAqB;AACvB;AAGA,IAAMC,YAAW;AAEjB,IAAM,YAAY;AAYlB,eAAe,WACb,QACA,OACuD;AACvD,MAAI;AACF,WAAO,MAAM,OAAO,OAAO,KAAK;AAAA,EAClC,SAAS,OAAO;AACd,UAAM,OAAQ,OAAoC;AAClD,QAAI,SAAS,aAAa;AACxB,aAAO,EAAE,MAAM,CAAC,GAAG,QAAQ,MAAM;AAAA,IACnC;AACA,UAAM;AAAA,EACR;AACF;AAEO,SAAS,gBAAgB,QAAuC;AACrE,SAAO;AAAA,IACL,GAAG;AAAA,IAEH,OAAO,KAA2B;AAChC,UAAIA,UAAS,KAAK,GAAG,GAAG;AACtB,eAAO,EAAE,WAAW,KAAK,KAAK,kCAAkC,OAAO,MAAM;AAAA,MAC/E;AACA,UAAI,UAAU,KAAK,GAAG,GAAG;AACvB,eAAO;AAAA,UACL,WAAW;AAAA,UACX,KAAK;AAAA,UACL,OAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,OAAkC;AAC7C,YAAM,UAAU,MAAM,WAAW,QAAQ,KAAK;AAC9C,YAAM,OAAO,OAAiC,QAAQ,MAAM,gBAAgB;AAC5E,YAAM,OAAoB,CAAC;AAC3B,UAAI,UAAU;AAEd,iBAAW,WAAW,MAAM;AAI1B,cAAM,KAAK,KAAK,SAAS,EAAE;AAC3B,cAAM,QAAQ,KAAK,SAAS,KAAK;AACjC,cAAM,MAAM,KAAK,SAAS,GAAG;AAC7B,YAAI,EAAE,MAAM,SAAS,MAAM;AACzB,qBAAW;AACX;AAAA,QACF;AACA,aAAK,KAAK;AAAA,UACR,IAAI,aAAa,iBAAiB,IAAI,EAAE;AAAA,UACxC,QAAQ,iBAAiB;AAAA,UACzB,YAAY,iBAAiB;AAAA,UAC7B;AAAA,UACA;AAAA,UACA,UAAU,KAAK,SAAS,QAAQ;AAAA;AAAA;AAAA,UAGhC,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,QACL;AAAA,QACA;AAAA;AAAA;AAAA;AAAA,QAIA,eAAe,KAAK,SAAS;AAAA,QAC7B,oBACE;AAAA,QACF,QAAQ,QAAQ;AAAA,MAClB;AAAA,IACF;AAAA,IAEA,MAAM,UAAU,WAAwC;AAEtD,YAAM,MAAM,aAAa,KAAK,SAAS,IAAI,EAAE,KAAK,UAAU,IAAI,EAAE,IAAI,UAAU;AAChF,YAAM,UAAU,MAAM,OAAO,UAAU,GAAG;AAC1C,aAAO,EAAE,QAAQ,eAAe,QAAQ,IAAI,GAAG,QAAQ,QAAQ,OAAO;AAAA,IACxE;AAAA,EACF;AACF;AAEO,SAAS,eAAe,SAAgC;AAC7D,QAAM,SAAU,WAAW,CAAC;AAC5B,QAAM,SAAU,OAAO,eAAe,CAAC;AACvC,QAAM,SAAS,OAAO;AACtB,QAAM,QAAQ,MAAM,QAAQ,KAAK;AAMjC,QAAM,YAAY,KAAK,OAAO,IAAI;AAClC,QAAM,OAAO,cAAc,SAAS;AAEpC,SAAO;AAAA,IACL,GAAG,WAAW,gBAAgB;AAAA,IAC9B,IAAI,aAAa,iBAAiB,IAAI,SAAS,OAAO,IAAI,MAAM,gBAAgB,CAAC;AAAA,IACjF,OAAO,SAAS,OAAO,OAAO,SAAS,gBAAgB;AAAA,IACvD,KAAK,SAAS,OAAO,KAAK,OAAO,gBAAgB;AAAA,IACjD,UAAU,KAAK,OAAO,QAAQ;AAAA,IAC9B,YAAY,KAAK,SAAS,MAAM,OAAO,KAAK;AAAA,IAC5C,UAAU,KAAK;AAAA,IACf,WAAW;AAAA,IACX,WAAW,KAAK,QAAQ,KAAK,OAAO,IAAI;AAAA,IACxC,aAAa,SAAS,OAAO,WAAW;AAAA,IACxC,OAAO,SAAS,OAAO,KAAK;AAAA;AAAA;AAAA;AAAA,IAI5B,mBAAmB;AAAA,IACnB,aAAa,MAAM,OAAO,WAAW;AAAA,IACrC,aAAa,MAAM,OAAO,WAAW;AAAA,IACrC,cAAc,MAAM,OAAO,YAAY;AAAA,IACvC,UAAU,KAAK,OAAO,QAAQ;AAAA,IAC9B,QAAQ,KAAK,OAAO,MAAM;AAAA,IAC1B,QACE,UAAU,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM,QAAQ,KAAK,GAAG,KAAK,MAAM,QAAQ,IAAI,EAAE;AAAA,IACzF,WAAW,iBAAiB,OAAO,SAAS;AAAA,IAC5C,WAAW,CAAC;AAAA,IACZ,MAAM,CAAC;AAAA;AAAA,IAEP,SAAS;AAAA,EACX;AACF;;;AHjLO,IAAM,aAAkC,CAAC,iBAAiB,IAAI,iBAAiB,EAAE;AAEjF,SAAS,aAAa,QAAgB,UAAmB,CAAC,GAAoB;AACnF,QAAM,SAAS;AAAA,IACb,WAAW,OAAO;AAAA,IAClB,eAAe,OAAO;AAAA,IACtB,WAAW,OAAO;AAAA,IAClB,YAAY,OAAO;AAAA,IACnB,YAAY,OAAO;AAAA,IACnB,iBAAiB,OAAO;AAAA,IACxB,UAAU,OAAO;AAAA,EACnB;AAEA,QAAM,kBAAyC,EAAE,QAAQ,OAAO;AAChE,QAAM,kBAAyC,EAAE,QAAQ,OAAO;AAEhE,SAAO;AAAA,IACL,gBAAgB,QAAQ,YAAY,IAAI,eAAe,eAAe,CAAC;AAAA,IACvE,gBAAgB,QAAQ,YAAY,IAAI,eAAe,eAAe,CAAC;AAAA,EACzE;AACF;AAGO,SAAS,cACd,SACA,QACiB;AACjB,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,EAAE,CAAC;AACjF,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM;AAAA,MACJ,sCAAsC,QAAQ,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,MAC/E,YAAY,QAAQ,IAAI,CAAC,WAAW,OAAO,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IAC3D;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,OAAO,CAAC,WAAW,OAAO,SAAS,OAAO,EAAE,CAAC;AACpE,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM;AAAA,MACJ;AAAA,MACA,eAAe,QAAQ,IAAI,CAAC,WAAW,OAAO,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,SAAO;AACT;;;AIhDA,IAAM,cAAc;AACpB,IAAM,kBAAkB;AAiBjB,IAAM,0BAA0B;AAkBvC,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,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","names":["process","SITE_URL","count","z","z","VOCABULARY","text","count","text","fraction","text","heldBack","AL_ENDING","AUX_ENDING","EAUX_ENDING","EAU_ENDING","SIBILANT_LETTER","TRAILING_S_ANY_CASE","bounds","foldWord","text","count","text","text","names","z","z","z","names","z","z","z","z","resolveFactor","z","WHITESPACE","z","process"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/server.ts","../src/config.ts","../src/version.ts","../src/sources/registry.ts","../src/errors.ts","../src/sources/adapter.ts","../src/sources/cookbook.ts","../src/sources/goodfood.ts","../src/sources/marmiton.ts","../src/sources/ptitchef.ts","../src/sources/supertoinette.ts","../src/sources/ids.ts","../src/sources/wordings.ts","../src/sources/client.ts","../src/tools/compareRecipes.ts","../src/tools/recipeView.ts","../src/recipe/language.ts","../src/recipe/units.ts","../src/recipe/quantity.ts","../src/recipe/scale.ts","../src/recipe/sections.ts","../src/tools/shared.ts","../src/tools/arguments.ts","../src/tools/getRecipe.ts","../src/tools/scaleIngredients.ts","../src/tools/searchRecipes.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point.\n *\n * stdout carries the protocol and nothing else: every diagnostic goes to\n * stderr, because a stray line on stdout corrupts the session for the client.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const server = createServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = async () => {\n await server.close().catch(() => undefined);\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[mcp-recipes] fatal: ${message}\\n`);\n process.exit(1);\n});\n","/**\n * Wiring: one client over the registered sources, four tools, and the guidance\n * a model reads before using any of them.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { PROFILES } from \"./sources/registry.js\";\nimport { RecipesClient } from \"./sources/client.js\";\nimport {\n compareRecipesDescription,\n compareRecipesInput,\n compareRecipesOutput,\n runCompareRecipes,\n} from \"./tools/compareRecipes.js\";\nimport type { CompareRecipesArgs } from \"./tools/compareRecipes.js\";\nimport {\n getRecipeDescription,\n getRecipeInput,\n getRecipeOutput,\n runGetRecipe,\n} from \"./tools/getRecipe.js\";\nimport type { GetRecipeArgs } from \"./tools/getRecipe.js\";\nimport {\n runScaleIngredients,\n scaleIngredientsDescription,\n scaleIngredientsInput,\n scaleIngredientsOutput,\n} from \"./tools/scaleIngredients.js\";\nimport type { ScaleIngredientsArgs } from \"./tools/scaleIngredients.js\";\nimport {\n runSearchRecipes,\n searchRecipesDescription,\n searchRecipesInput,\n searchRecipesOutput,\n} from \"./tools/searchRecipes.js\";\nimport type { SearchRecipesArgs } from \"./tools/searchRecipes.js\";\nimport { PKG_VERSION } from \"./version.js\";\n\nexport interface CreateServerOptions {\n config?: Config;\n logger?: Logger;\n client?: RecipesClient;\n}\n\n/** Nothing here writes, uploads or deletes; every tool only reads. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\n/**\n * The guidance a model reads before choosing a tool.\n *\n * The sources are named from the registry rather than written into the prose,\n * so the sentence stays true when the registry grows. What it says about them\n * holds for any corpus this server reads.\n */\nexport function buildInstructions(sources: Array<{ name: string; language: string }>): string {\n const named = sources.map((source) => `${source.name} (${source.language})`).join(\", \");\n\n return [\n `Tools for recipes, reading ${sources.length} sources: ${named}. No API key and no account are needed.`,\n \"search_recipes asks every source at once and returns one list; every id it hands back names the source it came from, and get_recipe routes on that.\",\n \"Use compare_recipes when the question is how a dish differs between traditions: it reads each source's closest version and can rescale them all to the same number of servings so the lists stand comparison.\",\n \"Do not rescale quantities yourself. get_recipe and scale_ingredients keep an egg whole, halve what pours or cuts, move a small measurement to a smaller unit before rounding it, and flag what cannot be scaled, which is what stops answers like '2.4 eggs'.\",\n \"scale_ingredients works offline on a list from anywhere and reads French and English lines in one call, each in its own language.\",\n \"A source that fails is named as a source that failed. An answer holding rows from some sources is never evidence about what the others hold, and 'per_source' says which was which.\",\n \"The sources share no scale. Their result counts count different things and are never added; some carry reader ratings and some have no author and no rating by nature, so rows are interleaved rather than ranked.\",\n \"Nothing is converted between measuring systems, and a yield is repeated in the source's own words, because '4 à 6 personnes' and '4 personnes' are different claims. A page stating no yield cannot be rescaled and says so.\",\n \"Every result carries a url. Credit the source you took a recipe from and link it; some publish under a licence that requires it, and one that states no terms at all has not granted permission.\",\n ].join(\" \");\n}\n\n/**\n * The guidance as it reads for the sources this build registers, taken from the\n * registry itself so it cannot name a corpus the server does not read.\n */\nexport const INSTRUCTIONS = buildInstructions([...PROFILES]);\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = options.client ?? new RecipesClient({ config, logger });\n\n const server = new McpServer(\n { name: \"mcp-recipes\", version: PKG_VERSION },\n { instructions: buildInstructions(client.profiles) },\n );\n\n server.registerTool(\n \"search_recipes\",\n {\n title: \"Search every recipe source at once\",\n description: searchRecipesDescription,\n inputSchema: searchRecipesInput,\n outputSchema: searchRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runSearchRecipes(client, args as SearchRecipesArgs),\n );\n\n server.registerTool(\n \"get_recipe\",\n {\n title: \"Read one recipe, optionally rescaled\",\n description: getRecipeDescription,\n inputSchema: getRecipeInput,\n outputSchema: getRecipeOutput,\n annotations: READ_ONLY,\n },\n async (args) => runGetRecipe(client, args as GetRecipeArgs),\n );\n\n server.registerTool(\n \"scale_ingredients\",\n {\n title: \"Scale an ingredient list, in either language\",\n description: scaleIngredientsDescription,\n inputSchema: scaleIngredientsInput,\n outputSchema: scaleIngredientsOutput,\n annotations: {\n ...READ_ONLY,\n // Nothing is fetched, so this tool reaches nothing outside itself.\n openWorldHint: false,\n },\n },\n async (args) => runScaleIngredients(args as ScaleIngredientsArgs),\n );\n\n server.registerTool(\n \"compare_recipes\",\n {\n title: \"One dish, as each source writes it\",\n description: compareRecipesDescription,\n inputSchema: compareRecipesInput,\n outputSchema: compareRecipesOutput,\n annotations: READ_ONLY,\n },\n async (args) => runCompareRecipes(client, args as CompareRecipesArgs),\n );\n\n logger.info(\n `ready: ${client.profiles.length} sources (${client.profiles.map((source) => source.id).join(\", \")}), user-agent=\"${client.userAgent}\", ${client.intervalMs}ms between requests to one source`,\n );\n\n return server;\n}\n","/**\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 * One setting governs every source. Each is read through its own client, and\n * each of those keeps a pacing floor of its own that nothing here can lower, so\n * a value set here can only make this server more patient than it already is.\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 * Spacing between two requests to the same site.\n *\n * No source publishes a ceiling for a client like this one, so the number is\n * self-imposed politeness: a second between requests is far below what a person\n * browsing one of these sites generates, and the answers are cached, so a\n * conversation that circles back to a recipe costs no traffic at all.\n */\nexport const DEFAULT_INTERVAL_MS = 1000;\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 entry point.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n/** Beyond this a request would look hung rather than paced. */\nexport const MAX_ALLOWED_INTERVAL_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-recipes/${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-recipes] ${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 a site was left out of an answer.\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-recipes] 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-recipes] 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.RECIPES_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-recipes] error: RECIPES_LOG_LEVEL=\"${level}\" is not one of ${LOG_LEVELS.join(\", \")}; using error.\\n`,\n );\n }\n\n const custom = env.RECIPES_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: a site 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 \"RECIPES_MIN_INTERVAL_MS\",\n DEFAULT_INTERVAL_MS,\n MIN_ALLOWED_INTERVAL_MS,\n MAX_ALLOWED_INTERVAL_MS,\n ),\n timeoutMs: readInteger(env, \"RECIPES_TIMEOUT_MS\", 20_000, 1000, 120_000),\n maxRetries: readInteger(env, \"RECIPES_MAX_RETRIES\", 3, 0, 8),\n cacheTtlMs: readInteger(env, \"RECIPES_CACHE_TTL_MS\", 900_000, 0, 86_400_000),\n cacheMaxEntries: readInteger(env, \"RECIPES_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 sites this server reads can tell which build is calling from their own\n * logs.\n */\nexport const PKG_VERSION = \"3.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-recipes\";\nexport const ISSUES_URL = \"https://github.com/smeet666/mcp-recipes/issues\";\n","/**\n * The sources this server reads.\n *\n * One list, and everything above it is written for however long the list is.\n * Adding a corpus means writing an adapter and adding it here; no tool, no\n * merge and no error path has a branch per source.\n */\n\nimport { GoodFoodClient } from \"mcp-bbc-goodfood/client\";\nimport type { ClientOptions as GoodFoodClientOptions } from \"mcp-bbc-goodfood/client\";\nimport { MarmitonClient } from \"mcp-marmiton/client\";\nimport type { MarmitonClientOptions } from \"mcp-marmiton/client\";\nimport { PtitchefClient } from \"mcp-ptitchef/client\";\nimport type { ClientOptions as PtitchefClientOptions } from \"mcp-ptitchef/client\";\nimport { SupertoinetteClient } from \"mcp-supertoinette/client\";\nimport type { ClientOptions as SupertoinetteClientOptions } from \"mcp-supertoinette/client\";\nimport { CookbookClient } from \"mcp-wikibooks-cookbook/client\";\nimport type { ClientOptions as CookbookClientOptions } from \"mcp-wikibooks-cookbook/client\";\n\nimport type { Config, Logger } from \"../config.js\";\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { COOKBOOK_PROFILE, cookbookAdapter } from \"./cookbook.js\";\nimport type { CookbookReader } from \"./cookbook.js\";\nimport { GOODFOOD_PROFILE, goodfoodAdapter } from \"./goodfood.js\";\nimport type { GoodFoodReader } from \"./goodfood.js\";\nimport { MARMITON_PROFILE, marmitonAdapter } from \"./marmiton.js\";\nimport type { MarmitonReader } from \"./marmiton.js\";\nimport { PTITCHEF_PROFILE, ptitchefAdapter } from \"./ptitchef.js\";\nimport type { PtitchefReader } from \"./ptitchef.js\";\nimport { SUPERTOINETTE_PROFILE, supertoinetteAdapter } from \"./supertoinette.js\";\nimport type { SupertoinetteReader } from \"./supertoinette.js\";\n\n/**\n * A reader may be supplied in place of the one that talks to a site, so a\n * program embedding this server can put its own cache in front of a corpus, and\n * a test can drive it from fixed answers.\n *\n * Each key is the id of the source it stands in for, so what a caller has\n * replaced can be read off against what the registry builds.\n */\nexport interface Readers {\n marmiton?: MarmitonReader;\n cookbook?: CookbookReader;\n ptitchef?: PtitchefReader;\n goodfood?: GoodFoodReader;\n supertoinette?: SupertoinetteReader;\n}\n\n/**\n * Two bounds one of the readers takes that the rest do not: the largest page it\n * will read, and the time it gives a whole read including its retries.\n *\n * They are that reader's own defaults, repeated here because its options\n * require them. This server exposes no setting for either: a setting governing\n * one source out of five is one nobody can reason about, and the pacing,\n * timeout and retry settings that do govern every source are the ones a caller\n * can move.\n */\nconst PAGE_READ_BOUNDS = { maxBodyBytes: 8_000_000, budgetMs: 60_000 };\n\nexport function buildSources(config: Config, readers: Readers, logger: Logger): SourceAdapter[] {\n const shared = {\n userAgent: config.userAgent,\n minIntervalMs: config.minIntervalMs,\n timeoutMs: config.timeoutMs,\n maxRetries: config.maxRetries,\n cacheTtlMs: config.cacheTtlMs,\n cacheMaxEntries: config.cacheMaxEntries,\n logLevel: config.logLevel,\n };\n\n const marmitonOptions: MarmitonClientOptions = { config: shared };\n const cookbookOptions: CookbookClientOptions = { config: shared };\n const ptitchefOptions: PtitchefClientOptions = {\n config: { ...shared, ...PAGE_READ_BOUNDS },\n logger,\n };\n const goodfoodOptions: GoodFoodClientOptions = { config: shared, logger };\n const supertoinetteOptions: SupertoinetteClientOptions = { config: shared, logger };\n\n return [\n marmitonAdapter(readers.marmiton ?? new MarmitonClient(marmitonOptions)),\n cookbookAdapter(readers.cookbook ?? new CookbookClient(cookbookOptions)),\n ptitchefAdapter(readers.ptitchef ?? new PtitchefClient(ptitchefOptions)),\n goodfoodAdapter(readers.goodfood ?? new GoodFoodClient(goodfoodOptions)),\n supertoinetteAdapter(readers.supertoinette ?? new SupertoinetteClient(supertoinetteOptions)),\n ];\n}\n\n/** The sources a caller asked for, in the registry's own order. */\nexport function selectSources(\n sources: SourceAdapter[],\n wanted: readonly SourceId[] | undefined,\n): SourceAdapter[] {\n if (!wanted) {\n return sources;\n }\n\n const unknown = wanted.filter((id) => !sources.some((source) => source.id === id));\n if (unknown.length > 0) {\n throw invalidInput(\n `This server reads no source called ${unknown.map((id) => `\"${id}\"`).join(\", \")}.`,\n `It reads ${sources.map((source) => source.id).join(\", \")}.`,\n );\n }\n\n const chosen = sources.filter((source) => wanted.includes(source.id));\n if (chosen.length === 0) {\n throw invalidInput(\n \"A search needs at least one source.\",\n `Name one of ${sources.map((source) => source.id).join(\", \")}, or leave the argument out to ask them all.`,\n );\n }\n return chosen;\n}\n\n/** The profiles the registry holds, for a caller naming the sources in prose. */\nexport const PROFILES = [\n MARMITON_PROFILE,\n COOKBOOK_PROFILE,\n PTITCHEF_PROFILE,\n GOODFOOD_PROFILE,\n SUPERTOINETTE_PROFILE,\n] as const;\n","/**\n * One error type, carrying a code the caller can branch on.\n *\n * The distinction that matters most is between \"no site holds this\" and \"the\n * question could not be asked\". Collapsing the two lets a model report an\n * absence it never established, which is a false statement about the world\n * rather than a missing feature.\n */\n\nimport { ISSUES_URL } from \"./version.js\";\n\nexport type ErrorCode =\n /** A site answered, and holds no such recipe. */\n | \"not_found\"\n /** The arguments cannot produce a request. */\n | \"invalid_input\"\n /** A site asked this client to slow down. */\n | \"rate_limited\"\n /** A response 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 const ERROR_CODES: readonly ErrorCode[] = [\n \"not_found\",\n \"invalid_input\",\n \"rate_limited\",\n \"parse_failure\",\n \"network_error\",\n \"timeout\",\n];\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 RecipesError 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 = \"RecipesError\";\n this.code = code;\n this.details = details;\n }\n}\n\nexport const notFound = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"not_found\", message, details ?? {});\n\nexport const invalidInput = (message: string, hint?: string, cause?: unknown) =>\n new RecipesError(\"invalid_input\", message, { ...(hint ? { hint } : {}), cause });\n\nexport const rateLimited = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"rate_limited\", message, {\n hint: \"Wait a moment and ask again. This says nothing about whether the recipe exists.\",\n ...details,\n });\n\nexport const parseFailure = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"parse_failure\", message, {\n hint: `A site may have changed how it answers. Please report this at ${ISSUES_URL} with the arguments you used.`,\n ...details,\n });\n\nexport const networkError = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"network_error\", message, details ?? {});\n\nexport const timeout = (message: string, details?: ErrorDetails) =>\n new RecipesError(\"timeout\", message, details ?? {});\n\n/**\n * Read the code off a failure raised by the library that reads a source.\n *\n * Those libraries throw their own error classes carrying the six codes above,\n * and the classes are not part of the interface this server imports. Reading\n * the field keeps the taxonomy intact without depending on a class identity a\n * library is free to change, and anything unrecognisable is reported as a\n * network failure, which is the reading that claims least.\n */\nexport function toRecipesError(error: unknown): RecipesError {\n if (error instanceof RecipesError) {\n return error;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n const raw = error as { code?: unknown; details?: unknown } | null;\n const code = typeof raw?.code === \"string\" ? raw.code : \"\";\n const known = ERROR_CODES.find((candidate) => candidate === code);\n\n const details =\n raw && typeof raw.details === \"object\" && raw.details !== null\n ? (raw.details as ErrorDetails)\n : {};\n\n return new RecipesError(known ?? \"network_error\", message, details);\n}\n\n/**\n * Adopt a failure raised while reading a source, in this server's own words.\n *\n * The code is kept, since that is the taxonomy every layer branches on. The\n * sentence is not: what arrives with such a failure is written by whoever the\n * reader was talking to, in their vocabulary and about their internals, and\n * serving it as this answer's own sentence states as this server's finding\n * something it never established. It is quoted and credited to the source\n * instead, on one line, so the diagnostic survives without borrowing a voice\n * or a shape it has no claim to. The advice that came with it goes, because\n * advice addressed to somebody else's reader sends a caller somewhere this\n * server cannot answer for.\n */\nexport function fromSource(error: unknown, sourceName: string): RecipesError {\n if (error instanceof RecipesError) {\n return error;\n }\n\n const known = toRecipesError(error);\n const reported = known.message.replace(/\\s+/g, \" \").trim();\n const opening =\n known.code === \"not_found\"\n ? `${sourceName} holds nothing under this name.`\n : `${sourceName} could not be read.`;\n const message = reported === \"\" ? opening : `${opening} It reports: \"${reported}\"`;\n const details: ErrorDetails = {};\n if (known.details.url !== undefined) {\n details.url = known.details.url;\n }\n if (known.details.status !== undefined) {\n details.status = known.details.status;\n }\n\n switch (known.code) {\n case \"not_found\":\n return notFound(message, details);\n case \"invalid_input\":\n return invalidInput(message);\n case \"rate_limited\":\n return rateLimited(message, details);\n case \"parse_failure\":\n return parseFailure(message, details);\n case \"timeout\":\n return timeout(message, details);\n default:\n return networkError(message, details);\n }\n}\n","/**\n * What a source has to provide to be one.\n *\n * Everything above this file is written for however many sources the registry\n * holds. A source knows four things nothing else does: what it is called, how\n * to recognise one of its own identifiers, how to search it, and how to read one\n * recipe off it. Adding a corpus is writing those four and registering it.\n *\n * The helpers below are the ones every adapter needs, because no source answers\n * with a contract: a field can arrive missing, null, or holding something other\n * than the type it usually holds.\n */\n\nimport { parseFailure } from \"../errors.js\";\nimport type { Language } from \"../recipe/language.js\";\nimport type { RecipeDetail, RecipeRow, SourceId, SourceProfile } from \"../types.js\";\n\nconst NUMBER_AND_REST = /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/;\nconst RANGE_AND_REST =\n /^\\s*(\\d+(?:[.,]\\d+)?)\\s*(?:à|a|to|-|–|—|ou|or)\\s*(\\d+(?:[.,]\\d+)?)\\s*(.*)$/i;\n\n/** Rows that could be read, and how many could not. */\nexport interface ReadRows {\n rows: RecipeRow[];\n /** Rows the source sent in a shape this server could not read. */\n skipped: number;\n /** Rows the source said it saw, in the terms that source counts in. */\n reportedTotal: number | null;\n /** What `reportedTotal` counts on this source, in words. Null when it states none. */\n reportedTotalMeans: string | null;\n cached: boolean;\n}\n\nexport interface ReadRecipe {\n recipe: RecipeDetail;\n cached: boolean;\n}\n\n/** How a source recognised a raw identifier as one of its own. */\nexport interface Claim {\n /** The string this source's own reader takes. */\n reference: string;\n /** Why the shape was read this way, for an answer that has to say so. */\n why: string;\n /**\n * Whether reading it this way is a guess. A shape only this source mints is\n * not; a shape it would merely accept is, and the answer says so.\n */\n guess: boolean;\n}\n\nexport interface SourceAdapter extends SourceProfile {\n /**\n * Recognise a raw identifier, or decline it.\n *\n * A source claims a shape it mints and declines everything else, so a string\n * no source claims is refused, instead of being sent somewhere that would\n * answer it with a confident absence.\n */\n claims: (raw: string) => Claim | null;\n search: (query: string, limit: number) => Promise<ReadRows>;\n getRecipe: (reference: string) => Promise<ReadRecipe>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Reading a value a source sent, without trusting its shape */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The distinction these draw is between a row and a record. One row this server\n * cannot read is dropped and counted, because the rest of the list is still a\n * good answer. A record it cannot read is `parse_failure`, because there is\n * nothing left to return and a caller told \"network_error\" would retry a source\n * that has actually changed.\n */\nexport function text(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() !== \"\" ? value : null;\n}\n\nexport function textList(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n}\n\nexport function count(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nexport function required(value: unknown, field: string, source: SourceProfile): string {\n const found = text(value);\n if (found === null) {\n throw parseFailure(\n `${source.name} answered without a readable \"${field}\", so there is no recipe to return.`,\n );\n }\n return found;\n}\n\nexport function rowsOf<T>(value: unknown, source: SourceProfile): T[] {\n if (!Array.isArray(value)) {\n throw parseFailure(\n `${source.name} answered in a shape this server could not read: the list of results was missing.`,\n );\n }\n return value as T[];\n}\n\n/** Build the identifier this server hands out for a row from a source. */\nexport function namespacedId(source: SourceId, reference: string): string {\n return `${source}:${reference}`;\n}\n\n/**\n * Keep the entries a page actually published.\n *\n * A nutrition panel arrives with a slot for every figure the source knows how\n * to print, and the slots a page left empty are null. Passing those through\n * would put \"protein: null\" in front of a reader as though the page had said\n * something about protein.\n *\n * The panel is walked rather than typed, because each source names its slots\n * differently and a figure is repeated as the page wrote it either way.\n */\nexport function publishedFigures(panel: unknown): Record<string, string> | null {\n if (typeof panel !== \"object\" || panel === null) {\n return null;\n }\n const kept: Record<string, string> = {};\n for (const [key, value] of Object.entries(panel)) {\n if (typeof value === \"string\" && value.trim() !== \"\") {\n kept[key] = value.trim();\n }\n }\n return Object.keys(kept).length > 0 ? kept : null;\n}\n\n/**\n * Read a yield that names a span rather than a number.\n *\n * \"4 à 6 personnes\" and \"4 personnes\" are different claims, and a source that\n * reports the first as a plain count of four hands this server a factor that is\n * too large for anyone at the upper end. The span is read back off the published\n * wording so the answer can say which end it scaled from.\n */\nexport function readYieldSpan(published: string | null): {\n count: number | null;\n max: number | null;\n unit: string | null;\n} {\n if (published === null) {\n return { count: null, max: null, unit: null };\n }\n\n const span = RANGE_AND_REST.exec(published);\n if (span) {\n const [lowText = \"\", highText = \"\"] = span.slice(1);\n const low = Number(lowText.replace(\",\", \".\"));\n const high = Number(highText.replace(\",\", \".\"));\n if (Number.isFinite(low) && Number.isFinite(high) && high > low) {\n return { count: low, max: high, unit: text(span[3]) };\n }\n }\n\n const single = NUMBER_AND_REST.exec(published);\n if (single) {\n const [figure = \"\"] = single.slice(1);\n const value = Number(figure.replace(\",\", \".\"));\n if (Number.isFinite(value)) {\n return { count: value, max: null, unit: text(single[2]) };\n }\n }\n\n return { count: null, max: null, unit: null };\n}\n\n/** Make the language a source publishes in available to whatever reads its lines. */\nexport function detailBase(source: SourceProfile): {\n source: SourceId;\n sourceName: string;\n language: Language;\n attribution: string;\n} {\n return {\n source: source.id,\n sourceName: source.name,\n language: source.language,\n attribution: source.attribution,\n };\n}\n","/**\n * The Cookbook on the English Wikibooks, as a source.\n *\n * A wiki written by whoever edits it, published under a licence that requires\n * attribution, with no author and no reader rating by nature. Its identifiers\n * are page keys carrying the namespace every Cookbook page lives in.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\n/** A search row, as the Cookbook's own reader publishes one. */\nexport interface CookbookSummary {\n key: string;\n title: string;\n description: string | null;\n excerpt: string | null;\n imageUrl: string | null;\n sourceUrl: string;\n}\n\n/** A page, as the Cookbook's own reader publishes one. */\nexport interface CookbookRecipe {\n key: string;\n title: string;\n sourceUrl: string;\n license: { title: string; url: string } | null;\n description: string | null;\n category: string | null;\n servings: number | null;\n yieldText: string | null;\n yieldUnit: string | null;\n totalMinutes: number | null;\n ingredients: string[];\n equipment: string[];\n steps: string[];\n tips: string[];\n nutrition: unknown;\n sectionTitles: string[];\n}\n\n/** The part of the Cookbook's client this server uses. */\nexport interface CookbookReader {\n search: (\n query: string,\n limit: number,\n ) => Promise<{ data: { results: CookbookSummary[] }; cached: boolean }>;\n getRecipe: (reference: string) => Promise<{ data: CookbookRecipe; cached: boolean }>;\n}\n\nexport const COOKBOOK_PROFILE = {\n id: \"cookbook\",\n name: \"Wikibooks Cookbook\",\n homeUrl: \"https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents\",\n language: \"en\" as const,\n attribution: \"Source: Wikibooks Cookbook\",\n // The namespace holds pages about salt and about a dish made with it alike.\n mixesReferencePages: true,\n};\n\nconst SITE_URL = /^https?:\\/\\/en\\.wikibooks\\.org\\/wiki\\//i;\n/** The namespace every page in the Cookbook lives under. */\nconst NAMESPACE = /^cookbook\\s*:/i;\n\n/**\n * Every page in the Cookbook is filed under one namespace, so its title arrives\n * as \"Cookbook:Spaghetti alla Carbonara\". The prefix is where the page lives on\n * the wiki rather than what the dish is called, and repeating it on every row of\n * a list says nothing and crowds out the names. The key keeps it, because that\n * is the string the site resolves.\n */\nfunction dishName(title: string): string {\n return title.replace(NAMESPACE, \"\").trim() || title;\n}\n\nexport function cookbookAdapter(reader: CookbookReader): SourceAdapter {\n return {\n ...COOKBOOK_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n let key: string;\n try {\n key = decodeURIComponent(raw.replace(SITE_URL, \"\")).replace(/_/g, \" \");\n } catch (cause) {\n // A percent sign that opens no escape is common in a wiki title, and\n // it is the caller's string rather than a site that failed.\n throw invalidInput(\n `\"${raw}\" carries a percent sign that is not a valid escape, so the page name cannot be read.`,\n \"Pass the id search_recipes returned instead of the address.\",\n cause,\n );\n }\n return {\n reference: key,\n why: \"the address is a page on the English Wikibooks\",\n guess: false,\n };\n }\n\n if (NAMESPACE.test(raw)) {\n return {\n reference: raw,\n why: \"the name opens with the namespace every Cookbook page lives under\",\n guess: false,\n };\n }\n\n return null;\n },\n\n async search(query: string, limit: number): Promise<ReadRows> {\n const outcome = await reader.search(query, limit);\n const list = rowsOf<Partial<CookbookSummary>>(\n (outcome.data as { results?: unknown })?.results,\n COOKBOOK_PROFILE,\n );\n const rows: RecipeRow[] = [];\n let skipped = 0;\n\n for (const summary of list) {\n const key = text(summary?.key);\n const title = text(summary?.title) ?? key;\n const url = text(summary?.sourceUrl);\n if (!(key && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(COOKBOOK_PROFILE.id, key),\n source: COOKBOOK_PROFILE.id,\n sourceName: COOKBOOK_PROFILE.name,\n title: dishName(title),\n url,\n imageUrl: text(summary?.imageUrl),\n excerpt: text(summary?.excerpt) ?? text(summary?.description),\n });\n }\n\n return {\n rows,\n skipped,\n // The gateway states no total and offers no second page, so there is no\n // number to report. Counting the rows returned and calling it a total\n // would invent one.\n reportedTotal: null,\n reportedTotalMeans: null,\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: cookbookDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function cookbookDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<CookbookRecipe>;\n const license = recipe.license as Partial<NonNullable<CookbookRecipe[\"license\"]>> | null;\n const licenseTitle = text(license?.title);\n const licenseUrl = text(license?.url);\n\n const published = text(recipe.yieldText);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(COOKBOOK_PROFILE),\n id: namespacedId(COOKBOOK_PROFILE.id, required(recipe.key, \"key\", COOKBOOK_PROFILE)),\n title: dishName(required(recipe.title ?? recipe.key, \"title\", COOKBOOK_PROFILE)),\n url: required(recipe.sourceUrl, \"url\", COOKBOOK_PROFILE),\n // The page source carries no picture this server can address.\n imageUrl: null,\n yieldCount: count(recipe.servings) ?? span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: text(recipe.yieldUnit) ?? span.unit,\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n // The page's own headings, which the reader reports. An answer holding no\n // ingredient line uses them to say whether the page announced a list.\n publishedSections: Array.isArray(recipe.sectionTitles) ? textList(recipe.sectionTitles) : null,\n // The Cookbook publishes one time for a recipe rather than splitting it.\n prepMinutes: null,\n cookMinutes: null,\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n // The page is written by everyone who edited it, so it credits nobody in\n // particular and carries no reader rating.\n author: null,\n rating: null,\n nutrition: publishedFigures(recipe.nutrition),\n equipment: textList(recipe.equipment),\n tips: textList(recipe.tips),\n // The Cookbook publishes no resting time, says nothing about how a page\n // lays its method out, and keeps nothing behind a subscription.\n restMinutes: null,\n stepsAsOneBlock: null,\n withheld: null,\n // A licence is only stated when both halves of it arrived: a name with no\n // address, or an address with no name, is not terms anyone can follow.\n license: licenseTitle && licenseUrl ? { title: licenseTitle, url: licenseUrl } : null,\n };\n}\n","/**\n * BBC Good Food, as a source.\n *\n * An English recipe site addressing a recipe by the path of its page. Two of\n * its statements need care. Its total can sit on the largest number of rows one\n * search will serve, where it states a floor rather than a count. And it keeps\n * some recipes' ingredients and method for its subscribers while publishing the\n * rest of the page, which is a part withheld rather than a part this server\n * failed to read.\n *\n * A difficulty the site writes in a word of its own is not carried, since it\n * publishes no scale for it. Its rendition for readers in the United States is\n * not read either: taking lines from both would make a list in two measuring\n * systems that the site never published as one.\n */\n\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n readYieldSpan,\n required,\n rowsOf,\n text,\n} from \"./adapter.js\";\n\n/** A search row, as BBC Good Food's own reader publishes one. */\nexport interface GoodFoodRow {\n id: string;\n title: string;\n url: string;\n image_url: string | null;\n rating: number | null;\n rating_count: number | null;\n /** True where the recipe sits behind the site's subscription. */\n premium: boolean;\n total_minutes: number | null;\n difficulty: string | null;\n author: string | null;\n}\n\n/** A listing, as that reader publishes one. */\nexport interface GoodFoodReport {\n query: string;\n results: GoodFoodRow[];\n result_count: number;\n total_available: number | null;\n /** True where the total sits on the largest number of rows one search serves. */\n total_is_ceiling: boolean;\n rows_seen: number;\n restrictions_lifted: string[];\n}\n\n/** One line of an ingredient list, as that reader publishes it. */\nexport interface GoodFoodIngredient {\n text: string;\n amount: number | null;\n unit: string | null;\n item: string;\n note: string | null;\n /** The site's own normalised name for it, which no other source publishes. */\n term: string | null;\n}\n\nexport interface GoodFoodGroup {\n heading: string | null;\n ingredients: GoodFoodIngredient[];\n}\n\n/** A recipe, as that reader publishes one. */\nexport interface GoodFoodRecipe {\n id: string;\n title: string;\n url: string;\n premium: boolean;\n yield_text: string | null;\n yield_count: number | null;\n prep_minutes: number | null;\n cook_minutes: number | null;\n total_minutes: number | null;\n difficulty: string | null;\n diets: string[];\n author: string | null;\n rating: number | null;\n rating_count: number | null;\n description: string | null;\n /** Empty where the recipe sits behind the subscription. */\n ingredients: GoodFoodGroup[];\n /** Empty where the recipe sits behind the subscription. */\n steps: string[];\n nutrition: Array<{ label: string; value: number | null; unit: string }>;\n nutrition_per: string | null;\n us_edition: unknown;\n}\n\n/** The part of BBC Good Food's client this server uses. */\nexport interface GoodFoodReader {\n searchRecipes: (\n query: string,\n options?: { limit?: number },\n ) => Promise<{ data: GoodFoodReport; cached: boolean; skipped?: string[] }>;\n getRecipe: (id: string) => Promise<{ data: GoodFoodRecipe; cached: boolean; skipped?: string[] }>;\n}\n\nexport const GOODFOOD_PROFILE = {\n id: \"goodfood\",\n name: \"BBC Good Food\",\n homeUrl: \"https://www.bbcgoodfood.com\",\n language: \"en\" as const,\n attribution: \"Source: BBC Good Food\",\n // The search serves recipe pages; a question aimed at the site's reviews and\n // buying guides is answered with recipes all the same.\n mixesReferencePages: false,\n};\n\nconst SITE_URL = /^https?:\\/\\/(?:www\\.)?bbcgoodfood\\.com\\//i;\n/** The shape BBC Good Food mints: a recipe path. */\nconst REFERENCE = /^recipes\\/[^/]+$/;\n/** A segment walking up out of the path it was written in. The site names none. */\nconst DOT_SEGMENT = /(?:^|\\/)\\.\\.?(?:\\/|$)/;\nconst LEADING_SLASH = /^\\/+/;\n\nfunction asReference(raw: string): string | null {\n const path = raw.replace(LEADING_SLASH, \"\");\n return REFERENCE.test(path) && !DOT_SEGMENT.test(`/${path}`) ? path : null;\n}\n\n/** What the total counts, in words, for the way the site published it. */\nexport function meansFor(report: Partial<GoodFoodReport>): string | null {\n if (count(report.total_available) === null) {\n return null;\n }\n return report.total_is_ceiling === true\n ? \"recipes BBC Good Food says its catalogue matches this wording, cut at the largest number of rows one search will serve, so it states a floor rather than a count\"\n : \"recipes BBC Good Food says its whole catalogue matches this wording; it counts the catalogue rather than the rows served\";\n}\n\n/**\n * Repeat the panel the page published, in the site's own wording.\n *\n * The panel arrives as a list rather than as an object, and a figure the page\n * left empty carries a null value: passing it through would put a nutrient in\n * front of a reader that the page said nothing about.\n */\nfunction nutritionPanel(recipe: Partial<GoodFoodRecipe>): Record<string, string> | null {\n const facts = Array.isArray(recipe.nutrition) ? recipe.nutrition : [];\n const kept: Record<string, string> = {};\n\n for (const fact of facts) {\n const label = text(fact?.label);\n const value = count(fact?.value);\n if (label === null || value === null) {\n continue;\n }\n const unit = text(fact?.unit);\n kept[label] = unit === null ? String(value) : `${value} ${unit}`;\n }\n\n const per = text(recipe.nutrition_per);\n if (Object.keys(kept).length === 0) {\n return null;\n }\n // What the figures are stated per is part of the panel: a figure without it\n // is a number nobody can put beside another recipe's.\n if (per !== null) {\n kept.per = per;\n }\n return kept;\n}\n\n/**\n * The site answers a wording it matches nothing for with a total of zero and no\n * rows, so an absence arrives as an absence and there is no failure here to\n * read as one. Every error the reader raises is a real failure and travels on\n * as one.\n */\nexport function goodfoodAdapter(reader: GoodFoodReader): SourceAdapter {\n return {\n ...GOODFOOD_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n const path = asReference(new URL(raw).pathname);\n return path === null\n ? null\n : { reference: path, why: \"the address is a BBC Good Food recipe page\", guess: false };\n }\n const path = asReference(raw);\n return path === null\n ? null\n : {\n reference: path,\n why: \"the path is the shape BBC Good Food mints for a recipe\",\n guess: false,\n };\n },\n\n async search(query: string, limit: number): Promise<ReadRows> {\n // Only the page size is passed. The site's facets and its sort orders are\n // its own vocabulary, and ranking one source's rows while four others are\n // unranked would make a merged list that looks ordered and is not.\n const outcome = await reader.searchRecipes(query, { limit });\n const report = (outcome.data ?? {}) as Partial<GoodFoodReport>;\n const list = rowsOf<Partial<GoodFoodRow>>(report.results, GOODFOOD_PROFILE);\n const rows: RecipeRow[] = [];\n let skipped = outcome.skipped?.length ?? 0;\n\n for (const summary of list) {\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!(id && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(GOODFOOD_PROFILE.id, id),\n source: GOODFOOD_PROFILE.id,\n sourceName: GOODFOOD_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.image_url),\n // A row carries a title and a picture and no prose.\n excerpt: null,\n });\n }\n\n return {\n rows,\n skipped,\n reportedTotal: count(report.total_available),\n reportedTotalMeans: meansFor(report),\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: goodfoodDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function goodfoodDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<GoodFoodRecipe>;\n\n const published = text(recipe.yield_text);\n const span = readYieldSpan(published);\n const value = count(recipe.rating);\n\n const groups = Array.isArray(recipe.ingredients) ? recipe.ingredients : [];\n const lines: string[] = [];\n for (const group of groups) {\n // A heading is kept as a line of its own: dropped, two groups that each\n // call for cream would read as the same thing asked for twice.\n const heading = text(group?.heading);\n if (heading !== null) {\n lines.push(heading);\n }\n for (const entry of group?.ingredients ?? []) {\n const line = text(entry?.text);\n if (line !== null) {\n lines.push(line);\n }\n }\n }\n\n return {\n ...detailBase(GOODFOOD_PROFILE),\n id: namespacedId(GOODFOOD_PROFILE.id, required(recipe.id, \"id\", GOODFOOD_PROFILE)),\n title: required(recipe.title, \"title\", GOODFOOD_PROFILE),\n url: required(recipe.url, \"url\", GOODFOOD_PROFILE),\n // A row carries the picture; the recipe page's own is not read.\n imageUrl: null,\n yieldCount: count(recipe.yield_count) ?? span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: span.unit,\n ingredients: lines,\n steps: Array.isArray(recipe.steps)\n ? recipe.steps.filter((step): step is string => typeof step === \"string\")\n : [],\n publishedSections: null,\n prepMinutes: count(recipe.prep_minutes),\n cookMinutes: count(recipe.cook_minutes),\n totalMinutes: count(recipe.total_minutes),\n // The site publishes no resting time of its own.\n restMinutes: null,\n stepsAsOneBlock: null,\n // The page carries these and the site sells access to them. What is said\n // here is what the site did, so an answer never reports a wall as a page\n // this server could not read.\n withheld:\n recipe.premium === true\n ? {\n parts: [\"ingredients\", \"method\"],\n why: \"BBC Good Food keeps this recipe's ingredients and method for its subscribers, and publishes its title, its times, its rating and its nutrition\",\n }\n : null,\n // The site files a recipe under collections rather than under one category\n // on the page itself.\n category: null,\n author: text(recipe.author),\n rating: value === null ? null : { value, count: count(recipe.rating_count), max: 5 },\n nutrition: nutritionPanel(recipe),\n equipment: [],\n tips: [],\n // The site states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * Marmiton, as a source.\n *\n * A French recipe site with reader ratings, whose identifiers are digits and\n * whose search serves a single page it disallows paging past.\n */\n\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\n\n/** A search row, as Marmiton's own reader publishes one. */\nexport interface MarmitonSummary {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n}\n\n/** A recipe, as Marmiton's own reader publishes one. */\nexport interface MarmitonRecipe {\n id: string;\n title: string;\n url: string;\n imageUrl: string | null;\n ingredients: string[];\n steps: string[];\n recipeYield: { count: number | null; unit: string | null; text: string };\n prepMinutes: number | null;\n cookMinutes: number | null;\n totalMinutes: number | null;\n category: string | null;\n author: string | null;\n rating: { value: number; count: number | null; best: number | null } | null;\n nutrition: unknown;\n}\n\n/** The part of Marmiton's client this server uses. */\nexport interface MarmitonReader {\n search: (query: string) => Promise<{ data: MarmitonSummary[]; cached: boolean }>;\n getRecipe: (ref: {\n id?: string;\n url?: string;\n }) => Promise<{ data: MarmitonRecipe; cached: boolean }>;\n}\n\nexport const MARMITON_PROFILE = {\n id: \"marmiton\",\n name: \"Marmiton\",\n homeUrl: \"https://www.marmiton.org\",\n language: \"fr\" as const,\n attribution: \"Source: Marmiton\",\n // Every page the search returns is a recipe.\n mixesReferencePages: false,\n};\n\n/** Recipe pages, whose address ends in the numeric identifier. */\nconst SITE_URL = /^https?:\\/\\/(?:www\\.)?marmiton\\.org\\//i;\n/** The shape Marmiton mints: digits and nothing else. */\nconst REFERENCE = /^\\d+$/;\n\n/**\n * Ask Marmiton, and read the answer it gives to a wording it matches nothing\n * for.\n *\n * Marmiton serves a 404 on the results page when no recipe matches, which is\n * the site stating an absence. Handed on as a failure it would put \"Marmiton\n * did not answer\" in front of a reader for a search Marmiton answered, and\n * leave the corpus unspoken for. Every other failure stays a failure, so a site\n * that is unreachable is never read as a site that holds nothing.\n */\nasync function readSearch(\n reader: MarmitonReader,\n query: string,\n): Promise<{ data: MarmitonSummary[]; cached: boolean }> {\n try {\n return await reader.search(query);\n } catch (error) {\n const code = (error as { code?: string } | null)?.code;\n if (code === \"not_found\") {\n return { data: [], cached: false };\n }\n throw error;\n }\n}\n\nexport function marmitonAdapter(reader: MarmitonReader): SourceAdapter {\n return {\n ...MARMITON_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n return { reference: raw, why: \"the address is a Marmiton page\", guess: false };\n }\n if (REFERENCE.test(raw)) {\n return {\n reference: raw,\n why: \"a bare number is the shape Marmiton mints\",\n guess: false,\n };\n }\n return null;\n },\n\n async search(query: string): Promise<ReadRows> {\n const outcome = await readSearch(reader, query);\n const list = rowsOf<Partial<MarmitonSummary>>(outcome.data, MARMITON_PROFILE);\n const rows: RecipeRow[] = [];\n let skipped = 0;\n\n for (const summary of list) {\n // A row needs something to call it, somewhere to send a reader, and an\n // identifier that reads back. A row missing any of them is dropped\n // rather than returned with a hole in it.\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!(id && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(MARMITON_PROFILE.id, id),\n source: MARMITON_PROFILE.id,\n sourceName: MARMITON_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.imageUrl),\n // The result list carries a title and a picture and no prose, so\n // there is nothing to quote here.\n excerpt: null,\n });\n }\n\n return {\n rows,\n skipped,\n // Rows on the page, including the ones this server could not read: the\n // site saw them, and reporting only the readable ones would understate\n // what it holds.\n reportedTotal: rows.length + skipped,\n reportedTotalMeans:\n \"rows on the single page of results Marmiton serves; the site disallows paging, so this is not a catalogue count\",\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n // The reader takes an identifier or an address and tells them apart.\n const ref = ABSOLUTE_URL.test(reference) ? { url: reference } : { id: reference };\n const outcome = await reader.getRecipe(ref);\n return { recipe: marmitonDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function marmitonDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<MarmitonRecipe>;\n const yields = (recipe.recipeYield ?? {}) as Partial<MarmitonRecipe[\"recipeYield\"]>;\n const rating = recipe.rating as Partial<NonNullable<MarmitonRecipe[\"rating\"]>> | null | undefined;\n const value = count(rating?.value);\n\n // The reader splits the published yield at the first number, which leaves the\n // rest of a span sitting where the word being counted belongs: \"4 à 6\n // personnes\" arrives as a count of four and a unit of \"à 6 personnes\". The\n // wording is read again here so both ends of the span are known.\n const published = text(yields.text);\n const span = readYieldSpan(published);\n\n return {\n ...detailBase(MARMITON_PROFILE),\n id: namespacedId(MARMITON_PROFILE.id, required(recipe.id, \"id\", MARMITON_PROFILE)),\n title: required(recipe.title, \"title\", MARMITON_PROFILE),\n url: required(recipe.url, \"url\", MARMITON_PROFILE),\n imageUrl: text(recipe.imageUrl),\n yieldCount: span.count ?? count(yields.count),\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: span.unit ?? text(yields.unit),\n ingredients: textList(recipe.ingredients),\n steps: textList(recipe.steps),\n // The reader hands over the parts of a page it recognised and not the\n // headings the page carries, so there is nothing here to say how it is\n // laid out.\n publishedSections: null,\n prepMinutes: count(recipe.prepMinutes),\n cookMinutes: count(recipe.cookMinutes),\n totalMinutes: count(recipe.totalMinutes),\n category: text(recipe.category),\n author: text(recipe.author),\n rating:\n value === null ? null : { value, count: count(rating?.count), max: count(rating?.best) },\n nutrition: publishedFigures(recipe.nutrition),\n equipment: [],\n tips: [],\n // Marmiton publishes no resting time, no word on how it lays its method\n // out, and keeps nothing back.\n restMinutes: null,\n stepsAsOneBlock: null,\n withheld: null,\n // Marmiton states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * Ptitchef, as a source.\n *\n * A French recipe site addressing a recipe by the path of its page. It answers\n * a search in several ways and prints one total for all of them, so the same\n * number counts a category on one answer and the rows served on another; what\n * it counts is said in words beside it rather than left to be guessed.\n *\n * Two figures the site publishes are not carried. It estimates what a recipe\n * costs in euros, which is a price where another source ranks a recipe inside\n * its own list, and one field holding both would invite the two to be compared.\n * It writes a difficulty in a word of its own and states no scale for it, so\n * the word sits on no axis a caller could put beside another source's.\n */\n\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n publishedFigures,\n readYieldSpan,\n required,\n rowsOf,\n text,\n} from \"./adapter.js\";\n\n/** How the site answered a wording, as its own reader reports it. */\nexport type PtitchefKind =\n | \"topic\"\n | \"free_text\"\n | \"category\"\n | \"guide\"\n | \"standing\"\n | \"fridge\"\n | \"recipe\"\n | \"unmatched\";\n\n/** A search row, as Ptitchef's own reader publishes one. */\nexport interface PtitchefRow {\n id: string;\n title: string;\n url: string;\n image_url: string | null;\n rating: number | null;\n /** How many readers rated it. */\n rating_count: number | null;\n /** How many wrote a review, which counts a different thing. */\n review_count: number | null;\n category: string | null;\n difficulty: string | null;\n total_minutes: number | null;\n calories: string | null;\n ingredients_preview: string | null;\n}\n\n/** A listing, as that reader publishes one. */\nexport interface PtitchefListing {\n asked: string;\n kind: PtitchefKind;\n topic_slug: string | null;\n title: string | null;\n results: PtitchefRow[];\n result_count: number;\n rows_seen: number;\n folded: number;\n total_available: number | null;\n page: number;\n single_page: boolean;\n url: string;\n}\n\n/** A recipe, as that reader publishes one. */\nexport interface PtitchefRecipe {\n id: string;\n title: string;\n url: string;\n description: string | null;\n image_url: string | null;\n category: string | null;\n cuisine: string | null;\n difficulty: string | null;\n author: string | null;\n author_url: string | null;\n published: string | null;\n modified: string | null;\n rating: number | null;\n rating_count: number | null;\n review_count: number | null;\n prep_minutes: number | null;\n cook_minutes: number | null;\n total_minutes: number | null;\n yield_count: number | null;\n yield_text: string | null;\n yield_unit: string | null;\n ingredients: string[];\n steps: Array<{ text: string; image_url: string | null }>;\n /** True where the site published its method as one block of prose. */\n steps_are_one_block: boolean;\n /** The panel as the site prints it, slot by slot, unread slots left null. */\n nutrition: unknown;\n estimated_cost: string | null;\n keywords: string[];\n faq: unknown[];\n translations: unknown[];\n}\n\n/** The part of Ptitchef's client this server uses. */\nexport interface PtitchefReader {\n searchRecipes: (\n query: string,\n ) => Promise<{ data: PtitchefListing; cached: boolean; skipped?: string[] }>;\n getRecipe: (id: string) => Promise<{ data: PtitchefRecipe; cached: boolean; skipped?: string[] }>;\n}\n\nexport const PTITCHEF_PROFILE = {\n id: \"ptitchef\",\n name: \"Ptitchef\",\n homeUrl: \"https://www.ptitchef.com\",\n language: \"fr\" as const,\n attribution: \"Source: Ptitchef\",\n // Every row a listing serves names a recipe; a topic the site answers with a\n // guide of its own groups recipes under headings rather than mixing in pages\n // about ingredients.\n mixesReferencePages: false,\n};\n\nconst SITE_URL = /^https?:\\/\\/(?:www\\.)?ptitchef\\.com\\//i;\n/** The shape Ptitchef mints: a recipe path ending on the page's number. */\nconst REFERENCE = /^recettes\\/[^/]+\\/[^/]+-fid-\\d+$/;\n/** A segment walking up out of the path it was written in. The site names none. */\nconst DOT_SEGMENT = /(?:^|\\/)\\.\\.?(?:\\/|$)/;\n\nconst LEADING_SLASH = /^\\/+/;\n\nfunction asReference(raw: string): string | null {\n const path = raw.replace(LEADING_SLASH, \"\");\n return REFERENCE.test(path) && !DOT_SEGMENT.test(`/${path}`) ? path : null;\n}\n\n/**\n * What the one total the site prints counts, in words, for the way it answered.\n *\n * The site reads a wording and may send the reader to a category page of its\n * own, whose total counts that category rather than what the words matched.\n * Reporting the two the same way would let a caller read a count of a whole\n * category as a count of what it searched for.\n */\nexport function meansFor(listing: Partial<PtitchefListing>): string | null {\n if (count(listing.total_available) === null) {\n return null;\n }\n switch (listing.kind) {\n case \"topic\":\n case \"category\":\n case \"standing\":\n return \"recipes Ptitchef says the category page it answered this wording with holds; it counts that page rather than what the words matched\";\n case \"free_text\":\n case \"fridge\":\n return \"rows Ptitchef served on the single page it answered this wording with; the site offers no more for it\";\n case \"recipe\":\n return \"the one recipe Ptitchef answered this wording with, by opening its page\";\n default:\n // A guide states no total of its own, and a wording the site matched\n // nothing for was never counted.\n return null;\n }\n}\n\n/**\n * The site answers a wording it matches nothing for with `kind: \"unmatched\"`,\n * no rows and no total, so an absence arrives as an absence and there is no\n * failure here to read as one. Every error the reader raises is a real failure\n * and travels on as one.\n */\nexport function ptitchefAdapter(reader: PtitchefReader): SourceAdapter {\n return {\n ...PTITCHEF_PROFILE,\n\n claims(raw: string): Claim | null {\n if (SITE_URL.test(raw)) {\n const path = asReference(new URL(raw).pathname);\n return path === null\n ? null\n : { reference: path, why: \"the address is a Ptitchef recipe page\", guess: false };\n }\n const path = asReference(raw);\n return path === null\n ? null\n : {\n reference: path,\n why: \"the path is the shape Ptitchef mints for a recipe\",\n guess: false,\n };\n },\n\n async search(query: string): Promise<ReadRows> {\n // The wording is asked as the site's own search. Its categories and its\n // fridge listing are vocabularies of its own, and a slug from one of them\n // names nothing on any other source.\n const outcome = await reader.searchRecipes(query);\n const listing = (outcome.data ?? {}) as Partial<PtitchefListing>;\n const list = rowsOf<Partial<PtitchefRow>>(listing.results, PTITCHEF_PROFILE);\n const rows: RecipeRow[] = [];\n let skipped = outcome.skipped?.length ?? 0;\n\n for (const summary of list) {\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!(id && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(PTITCHEF_PROFILE.id, id),\n source: PTITCHEF_PROFILE.id,\n sourceName: PTITCHEF_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.image_url),\n excerpt: text(summary?.ingredients_preview),\n });\n }\n\n return {\n rows,\n skipped,\n reportedTotal: count(listing.total_available),\n reportedTotalMeans: meansFor(listing),\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: ptitchefDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function ptitchefDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<PtitchefRecipe>;\n\n const published = text(recipe.yield_text);\n const span = readYieldSpan(published);\n const value = count(recipe.rating);\n\n const steps = Array.isArray(recipe.steps) ? recipe.steps : [];\n const lines = Array.isArray(recipe.ingredients) ? recipe.ingredients : [];\n\n return {\n ...detailBase(PTITCHEF_PROFILE),\n id: namespacedId(PTITCHEF_PROFILE.id, required(recipe.id, \"id\", PTITCHEF_PROFILE)),\n title: required(recipe.title, \"title\", PTITCHEF_PROFILE),\n url: required(recipe.url, \"url\", PTITCHEF_PROFILE),\n imageUrl: text(recipe.image_url),\n yieldCount: count(recipe.yield_count) ?? span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: text(recipe.yield_unit) ?? span.unit,\n ingredients: lines.filter((line): line is string => typeof line === \"string\"),\n // A step carries a picture of itself, which is not part of the method.\n steps: steps.map((step) => text(step?.text)).filter((step): step is string => step !== null),\n publishedSections: null,\n prepMinutes: count(recipe.prep_minutes),\n cookMinutes: count(recipe.cook_minutes),\n totalMinutes: count(recipe.total_minutes),\n // The site publishes no resting time of its own.\n restMinutes: null,\n stepsAsOneBlock:\n typeof recipe.steps_are_one_block === \"boolean\" ? recipe.steps_are_one_block : null,\n withheld: null,\n category: text(recipe.category),\n author: text(recipe.author),\n // The site prints two counts beside a rating, one of readers who rated and\n // one of readers who wrote. Only the first is what a rating rests on, and\n // adding them would count twice anyone who did both.\n rating: value === null ? null : { value, count: count(recipe.rating_count), max: 5 },\n nutrition: publishedFigures(recipe.nutrition),\n equipment: [],\n tips: [],\n // The site states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * Supertoinette, as a source.\n *\n * A French recipe site addressing a recipe by a number. It prints no total on\n * a search page and publishes no nutrition panel, and both of those are stated\n * here as silence: a count nobody published is not a count of zero.\n */\n\nimport type { RecipeDetail, RecipeRow } from \"../types.js\";\nimport type { Claim, ReadRecipe, ReadRows, SourceAdapter } from \"./adapter.js\";\nimport {\n count,\n detailBase,\n namespacedId,\n readYieldSpan,\n required,\n rowsOf,\n text,\n textList,\n} from \"./adapter.js\";\n\n/** A search row, as Supertoinette's own reader publishes one. */\nexport interface SupertoinetteListingRow {\n id: string;\n title: string;\n title_as_published: string;\n url: string;\n image_url: string | null;\n description: string | null;\n categories: string[];\n}\n\n/** One line of an ingredient list, as that reader publishes it. */\nexport interface SupertoinetteIngredient {\n amount_text: string | null;\n label: string;\n /** The whole line as published, quantity included. */\n raw: string;\n /** The ingredient page the site linked the words to. Not read: the site\n * links the wrong page often enough that pairing recipes by it would pair\n * the wrong recipes. */\n sheet: unknown;\n is_heading: boolean;\n}\n\n/** A recipe, as Supertoinette's own reader publishes one. */\nexport interface SupertoinetteRecipe {\n id: string;\n title: string;\n title_as_published: string;\n url: string;\n description: string | null;\n yield_text: string | null;\n ingredients: SupertoinetteIngredient[];\n steps: string[];\n intro: string | null;\n prep_minutes: number | null;\n cook_minutes: number | null;\n total_minutes: number | null;\n /** Time the recipe stands, which the site prints on its own. */\n rest_minutes: number | null;\n category: string | null;\n author: string | null;\n rating: { value: number; count: number; scale: number } | null;\n /** The site publishes none, and the field says so rather than being absent. */\n nutrition: null;\n difficulty: { label: string } | null;\n cost_level: { label: string; level: number; scale: number } | null;\n images: string[];\n tags: unknown[];\n ingredient_sheets: unknown[];\n faq: unknown[];\n}\n\ninterface SupertoinetteListing {\n results: SupertoinetteListingRow[];\n rows_published: number;\n total_available: null;\n last_page: number;\n facets: unknown[];\n matched_nothing: boolean;\n url: string;\n}\n\n/** The part of Supertoinette's client this server uses. */\nexport interface SupertoinetteReader {\n searchRecipes: (request: { query: string; page: number; category: string | null }) => Promise<{\n data: { listing: SupertoinetteListing; dropped_category: string | null };\n cached: boolean;\n skipped?: string[];\n }>;\n getRecipe: (\n id: string,\n ) => Promise<{ data: SupertoinetteRecipe; cached: boolean; skipped?: string[] }>;\n}\n\nexport const SUPERTOINETTE_PROFILE = {\n id: \"supertoinette\",\n name: \"Supertoinette\",\n homeUrl: \"https://www.supertoinette.com\",\n language: \"fr\" as const,\n attribution: \"Source: Supertoinette\",\n // The reader sets aside any row that opens onto something other than a\n // recipe, so what arrives here is recipes.\n mixesReferencePages: false,\n};\n\n/** Recipe pages, whose address carries the number the site addresses it by. */\nconst SITE_RECIPE_URL = /^https?:\\/\\/(?:www\\.)?supertoinette\\.com\\/recette\\/([1-9][0-9]{0,9})\\//i;\n/** The shape Supertoinette mints: a number, and never a leading zero. */\nconst REFERENCE = /^[1-9][0-9]{0,9}$/;\n\n/**\n * The site answers a wording it matches nothing for with an empty listing and\n * `matched_nothing`, so an absence arrives as an absence and there is no\n * failure here to read as one. Every error the reader raises is a real failure\n * and travels on as one.\n */\nexport function supertoinetteAdapter(reader: SupertoinetteReader): SourceAdapter {\n return {\n ...SUPERTOINETTE_PROFILE,\n\n claims(raw: string): Claim | null {\n const address = SITE_RECIPE_URL.exec(raw);\n if (address?.[1]) {\n return {\n reference: address[1],\n why: \"the address is a Supertoinette recipe page\",\n guess: false,\n };\n }\n if (REFERENCE.test(raw)) {\n return {\n reference: raw,\n why: \"a bare number is the shape Supertoinette mints\",\n guess: false,\n };\n }\n return null;\n },\n\n async search(query: string): Promise<ReadRows> {\n // The site pages rather than serving a longer first page, and a wording\n // is asked as one search rather than followed across pages. No category\n // is named, because a facet belongs to the site that writes it.\n const outcome = await reader.searchRecipes({ query, page: 1, category: null });\n const listing = (outcome.data?.listing ?? {}) as Partial<SupertoinetteListing>;\n const list = rowsOf<Partial<SupertoinetteListingRow>>(listing.results, SUPERTOINETTE_PROFILE);\n const rows: RecipeRow[] = [];\n // Rows the reader itself set aside, each with the reason it gave.\n let skipped = outcome.skipped?.length ?? 0;\n\n for (const summary of list) {\n const id = text(summary?.id);\n const title = text(summary?.title);\n const url = text(summary?.url);\n if (!(id && title && url)) {\n skipped += 1;\n continue;\n }\n rows.push({\n id: namespacedId(SUPERTOINETTE_PROFILE.id, id),\n source: SUPERTOINETTE_PROFILE.id,\n sourceName: SUPERTOINETTE_PROFILE.name,\n title,\n url,\n imageUrl: text(summary?.image_url),\n excerpt: text(summary?.description),\n });\n }\n\n return {\n rows,\n skipped,\n // The site prints no total anywhere on a search page. The rows of one\n // page are not put here in its place: the site pages, so they would\n // understate what it holds rather than count it.\n reportedTotal: null,\n reportedTotalMeans: null,\n cached: outcome.cached,\n };\n },\n\n async getRecipe(reference: string): Promise<ReadRecipe> {\n const outcome = await reader.getRecipe(reference);\n return { recipe: supertoinetteDetail(outcome.data), cached: outcome.cached };\n },\n };\n}\n\nexport function supertoinetteDetail(payload: unknown): RecipeDetail {\n const recipe = (payload ?? {}) as Partial<SupertoinetteRecipe>;\n const rating = recipe.rating as Partial<NonNullable<SupertoinetteRecipe[\"rating\"]>> | null;\n const value = count(rating?.value);\n\n // The site states its yield in words alone, so both the number and what it\n // counts are read back off the wording it published.\n const published = text(recipe.yield_text);\n const span = readYieldSpan(published);\n\n const lines = Array.isArray(recipe.ingredients) ? recipe.ingredients : [];\n\n return {\n ...detailBase(SUPERTOINETTE_PROFILE),\n id: namespacedId(SUPERTOINETTE_PROFILE.id, required(recipe.id, \"id\", SUPERTOINETTE_PROFILE)),\n title: required(recipe.title, \"title\", SUPERTOINETTE_PROFILE),\n url: required(recipe.url, \"url\", SUPERTOINETTE_PROFILE),\n imageUrl: text(recipe.images?.[0]),\n yieldCount: span.count,\n yieldMax: span.max,\n yieldText: published,\n yieldUnit: span.unit,\n // The whole line as printed, headings included: a heading dropped would\n // merge two groups of a recipe that may each call for the same thing.\n // The ingredient page the site links a line to is not carried, since the\n // site links the wrong page often enough that pairing recipes by it would\n // pair the wrong recipes.\n ingredients: lines\n .map((line) => text(line?.raw))\n .filter((line): line is string => line !== null),\n steps: textList(recipe.steps),\n publishedSections: null,\n prepMinutes: count(recipe.prep_minutes),\n cookMinutes: count(recipe.cook_minutes),\n totalMinutes: count(recipe.total_minutes),\n // The site prints this on its own, and its own total already holds it.\n // Adding it to anything here would count it twice.\n restMinutes: count(recipe.rest_minutes),\n // The reader hands over the steps it read and says nothing about whether\n // the page numbered them.\n stepsAsOneBlock: null,\n withheld: null,\n category: text(recipe.category),\n author: text(recipe.author),\n // The scale is published, so a value is never read against a guess.\n rating:\n value === null ? null : { value, count: count(rating?.count), max: count(rating?.scale) },\n // This site publishes no nutrition panel at all, so there is nothing here\n // to repeat. An empty panel would read as a page that printed one and left\n // every figure out.\n nutrition: null,\n equipment: [],\n tips: [],\n // The site states no licence on a recipe page, and silence is not a grant.\n license: null,\n };\n}\n","/**\n * Identifiers that name the source they came from.\n *\n * Every source hands back an opaque string, and a caller holding a merged list\n * has no way to tell one source's string from another's. Every identifier this\n * server returns therefore carries a prefix, and `get_recipe` routes on it\n * rather than trying each source in turn.\n */\n\nimport { invalidInput } from \"../errors.js\";\nimport type { SourceId } from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\n\nconst SEPARATOR = \":\";\n\nexport interface ResolvedId {\n source: SourceAdapter;\n /** The string that source's own reader takes. */\n reference: string;\n /** Set when the shape was read rather than stated, so the answer can say so. */\n inferred: string | null;\n}\n\n/**\n * Work out which source an identifier belongs to.\n *\n * A prefixed identifier is routed on what it says. A raw one is offered to each\n * source in turn, and a source claims only the shapes it mints: digits for one,\n * a namespaced page key for another. Exactly one claim routes the read and the\n * answer says which reading it used.\n *\n * More than one claim is refused rather than resolved, because picking a winner\n * would send the read somewhere a caller did not intend and answer with a\n * confident recipe for the wrong dish. No claim at all is refused too: a bare\n * dish name is a search, not an identifier, and guessing a page name from it\n * returns whatever that guess happens to hit.\n */\nexport function resolveId(rawId: string, sources: readonly SourceAdapter[]): ResolvedId {\n const trimmed = rawId.trim();\n if (trimmed === \"\") {\n throw invalidInput(\n \"A recipe identifier is required.\",\n `Use one of the ids search_recipes returned, such as ${example(sources)}.`,\n );\n }\n\n for (const source of sources) {\n const prefix = `${source.id}${SEPARATOR}`;\n if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase())) {\n continue;\n }\n const reference = trimmed.slice(prefix.length).trim();\n if (reference === \"\") {\n throw invalidInput(\n `\"${trimmed}\" names a source and no recipe.`,\n \"Put the source's own identifier after the colon.\",\n );\n }\n return { source, reference, inferred: null };\n }\n\n const claimed = sources\n .map((source) => ({ source, claim: source.claims(trimmed) }))\n .filter((entry): entry is { source: SourceAdapter; claim: NonNullable<typeof entry.claim> } =>\n Boolean(entry.claim),\n );\n\n if (claimed.length > 1) {\n throw invalidInput(\n `\"${trimmed}\" could be an identifier on ${names(claimed.map((entry) => entry.source))}, so it names no one recipe.`,\n `Spell it with its source, as in ${claimed.map((entry) => `${entry.source.id}:${trimmed}`).join(\" or \")}.`,\n );\n }\n\n const only = claimed[0];\n if (!only) {\n throw invalidInput(\n `\"${trimmed}\" is not an identifier any of the sources this server reads would mint.`,\n `Call search_recipes with \"${trimmed}\" as the query and pass an id from a row, ` +\n `which reads like ${example(sources)}.`,\n );\n }\n\n return {\n source: only.source,\n reference: only.claim.reference,\n inferred: only.claim.guess\n ? `${only.claim.why}, which is a guess: check the recipe that comes back is the one you meant`\n : only.claim.why,\n };\n}\n\n/** Sources named the way a sentence names them. */\nfunction names(sources: readonly { name: string }[]): string {\n const all = sources.map((source) => source.name);\n if (all.length <= 1) {\n return all.join(\"\");\n }\n return `${all.slice(0, -1).join(\", \")} and ${all.at(-1)}`;\n}\n\n/** An identifier of the shape this server hands out, for a hint. */\nfunction example(sources: readonly SourceAdapter[]): string {\n const first = sources[0];\n return first ? `${first.id}${SEPARATOR}<the source's own id>` : \"<source>:<id>\";\n}\n\n/** Which source an identifier names, without resolving what it points at. */\nexport function sourceOf(rawId: string, sources: readonly SourceAdapter[]): SourceId | null {\n const prefix = rawId.split(SEPARATOR)[0]?.toLowerCase();\n return sources.find((source) => source.id.toLowerCase() === prefix)?.id ?? null;\n}\n","/**\n * Several wordings from one question.\n *\n * A person asks for a recipe in a sentence, and a recipe index reads none of\n * it. One of the indexes behind this server requires every word given to appear\n * on the same page, so a sentence returns nothing where the corpus holds several\n * of the dish. The other ranks pages on the words it was handed, so the words\n * framing the question pull up whatever happens to share them and push the dish\n * down the list or out of it. Both come back to a reader looking like a\n * statement about a corpus when they are statements about a wording. Deriving\n * the shorter wordings and asking for the union is what keeps the two apart.\n *\n * Every wording is built from the words the question carried, with no\n * dictionary, no corpus statistics and no translation. That has two\n * consequences worth stating. A derived wording is one a reader can retype by\n * hand, so an answer built from several of them stays checkable. And the\n * derivations that would need a lexicon are not made: a question asked in one\n * language is never turned into the other, because a source that publishes in\n * the other language and holds nothing under the words written is an honest\n * absence, while a guessed translation is a word nobody wrote.\n *\n * The order runs from the wording closest to what was asked to the widest.\n *\n * A question also states what the recipe must not hold, and that part is read\n * by the role its words play rather than by the turn of phrase they are written\n * in: a negation, a sentence about how a food affects the person, or the name\n * of a diet. All three put a food out of the dish, so all three are set aside\n * from the derived wordings and named back to the caller. Reading them off a\n * list of phrasings is what lets a declared allergen become a word to search\n * for, and there is no phrasing whose absence from such a list is safe.\n */\n\nconst DIGITS_ONLY = /^\\d+$/;\nconst WORD_SEPARATORS = /[^\\p{L}\\p{N}-]+/u;\n\n/** One wording, and how it was arrived at. */\nexport interface Wording {\n query: string;\n /** How this wording was derived from the question, in words. */\n derivation: string;\n}\n\n/**\n * The most searches one source receives for one question.\n *\n * A ceiling per source rather than a budget shared between them, because the\n * sources are asked in parallel and each is paced on its own, so the worst case\n * is three of one source's intervals rather than six. Three is also the whole\n * ladder: the question as asked, the dish words, and the word naming the dish.\n */\nexport const MAX_WORDINGS_PER_SOURCE = 3;\n\n/* -------------------------------------------------------------------------- */\n/* The words a question is built out of, as opposed to the words naming a dish */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Words that frame a question rather than name what is cooked.\n *\n * Both languages are applied to every question. Telling them apart would need a\n * detector, and a detector that guesses wrong strips the wrong words; the two\n * lists barely overlap, and where they do the word frames a question in both.\n *\n * A word is left out of these lists wherever it is also something a person\n * eats. French \"son\" is a possessive and is also bran, so it stays. The same\n * caution governs anything added here: the cost of keeping a framing word is a\n * diluted ranking, and the cost of dropping an ingredient is the dish.\n */\nconst FRENCH_FRAME = new Set([\n \"a\",\n \"ai\",\n \"aimerais\",\n \"au\",\n \"aurais\",\n \"auriez\",\n \"aux\",\n \"avec\",\n \"avons\",\n \"bcp\",\n \"beaucoup\",\n \"bien\",\n \"bonjour\",\n \"bonne\",\n \"bonnes\",\n \"cherche\",\n \"chercher\",\n \"cherchez\",\n \"connais\",\n \"connaissez\",\n \"coucou\",\n \"ce\",\n \"ces\",\n \"cette\",\n \"comment\",\n \"d\",\n \"dans\",\n \"de\",\n \"des\",\n \"du\",\n \"en\",\n \"est\",\n \"et\",\n \"facile\",\n \"faire\",\n \"fais\",\n \"fait\",\n \"idee\",\n \"idees\",\n \"idée\",\n \"idées\",\n \"j\",\n \"je\",\n \"l\",\n \"la\",\n \"le\",\n \"les\",\n \"ma\",\n \"meilleur\",\n \"meilleure\",\n \"merci\",\n \"mes\",\n \"mon\",\n \"ou\",\n \"par\",\n \"peux\",\n \"plait\",\n \"plaît\",\n \"plus\",\n \"pour\",\n \"qu\",\n \"que\",\n \"quel\",\n \"quelle\",\n \"quelles\",\n \"quels\",\n \"qui\",\n \"rapide\",\n \"recette\",\n \"recettes\",\n \"recherche\",\n \"s\",\n \"salut\",\n \"se\",\n \"si\",\n \"simple\",\n \"sommes\",\n \"souhaite\",\n \"stp\",\n \"suis\",\n \"sur\",\n \"svp\",\n \"tres\",\n \"très\",\n \"un\",\n \"une\",\n \"veux\",\n \"voudrais\",\n \"vous\",\n \"à\",\n]);\n\nconst ENGLISH_FRAME = new Set([\n \"a\",\n \"about\",\n \"am\",\n \"an\",\n \"and\",\n \"any\",\n \"anything\",\n \"are\",\n \"at\",\n \"be\",\n \"best\",\n \"by\",\n \"can\",\n \"could\",\n \"do\",\n \"does\",\n \"easy\",\n \"find\",\n \"for\",\n \"from\",\n \"get\",\n \"good\",\n \"hello\",\n \"hi\",\n \"how\",\n \"i\",\n \"in\",\n \"into\",\n \"is\",\n \"it\",\n \"like\",\n \"looking\",\n \"make\",\n \"making\",\n \"me\",\n \"my\",\n \"need\",\n \"of\",\n \"on\",\n \"or\",\n \"please\",\n \"quick\",\n \"recipe\",\n \"recipes\",\n \"should\",\n \"simple\",\n \"some\",\n \"something\",\n \"suggest\",\n \"thank\",\n \"thanks\",\n \"that\",\n \"the\",\n \"less\",\n \"over\",\n \"than\",\n \"to\",\n \"under\",\n \"want\",\n \"what\",\n \"which\",\n \"will\",\n \"with\",\n \"would\",\n \"you\",\n \"your\",\n]);\n\n/**\n * What a condition is measured in.\n *\n * \"under 300 calories\" and \"in 20 minutes\" set a bound, and the word naming the\n * dimension is left in the sentence once the number has gone. No page is named\n * for it, so carrying it into a derived wording ranks pages that merely mention\n * counting calories.\n */\nconst CONDITION_UNITS = new Set([\n \"calorie\",\n \"calories\",\n \"couvert\",\n \"couverts\",\n \"heure\",\n \"heures\",\n \"kcal\",\n \"min\",\n \"mins\",\n \"minute\",\n \"minutes\",\n \"part\",\n \"parts\",\n \"people\",\n \"person\",\n \"persons\",\n \"personne\",\n \"personnes\",\n \"portion\",\n \"portions\",\n \"serving\",\n \"servings\",\n]);\n\n/**\n * Words whose whole job is to take something out of the dish.\n *\n * A negation is a function word, and function words are a closed class: the\n * ones a language has are the ones it has, and covering the class covers every\n * phrasing built on it, whether the question writes \"sans beurre\", \"without\n * butter\", \"no butter\" or \"pas de beurre\". That is what makes this a rule\n * rather than a collection of turns of phrase, and it is why the verbs that do\n * the same work, \"avoid\" and \"éviter\", sit here beside them.\n *\n * The price is a question such as \"no bake cookies\", where the negation is part\n * of a dish's name. It costs a caveat the reader can discard. Missing an\n * exclusion costs the reader the thing they wrote the question to avoid, so the\n * two errors are not weighed the same.\n */\nconst NEGATORS = new Set([\n \"aucun\",\n \"aucune\",\n \"avoid\",\n \"avoiding\",\n \"eviter\",\n \"evite\",\n \"evitez\",\n \"exclude\",\n \"excluding\",\n \"exclure\",\n \"except\",\n \"ni\",\n \"no\",\n \"non\",\n \"not\",\n \"pas\",\n \"sans\",\n \"sauf\",\n \"without\",\n \"zero\",\n]);\n\n/**\n * Words saying that a food does something to the person rather than to the\n * dish.\n *\n * A sentence about the eater names what the recipe must not hold just as\n * plainly as a negation does, and it is the one condition whose failure is a\n * matter of health, so it is read as its own kind and reported as one.\n */\nconst HEALTH_MARKERS = new Set([\n \"allergene\",\n \"allergenes\",\n \"allergen\",\n \"allergens\",\n \"allergic\",\n \"allergie\",\n \"allergies\",\n \"allergique\",\n \"allergiques\",\n \"allergy\",\n \"celiac\",\n \"coeliaque\",\n \"intolerance\",\n \"intolerances\",\n \"intolerant\",\n \"intolerante\",\n \"intolerantes\",\n \"intolerants\",\n]);\n\n/**\n * Diets, each of which names a class of recipes by what it leaves out.\n *\n * Naming one states an exclusion without writing a negation anywhere, which is\n * why a reading built on negations alone lets it through and hands the word\n * back to the index as though it named a dish.\n */\nconst DIETS = new Set([\n \"casher\",\n \"cetogene\",\n \"halal\",\n \"kascher\",\n \"keto\",\n \"ketogenic\",\n \"kosher\",\n \"paleo\",\n \"pescatarian\",\n \"pescetarien\",\n \"pescetarienne\",\n \"vegan\",\n \"vegane\",\n \"veganes\",\n \"vegans\",\n \"vegetalien\",\n \"vegetalienne\",\n \"vegetaliennes\",\n \"vegetaliens\",\n \"vegetarian\",\n \"vegetarians\",\n \"vegetarien\",\n \"vegetarienne\",\n \"vegetariennes\",\n \"vegetariens\",\n \"veggie\",\n]);\n\n/**\n * Where a condition ends.\n *\n * \"sans farine de blé et sans oeufs\" names two things to leave out, and the\n * conjunction is where the first one stops.\n */\nconst CONDITION_ENDS = new Set([\n \"et\",\n \"and\",\n \"ou\",\n \"or\",\n \"pour\",\n \"for\",\n \"avec\",\n \"with\",\n \"mais\",\n \"but\",\n \"dans\",\n \"sur\",\n]);\n\n/**\n * Words joining a marker to what it names, which name nothing themselves.\n *\n * \"pas de beurre\" and \"allergique aux noix\" put one of these between the word\n * carrying the condition and the food it is about. One of them standing right\n * after a marker is what says the food comes after it, which is the only sign\n * in the sentence of which side the marker reaches towards.\n */\nconst CONDITION_JOINERS = new Set([\n \"a\",\n \"au\",\n \"aux\",\n \"d\",\n \"de\",\n \"des\",\n \"du\",\n \"from\",\n \"l\",\n \"la\",\n \"le\",\n \"les\",\n \"my\",\n \"of\",\n \"the\",\n \"to\",\n]);\n\n/**\n * Words that carry a food's name on into a second noun.\n *\n * \"farine de blé\" and \"noix de coco\" are one food written in three words, and\n * the middle one is what says so. A food's name stops wherever no such word\n * carries it further, which is what keeps \"sans gluten gateau au chocolat\" from\n * reading as a condition on chocolate cake.\n */\nconst FOOD_LINKERS = new Set([\"d\", \"de\", \"des\", \"du\", \"of\"]);\n\n/**\n * How many words a condition names before the sentence resumes.\n *\n * What a recipe has to leave out is a food, so it is a short noun phrase:\n * \"beurre\", \"farine de blé\", \"noix de coco\". Past that the words belong to the\n * question again, and a scope with no end turns a negation used inside a dish's\n * name into a condition swallowing the rest of the sentence.\n */\nconst CONDITION_WORDS = 3;\n\n/* -------------------------------------------------------------------------- */\n/* Conditions */\n/* -------------------------------------------------------------------------- */\n\n/** How a question put a condition, which decides how it is reported back. */\nexport type ConditionKind =\n /** The question wrote the recipe must not hold this. */\n | \"excluded\"\n /** The question said the person reacts to this food. */\n | \"allergy\"\n /** The question named a diet, which is a class defined by what it leaves out. */\n | \"diet\";\n\n/** One thing a question asks the recipe not to hold. */\nexport interface Condition {\n /**\n * The words the question used for the food, or null where the question states\n * a condition and the sentence does not say plainly which food it covers.\n *\n * A word named here is a word the recipe must not hold and a word no wording\n * looks for, so naming the wrong one both hides a dish and leaves the food\n * being avoided in the search. Where the reading is not certain, the caller is\n * told a condition was stated and that its food was not read, which is a\n * smaller claim than either.\n */\n named: string | null;\n kind: ConditionKind;\n}\n\n/** What a question asks the dish to avoid, and how many it has to serve. */\nexport interface Conditions {\n /** What the dish must not hold, in the words the question used. */\n conditions: Condition[];\n /** How many the dish has to serve, when the question said. */\n servings: number | null;\n}\n\n/** How many the question asks the dish to serve, written either way round. */\nconst SERVING_PHRASES = [\n /\\bpour\\s+(\\d{1,3})\\s*(?:personnes?|parts?|couverts?)\\b/iu,\n /\\bfor\\s+(\\d{1,3})\\s*(?:people|persons?|servings?|portions?)\\b/iu,\n /\\bserves?\\s+(\\d{1,3})\\b/iu,\n /\\b(\\d{1,3})\\s*(?:personnes?|people|servings?)\\b/iu,\n];\n\n/** A food and the marker written as a single word: \"gluten-free\", \"sugarfree\". */\nconst FREE_OF = /\\b([\\p{L}]{3,})[\\s-]?free\\b/iu;\n\n/** What one reading of a question found in it. */\ninterface ReadQuestion {\n /** The words that name what is cooked, in the order the question wrote them. */\n dish: string[];\n conditions: Condition[];\n}\n\n/** Lowercase and strip diacritics, so \"végétarien\" and \"vegetarien\" read alike. */\nfunction foldWord(word: string): string {\n return word\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/œ/gu, \"oe\");\n}\n\n/**\n * Whether a word left over from a question names the dish.\n *\n * The framing of a question is not the dish it asks for, a number counts eaters\n * or minutes rather than naming anything, and what an apostrophe leaves behind\n * once it has split an elision is not a word at all.\n */\nfunction namesTheDish(word: string, key: string): boolean {\n if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) {\n return false;\n }\n if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) {\n return false;\n }\n if (DIGITS_ONLY.test(word)) {\n return false;\n }\n if (word.length < 2) {\n return false;\n }\n return !CONDITION_UNITS.has(word);\n}\n\n/**\n * Read a question once, deciding for every word whether it names the dish or\n * states a condition on it.\n *\n * One pass rather than two, because the two answers have to agree: a word read\n * as a condition here and as a dish word there is a condition reported to the\n * caller and searched for at the same time, which is the shape of the worst\n * thing this file can do. A single reading cannot disagree with itself.\n */\nfunction readQuestion(question: string): ReadQuestion {\n const words = tokenise(question);\n const conditions: Condition[] = [];\n const dish: string[] = [];\n\n const state = (named: string | null, kind: ConditionKind): void => {\n const trimmed = named === null ? null : named.trim();\n if (trimmed === \"\") {\n return;\n }\n if (conditions.some((held) => held.named === trimmed && held.kind === kind)) {\n return;\n }\n conditions.push({ named: trimmed, kind });\n };\n\n for (let index = 0; index < words.length; index += 1) {\n const word = words[index] ?? \"\";\n const key = foldWord(word);\n\n // A hyphen joins everywhere else, so \"sans-gluten\" and \"no-sugar\" arrive as\n // one word with the negation inside it.\n const hyphen = word.indexOf(\"-\");\n if (hyphen > 0 && NEGATORS.has(foldWord(word.slice(0, hyphen)))) {\n state(word.slice(hyphen + 1), \"excluded\");\n continue;\n }\n\n // \"gluten-free\" writes the food and the marker as one word.\n const free = FREE_OF.exec(word);\n if (free) {\n state(free[1] ?? \"\", \"excluded\");\n continue;\n }\n\n // \"free\" is also a price and a way of keeping hens, so it states a\n // condition only where the sentence puts a food on one side of it.\n if (key === \"free\") {\n const scope = scopeOf(words, index, dish);\n if (scope.named !== null) {\n state(scope.named, \"excluded\");\n index = scope.next - 1;\n }\n continue;\n }\n\n // A negation stands before what it negates, in both languages: \"sans\n // beurre\", \"without butter\", \"pas de beurre\".\n if (NEGATORS.has(key)) {\n const forward = takeFood(words, index + 1);\n state(forward.named === \"\" ? null : forward.named, \"excluded\");\n // The marker itself never names a dish, so it leaves whether or not it\n // turned out to scope over anything.\n index = forward.next - 1;\n continue;\n }\n\n if (HEALTH_MARKERS.has(key)) {\n const scope = scopeOf(words, index, dish);\n state(scope.named, \"allergy\");\n index = scope.next - 1;\n continue;\n }\n\n if (DIETS.has(key)) {\n state(word, \"diet\");\n continue;\n }\n\n if (namesTheDish(word, key) && !dish.includes(word)) {\n dish.push(word);\n }\n }\n\n return { dish, conditions };\n}\n\n/**\n * The food a marker covers, on whichever side of it the sentence put one.\n *\n * A joining word straight after the marker is the sentence saying its food\n * follows: \"allergique aux noix\", \"allergic to peanuts\", \"free of dairy\". With\n * no such word the food is the one the marker was written onto the end of:\n * \"peanut allergy\", \"lactose intolerant\", \"gluten free\". The two orders belong\n * to the two languages these sources publish in, so reading only one of them\n * leaves every question written in the other unread.\n *\n * Read the wrong way round, a marker takes a word out of the dish and leaves\n * the food being avoided in the search, which is both halves of the harm at\n * once. So a marker with a food on neither side covers none, and says so.\n *\n * A food found before the marker was already read as a word of the dish, and\n * leaves it here: the same word cannot be a thing to avoid and a thing to look\n * for.\n */\nfunction scopeOf(\n words: string[],\n marker: number,\n dish: string[],\n): { named: string | null; next: number } {\n if (CONDITION_JOINERS.has(foldWord(words[marker + 1] ?? \"\"))) {\n const forward = takeFood(words, marker + 1);\n return { named: forward.named === \"\" ? null : forward.named, next: forward.next };\n }\n\n const before = words[marker - 1];\n if (before !== undefined) {\n const held = dish.lastIndexOf(before);\n if (held !== -1) {\n dish.splice(held, 1);\n return { named: before, next: marker + 1 };\n }\n }\n\n return { named: null, next: marker + 1 };\n}\n\n/**\n * The food named from a point in a sentence onwards, and where the sentence\n * picks up again.\n *\n * A food is a short noun phrase, and it ends where nothing carries it further.\n * Taking a fixed number of words instead lets a condition run on into the dish,\n * so \"sans gluten gateau au chocolat\" comes back as a condition on the cake.\n */\nfunction takeFood(words: string[], from: number): { named: string; next: number } {\n let index = from;\n // An article or a quantifier can stand between the marker and the food:\n // \"pas de beurre\", \"without any butter\".\n while (index < words.length && isFiller(words[index] ?? \"\")) {\n index += 1;\n }\n\n const named: string[] = [];\n while (index < words.length && named.length < CONDITION_WORDS) {\n const word = words[index] ?? \"\";\n if (!namesFood(word)) {\n break;\n }\n named.push(word);\n index += 1;\n\n const beyond = words[index + 1];\n if (\n named.length + 2 <= CONDITION_WORDS &&\n FOOD_LINKERS.has(foldWord(words[index] ?? \"\")) &&\n beyond !== undefined &&\n namesFood(beyond)\n ) {\n named.push(words[index] ?? \"\");\n index += 1;\n continue;\n }\n break;\n }\n\n return { named: named.join(\" \"), next: index };\n}\n\n/** A word standing between a marker and its food, naming no food itself. */\nfunction isFiller(word: string): boolean {\n const key = foldWord(word);\n return CONDITION_JOINERS.has(key) || FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key);\n}\n\n/** Whether a word can be part of the name of something a recipe holds. */\nfunction namesFood(word: string): boolean {\n const key = foldWord(word);\n if (key.length < 2) {\n return false;\n }\n if (DIGITS_ONLY.test(key)) {\n return false;\n }\n if (key === \"free\") {\n return false;\n }\n if (CONDITION_ENDS.has(key)) {\n return false;\n }\n if (NEGATORS.has(key) || HEALTH_MARKERS.has(key)) {\n return false;\n }\n if (CONDITION_UNITS.has(key)) {\n return false;\n }\n return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));\n}\n\n/**\n * Read what the question asks the recipe to avoid or to serve.\n *\n * These are the parts of a question no recipe index answers. A page is not\n * named for what it leaves out, and the number of people at a table is not\n * printed in a title at all, so both are read here and handled as what they\n * are rather than as words to look for.\n */\nexport function readConditions(question: string): Conditions {\n const { conditions } = readQuestion(question);\n\n let servings: number | null = null;\n for (const phrase of SERVING_PHRASES) {\n const found = phrase.exec(question);\n if (found) {\n const value = Number(found[1]);\n if (Number.isFinite(value) && value > 0) {\n servings = value;\n break;\n }\n }\n }\n\n return { conditions, servings };\n}\n\n/* -------------------------------------------------------------------------- */\n/* The ladder */\n/* -------------------------------------------------------------------------- */\n\nexport function deriveWordings(question: string): Wording[] {\n const asked = tidy(question);\n const wordings: Wording[] = [];\n const seen = new Set<string>();\n\n const offer = (candidate: string, derivation: string): void => {\n const clean = tidy(candidate);\n if (clean === \"\") {\n return;\n }\n // Two wordings differing only in case or spacing are one wording to these\n // indexes, and sending both spends an interval to learn nothing.\n const key = clean.toLowerCase();\n if (seen.has(key)) {\n return;\n }\n seen.add(key);\n wordings.push({ query: clean, derivation });\n };\n\n offer(asked, \"the words as asked, condition words included\");\n\n const dish = dishWords(asked);\n offer(\n dish.join(\" \"),\n \"the words naming the dish, with the words framing the question and the conditions set aside, because a page is named for what it holds\",\n );\n\n offer(\n dish[0] ?? \"\",\n \"the leading dish word on its own, because an index that requires every word given returns nothing for a phrase and a great deal for the word naming the dish\",\n );\n\n // The ladder is three rungs by construction; the ceiling holds whatever a\n // future rung would add to it.\n return wordings.slice(0, MAX_WORDINGS_PER_SOURCE);\n}\n\n/**\n * Whether a row's title carries a word naming the dish that was asked for.\n *\n * This decides whether a wording answered, and nothing else: no row is dropped\n * and no row is moved for failing it. A source that ranks on the words it was\n * handed answers a sentence with a page of recipes that share its framing\n * words, and counting those as an answer would stop the search on the page that\n * proves it has not found the dish.\n *\n * Diacritics and punctuation fold away, so \"crêpes\" reads the same as \"crepes\"\n * and a namespace in front of a page name settles nothing. A question that\n * names no dish has nothing to check against, and every row passes.\n */\nexport function namesDish(title: string, question: string): boolean {\n const words = namingWords(question);\n if (words.length === 0) {\n return true;\n }\n return dishWordsMissing(title, question).length < words.length;\n}\n\n/**\n * The words naming the dish that a title does not carry.\n *\n * Sharing one word is what makes a row worth offering, and it is not what makes\n * a row the dish. \"Biscuits and gravy\" and a tin of Christmas biscuits share\n * the biscuit and nothing else, so a caller weighing one against the other has\n * to be told which of the words asked for went unanswered.\n *\n * Empty for a title carrying all of them, and empty for a question that names\n * no dish, since there is then nothing to go unanswered.\n */\nexport function dishWordsMissing(title: string, question: string): string[] {\n const haystack = ` ${fold(title)} `;\n return namingWords(question).filter((word) => !haystack.includes(` ${fold(word)}`));\n}\n\n/** The words of a question long enough to name what is cooked. */\nfunction namingWords(question: string): string[] {\n return dishWords(question).filter((word) => word.length > 2);\n}\n\nfunction fold(value: string): string {\n return value\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/œ/gu, \"oe\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \");\n}\n\n/**\n * The words of a question that name what is cooked.\n *\n * The framing words go because they rank pages that merely share them. The\n * condition words go for a different reason, and it is worth stating: a search\n * for \"sans gluten\" ranks pages about gluten, and a search for \"6 personnes\"\n * ranks nothing at all, so carrying them into a derived wording buries the dish\n * without excluding anything. Dropping them silently would be the worse move,\n * since the recipe found may hold exactly what the person was avoiding, and\n * that is why the question as asked is sent first and every condition set aside\n * is named back to the caller.\n */\nfunction dishWords(question: string): string[] {\n return readQuestion(question).dish;\n}\n\n/**\n * The words of a question, lowercased.\n *\n * An apostrophe separates: \"l'origan\" is the article and the herb, and the herb\n * is the word an index holds. A hyphen joins, because \"pot-au-feu\" and\n * \"gluten-free\" are each one word wherever they are written.\n */\nfunction tokenise(value: string): string[] {\n return value\n .toLowerCase()\n .replace(/['’]/gu, \" \")\n .split(WORD_SEPARATORS)\n .map((word) => word.replace(/^-+|-+$/g, \"\"))\n .filter((word) => word !== \"\");\n}\n\nfunction tidy(value: string): string {\n return value.replace(/\\s+/g, \" \").trim();\n}\n","/**\n * The one place that talks to the sources.\n *\n * It asks them all at once and merges what comes back, so a question is\n * answered by whichever of them holds something. It imports nothing from the\n * MCP layer and is published on its own, so the same code serves a plain\n * script.\n *\n * A source that fails is reported as a source that failed. It is never folded\n * into the answer as an absence: \"Marmiton was unreachable\" and \"Marmiton holds\n * no such recipe\" are different statements about the world, and a caller that\n * cannot tell them apart will say the second when only the first is true.\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 { fromSource, invalidInput, timeout as timedOut } from \"../errors.js\";\nimport type {\n MergedSearch,\n RecipeDetail,\n RecipeRow,\n SourceId,\n SourceReport,\n WordingAttempt,\n} from \"../types.js\";\nimport type { SourceAdapter } from \"./adapter.js\";\nimport { resolveId } from \"./ids.js\";\nimport type { ResolvedId } from \"./ids.js\";\nimport { buildSources, selectSources } from \"./registry.js\";\nimport type { Readers } from \"./registry.js\";\nimport { MAX_WORDINGS_PER_SOURCE, deriveWordings, namesDish } from \"./wordings.js\";\nimport type { Wording } from \"./wordings.js\";\n\nexport type { SourceAdapter, ReadRecipe, ReadRows } from \"./adapter.js\";\nexport type { CookbookReader } from \"./cookbook.js\";\nexport type { MarmitonReader } from \"./marmiton.js\";\nexport type { Readers } from \"./registry.js\";\nexport type {\n MergedSearch,\n RecipeDetail,\n RecipeRow,\n SourceId,\n SourceProfile,\n SourceReport,\n WordingAttempt,\n} from \"../types.js\";\n\nconst LETTER_OR_DIGIT = /[\\p{L}\\p{N}]/u;\n\nexport interface RecipesClientOptions {\n config?: Partial<Config>;\n logger?: Logger;\n /** Stands in for one or more of the site readers. */\n readers?: Readers;\n /** Replaces the registry outright, for a program bringing its own corpora. */\n sources?: SourceAdapter[];\n}\n\n/** The largest number of rows any one source is asked for in a single call. */\nexport const MAX_LIMIT_PER_SOURCE = 25;\n\nexport interface SearchOptions {\n /**\n * Whether a question may be sent in wordings derived from it as well as in\n * the words it was written in.\n *\n * On unless a caller turns it off. A recipe index answers the words it is\n * handed, so a question written as a sentence comes back empty from a corpus\n * holding several of the dish, and an empty answer is read as a corpus that\n * holds nothing. Off, a caller gets exactly the wording they typed and the\n * answer names the wordings that were not sent.\n */\n fanOut?: boolean;\n}\n\n/**\n * The pacing this server owes each source, 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 setting is held to the same range\n * the environment parser enforces: `timeoutMs: 0` is the usual way of writing\n * \"no deadline\", and a retry count of a hundred thousand aimed at a site paced\n * in milliseconds is hours of traffic from a single call.\n */\nfunction withGuarantees(config: Config): Config {\n const defaults = loadConfig({});\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 userAgent:\n claimed === \"\" || claimed.includes(identifier) ? 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, 120_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 logLevel: config.logLevel ?? defaults.logLevel,\n };\n}\n\n/**\n * A wall clock over one source, as a backstop under the deadline that source's\n * own reader keeps.\n *\n * That reader times out its own requests, so this only matters when it cannot:\n * a socket that stalls without erroring, a stand-in reader, a bug in a\n * dependency free to change under a caret range. Without it, one source that\n * never answers holds the whole call open and the rows the others had ready in\n * milliseconds are never returned.\n *\n * The allowance covers every attempt a reader is entitled to make, plus the\n * pacing between them, so this never fires before the reader's own deadline has.\n */\nfunction withDeadline<T>(work: Promise<T>, ms: number, source: SourceAdapter): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const alarm = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () =>\n reject(\n timedOut(`${source.name} did not answer within ${ms}ms and was left out of this answer.`),\n ),\n ms,\n );\n // A pending timer must not hold the process open once the answer is out.\n timer.unref?.();\n });\n return Promise.race([work, alarm]).finally(() => clearTimeout(timer));\n}\n\n/** What one source produced for one question, whether or not it produced anything. */\ninterface Attempt {\n source: SourceAdapter;\n rows: RecipeRow[];\n /** Whether rows naming the dish were put in front of rows that do not. */\n preferredByName: boolean;\n cached: boolean;\n reportedTotal: number | null;\n reportedTotalMeans: string | null;\n skipped: number;\n error: { code: string; message: string; hint?: string } | null;\n wordings: WordingAttempt[];\n}\n\nfunction reportOf(attempt: Attempt, count: number): SourceReport {\n return {\n source: attempt.source.id,\n name: attempt.source.name,\n status: attempt.error ? \"failed\" : \"answered\",\n count,\n reportedTotal: attempt.reportedTotal,\n reportedTotalMeans: attempt.reportedTotalMeans,\n skipped: attempt.skipped,\n mixesReferencePages: attempt.source.mixesReferencePages,\n cached: attempt.cached,\n error: attempt.error,\n wordings: attempt.wordings,\n preferredByName: attempt.preferredByName,\n };\n}\n\n/** A wording that was never sent, and the reason it was not. */\nfunction heldBack(wording: Wording, because: string): WordingAttempt {\n return {\n query: wording.query,\n derivation: wording.derivation,\n ran: false,\n count: null,\n added: null,\n notRunBecause: because,\n error: null,\n };\n}\n\n/**\n * Keep the rows a wording brought back that no earlier wording already did, and\n * count how many of them name the dish that was asked for.\n *\n * The identifier names its source, so two sources minting the same reference\n * stay two rows while one recipe reached twice stays one.\n */\nfunction keepNewRows<T extends { id: string; title: string }>(\n read: { rows: readonly T[] },\n seen: Set<string>,\n rows: T[],\n question: string,\n): { added: number; onTopic: number } {\n let added = 0;\n let onTopic = 0;\n\n for (const row of read.rows) {\n if (seen.has(row.id)) {\n continue;\n }\n seen.add(row.id);\n rows.push(row);\n added += 1;\n if (namesDish(row.title, question)) {\n onTopic += 1;\n }\n }\n\n return { added, onTopic };\n}\n\n/**\n * Interleave rows so no source opens the list twice before another has opened\n * it once.\n *\n * Ranking them against each other would need a score every source carries, and\n * there is none: one has reader ratings, another has no author and no rating by\n * nature, and a single ordering would rank on a field most rows cannot have.\n * Taking one from each in turn is an order a caller can describe exactly, which\n * is what the answer does.\n */\nexport function interleave(groups: RecipeRow[][]): RecipeRow[] {\n const merged: RecipeRow[] = [];\n const longest = Math.max(0, ...groups.map((group) => group.length));\n for (let index = 0; index < longest; index += 1) {\n for (const group of groups) {\n const row = group[index];\n if (row) {\n merged.push(row);\n }\n }\n }\n return merged;\n}\n\nexport class RecipesClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly sources: SourceAdapter[];\n\n constructor(options: RecipesClientOptions = {}) {\n this.config = withGuarantees({ ...loadConfig(), ...options.config });\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.sources = options.sources ?? buildSources(this.config, options.readers ?? {}, this.logger);\n }\n\n /** What every source sees this client call itself. */\n get userAgent(): string {\n return this.config.userAgent;\n }\n\n get intervalMs(): number {\n return this.config.minIntervalMs;\n }\n\n get timeoutMs(): number {\n return this.config.timeoutMs;\n }\n\n get maxRetries(): number {\n return this.config.maxRetries;\n }\n\n /** The sources this server reads, in the order an answer takes them. */\n get profiles(): Array<{ id: SourceId; name: string; language: string; homeUrl: string }> {\n return this.sources.map((source) => ({\n id: source.id,\n name: source.name,\n language: source.language,\n homeUrl: source.homeUrl,\n }));\n }\n\n /**\n * The backstop deadline over one source, covering every attempt it is\n * entitled to make and the pacing between them.\n */\n private get deadlineMs(): number {\n const attempts = this.config.maxRetries + 1;\n return this.config.timeoutMs * attempts + this.config.minIntervalMs * this.config.maxRetries;\n }\n\n /**\n * Ask every source the same question and merge the rows.\n *\n * The calls go out together, so the answer takes as long as the slowest\n * source rather than as long as all of them. Whichever of them fail, the rest\n * still come back, and the report says which was which.\n */\n async searchRecipes(\n query: string,\n limitPerSource: number,\n wanted?: readonly SourceId[],\n options: SearchOptions = {},\n ): Promise<MergedSearch> {\n const trimmed = query.trim();\n if (trimmed === \"\") {\n throw invalidInput(\"A search needs something to look for.\", \"Name a dish or an ingredient.\");\n }\n // A recipe index handed nothing it can match on answers with the page it\n // shows by default, and those rows would come back as this question's\n // recipes. One character is a word in the scripts that write one that way,\n // so what is asked for is a letter or a digit rather than a length.\n if (!LETTER_OR_DIGIT.test(trimmed)) {\n throw invalidInput(\n `\"${trimmed}\" carries no letter and no digit, so there is no word to search for.`,\n \"Name a dish or an ingredient.\",\n );\n }\n\n const chosen = selectSources(this.sources, wanted);\n const limit = Math.max(1, Math.min(MAX_LIMIT_PER_SOURCE, Math.trunc(limitPerSource)));\n const fanOut = options.fanOut ?? true;\n\n const attempts = await Promise.all(\n chosen.map((source) => this.searchOne(source, trimmed, limit, fanOut)),\n );\n\n const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));\n return {\n rows: interleave(answered.map((one) => one.rows)),\n reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows.length)),\n };\n }\n\n /**\n * Ask one source the question in several wordings, and union what comes back.\n *\n * The wordings go out one after another rather than together, because each\n * source is paced and a burst would spend the whole ladder's politeness at\n * once. They stop early: a wording is only sent when the ones before it left\n * the source short of what was asked for, so the ordinary case where the\n * question names a dish costs one request.\n *\n * Each wording is a different search rather than a further page of the same\n * one. One of these sites serves a single page of results and disallows\n * paging past it, so a second page is never asked for, here or anywhere else.\n */\n private async searchOne(\n source: SourceAdapter,\n question: string,\n limit: number,\n fanOut: boolean,\n ): Promise<Attempt> {\n const derived = deriveWordings(question);\n const wordings: WordingAttempt[] = [];\n const rows: RecipeRow[] = [];\n const seen = new Set<string>();\n\n let cached = false;\n let skipped = 0;\n let reportedTotal: number | null = null;\n let reportedTotalMeans: string | null = null;\n let answered = false;\n let failure: { code: string; message: string; hint?: string } | null = null;\n /**\n * Rows whose title carries a word naming the dish.\n *\n * This is what \"enough\" counts, rather than rows. A source that ranks on\n * the words it was handed fills a page with recipes sharing a question's\n * framing words, and stopping on the size of that page would stop on the\n * evidence that the dish has not been found. Every row is kept either way.\n */\n let onTopic = 0;\n\n for (const [index, wording] of derived.entries()) {\n if (index > 0 && !fanOut) {\n wordings.push(\n heldBack(wording, \"fan_out was turned off, so only the words as asked were sent\"),\n );\n continue;\n }\n if (index >= MAX_WORDINGS_PER_SOURCE) {\n wordings.push(\n heldBack(\n wording,\n `the ceiling of ${MAX_WORDINGS_PER_SOURCE} searches of one source was reached`,\n ),\n );\n continue;\n }\n if (failure !== null) {\n wordings.push(\n heldBack(wording, `${source.name} did not answer the wording before this one`),\n );\n continue;\n }\n if (onTopic >= limit) {\n wordings.push(\n heldBack(\n wording,\n \"the wordings already sent returned as many rows naming the dish as were asked for\",\n ),\n );\n continue;\n }\n\n try {\n const read = await withDeadline(\n source.search(wording.query, limit),\n this.deadlineMs,\n source,\n );\n answered = true;\n cached = cached || read.cached;\n skipped += read.skipped;\n if (reportedTotal === null && reportedTotalMeans === null) {\n reportedTotal = read.reportedTotal;\n reportedTotalMeans = read.reportedTotalMeans;\n }\n if (read.skipped > 0) {\n this.logger.warn(\n `${source.name} sent ${read.skipped} row(s) this server could not read; they were left out.`,\n );\n }\n\n const kept = keepNewRows(read, seen, rows, question);\n const added = kept.added;\n onTopic += kept.onTopic;\n\n wordings.push({\n query: wording.query,\n derivation: wording.derivation,\n ran: true,\n count: read.rows.length,\n added,\n notRunBecause: null,\n error: null,\n });\n } catch (error) {\n const known = fromSource(error, source.name);\n this.logger.warn(\n `${source.name} did not answer \"${wording.query}\": [${known.code}] ${known.message}`,\n );\n const reason = {\n code: known.code,\n message: known.message,\n ...(known.details.hint ? { hint: known.details.hint } : {}),\n };\n wordings.push({\n query: wording.query,\n derivation: wording.derivation,\n ran: true,\n count: null,\n added: null,\n notRunBecause: null,\n error: reason,\n });\n failure = reason;\n }\n }\n\n // Where several wordings contributed, the rows naming the dish are put in\n // front of the rows that do not, each group keeping the order it arrived\n // in. Without this, a first wording answering with a page of near-misses\n // fills the limit and cuts away the rows a later wording found. A single\n // wording is left in the order its source returned it, since there is\n // nothing to rescue it from.\n const contributing = wordings.filter((attempt) => (attempt.added ?? 0) > 0).length;\n const ordered =\n contributing > 1\n ? [\n ...rows.filter((row) => namesDish(row.title, question)),\n ...rows.filter((row) => !namesDish(row.title, question)),\n ]\n : rows;\n\n // A source that answered one wording answered. Reporting it as failed\n // because a later wording timed out would hide the rows it did return\n // behind a claim that it said nothing.\n return {\n source,\n rows: ordered,\n preferredByName: contributing > 1,\n cached,\n reportedTotal,\n reportedTotalMeans,\n skipped,\n error: answered ? null : failure,\n wordings,\n };\n }\n\n /**\n * Read one recipe, from whichever source the identifier names.\n *\n * Only that source is called. Trying another after a miss would turn \"this\n * source has no page by this name\" into somebody else's recipe with a\n * different title, which is a worse answer than the absence it replaced.\n */\n async getRecipe(\n id: string,\n ): Promise<{ recipe: RecipeDetail; cached: boolean; read: ResolvedId }> {\n const read = resolveId(id, this.sources);\n try {\n const outcome = await withDeadline(\n read.source.getRecipe(read.reference),\n this.deadlineMs,\n read.source,\n );\n return { recipe: outcome.recipe, cached: outcome.cached, read };\n } catch (error) {\n throw fromSource(error, read.source.name);\n }\n }\n}\n","/**\n * compare_recipes: the same dish, as each source writes it.\n *\n * This is the one tool that exists because there is more than one source. A\n * French kitchen and an English one write the same dish differently, and the\n * difference is the answer: what each asks for, what each measures in, how many\n * it serves, and what each source publishes about a recipe at all.\n *\n * Nothing is ranked. The sources share no score, and declaring a winner would\n * mean inventing one.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { dishWordsMissing } from \"../sources/wordings.js\";\nimport type { RecipeDetail, SourceId } from \"../types.js\";\nimport { buildRecipeView, recipeSchema, renderYield, SECTIONS } from \"./recipeView.js\";\nimport type { RecipePayload, Section } from \"./recipeView.js\";\nimport type { RecipeView } from \"./recipeView.js\";\nimport {\n creditLine,\n fitLines,\n mustKeep,\n omittedLinesLine,\n noteTexts,\n ok,\n quoteForeign,\n roomForBody,\n reportNotes,\n reportSchema,\n toReportPayload,\n toToolError,\n withRead,\n} from \"./shared.js\";\nimport type { Note } from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/**\n * Why nothing was compared.\n *\n * A version that was offered and could not be read is a different statement\n * from no source offering one at all: the first says the dish is out there.\n */\nfunction nothingWasCompared(someWereUnread: boolean, dish: string): string {\n if (someWereUnread) {\n return `Every version of \"${quoteForeign(dish)}\" that was offered could not be read, so nothing was compared.`;\n }\n return `No source offered a recipe for \"${quoteForeign(dish)}\".`;\n}\n\nexport const compareRecipesDescription = [\n \"Take a dish and show how each source writes it, side by side.\",\n \"Each source's closest match is read in full, and all of them can be rescaled to the same number of servings so the ingredient lists stand comparison.\",\n \"The answer states what differs and leaves it there: the quantities each asks for, what each measures in, what each yields, and which fields each source publishes at all.\",\n \"No version is ranked above another. Some sources carry reader ratings and some have no author and no rating by nature, so there is no score they share.\",\n \"When only one source answers, the answer says so and shows that one rather than presenting part of a comparison as the whole of it.\",\n].join(\" \");\n\nexport const compareRecipesInput = strictInput({\n dish: z\n .string()\n .min(1)\n .max(200)\n .describe(\"The dish to compare, in any of the languages the sources publish in.\"),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale every version to this many, which is what makes the lists comparable.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\"])\n .describe(\"Which parts of each version to return. Several full recipes is a lot of text.\"),\n max_steps: z.number().int().min(1).max(100).default(10).describe(\"Steps to return per version.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n sources: z\n .array(z.string())\n .optional()\n .describe(\n \"Sources to compare, by id. Left out, every source is read, which is the point of this tool. Name two or three to put particular traditions side by side. The ids are the ones 'per_source' reports.\",\n ),\n});\n\nexport const compareRecipesOutput = z.object({\n dish: z.string(),\n versions: z.array(recipeSchema),\n differences: z\n .array(z.string())\n .describe(\"What differs between the versions, stated as fact. No version is ranked.\"),\n per_source: z.array(reportSchema),\n notes: z.array(z.string()),\n});\n\nexport type CompareRecipesArgs = z.infer<typeof compareRecipesInput>;\n\n/** Ingredient lines each version shows in the text block. */\nconst TEXT_INGREDIENT_LINES = 8;\n\n/** Differences the text block states before pointing at the rest. */\nconst TEXT_DIFFERENCE_LINES = 4;\n\n/** Sources named the way a sentence names them. */\nfunction listNames(names: string[]): string {\n if (names.length <= 1) {\n return names.join(\"\");\n }\n return `${names.slice(0, -1).join(\", \")} and ${names.at(-1)}`;\n}\n\n/**\n * The verb that follows a list of sources.\n *\n * A comparison over two sources named one at a time often enough for the\n * singular to be right by accident; over five it is wrong as often as not, and\n * a sentence that misagrees reads as a sentence written about something else.\n */\nfunction agrees(names: string[], singular: string, plural: string): string {\n return names.length === 1 ? singular : plural;\n}\n\n/**\n * Which versions actually answer to the dish that was asked for.\n *\n * A title sharing one word with the question is not the dish: \"biscuits and\n * gravy\" and a tin of Christmas biscuits share the biscuit, and setting them\n * side by side would present a search that missed as a difference between two\n * traditions.\n */\nfunction weighTitlesAgainstTheDish(\n payloads: readonly RecipeView[\"payload\"][],\n dish: string,\n notes: RecipeView[\"notes\"][number][],\n): Array<{ payload: RecipeView[\"payload\"]; missing: readonly string[] }> {\n const weighed = payloads.map((payload) => ({\n payload,\n missing: dishWordsMissing(payload.title, dish),\n }));\n const carriesDish = weighed.some((one) => one.missing.length === 0);\n\n for (const { payload, missing } of weighed) {\n if (missing.length === 0) {\n continue;\n }\n notes.push(\n mustKeep(\n `${payload.source_name}'s closest row is ${quoteForeign(payload.title)}, whose title ` +\n `carries no ${missing.map((word) => `\"${quoteForeign(word)}\"`).join(\" and no \")}. ` +\n \"Read it as a candidate to check rather than as that dish.\",\n ),\n );\n }\n\n if (payloads.length > 0 && !carriesDish) {\n notes.push(\n `No version here carries the whole of \"${quoteForeign(dish)}\" in its title. These are ` +\n \"the closest rows each source returned for that spelling, so read them as candidates to \" +\n \"check rather than as that dish.\",\n );\n }\n\n if (payloads.length === 1 && carriesDish) {\n notes.push(\n mustKeep(\n \"This is one version rather than a comparison. Read it as what that one source publishes.\",\n ),\n );\n }\n\n // A difference is a statement about one dish written two ways. Setting a\n // row that answers to another name beside it turns a search that missed\n // into a claim about two traditions.\n\n return weighed;\n}\n\n/**\n * Say what the versions do differently, and say nothing where they agree.\n *\n * A field one source has and another does not is the most useful thing a\n * comparison can report, and the least tempting to turn into a ranking: a wiki\n * has no author because everyone edited the page, which says nothing about the\n * recipe. Where every version says the same thing there is no difference to\n * report, and reporting one anyway costs a reader's trust in the rest.\n *\n * A difference is only stated about a part of the recipe the answer carries.\n * The sections a call leaves out come back empty, and the answer says so, so a\n * sentence comparing one of them describes something the reader was told is not\n * there and cannot check.\n */\nfunction describeDifferences(\n recipes: RecipeDetail[],\n payloads: RecipePayload[],\n sections: readonly Section[],\n): string[] {\n if (recipes.length < 2) {\n return [];\n }\n const differences: string[] = [];\n const nameOf = (recipe: RecipeDetail) => recipe.sourceName;\n\n const stated = payloads.map((payload) => ({\n name: payload.source_name,\n text: payload.yield.original_text ?? \"no stated amount\",\n }));\n if (new Set(stated.map((one) => one.text)).size > 1) {\n differences.push(\n `${stated.map((one) => `${one.name} yields ${quoteForeign(one.text)}`).join(\"; \")}.`,\n );\n }\n\n const counts = payloads.map((payload) => payload.ingredients.length);\n if (new Set(counts).size > 1 && counts.every((value) => value > 0)) {\n differences.push(\n `${payloads\n .map(\n (payload) =>\n `${payload.source_name} lists ${payload.ingredients.length} ingredient lines`,\n )\n .join(\"; \")}.`,\n );\n }\n\n if (sections.includes(\"times\")) {\n const timed = recipes.filter((recipe) => recipe.totalMinutes !== null);\n const untimed = recipes.filter((recipe) => recipe.totalMinutes === null);\n if (timed.length > 0 && new Set(timed.map((recipe) => recipe.totalMinutes)).size > 1) {\n differences.push(\n `${timed.map((recipe) => `${nameOf(recipe)} states ${recipe.totalMinutes} minutes`).join(\"; \")}.`,\n );\n }\n if (timed.length > 0 && untimed.length > 0) {\n differences.push(\n `${listNames(untimed.map(nameOf))} ${agrees(untimed.map(nameOf), \"states\", \"state\")} no time for this recipe, so there is nothing there to compare against.`,\n );\n }\n }\n\n const rated = recipes.filter((recipe) => recipe.rating !== null);\n if (rated.length > 0 && rated.length < recipes.length) {\n differences.push(\n `Only ${listNames(rated.map(nameOf))} ${agrees(rated.map(nameOf), \"carries\", \"carry\")} a reader rating, so the versions cannot be compared on one.`,\n );\n }\n\n const credited = recipes.filter((recipe) => recipe.author !== null);\n if (credited.length > 0 && credited.length < recipes.length) {\n differences.push(\n `Only ${listNames(credited.map(nameOf))} ${agrees(credited.map(nameOf), \"credits\", \"credit\")} an author; the rest are written by whoever edited the page.`,\n );\n }\n\n for (const recipe of recipes) {\n const license = recipe.license;\n if (license === null) {\n continue;\n }\n differences.push(\n `${nameOf(recipe)} publishes under ${quoteForeign(license.title)}, which asks for attribution.`,\n );\n }\n const silent = recipes.filter((recipe) => recipe.license === null);\n if (silent.length > 0 && silent.length < recipes.length) {\n differences.push(\n `${listNames(silent.map(nameOf))} ${agrees(silent.map(nameOf), \"states\", \"state\")} no terms of use. Silence is not permission.`,\n );\n }\n\n return differences;\n}\n\nexport async function runCompareRecipes(\n client: RecipesClient,\n args: CompareRecipesArgs,\n): Promise<ToolResult> {\n try {\n // A few rows from each source is enough: the point is the traditions side\n // by side rather than a long list from any of them.\n const merged = await client.searchRecipes(\n args.dish,\n 3,\n args.sources as readonly SourceId[] | undefined,\n );\n\n const best = new Map<SourceId, string>();\n for (const row of merged.rows) {\n if (!best.has(row.source)) {\n best.set(row.source, row.id);\n }\n }\n\n const offered = [...best.entries()];\n const reads = await Promise.allSettled(offered.map(([, id]) => client.getRecipe(id)));\n\n const recipes: RecipeDetail[] = [];\n /** Sources whose row was found and whose page then could not be read. */\n const unread = new Map<SourceId, { code: string; message: string }>();\n /** Sources whose row was opened, whichever way that went. */\n const opened = new Set<SourceId>();\n\n reads.forEach((read, index) => {\n const source = offered[index]?.[0];\n if (source === undefined) {\n return;\n }\n opened.add(source);\n if (read.status === \"fulfilled\") {\n recipes.push(read.value.recipe);\n return;\n }\n const reason = read.reason as { code?: string; message?: string } | undefined;\n unread.set(source, {\n code: reason?.code ?? \"network_error\",\n message: reason?.message ?? \"unknown\",\n });\n });\n\n const views = recipes.map((recipe) =>\n buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n label: recipe.sourceName,\n // The choice of sections belongs to the call rather than to a version,\n // so it is said once below instead of once per recipe.\n announceSections: false,\n }),\n );\n const payloads = views.map((view) => view.payload);\n\n const notes: Note[] = reportNotes(merged.reports);\n for (const view of views) {\n notes.push(...view.notes);\n }\n\n const omitted = SECTIONS.filter((section) => !args.sections.includes(section));\n if (omitted.length > 0) {\n notes.push(\n `Not requested, so not returned on any version: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n // Three reasons a source can be missing from a comparison, and they are\n // three different statements about the world. Reading a fetch failure as\n // \"that source offered nothing\" is the one that turns a bad minute into a\n // claim about what a corpus holds.\n for (const report of merged.reports) {\n if (recipes.some((recipe) => recipe.source === report.source)) {\n continue;\n }\n const failedRead = unread.get(report.source);\n if (report.status === \"failed\") {\n notes.push(\n mustKeep(\n `${report.name} is missing from this comparison because its search did not answer (${quoteForeign(\n `${report.error?.code ?? \"unknown\"}: ${report.error?.message ?? \"unknown\"}`,\n )}). Nothing here is evidence about what it holds.`,\n ),\n );\n } else if (failedRead) {\n notes.push(\n mustKeep(\n `${report.name}'s search answered and offered a version, and that version could not be read (${quoteForeign(\n `${failedRead.code}: ${failedRead.message}`,\n )}). The failure is in the reading, so nothing here says whether ${report.name} holds this dish.`,\n ),\n );\n } else {\n notes.push(\n mustKeep(\n `${report.name} answered and offered nothing close enough to \"${quoteForeign(args.dish)}\" to compare.`,\n ),\n );\n }\n }\n\n // One of these indexes answers almost any spelling with its closest row, so\n // a dish nobody publishes still comes back as a recipe. Presenting that row\n // as a source's version of the dish states as fact the one thing the search\n // did not establish, and sharing one word of a name is not carrying it:\n // \"biscuits and gravy\" and a tin of Christmas biscuits share the biscuit.\n const weighed = weighTitlesAgainstTheDish(payloads, args.dish, notes);\n const comparable = recipes.filter((recipe) =>\n weighed.some(\n ({ payload, missing }) =>\n missing.length === 0 && payload.id === recipe.id && payload.source === recipe.source,\n ),\n );\n const differences =\n comparable.length === payloads.length\n ? describeDifferences(recipes, payloads, args.sections as readonly Section[])\n : [];\n if (payloads.length > 1 && differences.length === 0 && comparable.length < payloads.length) {\n notes.push(\n mustKeep(\n \"Nothing is set side by side here: not every version's title carries the dish that was \" +\n \"asked for, so what separates them may be that they are different dishes.\",\n ),\n );\n }\n if (payloads.length > 1) {\n notes.push(\n \"Quantities are shown in the units each source published. Nothing was converted between \" +\n \"measuring systems, so a gram in one version and a cup in another are two ways of \" +\n \"writing a recipe rather than one quantity restated.\",\n );\n }\n\n const credit = creditLine(\n payloads.map((payload) => ({ attribution: payload.attribution, url: payload.url })),\n );\n // The differences a reader most needs are the ones about the recipes\n // themselves, and they come first. The rest stay in the structured output\n // rather than taking the room the versions need.\n const shownDifferences = differences.slice(0, TEXT_DIFFERENCE_LINES);\n const moreDifferences = differences.length - shownDifferences.length;\n const differencesBlock =\n differences.length > 0\n ? `What differs:\\n${shownDifferences.map((line) => `- ${line}`).join(\"\\n\")}${\n moreDifferences > 0\n ? `\\n- … and ${moreDifferences} more, in full in the structured output`\n : \"\"\n }\\n\\n`\n : \"\";\n\n // Every version has to fit, so the room left after the differences and the\n // trailer is shared out equally: each version shows an opening rather than\n // the first one filling the block and the rest being cut away.\n const share =\n payloads.length === 0\n ? 0\n : (roomForBody({ notes, credit }) - differencesBlock.length) / payloads.length;\n\n const versionBlocks = payloads.map((payload) => {\n const head = [\n `${quoteForeign(payload.source_name)}: ${quoteForeign(payload.title)}`,\n ` ${quoteForeign(payload.url)}`,\n // The yield is the page's, and the list under it is the one the factor\n // produced. Stating the first alone above the second reads as a list\n // for that many people.\n ` ${renderYield(payload)}`,\n ];\n const listed = payload.ingredients\n .slice(0, TEXT_INGREDIENT_LINES)\n .map((entry) => ` - ${quoteForeign(entry.text)}`);\n const fitted = fitLines(listed, share - head.join(\"\\n\").length);\n\n const lines = [...head, ...fitted.lines];\n const hidden = payload.ingredients.length - fitted.lines.length;\n if (hidden > 0) {\n lines.push(\n ` ${omittedLinesLine(fitted.lines.length, payload.ingredients.length, \"ingredient lines\")}`,\n );\n }\n return lines.join(\"\\n\");\n });\n\n const body =\n payloads.length === 0\n ? nothingWasCompared(unread.size > 0, args.dish)\n : versionBlocks.join(\"\\n\\n\");\n\n // What differs comes first. It is the answer to the question that was\n // asked, and it is short, so it survives a text block cut to fit while\n // several full ingredient lists never could.\n const summary = `${differencesBlock}${body}`;\n\n return ok(\n {\n dish: args.dish,\n versions: payloads,\n differences,\n // Two moments per source, told apart: whether the search answered, and\n // what became of the row it offered.\n per_source: merged.reports.map((report) => {\n const payload = toReportPayload(report);\n if (!opened.has(report.source)) {\n return payload;\n }\n const failedRead = unread.get(report.source);\n return withRead(\n payload,\n failedRead\n ? { status: \"failed\", error: { code: failedRead.code, message: failedRead.message } }\n : { status: \"read\", error: null },\n );\n }),\n notes: noteTexts(notes),\n },\n summary,\n { notes, credit },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n\nexport type { SourceReport } from \"../types.js\";\n","/**\n * Turning one recipe into the payload a tool returns.\n *\n * Both `get_recipe` and `compare_recipes` need the same thing: a yield stated\n * as published and as asked for, an ingredient list scaled to match, and the\n * sentences that qualify both. Sharing it means the two tools cannot drift into\n * describing the same recipe differently.\n */\n\nimport { z } from \"zod\";\nimport { passthroughIngredients, scaleIngredients } from \"../recipe/scale.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport { headingFor } from \"../recipe/sections.js\";\nimport type { PagePart } from \"../recipe/sections.js\";\nimport type { RecipeDetail } from \"../types.js\";\nimport {\n ingredientSchema,\n labelNote,\n mustKeep,\n quoteForeign,\n toIngredientPayload,\n truncate,\n} from \"./shared.js\";\nimport type { Note } from \"./shared.js\";\n\nconst SERVING_WORD = /personne|people|serving|portion|part\\b/i;\n\n/**\n * What the other half of the recipe holds, when this half is being read alone.\n *\n * A page whose ingredients were read and whose method was not is a page that\n * has one, and saying so is what stops a caller concluding the recipe is a list\n * with no procedure.\n */\nfunction whatTheOtherPartHolds(\n part: PagePart,\n recipe: { steps: readonly unknown[]; ingredients: readonly unknown[] },\n): string | null {\n if (part === \"ingredients\") {\n return recipe.steps.length > 0\n ? `${recipe.steps.length} step(s) of method were read from it`\n : null;\n }\n return recipe.ingredients.length > 0\n ? `${recipe.ingredients.length} ingredient line(s) were read from it`\n : null;\n}\n\nexport const yieldSchema = z.object({\n original_count: z\n .number()\n .nullable()\n .describe(\"Servings the page states as a number. Null when it states none.\"),\n original_max: z\n .number()\n .nullable()\n .describe(\"The upper end when the page states a span, as in '4 à 6 personnes'.\"),\n original_text: z\n .string()\n .nullable()\n .describe(\n \"The yield in the page's own words. '4 à 6 personnes' and '4 personnes' are different claims, so the wording is kept.\",\n ),\n requested: z.number().int().nullable().describe(\"Servings asked for, when any were.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"What the yield counts when it counts something other than people, such as 'balls'.\"),\n factor: z\n .number()\n .nullable()\n .describe(\"What the quantities were multiplied by. Null when nothing was rescaled.\"),\n});\n\nexport const ratingSchema = z.object({\n value: z.number(),\n count: z.number().int().nullable().describe(\"How many readers rated it, when the source says.\"),\n max: z\n .number()\n .nullable()\n .describe(\"The top of the scale. A rating read without its scale means nothing.\"),\n});\n\nexport const recipeSchema = z.object({\n id: z.string(),\n source: z.string(),\n source_name: z.string(),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language this recipe is written in.\"),\n title: z.string(),\n url: z.string(),\n image_url: z.string().nullable(),\n yield: yieldSchema,\n ingredients: z.array(ingredientSchema),\n steps: z.array(z.string()),\n prep_minutes: z.number().int().nullable(),\n cook_minutes: z.number().int().nullable(),\n total_minutes: z.number().int().nullable(),\n rest_minutes: z\n .number()\n .int()\n .nullable()\n .describe(\n \"Time the recipe stands, from a source that publishes it apart. It is in no other time here, and belongs to no other source's cooking time. Null where the source publishes none, which is not a dish that needs no rest.\",\n ),\n steps_as_one_block: z\n .boolean()\n .nullable()\n .describe(\n \"Whether the source publishes the method as one block of prose rather than as steps of its own. Null where the source says neither, so a single entry is not read as step one of several.\",\n ),\n withheld: z\n .object({\n parts: z.array(z.string()).describe(\"Which parts, named as 'ingredients' or 'method'.\"),\n why: z.string().describe(\"What the source says about keeping them back.\"),\n })\n .nullable()\n .describe(\n \"A part the source published nothing of because it keeps it for its subscribers. Null from a source that withholds nothing, which is a different statement from an empty list.\",\n ),\n category: z.string().nullable(),\n author: z\n .string()\n .nullable()\n .describe(\"Null on a source written by everyone who edited the page.\"),\n rating: ratingSchema.nullable().describe(\"Null on a source that carries no reader rating.\"),\n nutrition: z\n .record(z.string(), z.string())\n .nullable()\n .describe(\"The panel as published, key by key. Figures the page omits are absent, never zero.\"),\n equipment: z.array(z.string()),\n tips: z.array(z.string()),\n license: z\n .object({ title: z.string(), url: z.string() })\n .nullable()\n .describe(\"Terms the page is published under, when the source states them.\"),\n attribution: z.string().describe(\"Show this, with the url, when repeating anything from here.\"),\n sections_returned: z\n .array(z.string())\n .describe(\"The sections this answer carries. Anything outside this list was not looked at.\"),\n sections_omitted: z\n .array(z.string())\n .describe(\n \"Sections nobody asked for. A field belonging to one of these is empty because it was not requested, never because the page states nothing.\",\n ),\n scaling_summary: z.object({\n scaled_count: z.number().int(),\n rounded_count: z.number().int(),\n unscaled_count: z.number().int(),\n }),\n});\n\nexport type RecipePayload = z.infer<typeof recipeSchema>;\n\n/** The sections a caller can ask for, since a full recipe is a lot of text. */\nexport const SECTIONS = [\n \"ingredients\",\n \"steps\",\n \"times\",\n \"nutrition\",\n \"tips\",\n \"equipment\",\n] as const;\nexport type Section = (typeof SECTIONS)[number];\n\nexport interface RecipeView {\n payload: RecipePayload;\n notes: Note[];\n ingredients: ScaledIngredient[];\n}\n\n/**\n * Work out what to multiply by, and say when nothing can be.\n *\n * A page that states no number of servings cannot be rescaled: dividing by a\n * yield nobody wrote would answer with quantities for a number of people the\n * page never claimed. Such a recipe comes back as published, and the answer\n * says why.\n */\nfunction resolveFactor(\n recipe: RecipeDetail,\n servings: number | null,\n): { factor: number | null; notes: Note[] } {\n if (servings === null) {\n return { factor: null, notes: [] };\n }\n\n // A page whose ingredient list is kept for subscribers states its servings\n // perfectly well, so the sentence about a page that names none would be\n // false. What is missing is the lines, and a factor over nothing is a factor\n // nobody can use.\n const held = withheldPart(recipe, \"ingredients\");\n if (held !== null) {\n return {\n factor: null,\n notes: [\n mustKeep(\n `${held}, so there is nothing here to put to ${servings}. ` +\n `The page states what it serves; read the lines at ${quoteForeign(recipe.url)}.`,\n ),\n ],\n };\n }\n\n if (recipe.yieldCount === null || recipe.yieldCount <= 0) {\n return {\n factor: null,\n notes: [\n mustKeep(\n `${recipe.sourceName} states no number of servings for this recipe, so the quantities are ` +\n \"as published. Use scale_ingredients with a factor you choose if you know what it serves.\",\n ),\n ],\n };\n }\n\n const factor = servings / recipe.yieldCount;\n const notes: Note[] = [];\n\n if (factor === 1) {\n return {\n factor: 1,\n notes: [`The page already yields ${recipe.yieldCount}, so nothing was multiplied.`],\n };\n }\n\n // A page that says \"4 à 6 personnes\" has named a span, and there is no one\n // factor that serves both ends of it. Scaling from the lower end is the\n // choice that leaves nobody short, and saying so is what stops the number\n // being read as the page's own arithmetic.\n if (recipe.yieldMax !== null && recipe.yieldMax > recipe.yieldCount) {\n notes.push(\n `The page states a span rather than a number: ${quoteForeign(recipe.yieldText ?? \"\")}. ` +\n `The factor was taken from the lower end, so ${servings} is what this makes at that end. ` +\n `Taken from ${recipe.yieldMax} it would be ${round(servings / recipe.yieldMax)} instead.`,\n );\n }\n\n const unit = recipe.yieldUnit;\n if (unit && !SERVING_WORD.test(unit)) {\n notes.push(\n `This page states its yield in ${quoteForeign(unit)} rather than in servings, so asking for ` +\n `${servings} multiplied the quantities by ${round(factor)} to give ${servings} ${quoteForeign(unit)}.`,\n );\n }\n\n return { factor, notes };\n}\n\nfunction round(value: number): number {\n return Math.round(value * 1000) / 1000;\n}\n\n/** One part of a recipe, in the words an answer needs to talk about it. */\ninterface PartWords {\n part: PagePart;\n /** A single entry of it, as a sentence names one. */\n entry: string;\n /** The whole of it, as a page publishes it. */\n whole: string;\n /** The reading of an empty one that has to be shut off. */\n caution: string;\n}\n\nconst INGREDIENTS_PART: PartWords = {\n part: \"ingredients\",\n entry: \"ingredient line\",\n whole: \"ingredient list\",\n caution: \"An empty list here is never evidence that an ingredient is absent from the dish.\",\n};\n\nconst METHOD_PART: PartWords = {\n part: \"method\",\n entry: \"step of method\",\n whole: \"method\",\n caution: \"An empty method here is never evidence that the dish is made without one.\",\n};\n\n/**\n * What a part of a recipe that came back empty is allowed to say.\n *\n * Reading nothing off a page is a fact about this server, and stating it as a\n * fact about the page turns a failure of this reader into a claim the page\n * never made. Someone looking for a dish without butter, told that a page\n * publishes no ingredients, concludes there is no butter in it; the page can\n * open on butter.\n *\n * Two things the page itself shows settle it. A heading announcing the part\n * says the page published one. Another part of the recipe that was read says\n * the page is a recipe. Either of them makes an empty result this server's\n * failure, and the answer says so. Where the page shows neither, the answer\n * says the two cannot be told apart from here rather than picking whichever\n * reads more fluently.\n */\n/**\n * What the source says about keeping this part back, when it keeps it back.\n *\n * Null covers the ordinary case, a source that withholds nothing, and the case\n * of a source that withholds a different part of the same recipe.\n */\nfunction withheldPart(recipe: RecipeDetail, part: PagePart): string | null {\n if (recipe.withheld === null || !recipe.withheld.parts.includes(part)) {\n return null;\n }\n return recipe.withheld.why;\n}\n\nfunction emptyPartNote(recipe: RecipeDetail, words: PartWords): Note {\n // The source has said which of the two readings below is the right one, so\n // offering them is a question that has an answer.\n const held = withheldPart(recipe, words.part);\n if (held !== null) {\n return mustKeep(\n `${held}, so no ${words.entry} is in this answer. The page publishes one and this ` +\n \"server did not fail to read it; a reader who has the subscription reads the \" +\n `${words.whole} at ${quoteForeign(recipe.url)}. ${words.caution}`,\n );\n }\n\n const heading = headingFor(words.part, recipe.publishedSections);\n const counterpart = whatTheOtherPartHolds(words.part, recipe);\n\n const shown = [\n heading === null ? null : `the page heads a section \"${quoteForeign(heading)}\"`,\n counterpart,\n ].filter((clause): clause is string => clause !== null);\n\n if (shown.length > 0) {\n return mustKeep(\n `No ${words.entry} was read from this page, yet ${shown.join(\" and \")}. This server failed ` +\n `to read the ${words.whole} the page carries; read it at the url. ${words.caution}`,\n );\n }\n\n // Naming the headings as evidence requires having seen them, and a source\n // that reports none has shown nothing either way.\n const looked = recipe.publishedSections === null ? \"\" : \", which heads no section announcing one\";\n return mustKeep(\n `No ${words.entry} was read from this page${looked}. A page publishing no ${words.whole} and ` +\n \"one written in a layout this server cannot follow look the same from here, so read the url \" +\n `before settling which this is. ${words.caution}`,\n );\n}\n\nexport interface BuildOptions {\n servings: number | null;\n sections: readonly Section[];\n /** Steps to return, since a long method is the bulk of a recipe. */\n maxSteps?: number;\n /** Characters kept per step. */\n maxStepChars?: number;\n /**\n * Names this recipe at the front of each of its notes. An answer carrying\n * several recipes needs to say which one a note is about; an answer carrying\n * one does not, and the name would only repeat what the answer already says.\n */\n label?: string;\n /**\n * Whether the choice of sections is said here. An answer built from several\n * recipes says it once for the call rather than once per recipe.\n */\n announceSections?: boolean;\n}\n\n/**\n * What has to be said about the parts of the recipe themselves.\n *\n * An empty list returned in silence reads as a recipe that needs no ingredients,\n * and an empty method as a dish that needs no cooking. A method published as one\n * paragraph reads as the first step of several unless the answer says otherwise,\n * and only the source knows which it is.\n */\nfunction partNotes(recipe: RecipeDetail, wants: (section: Section) => boolean): Note[] {\n const notes: Note[] = [];\n\n if (wants(\"ingredients\") && recipe.ingredients.length === 0) {\n notes.push(emptyPartNote(recipe, INGREDIENTS_PART));\n }\n if (wants(\"steps\") && recipe.steps.length === 0) {\n notes.push(emptyPartNote(recipe, METHOD_PART));\n }\n if (wants(\"steps\") && recipe.stepsAsOneBlock === true && recipe.steps.length > 0) {\n notes.push(\n mustKeep(\n `${recipe.sourceName} publishes this method as one block of prose rather than as steps ` +\n \"of its own, so what is here is that block and not the first step of several.\",\n ),\n );\n }\n\n return notes;\n}\n\nexport function buildRecipeView(recipe: RecipeDetail, options: BuildOptions): RecipeView {\n const { factor, notes: yieldNotes } = resolveFactor(recipe, options.servings);\n const wants = (section: Section) => options.sections.includes(section);\n\n const ingredients =\n factor === null || factor === 1\n ? passthroughIngredients(recipe.ingredients, recipe.language)\n : scaleIngredients(recipe.ingredients, { factor, language: recipe.language });\n\n const notes: Note[] = [...yieldNotes, ...partNotes(recipe, wants)];\n\n const rounded = ingredients.filter((entry) => entry.scaling === \"rounded\");\n const unscaled = ingredients.filter((entry) => entry.scaling === \"unscaled\");\n\n if (rounded.length > 0) {\n notes.push(\n `${rounded.length} line(s) did not land on the exact product; each says in its own note ` +\n \"what happened to it.\",\n );\n }\n if (unscaled.length > 0 && factor !== null && factor !== 1) {\n notes.push(\n `${unscaled.length} line(s) carry no quantity to multiply and are repeated as published.`,\n );\n }\n if (recipe.license) {\n notes.push(\n mustKeep(\n `Published under ${quoteForeign(recipe.license.title)}: ${quoteForeign(recipe.license.url)}`,\n ),\n );\n }\n\n const omitted = SECTIONS.filter((section) => !wants(section));\n if (options.announceSections !== false && omitted.length > 0) {\n notes.push(\n `Not requested, so not returned: ${omitted.join(\", \")}. Name them in 'sections' to see them.`,\n );\n }\n\n const steps = wants(\"steps\") ? recipe.steps : [];\n const shownSteps = steps\n .slice(0, options.maxSteps ?? steps.length)\n .map((step) => truncate(step, options.maxStepChars ?? step.length));\n if (steps.length > shownSteps.length) {\n notes.push(\n `The method runs to ${steps.length} steps and ${shownSteps.length} are here. Raise ` +\n \"'max_steps' for the rest.\",\n );\n }\n\n const payload: RecipePayload = {\n id: recipe.id,\n source: recipe.source,\n source_name: recipe.sourceName,\n language: recipe.language,\n title: recipe.title,\n url: recipe.url,\n image_url: recipe.imageUrl,\n yield: {\n original_count: recipe.yieldCount,\n original_max: recipe.yieldMax,\n original_text: recipe.yieldText,\n requested: options.servings,\n unit: recipe.yieldUnit,\n factor: factor === null ? null : round(factor),\n },\n ingredients: wants(\"ingredients\") ? ingredients.map(toIngredientPayload) : [],\n steps: shownSteps,\n prep_minutes: wants(\"times\") ? recipe.prepMinutes : null,\n cook_minutes: wants(\"times\") ? recipe.cookMinutes : null,\n total_minutes: wants(\"times\") ? recipe.totalMinutes : null,\n rest_minutes: wants(\"times\") ? recipe.restMinutes : null,\n steps_as_one_block: recipe.stepsAsOneBlock,\n withheld: recipe.withheld,\n category: recipe.category,\n author: recipe.author,\n rating: recipe.rating,\n nutrition: wants(\"nutrition\") ? recipe.nutrition : null,\n equipment: wants(\"equipment\") ? recipe.equipment : [],\n tips: wants(\"tips\") ? recipe.tips : [],\n license: recipe.license,\n attribution: recipe.attribution,\n sections_returned: [...options.sections],\n sections_omitted: omitted,\n scaling_summary: {\n scaled_count: ingredients.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: rounded.length,\n unscaled_count: unscaled.length,\n },\n };\n\n if (options.label) {\n const label = quoteForeign(options.label);\n return { payload, notes: notes.map((line) => labelNote(label, line)), ingredients };\n }\n return { payload, notes, ingredients };\n}\n\n/** The yield as a reader would say it, with what was asked for beside it. */\nexport function renderYield(payload: RecipePayload): string {\n const published = payload.yield.original_text ?? \"an amount the page does not state\";\n if (payload.yield.factor === null) {\n return `Yields ${quoteForeign(published)} (as published).`;\n }\n return `Yields ${quoteForeign(published)} as published, scaled by ${payload.yield.factor} for ${payload.yield.requested}.`;\n}\n","/**\n * Which language an ingredient line is written in.\n *\n * A list can hold French lines and English lines at once, because a cook\n * comparing two versions of a dish keeps both in front of them. The language\n * decides how a number is written, how a noun agrees with it, and which\n * vocabulary of measures applies, so it is settled per line rather than once\n * for a whole list.\n */\n\nexport type Language = \"fr\" | \"en\";\n\n/** What a caller may ask for: a language, or a reading taken per line. */\nexport type LanguageChoice = Language | \"auto\";\n\n/**\n * Structural words, which are the reliable signal.\n *\n * A line names its ingredient in whatever language the page is written in, and\n * an ingredient name travels: a French recipe asks for \"Mountain Dew\" and an\n * English one for \"crème fraîche\". The small words that hold a line together do\n * not travel, so \"de\", \"à\" and \"of\" say more about the line than the name of\n * the food does.\n */\nconst FRENCH_WORDS =\n /(?:^|[\\s(])(?:de|du|des|à|au|aux|la|le|les|un|une|quelques|dans|avec|sans|pour|selon|environ)(?=[\\s),.]|$)|\\bd'|\\bl'|\\bqu'/iu;\n\n/**\n * The bare article \"a\" is left out on purpose. French recipes abbreviate\n * \"cuillère à soupe\" as \"c a s\", dropping the accent, and counting that \"a\" as\n * English evidence would send a French line through the English vocabulary.\n */\nconst ENGLISH_WORDS =\n /(?:^|[\\s(])(?:of|an|the|and|with|without|for|into|about|each|per|plus|freshly|finely|roughly|coarsely|optional)(?=[\\s),.]|$)/iu;\n\n/** Letters English does not write, which no ingredient name borrows either. */\nconst FRENCH_LETTERS = /[àâäçéèêëîïôöùûüœ]/iu;\n\n/**\n * Foods each language names its own way.\n *\n * Weak evidence, and deliberately so: a name is the part of a line most likely\n * to be borrowed, and a structural word outranks any number of these. It is\n * enough to settle a line that carries nothing else, such as \"6 oeufs\", where\n * every other signal is silent.\n */\nconst FRENCH_FOODS =\n /\\b(?:oeufs?|œufs?|sel|poivre|sucres?|farine|beurre|lait|crème|creme|huile|ail|oignons?|fromage|pain|poulet|boeuf|bœuf|riz|pâtes|pates|jambon|citrons?|pommes?|vin|eau|persil|thym|miel|jaunes?|blancs?)\\b/iu;\n\nconst ENGLISH_FOODS =\n /\\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|garlic|onions?|cheese|bread|chicken|beef|rice|pasta|ham|lemons?|apples?|wine|water|parsley|thyme|honey|yolks?|whites?)\\b/iu;\n\n/** Weight of one structural signal, which outranks a vocabulary match. */\nconst STRUCTURAL = 3;\n/** Weight of a measure only one of the two vocabularies carries. */\nconst VOCABULARY = 2;\n/** Weight of a food name, which is the signal a line borrows most readily. */\nconst NAME = 1;\n\nexport interface LanguageEvidence {\n language: Language;\n french: number;\n english: number;\n}\n\n/**\n * Read the language off one line.\n *\n * `frenchUnit` and `englishUnit` say whether each vocabulary recognised a\n * measure in the line. They count for less than a structural word, because a\n * French line can name an English measure in passing: \"1 dose (cup) de Mountain\n * Dew\" is French with an English gloss inside it, and the \"de\" is what settles\n * it.\n *\n * English is the answer when nothing points either way, which is what an\n * ingredient with no small words and no measure gets.\n */\nexport function readLanguage(\n line: string,\n evidence: { frenchUnit: boolean; englishUnit: boolean } = {\n frenchUnit: false,\n englishUnit: false,\n },\n): LanguageEvidence {\n let french = 0;\n let english = 0;\n\n if (FRENCH_WORDS.test(line)) {\n french += STRUCTURAL;\n }\n if (FRENCH_LETTERS.test(line)) {\n french += STRUCTURAL;\n }\n if (ENGLISH_WORDS.test(line)) {\n english += STRUCTURAL;\n }\n if (evidence.frenchUnit) {\n french += VOCABULARY;\n }\n if (evidence.englishUnit) {\n english += VOCABULARY;\n }\n if (FRENCH_FOODS.test(line)) {\n french += NAME;\n }\n if (ENGLISH_FOODS.test(line)) {\n english += NAME;\n }\n\n return { language: french > english ? \"fr\" : \"en\", french, english };\n}\n\n/** The language a list is written in, when a caller wants one answer for all of it. */\nexport function readListLanguage(lines: string[]): Language {\n let french = 0;\n let english = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n }\n return french > english ? \"fr\" : \"en\";\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst FUL_MEASURE = /^[a-z]{3,}fuls?$/;\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst HOUSEHOLD_MEASURE =\n /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in both languages: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French line and an English line alike,\n * and they take no plural mark in either.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n // A page glossing a metric weight names the pound, which French writes as a\n // livre: \"450 g (1 livre) de spaghetti\".\n livre: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n // A dish of the book standing as an ingredient of another: \"1 recipe Flaky\n // Pie Crust\". The count is what the word measures, so the plural mark belongs\n // on it and not on the name of the dish.\n recipe: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n recipes: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return (\n text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n};\n\nexport function unitKeys(language: Language): string[] {\n return UNIT_KEYS[language];\n}\n\nexport function lookupUnit(text: string, language: Language): UnitInfo | null {\n return VOCABULARY[language][normalizeUnitKey(text)] ?? null;\n}\n\n/**\n * Matches a number followed by a measure anywhere in a piece of text.\n *\n * Used to spot a quantity the parser did not take, such as the second half of\n * \"1 Tbsp vanilla sugar OR 1 tsp vanilla extract\", which would otherwise sit in\n * a scaled line still saying what the original said.\n */\nconst EMBEDDED: Record<Language, RegExp> = {\n fr: buildEmbedded(\"fr\"),\n en: buildEmbedded(\"en\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // A quantity keeps the same shape whether the measure stands against the\n // figure or behind the word introducing it: \"3/4 tasse\" and \"3/4 de tasse\"\n // are one quantity written twice. Reading only the first spelling leaves the\n // second sitting on a scaled line, still saying what the page said.\n const joiner = \"(?:(?:de|du|des|d|of|a|an|the)\\\\s+)?\";\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*${joiner}(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\n/**\n * Nouns naming what a food is sold in.\n *\n * A measure written in front of one of them gives what that container holds:\n * \"12 oz can\" is the size of the tin rather than an amount to weigh out, and\n * doubling the recipe means opening a second tin rather than finding a bigger\n * one.\n *\n * Written in either language, because what a word names has nothing to do with\n * which language names it.\n */\nexport const CONTAINER_NOUN =\n /^(?:cans?|tins?|jars?|packets?|packages?|packs?|box|boxes|bottles?|cartons?|bags?|tubs?|sachets?|blocks?|blocs?|boites?|pots?|briques?|bocaux|bocals?|barquettes?)\\b/i;\n\n/**\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a French line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(normalizeUnitKey(text));\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a French measure named with a container or a gesture the vocabulary has\n * no entry for.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it: a bouchon, a poignée, a ramequin hold what they hold, and the recipe's\n * proportion lives in how many are asked for. French marks that grammatically,\n * by placing the noun between the amount and the partitive that introduces the\n * thing measured: \"un bouchon de rhum\", \"2 bouquets de persil\". A noun in that\n * position measures whatever follows it, so a container nobody thought to list\n * is read by the same rule as the ones that are, and the vocabulary only has to\n * carry the words whose plural or spelling the rule would get wrong.\n *\n * The amount has to come first. A line opening on the noun, as in \"beurre\n * pommade\", carries no quantity, and inventing one from the grammar would put a\n * number where the recipe wrote none.\n */\nexport function readPartitiveMeasure(text: string): { unit: UnitInfo; rest: string } | null {\n const match = HEAD_BEFORE_DE.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n if (word.length < 3) {\n return null;\n }\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {\n return null;\n }\n if (lookupUnit(word, \"fr\")) {\n return null;\n }\n\n const canonical = frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!FUL_MEASURE.test(key)) {\n return null;\n }\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (EAUX_ENDING.test(word)) {\n return word.slice(0, -1);\n }\n if (AUX_ENDING.test(word)) {\n return `${word.slice(0, -3)}al`;\n }\n if (VOWEL_THEN_S.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/** The plural French writes for a noun, or the noun itself when it takes no mark. */\nfunction frenchPlural(word: string): string {\n if (SIBILANT_LETTER.test(word)) {\n return word;\n }\n if (EAU_ENDING.test(word)) {\n return `${word}x`;\n }\n if (AL_ENDING.test(word)) {\n return `${word.slice(0, -2)}aux`;\n }\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n};\n\n/** The everyday equivalence for an approximate measure, when there is a settled one. */\nexport function approximateEquivalent(unit: UnitInfo): string | null {\n return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;\n}\n\n/**\n * Ladders, used to keep a scaled amount at a human size.\n *\n * Multiplying a recipe by thirty is arithmetically fine and practically poor:\n * \"8335 g of sugar\" is correct, and nobody weighs eight thousand grams. Each\n * measured unit therefore knows the unit above and below it, so a large amount\n * climbs the ladder and a small one comes back down. Each system keeps its own\n * ladder, because converting between them changes what the recipe said.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n /** Which vocabulary the step's spelling comes from. */\n language: Language;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"kg\", per: 1000, language: \"en\" },\n ml: { to: \"l\", per: 1000, language: \"en\" },\n cl: { to: \"l\", per: 100, language: \"en\" },\n dl: { to: \"l\", per: 10, language: \"en\" },\n oz: { to: \"lb\", per: 16, language: \"en\" },\n ounce: { to: \"pound\", per: 16, language: \"en\" },\n \"fl oz\": { to: \"pint\", per: 20, language: \"en\" },\n pint: { to: \"quart\", per: 2, language: \"en\" },\n quart: { to: \"gallon\", per: 4, language: \"en\" },\n};\n\nconst DEMOTIONS: Record<string, UnitStep> = {\n // Spoons and cups hold a fixed volume, so a share of one is stated in the\n // smaller spoon rather than as a fraction no measuring set carries. The\n // spelling of the step matches the spelling of the unit it comes from, so a\n // line written in abbreviations stays in abbreviations, and a French line\n // stays in French.\n cup: { to: \"tablespoon\", per: 16, language: \"en\" },\n tablespoon: { to: \"teaspoon\", per: 3, language: \"en\" },\n tbsp: { to: \"tsp\", per: 3, language: \"en\" },\n \"cuillere a soupe\": { to: \"cuillere a cafe\", per: 3, language: \"fr\" },\n tasse: { to: \"cuillere a soupe\", per: 16, language: \"fr\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A gesture keeps its own answer. A pincée, a poignée, a pinch is the amount a\n * hand produces in one go, and there is no half of a hand: the size of one is\n * the cook's and the count is the whole of what the measure can say, so the\n * count lands on a whole and the line reports that it moved.\n *\n * A doubtful word takes the half. A cook reading \"1/2 sachet\" knows what to do\n * with it, so a measure answers `half` apart from the few named below, and the\n * one thing that does not divide is decided from the item's own name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"approximate\") {\n return \"whole\";\n }\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A tranche is already cut off something larger, and the board that produced\n * one takes a corner off it in the same gesture: a quarter of a tranche de pain\n * is a crouton.\n *\n * Each measure is listed in either language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n *\n * Both directions ask whether the unit can hold the figure. A kitchen reads two\n * decimals and no more, so 2468 g written in kilos is 2.47 and the eight grams\n * are gone. A quantity the bigger unit cannot state stays where the page wrote\n * it, and a quantity the page's own unit cannot state walks down to the one that\n * can, so the same mass comes out the same however the page spelled it: \"1234 g\"\n * and \"1 kg 234\" are one quantity and answer with one figure.\n */\nexport function chooseReadableUnit(unit: UnitInfo, amount: number): ChosenUnit {\n if (unit.kind !== \"measured\" || !Number.isFinite(amount) || amount <= 0) {\n return { unit, ratio: 1 };\n }\n\n let current = unit;\n let ratio = 1;\n\n while (amount * ratio < 1) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per && writesExactly((amount * ratio) / up.per)) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n while (!writesExactly(amount * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/** Whether a figure survives being written with the two decimals a kitchen reads. */\nfunction writesExactly(value: number): boolean {\n return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) {\n return unit.canonical;\n }\n return unit.plural ?? `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice, LanguageEvidence } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n CONTAINER_NOUN,\n demoteUnit,\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nconst BRACKETED_TAIL = /\\s*(\\([^()]*\\))\\s*$/;\nconst LEADING_ARTICLE = /^(?:of\\s+)?an?\\s+/i;\nconst LEADING_ARTICLE_FRENCH = /^(?:une|un)\\s+/i;\nconst LEADING_INDEFINITE_WITH_SPACE = /^an?\\s+/i;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst OF_OPENING = /^of\\s+/i;\nconst OPENING_ARTICLE_FRENCH = /^\\s*(un|une)\\b\\s*/i;\nconst REPEATED_COMMA_GROUPS = /^\\s*\\d+,\\d+,\\d/;\nconst SLASH_OUTSIDE_A_FRACTION = /(?<!\\d)\\/|\\/(?!\\d)/;\n\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst DIGIT = /\\d/;\nconst ENGLISH_RANGE_JOINER = /^\\s+(to|or)\\s+/i;\nconst FRACTION_WORD =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i;\nconst FRENCH_RANGE_JOINER = /^\\s+(à|a|ou)\\s+/i;\nconst GROUPED_DECIMAL = /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/;\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_DASH = /^\\s*(–|—|-)\\s*/;\nconst LEADING_INDEFINITE = /^an?\\s/i;\nconst LEADING_OF = /^\\s*of\\s+/i;\nconst LEADING_QUANTITY = /^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst PARTITIVE_BEFORE_BRACKET = /^(?:de\\s+|du\\s+|des\\s+|d'|of\\s+)(?=\\()/i;\nconst PARTITIVE_OPENING = /^(?:de\\s|d'|du\\s|des\\s|,)/i;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst TRAILING_S = /s$/;\nconst VAGUE_ARTICLE = /^(?:un|une|quelques|of\\s+an?|an?)\\s+/iu;\nconst WHITESPACE = /\\s+/;\n\n/**\n * The measure the line leads with: the one standing straight after the amount,\n * or the one a size word stands in front of.\n */\nfunction leadingMeasure<T extends { unit: unknown }>(direct: T, behind: T | null): T {\n if (direct.unit) {\n return direct;\n }\n return behind?.unit ? behind : direct;\n}\n\n/**\n * How a line writes the equivalents beside its measure, when it writes any.\n *\n * Three shapes and three renderings: a slash beside the amount, a trailing\n * restatement, or a bracketed group. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(\n count: number,\n slashed: boolean,\n trailing: boolean,\n): \"slash\" | \"trailing\" | \"bracket\" | null {\n if (count === 0) {\n return null;\n }\n if (slashed) {\n return \"slash\";\n }\n if (trailing) {\n return \"trailing\";\n }\n return \"bracket\";\n}\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Named HTML entities a page writes where the character itself would do.\n *\n * The fractions are the ones that matter: a line reading \"3&frac12; cups\" holds\n * three and a half cups, and a reader that does not decode it sees three and\n * carries the rest into the item name, where doubling the line loses half a cup\n * without saying so.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n frac12: \"\\u00bd\",\n frac13: \"\\u2153\",\n frac23: \"\\u2154\",\n frac14: \"\\u00bc\",\n frac34: \"\\u00be\",\n frac15: \"\\u2155\",\n frac18: \"\\u215b\",\n nbsp: \" \",\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n deg: \"\\u00b0\",\n times: \"\\u00d7\",\n minus: \"\\u2212\",\n ndash: \"\\u2013\",\n mdash: \"\\u2014\",\n};\n\n/**\n * Turn HTML entities back into the characters they stand for.\n *\n * A numeric entity names a code point directly, and the fraction glyphs a\n * recipe uses live well inside the range a page can write that way, so\n * \"&#8532;\" is two thirds and reads as such once decoded.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]+);/gi, (whole, hex: string) =>\n codePoint(Number.parseInt(hex, 16), whole),\n )\n .replace(/&#(\\d+);/g, (whole, digits: string) => codePoint(Number(digits), whole))\n .replace(\n /&([a-z][a-z0-9]*);/gi,\n (whole, name: string) => NAMED_ENTITIES[name.toLowerCase()] ?? whole,\n );\n}\n\n/** A code point a page named, or the entity as published when it names none. */\nfunction codePoint(value: number, published: string): string {\n if (!Number.isInteger(value) || value < 32 || value > 0x10ffff) {\n return published;\n }\n try {\n return String.fromCodePoint(value);\n } catch {\n return published;\n }\n}\n\n/**\n * Empty brackets, which a page leaves behind when it carried a conversion the\n * source did not render. They say nothing and belong in no answer.\n */\nconst EMPTY_BRACKETS = /\\(\\s*\\)/g;\n\n/**\n * Prepare a published line for reading: entities decoded, empty brackets\n * dropped, runs of spaces squeezed.\n */\nfunction readable(text: string): string {\n return decodeEntities(text).replace(EMPTY_BRACKETS, \" \").replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Signs and words a page puts before a number to say it is not exact.\n *\n * The number behind one of them is still a number, and reading none at all\n * hands back the line with a note saying it carries no quantity, which is\n * untrue. It is read, scaled and given back with the mark the page put on it,\n * so the answer stays as loose as the page was.\n */\nconst APPROXIMATION_PREFIX: Record<Language, RegExp> = {\n en: /^(?:~|\\u2248|about|approx\\.?|approximately|around|roughly)\\s*/i,\n fr: /^(?:~|\\u2248|environ|approximativement|\\u00e0 peu pr\\u00e8s|a peu pres)\\s*/i,\n};\n\n/**\n * Words that introduce what a container holds.\n *\n * \"1 pot de 500 g de miel\" counts pots and says how much honey is in one, so\n * the count is the recipe's to multiply. The partitive standing after the\n * measure is what marks the line as a container and its contents.\n */\nconst CONTAINER_CONTENTS = /^\\s*(?:de\\s|d'|du\\s|des\\s)/i;\n\n/**\n * Whether the measure standing behind an item gives the size of one of them,\n * as in \"1 dinde de 3 kg\".\n *\n * The count and the measure answer different questions: how many birds, and how\n * heavy one bird is. A cook serving half again as many people takes a heavier\n * bird, so the count belongs to the page rather than to the factor.\n *\n * Read on the item alone, which is what the line counts once its own measure\n * has been taken off it, so \"450 g (1 livre) de spaghetti\" never reaches here:\n * that line counts grams, and its bracket restates the same quantity.\n */\nfunction statesItemSize(item: string): boolean {\n const attached = ATTACHED_DE.exec(item);\n if (!attached) {\n return false;\n }\n\n const named = item.slice(0, attached.index).trim();\n if (!named || DIGIT.test(named)) {\n return false;\n }\n\n return isStatedSize(item.slice(attached.index + attached[0].length));\n}\n\n/** A mass or a volume standing on its own, with nothing it is the amount of. */\nfunction isStatedSize(text: string): boolean {\n const size = parseLeadingQuantity(text, \"fr\");\n if (!size) {\n return false;\n }\n\n const measure = takeUnit(text.slice(size.length).trimStart(), \"fr\");\n if (measure.unit?.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\n\n/**\n * A second measure a line writes straight after the first, both of them naming\n * one quantity: \"1 lb 4 oz beef\", \"2 kg 300 g de farine\", and the French\n * \"1 kg 500\", which leaves the smaller unit for the reader to supply.\n *\n * What joins the two members is the ladder: the second is written in the unit\n * the first steps down to, and it holds less than one of the first. A measure\n * from another ladder restates the quantity instead of continuing it, and a\n * number followed by a food names a second ingredient; neither is folded in\n * here.\n *\n * The share the second member adds is returned rather than the number itself,\n * so the quantity comes out as one amount in the unit the line opened with:\n * 1 lb 4 oz is 1.25 lb, and doubling it gives the 2.5 lb a butcher weighs.\n * Reading only the first member leaves the rest of the quantity sitting in the\n * item name, where doubling the line loses it.\n *\n * The two members can be joined by a sign or a word saying they add up, as in\n * \"2 c. à s. + 1 c. à c.\". The joiner is read wherever the same ladder relation\n * holds, since it states outright what juxtaposition leaves implicit.\n */\nfunction takeCompoundMember(\n text: string,\n unit: UnitInfo | null,\n language: Language,\n): { adds: number; rest: string } | null {\n if (!unit) {\n return null;\n }\n const step = demoteUnit(unit);\n if (!step) {\n return null;\n }\n\n const joiner = ADDS_UP.exec(text);\n const behindTheJoiner = joiner ? text.slice(joiner[0].length) : text;\n\n const second = parseLeadingQuantity(behindTheJoiner, language);\n if (!second || second.amount <= 0 || second.amount >= step.per) {\n return null;\n }\n\n const after = behindTheJoiner.slice(second.length);\n const measure = takeUnit(after.trimStart(), language);\n if (measure.unit) {\n if (measure.unit.canonical !== step.unit.canonical) {\n return null;\n }\n return { adds: second.amount / step.per, rest: measure.rest.trimStart() };\n }\n\n // The unwritten form: \"1 kg 500\" is a kilo and five hundred grams. It is read\n // only where a mass or a volume opened the line, and only where nothing but\n // the partitive follows the figure, so \"1 cup 2 eggs\" stays two eggs.\n if (unit.kind !== \"measured\") {\n return null;\n }\n if (!Number.isInteger(second.amount)) {\n return null;\n }\n const trailing = after.trimStart();\n if (trailing !== \"\" && !PARTITIVE_OPENING.test(trailing)) {\n return null;\n }\n\n return { adds: second.amount / step.per, rest: trailing };\n}\n\n/** How a line joins two measures that add up to one quantity. */\nconst ADDS_UP = /^\\s*(?:\\+|et|and|plus)\\s+/i;\n\n/**\n * A mark a page opens an ingredient line with, in front of the quantity.\n *\n * A bullet, a dash or a picture of the food is decoration: it says nothing\n * about how much, and a reader stopping at it answers that the line carries no\n * quantity, so a doubled recipe keeps the published amount of whatever the mark\n * stood in front of. It is set aside for reading and put back for writing.\n *\n * The signs that say an amount is loose are excluded, because those do carry a\n * claim about the figure behind them, and the mark has to be followed by space\n * so that a bracket or a decimal point opening a line is left where it is.\n */\nconst LEADING_DECORATION = /^(?:(?!~|≈|\\()[^\\p{L}\\p{N}\\s])+(?=\\s)/u;\n\n/** The mark a line opens with, or null when it opens on the quantity. */\nfunction takeDecoration(text: string): string | null {\n const match = LEADING_DECORATION.exec(text);\n return match ? match[0] : null;\n}\n\n/** Whether a word names something a food is sold in. */\nfunction namesContainer(word: string | undefined): boolean {\n return word !== undefined && CONTAINER_NOUN.test(normalizeUnitKey(word));\n}\n\n/**\n * Measures of time, in either language.\n *\n * An ingredient list carries lines that state a length rather than an amount:\n * a rest, a proof, a marinade, a bake. The factor says how much of the dish to\n * make, and how long a dough takes to rise is no part of that.\n */\nconst TIME_UNIT: Record<Language, RegExp> = {\n en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\\b/i,\n fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\\b/i,\n};\n\n/**\n * The letters a language glues to a figure to make it a rank: the \"er\" of\n * \"1er choix\", the \"e\" of \"2e couche\", the \"st\" of \"1st choice\".\n *\n * A rank names a position rather than an amount, so the line carries nothing to\n * multiply. The letters have to sit against the figure; a line that puts a\n * space between them wrote a number and then a word.\n */\nconst ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\\b/i;\n\n/**\n * A line that states its amount for one eater.\n *\n * The factor already says how many people the recipe is being made for, so\n * multiplying an amount that is per person applies it twice and asks for twice\n * as much on every plate.\n */\nconst PER_PERSON: Record<Language, RegExp> = {\n en: /\\bper\\s+(?:person|head|serving|guest|diner)\\b/i,\n fr: /\\bpar\\s+(?:personne|convive|part|t\\u00eate)\\b/i,\n};\n\n/**\n * Size and preparation words a recipe puts between the number and the measure.\n *\n * \"1 small handful\" counts handfuls and says how full one was. Reading the\n * adjective as the thing being counted loses the measure, and with it the fact\n * that a handful is held to no better than the hand: the line comes back as an\n * exact count of something the page never named.\n */\nconst MEASURE_ADJECTIVES: Record<Language, Set<string>> = {\n en: new Set([\n \"big\",\n \"generous\",\n \"good\",\n \"heaped\",\n \"heaping\",\n \"large\",\n \"level\",\n \"medium\",\n \"scant\",\n \"small\",\n ]),\n fr: new Set([\n \"beau\",\n \"belle\",\n \"bon\",\n \"bonne\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"petit\",\n \"petite\",\n ]),\n};\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(\n text: string,\n language: Language,\n): { adjective: string | null; rest: string } {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return { adjective: null, rest: text };\n }\n\n const [adjective = \"\"] = match.slice(1);\n const folded = normalizeUnitKey(adjective);\n // The word can be written in the plural where the count is, as in \"2 grosses\n // cuillères\", and the list carries the singular.\n const listed =\n MEASURE_ADJECTIVES[language].has(folded) ||\n MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, \"\"));\n if (!listed) {\n return { adjective: null, rest: text };\n }\n\n return { adjective, rest: text.slice(match[0].length) };\n}\n\n/**\n * A comma neither reading can account for.\n *\n * English groups thousands in threes and marks the decimal with a point;\n * French marks the decimal with a comma and never groups with one. A comma\n * followed by anything other than three digits is therefore not an English\n * number, and a second comma group is not a French one. Where the line gives no\n * sign which language it is in, \"1,500\" is fifteen hundred under one reading\n * and one and a half under the other, and choosing wrong is wrong by a factor\n * of a thousand. Neither is safe, so the line goes back as published and says\n * why.\n */\nconst COMMA_GROUPED = /^\\s*\\d{1,3}(?:,\\d{3})+(?!\\d)/;\nconst COMMA_DECIMAL = /^\\s*\\d+,\\d+/;\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2] ?? \"\"] ?? Number.NaN;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) {\n return null;\n }\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (language === \"fr\" ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);\n if (decimal) {\n const [written = \"\"] = decimal.slice(1);\n const amount = Number(\n language === \"fr\" ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n if (language === \"en\") {\n const written = parseWrittenFraction(trimmed);\n if (written) {\n return { amount: written.amount, length: offset + written.length };\n }\n }\n\n return null;\n}\n\n/** How many of the part a line names: \"two thirds\", \"a quarter\", \"half\". */\nconst WRITTEN_NUMERATORS: Record<string, number> = { a: 1, an: 1, one: 1, two: 2, three: 3 };\n\n/** What the part is a part of. */\nconst WRITTEN_DENOMINATORS: Record<string, number> = {\n half: 2,\n halves: 2,\n third: 3,\n thirds: 3,\n quarter: 4,\n quarters: 4,\n fourth: 4,\n fourths: 4,\n};\n\n/**\n * Read a fraction an English line spells out, as in \"half a bottle\" or \"two\n * thirds of a cup\".\n *\n * Recipes write the small fractions in words as readily as in figures, and a\n * line opening on one of them carries an amount like any other. The numerator\n * is optional because \"half\" alone is the common form.\n *\n * What follows decides whether the words are a quantity at all. A share of one\n * thing, \"half a lemon\" or \"two thirds of a cup\", states an amount of its own.\n * A share of a definite thing, \"half of the dough\", points back at an amount\n * stated elsewhere, and multiplying it would answer with a number that belongs\n * to another line.\n */\nfunction parseWrittenFraction(text: string): ParsedQuantity | null {\n const match = FRACTION_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[(match[2] ?? \"\").toLowerCase()];\n if (!(numerator && denominator)) {\n return null;\n }\n\n const rest = text.slice(match[0].length).replace(LEADING_OF, \"\").trimStart();\n if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, \"en\").unit)) {\n return null;\n }\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = language === \"fr\" ? FRENCH_RANGE_JOINER : ENGLISH_RANGE_JOINER;\n const separator = written.exec(after) ?? LEADING_DASH.exec(after);\n if (!separator) {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) {\n return null;\n }\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1] ?? \"\",\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\n/**\n * Why a line that shows a figure is still not the factor's to multiply.\n *\n * Each of these is a reading the parser can make and a scaling it must not do,\n * and they are kept apart from a line with no figure at all so the answer can\n * say which of the two it is looking at.\n */\nexport type HeldBack =\n /** \"4 to 5-pound roast\": the figures give the size of one, not how many. */\n | \"sizeQualifier\"\n /** \"1 dinde de 3 kg\": the measure behind the item weighs one of them. */\n | \"itemSize\"\n /** \"12 oz can tomatoes\": the measure gives what the tin holds, and no count is written. */\n | \"containerSize\"\n /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"2 h de repos\": the figure measures a length of time rather than an amount. */\n | \"duration\"\n /** \"1,500 g\" with nothing to say whether the comma groups or divides. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n /**\n * Why the figure on this line must not be multiplied, when there is such a\n * reason. Null for the ordinary line, whose amount is the factor's to scale.\n */\n heldBack: HeldBack | null;\n /**\n * The mark the page opened the line with, in front of the quantity, such as a\n * bullet or a picture of the food. It goes back where the page had it.\n */\n decoration: string | null;\n /**\n * The sign or word the page put before the amount to say it is loose, as in\n * the \"~\" of \"~1 cup water\". Null when the page stated the amount plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"small\"\n * of \"1 small handful\". It goes back in front of the measure so the answer\n * reads the way the page did.\n */\n measureAdjective: string | null;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets beside the amount, as\n * in \"450 g (1 pound)\", after a slash, as in \"500 g / 1.1 lb\", or in a\n * bracket closing the line, as in \"1 cup milk (240 ml)\". The rewrite puts\n * them back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | \"trailing\" | null;\n /**\n * The word the bracket opened on, as in the \"soit\" of \"(soit 3/4 de tasse)\".\n * Null when the line stated the equivalent without one.\n */\n alternateIntro: string | null;\n /**\n * A bracket giving what one container holds, as in \"1 (14 oz) can\", exactly\n * as the page wrote it. The count is the factor's to multiply and this figure\n * is not, so it goes back untouched.\n */\n capacity: string | null;\n /** What the amount and the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/**\n * Articles a French line writes where a digit would go, and what they count as.\n *\n * \"un\" and \"une\" are the number one written out, so a line using one has stated\n * a count. A word that says merely that there are several, \"quelques\" as much\n * as \"plusieurs\" or the English \"a few\", has stated none: any figure put behind\n * it is this server's reading rather than the page's quantity, and a reading\n * multiplied is a number nobody wrote.\n */\nconst FRENCH_ARTICLES: Record<string, number> = { un: 1, une: 1 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key, language);\n if (!unit) {\n continue;\n }\n const rest = afterKey(text, key);\n if (rest === null) {\n continue;\n }\n return { unit, rest };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(WHITESPACE);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) {\n return { unit: load, rest: words.slice(1).join(\" \") };\n }\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * What stands after the measure a key names, or null when the key does not line\n * up with the words the line is written in.\n *\n * How many words a key spells and how many words the line spends on it are\n * different numbers: normalising turns the abbreviation \"c.à.s\" into the three\n * words \"c a s\", and the line writes it as one. Consuming the key's own word\n * count would take two words of the ingredient with it, so the words the line\n * wrote are consumed one at a time until they normalise to the key.\n */\nfunction afterKey(text: string, key: string): string | null {\n const words = text.trim().split(WHITESPACE);\n for (let count = 1; count <= words.length; count += 1) {\n if (normalizeUnitKey(words.slice(0, count).join(\" \")) !== key) {\n continue;\n }\n return words.slice(count).join(\" \");\n }\n return null;\n}\n\n/**\n * A measure inside brackets can be stated in the other language's vocabulary: a\n * French page glosses grams in ounces, and an English page glosses cups in\n * millilitres. The line's own language is tried first, so a word both\n * vocabularies carry keeps the spelling and the plural of the line it sits in.\n */\nfunction takeUnitEitherLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) {\n return first;\n }\n return takeUnit(text, language === \"fr\" ? \"en\" : \"fr\");\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n return readEvidence(line).language;\n}\n\n/** The language a line reads as, with the weight each side gathered. */\nfunction readEvidence(line: string): LanguageEvidence {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(LEADING_QUANTITY, \"\").trimStart();\n const afterArticle = afterFigures.replace(VAGUE_ARTICLE, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit });\n}\n\n/**\n * The measure standing after the amount, and the size word in front of it.\n *\n * The adjective is only an adjective when a measure stands behind it. In\n * \"1 cleaned leek green\" the words that follow name the food itself, and taking\n * one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n rest: string,\n language: Language,\n fromArticle: boolean,\n): {\n leading: ReturnType<typeof takeLeadingUnit>;\n described: { adjective: string | null; rest: string };\n behind: ReturnType<typeof takeLeadingUnit> | null;\n} {\n const direct = takeLeadingUnit(rest, language, fromArticle);\n const described = direct.unit ? { adjective: null, rest } : takeMeasureAdjective(rest, language);\n const behind = described.adjective\n ? takeLeadingUnit(described.rest, language, fromArticle)\n : null;\n\n return { leading: leadingMeasure(direct, behind), described, behind };\n}\n\n/**\n * Whether the figure the line opens with counts anything, and what it is\n * instead when it does not.\n *\n * Three ways a figure is not a quantity: it is joined to a word by a hyphen and\n * describes one thing, it names a rank in an order, or it measures a length of\n * time, which belongs to the method rather than to the proportions. `false`\n * means the figure counts.\n */\nfunction whatTheFigureIsNot(\n behindFigure: string,\n couldBeARank: boolean,\n language: Language,\n): HeldBack | null | false {\n if (HYPHENATED_TAIL.test(behindFigure)) {\n return \"sizeQualifier\";\n }\n if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {\n return null;\n }\n if (TIME_UNIT[language].test(behindFigure.trimStart())) {\n return \"duration\";\n }\n return false;\n}\n\n/**\n * The equivalents a line writes beside its measure, in whichever of the three\n * shapes it wrote them.\n *\n * A bracketed group, a slash beside the amount, or a restatement trailing the\n * item: only one of them is read, because a line offering two would be stating\n * the same quantity three times.\n */\nfunction takeEquivalents(\n text: string,\n language: Language,\n): {\n rest: string;\n bracketed: ReturnType<typeof takeAlternates>;\n slashed: ReturnType<typeof takeSlashAlternates>;\n item: string;\n trailing: ReturnType<typeof takeTrailingAlternates>;\n group:\n | ReturnType<typeof takeTrailingAlternates>\n | Omit<ReturnType<typeof takeAlternates>, \"rest\">\n | null;\n} {\n const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text);\n const bracketed = takeAlternates(\n introducedBracket ? text.slice(introducedBracket[0].length) : text,\n language,\n );\n let rest = bracketed.measures.length > 0 ? bracketed.rest : text;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) {\n rest = slashed.rest;\n }\n\n let item = stripItemLead(rest, language);\n\n const trailing =\n bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;\n if (trailing) {\n item = trailing.item;\n }\n\n const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);\n // A measure inside a bracket beside a container gives what one of them holds,\n // as in \"1 (14 oz) can\". The recipe's proportion lives in how many tins are\n // opened, and a tin of twice the size is a tin no shop sells, so the figure\n // goes back exactly as the page wrote it.\n\n return { rest, item, bracketed, slashed, trailing, group };\n}\n\n/**\n * Whether the figures on the line give the size of something rather than how\n * many of it the recipe wants, and of what.\n *\n * Three ways a line says it. Without a measure, the words behind the count\n * carry the mass. With a partitive, \"une dinde de 3 kg\" writes the noun where a\n * measure stands and the partitive takes it for one — a noun the vocabulary\n * lists as a measure keeps counting, since a pot is a thing to buy more of,\n * while a noun read as a measure only for standing there names the food itself.\n * And \"12 oz can tomatoes\" qualifies the tin, leaving how many tins unwritten.\n */\nfunction whatTheFiguresSize(\n leading: { unit: UnitInfo | null; partitive: boolean },\n item: string,\n language: Language,\n): HeldBack | null {\n if (language === \"fr\" && !leading.unit && statesItemSize(item)) {\n return \"itemSize\";\n }\n\n // \"une dinde de 3 kg\" writes the noun where a measure stands, and the\n // partitive takes it for one. A noun the vocabulary lists as a measure keeps\n // counting, since a pot or a boîte is a thing to buy more of; a noun read as\n // a measure only for standing there names the food itself, and a mass behind\n // it is the size of one of them.\n if (language === \"fr\" && leading.partitive && isStatedSize(item)) {\n return \"itemSize\";\n }\n\n // \"12 oz can tomatoes\": the measure qualifies the tin, and how many tins the\n // recipe wants is not written anywhere on the line.\n if (leading.unit?.kind === \"measured\" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {\n return \"containerSize\";\n }\n\n return null;\n}\n\n/**\n * What the words behind the figure say the figure counts.\n *\n * \"two thirds of a cup\" names a share of one cup, and the measure stands behind\n * the preposition and the article that introduce it. \"2 dozen mushrooms\" counts\n * mushrooms, twelve to the dozen, so the multiplier is folded into the amount\n * and the line goes on to be read as the count of a thing it now is.\n */\nfunction whatTheCountIsOf(\n behindFigure: string,\n language: Language,\n): { rest: string; multiplier: ReturnType<typeof readCountMultiplier> } {\n let rest = behindFigure.trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") {\n rest = rest.replace(LEADING_ARTICLE, \"\");\n }\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n\n return { rest, multiplier };\n}\n\n/**\n * What a container holds, when the bracket beside it states a capacity rather\n * than an equivalent.\n *\n * \"2 boîtes (400 g)\" names two tins of that size, and the figure is the page's\n * own rather than the factor's, so it goes back between the count and the\n * container exactly as published. A bracket that restates the quantity in\n * another unit is a different thing and stays an equivalent.\n */\nfunction capacityOfTheContainer(\n group: { measures: ReadonlyArray<{ unit: UnitInfo | null }>; published: string } | null,\n measureName: string | undefined,\n item: string,\n): string | null {\n if (!group?.measures.every((measure) => measure.unit?.kind === \"measured\")) {\n return null;\n }\n if (!(namesContainer(measureName) || namesContainer(item))) {\n return null;\n }\n return group.published;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const marked = readable(line);\n const decoration = takeDecoration(marked);\n const text = decoration ? marked.slice(decoration.length).trimStart() : marked;\n const evidence = readEvidence(text);\n const language = choice === \"auto\" ? evidence.language : choice;\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n language,\n heldBack,\n decoration,\n approximation: null,\n measureAdjective: null,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n alternateIntro: null,\n capacity: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (unreadableComma(text, language, evidence)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX[language].exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated, language);\n const article = range ? null : readArticle(stated, language);\n const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;\n if (!quantity) {\n return empty(null);\n }\n\n // A figure joined to a word by a hyphen describes one thing rather than\n // counting things: \"4 to 5-pound roast\" is one roast that weighs that much.\n const notAnAmount = whatTheFigureIsNot(\n stated.slice(quantity.length),\n quantity !== article,\n language,\n );\n if (notAnAmount !== false) {\n return empty(notAnAmount);\n }\n\n const counted = whatTheCountIsOf(stated.slice(quantity.length), language);\n let rest = counted.rest;\n const multiplier = counted.multiplier;\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, language, fromArticle);\n const { leading, described, behind } = measure;\n rest = leading.rest;\n\n // A range gives two amounts, and a second member behind it would belong to\n // one of them without saying which.\n const compound = range ? null : takeCompoundMember(rest, leading.unit, language);\n if (compound) {\n rest = compound.rest;\n }\n const whole = quantity.amount + (compound?.adds ?? 0);\n\n // The partitive can stand between the measure and the bracket restating it,\n // as in \"150 g de (3/4 de tasse) de sucre\". It introduces the equivalent\n // rather than the food, so it is stepped over and the bracket read behind it.\n const equivalents = takeEquivalents(rest, language);\n const { slashed, trailing, group } = equivalents;\n rest = equivalents.rest;\n const item = equivalents.item;\n const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);\n\n // A counted thing whose size the line states: the number the line opens with\n // is one bird, and the measure behind it is what that bird weighs.\n const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);\n if (sizedRatherThanCounted !== null) {\n return empty(sizedRatherThanCounted);\n }\n\n const alternates = capacity === null ? (slashed?.measures ?? group?.measures ?? []) : [];\n\n return {\n original,\n language,\n heldBack: PER_PERSON[language].test(text) ? \"perPerson\" : null,\n decoration,\n approximation: loose ? loose[0] : null,\n measureAdjective: behind?.unit ? described.adjective : null,\n amount: whole * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates,\n alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),\n alternateIntro: capacity === null ? (group?.intro ?? null) : null,\n capacity,\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Whether the comma in a leading number is one this line cannot settle.\n *\n * A group of exactly three digits reads as thousands in English and as a\n * decimal in French, so it needs the line to say which language it is in; where\n * nothing does, both readings stand and they differ by a factor of a thousand.\n * A group of any other size is not an English number at all, and a second group\n * is not a French one.\n */\nfunction unreadableComma(text: string, language: Language, evidence: LanguageEvidence): boolean {\n if (COMMA_GROUPED.test(text)) {\n const settled = evidence.french !== evidence.english;\n return !settled;\n }\n if (language === \"en\") {\n return COMMA_DECIMAL.test(text);\n }\n return REPEATED_COMMA_GROUPS.test(text);\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (language === \"fr\") {\n const match = OPENING_ARTICLE_FRENCH.exec(text);\n if (!match) {\n return null;\n }\n if (!counts(text.slice(match[0].length))) {\n return null;\n }\n const [word = \"\"] = match.slice(1);\n const amount = FRENCH_ARTICLES[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n return { amount, length: match[0].length, word };\n }\n\n const article = LEADING_INDEFINITE_WITH_SPACE.exec(text);\n if (!article) {\n return null;\n }\n if (!counts(text.slice(article[0].length))) {\n return null;\n }\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];\n if (times === undefined) {\n return null;\n }\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string; partitive: boolean } {\n const taken = takeUnit(text, language);\n if (taken.unit) {\n return { ...taken, partitive: false };\n }\n\n if (language === \"fr\" && fromArticle) {\n const measure = readPartitiveMeasure(text);\n // A noun the vocabulary never listed, read as a measure for standing\n // between the article and the partitive.\n if (measure) {\n return { unit: measure.unit, rest: measure.rest, partitive: true };\n }\n }\n\n return { ...taken, partitive: false };\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): string {\n if (language === \"fr\") {\n return text.replace(LEADING_PARTITIVE, \"\").replace(LEADING_ARTICLE_FRENCH, \"\").trim();\n }\n return text.replace(OF_OPENING, \"\").replace(LEADING_INDEFINITE_WITH_SPACE, \"\").trim();\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): BracketedMeasures {\n const none: BracketedMeasures = { measures: [], intro: null, published: \"\", rest: text };\n if (!text.startsWith(\"(\")) {\n return none;\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return none;\n }\n\n const read = readBracket(text.slice(0, close + 1), language);\n if (!read) {\n return none;\n }\n return { ...read, rest: text.slice(close + 1).trimStart() };\n}\n\ninterface BracketedMeasures {\n measures: Measure[];\n /** What introduced the restatement, as the line wrote it. */\n intro: string | null;\n /** The bracket exactly as published, for a figure that must not be scaled. */\n published: string;\n rest: string;\n}\n\n/**\n * Words a page puts inside a bracket to announce that what follows restates the\n * quantity beside it rather than adding to it.\n */\nconst EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\\.?|approximately|or|ou|=)\\s*/i;\n\n/**\n * The partitive a line can put between a figure and the measure it names.\n *\n * \"3/4 de tasse\" and \"3/4 tasse\" are one quantity written two ways, and a\n * reader that only knows the second leaves the first sitting in prose beside an\n * amount that moved.\n */\nconst MEASURE_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+|of\\s+)/i;\n\n/** The measure a figure names, allowing for the partitive introducing it. */\nfunction takeMeasureAfterQuantity(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const direct = takeUnitEitherLanguage(text, language);\n if (direct.unit) {\n return direct;\n }\n\n const partitive = MEASURE_PARTITIVE.exec(text);\n if (!partitive) {\n return direct;\n }\n return takeUnitEitherLanguage(text.slice(partitive[0].length), language);\n}\n\n/**\n * Read a whole bracket as a group of equivalent measures, or nothing.\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", is prose, and scaling prose is not something a reader can check.\n */\nfunction readBracket(bracket: string, language: Language): Omit<BracketedMeasures, \"rest\"> | null {\n const inside = bracket.slice(1, -1);\n const intro = EQUIVALENT_INTRODUCER.exec(inside);\n const body = intro ? inside.slice(intro[0].length) : inside;\n // A slash separates two ways of stating the quantity, and it also writes a\n // fraction. One sitting between two digits belongs to the number, so\n // \"(3/4 de tasse)\" is one measure rather than a three and a four.\n const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) {\n return null;\n }\n\n const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") {\n return null;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: after.unit,\n });\n }\n\n if (measures.length === 0) {\n return null;\n }\n return { measures, intro: intro ? intro[0].trim() : null, published: bracket };\n}\n\n/**\n * Read a bracket a line closes on, as in \"150 g de sucre (soit 3/4 de tasse)\".\n *\n * A page states an equivalence wherever it reads best, and after the name of\n * the food is as common as beside the figure. Both say the same thing about the\n * same quantity, so both move with it: a doubled line whose closing bracket\n * still reads as published tells the cook that 300 g is three quarters of a\n * cup.\n */\nfunction takeTrailingAlternates(\n item: string,\n language: Language,\n): (Omit<BracketedMeasures, \"rest\"> & { item: string }) | null {\n const closing = BRACKETED_TAIL.exec(item);\n if (!closing) {\n return null;\n }\n\n const read = readBracket(closing[1] ?? \"\", language);\n if (!read) {\n return null;\n }\n\n const head = item.slice(0, closing.index).trim();\n // A bracket standing on its own is the quantity itself rather than a\n // restatement of one, and there would be nothing left for it to qualify.\n if (head === \"\") {\n return null;\n }\n\n return { ...read, item: head };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) {\n break;\n }\n\n const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) {\n break;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return language === \"fr\" ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) {\n return \"\";\n }\n if (Number.isInteger(amount)) {\n return String(amount);\n }\n if (options.fractions === false) {\n return decimal(amount);\n }\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: [number, string][] = [\n [0.25, \"1/4\"],\n [1 / 3, \"1/3\"],\n [0.5, \"1/2\"],\n [2 / 3, \"2/3\"],\n [0.75, \"3/4\"],\n ];\n for (const [value, label] of known) {\n if (Math.abs(rest - value) < 0.02) {\n return whole > 0 ? `${whole} ${label}` : label;\n }\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French and in English.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act\n * on. Multiplying every number by the factor is arithmetically correct and\n * practically useless: it produces \"2,4 oeufs\" and \"0.67 pinch of salt\" with\n * the same confidence as \"267 g flour\". Each line is therefore classified by\n * what its measure allows, and the classification travels with the result so\n * the caller can see what was computed and what was left alone.\n *\n * Leaving a line alone is a decision of the same weight. A quantity a recipe\n * states loosely still holds a share of the dish, and a leavening agent left at\n * one pinch for twenty-five servings is a recipe that does not rise.\n *\n * The two languages are read by one set of rules. A list holding French lines\n * and English lines comes back scaled by the same arithmetic, with each line\n * written the way its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst CLOVE = /\\bcloves?\\b/i;\nconst CONSONANT_THEN_Y = /[^aeiou]y$/i;\nconst DOUBLE_S_ENDING = /ss$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst F_ENDING = /(?:[^f]f|fe)$/i;\nconst F_OR_FE_ENDING = /fe?$/i;\nconst GARLIC = /\\bgarlic\\b/i;\nconst IES_ENDING = /ies$/i;\nconst LETTER = /\\p{L}/u;\nconst LETTERS_ONLY = /^[A-Za-z]+$/;\nconst OF_JOINER = / of /i;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\nconst X_ENDING = /x$/i;\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n *\n * A whole number is already what a scale shows, so it is left where the\n * arithmetic put it: 1234 g doubled is 2468 g, and a step of five grams would\n * report 2470 for a product that needed no rounding at all.\n */\nfunction roundMeasured(value: number): number {\n if (Number.isInteger(value)) {\n return value;\n }\n if (value >= 100) {\n return roundTo(value, 5);\n }\n if (value >= 10) {\n return roundTo(value, 1);\n }\n if (value >= 1) {\n return roundTo(value, 0.1);\n }\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) {\n return { value: floor, clamped: true };\n }\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) {\n return { value: floor, clamped: true };\n }\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0] ?? floor;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n if (value < 1) {\n const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(\n (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION),\n );\n let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) {\n return false;\n }\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\n/**\n * Whether a bound came out as the exact product, judged in the finer of the two\n * units involved.\n *\n * An absolute tolerance only means what it says at one scale. Read in kilos, a\n * hundredth is ten grams, so a quantity moved from 2468 g to 2470 g passes as\n * exact for being 0.002 kg away; read in grams, the same gap is two grams and\n * the answer is that the value moved.\n */\nfunction boundLandedExactly(bound: ScaledBound): boolean {\n const fine = Math.max(bound.ratio, 1);\n return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n /** How many of the unit that came back fit in one of the unit the recipe wrote. */\n ratio: number;\n}\n\ninterface ScaledMeasure {\n /**\n * One bound for a single measure, two for a range, in that order. The shape\n * says so, because every reader takes the first without checking: a measure\n * that scaled to nothing is not a thing this can produce.\n */\n bounds: [ScaledBound, ...ScaledBound[]];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n /** What the page published at one end of the measure, and what it scales to. */\n interface End {\n published: number;\n raw: number;\n }\n\n const ends: [End, ...End[]] =\n high === null\n ? [{ published: low, raw: low * factor }]\n : [\n { published: low, raw: low * factor },\n { published: high, raw: high * factor },\n ];\n\n /** Read both ends, keeping the shape every caller of the bounds relies on. */\n const eachEnd = <T>(read: (end: End) => T): [T, ...T[]] => [\n read(ends[0]),\n ...ends.slice(1).map(read),\n ];\n\n const raws = ends.map((end) => end.raw);\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : low * factor;\n\n /** Both bounds share one unit, and each keeps the precision that unit affords. */\n const inUnit = (target: UnitInfo, ratio: number): ScaledMeasure => ({\n bounds: eachEnd(({ published, raw }) => {\n const exact = raw * ratio;\n // The rounding happens in the smaller of the two units, so moving to a\n // bigger one never throws away precision the page wrote: 1500 g rounded\n // as kilos is 2, and rounded as grams it is the 1.5 kg a scale shows.\n const rounded =\n ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);\n // At the bottom of a ladder, keep what precision is left rather than\n // deleting the ingredient.\n const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;\n // Rounding to a step of five grams above a hundred can round upwards, and\n // a recipe being made smaller must never come out asking for more than\n // the page published.\n const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n ratio,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) {\n return inUnit(stepped.unit, stepped.ratio);\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(raw, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, divisibility, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n}\n\n/**\n * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in either language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genievres?|genevriers?|badianes?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n \")\\\\b\",\n \"iu\",\n);\n\n/**\n * A juice, the one counted thing whose division stops at the half.\n *\n * Half the juice of a lemon is taken by squeezing half the fruit, which is a\n * step a recipe writes. A quarter of one has to be poured out and measured\n * back, and no recipe asks for that.\n *\n * It reads before the fruit, which a knife divides further on its own.\n */\nconst HALVED_ITEM = /\\b(?:jus|juices?)\\b/iu;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An egg comes out of its shell whole, and so does the yolk or the white a\n * recipe asks for on its own: half of one means beating it and weighing the\n * result, which no recipe asks for, and there is no way to keep the other half.\n * A count of them therefore lands on a whole number, whichever side of the half\n * the arithmetic fell on.\n *\n * This list is short on purpose, and a word joins it only when half of the\n * thing is genuinely not something a cook can measure out. Anything that is\n * poured, weighed or cut halves, and takes the half.\n *\n * Two more belong here for reasons the criterion cannot reach on its own:\n *\n * - a clou de girofle is a dried flower bud, dropped into the pot and fished\n * back out of it. Nothing about it is measured, so there is no half of one to\n * take;\n * - a zeste is what comes off one fruit in one go. A line asking for the zeste\n * of a citron is asking for all of it, and a share of a zeste names no amount\n * a cook stops at.\n */\nconst WHOLE_ITEM =\n /\\b(?:eggs?|yolks?|egg\\s+whites?|oeufs?|œufs?|jaunes?\\s+d['e]|clous?|zestes?|zests?)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and the share of\n * one a recipe asks for is the half a knife makes of it. A clove on its own is\n * the dried flower bud, the clou de girofle, dropped into the pot and fished\n * back out of it: nothing about it is measured, so there is no share of one to\n * take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : CLOVE.test(item);\n if (!counted) {\n return null;\n }\n return GARLIC.test(item) ? \"half\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) {\n return null;\n }\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\n/**\n * Accents removed and the ligature spelled out, so \"échalote\" and \"echalote\"\n * hit one entry.\n *\n * A word boundary sits between an ASCII letter and a non-letter and nowhere\n * else, so a pattern opening on \"é\" never matches at the start of a word:\n * folding the item is what lets the lists below be written once, in plain\n * letters.\n */\nfunction foldItem(item: string): string {\n return item\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\u0153/g, \"oe\")\n .replace(/\\u0152/g, \"OE\");\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const key = foldItem(item);\n const clove = cloveDivisibility(unit, key);\n if (clove) {\n return clove;\n }\n if (unit && !countsBarePieces(unit)) {\n return unitDivisibility(unit);\n }\n const blanc = blancDivisibility(key);\n if (blanc) {\n return blanc;\n }\n if (WHOLE_ITEM.test(key)) {\n return \"whole\";\n }\n if (PORTION_SIZED_ITEM.test(key)) {\n return \"whole\";\n }\n if (HALVED_ITEM.test(key)) {\n return \"half\";\n }\n if (HALVED_CUT.test(key)) {\n return \"half\";\n }\n if (QUARTERED_MEASURE.test(key)) {\n return \"quarter\";\n }\n return QUARTERED_ITEM.test(key) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/**\n * Keep the capitalisation the line used while looking the word up in lower\n * case.\n *\n * A page writes its ingredients in whichever case it likes, and the shape of a\n * word is the page's rather than this server's: a list set in capitals reads as\n * a list somebody typed over when one line of it comes back in lower case.\n * Three shapes carry: all capitals, one leading capital, and everything else,\n * which keeps the replacement as the vocabulary spells it.\n */\nfunction matchCase(source: string, replacement: string): string {\n const hasLetter = LETTER.test(source);\n if (hasLetter && source === source.toUpperCase()) {\n return replacement.toUpperCase();\n }\n const first = source.slice(0, 1);\n if (\n first !== \"\" &&\n first === first.toUpperCase() &&\n source.slice(1) === source.slice(1).toLowerCase()\n ) {\n return (replacement[0] ?? \"\").toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (SIBILANT_ENDING.test(word)) {\n return `${word}es`;\n }\n if (CONSONANT_THEN_Y.test(word)) {\n return `${word.slice(0, -1)}ies`;\n }\n if (F_ENDING.test(word)) {\n return `${word.replace(F_OR_FE_ENDING, \"\")}ves`;\n }\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (IES_ENDING.test(word) && word.length > 4) {\n return `${word.slice(0, -3)}y`;\n }\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (SIBILANT_PLURAL.test(word)) {\n return word.slice(0, -2);\n }\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (DOUBLE_S_ENDING.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word)) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = OF_JOINER.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words.at(-1) ?? \"\";\n if (!LETTERS_ONLY.test(last) || last.length <= 2) {\n return item;\n }\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) {\n words[words.length - 1] = plural;\n } else if (!wantsPlural && isPlural) {\n words[words.length - 1] = singular;\n } else {\n return item;\n }\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\n/**\n * French nouns in -ou whose plural takes an -x.\n *\n * The ending settles nothing on its own: \"clou\" and \"trou\" take the ordinary\n * -s, and only these few take the -x. A cabbage is the one a recipe counts, and\n * the others are here because the list is the rule.\n */\nconst FRENCH_OU_PLURAL_IN_X = new Set([\n \"bijou\",\n \"caillou\",\n \"chou\",\n \"genou\",\n \"hibou\",\n \"joujou\",\n \"pou\",\n]);\n\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"mur\",\n \"mure\",\n \"vert\",\n \"verte\",\n \"rouge\",\n \"jaune\",\n \"noir\",\n \"noire\",\n \"blanc\",\n \"blanche\",\n \"rond\",\n \"ronde\",\n \"hache\",\n \"hachee\",\n \"coupe\",\n \"coupee\",\n \"rape\",\n \"rapee\",\n \"pele\",\n \"pelee\",\n \"epluche\",\n \"epluchee\",\n \"denoyaute\",\n \"denoyautee\",\n \"emince\",\n \"emincee\",\n]);\n\n/** Lowercase and strip accents, so \"entière\" and \"entiere\" hit the same entry. */\nfunction foldWord(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The trailing adjective agreed with the count, or null when it is left alone. */\nfunction agreeTrailingAdjective(word: string, wantsPlural: boolean): string | null {\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\");\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return null;\n }\n if (wantsPlural === isPlural) {\n return null;\n }\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * The head of a French noun phrase, put in the number its count asks for.\n *\n * The marks are not uniform: \"morceau\" and \"chou\" take -x where an ordinary\n * noun takes -s, \"bocal\" takes -aux, a word ending in -s, -x or -z takes no\n * mark at all, and \"ananas\" or \"couscous\" carry their -s in the singular. A\n * head already in the number wanted is left as written.\n */\nfunction agreeFrenchHead(\n head: string,\n number: { wantsPlural: boolean; isPlural: boolean; ouPlural: boolean },\n): string {\n const { wantsPlural, isPlural, ouPlural } = number;\n\n if (wantsPlural && !isPlural) {\n if (SIBILANT_LETTER.test(head)) {\n return head;\n }\n if (EAU_ENDING.test(head)) {\n return `${head}x`;\n }\n if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) {\n return `${head}x`;\n }\n if (AL_ENDING.test(head)) {\n return `${head.slice(0, -2)}aux`;\n }\n return `${head}s`;\n }\n\n if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n return head.slice(0, -1);\n }\n if (AUX_ENDING.test(head)) {\n return `${head.slice(0, -3)}al`;\n }\n if (ouPlural) {\n return head.slice(0, -1);\n }\n if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n return head;\n }\n return head.slice(0, -1);\n }\n\n return head;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) {\n return item;\n }\n\n const wantsPlural = amount >= 2;\n // A final -x marks the plural of the few nouns in -ou that take one, and it\n // belongs to the singular of everything else (\"prix\", \"houx\").\n const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));\n const isPlural = PLURAL_ENDING.test(head) || ouPlural;\n\n words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });\n\n const last = words.length - 1;\n const trailing = last > 0 ? words[last] : undefined;\n if (trailing !== undefined) {\n const adjective = agreeTrailingAdjective(trailing, wantsPlural);\n if (adjective) {\n words[last] = adjective;\n }\n }\n\n return words.join(\" \");\n}\n\n/**\n * Agree the adjective a line put in front of its measure, as in the \"grosse\" of\n * \"1 grosse pincée\".\n *\n * The word qualifies the measure, so in French it takes the number the measure\n * takes: \"2 grosses pincées\". English adjectives do not decline, and a French\n * word outside the declinable list stays as the recipe wrote it, for the same\n * reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number, language: Language): string {\n if (language === \"en\") {\n return word;\n }\n\n const wantsPlural = amount >= 2;\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\") && !AGREEABLE_ADJECTIVES.has(folded);\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return word;\n }\n if (wantsPlural === isPlural) {\n return word;\n }\n return wantsPlural ? `${singular}s` : singular;\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n return language === \"fr\" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * Agree the container a line counts, which is the word the item opens with.\n *\n * \"2 (14 oz) cans tomatoes\" counts tins and names what is in them, so the\n * number belongs to the tin. English otherwise agrees the last word before a\n * comma, which would put the plural mark on the tomatoes and leave the tin\n * reading as one.\n */\nfunction agreeCountedContainer(item: string, amount: number, language: Language): string {\n if (language === \"fr\") {\n return agreeInFrench(item, amount);\n }\n const space = item.indexOf(\" \");\n if (space < 0) {\n return agreeInEnglish(item, amount);\n }\n return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language): string {\n if (!item) {\n return \"\";\n }\n if (language === \"en\") {\n return ` ${item}`;\n }\n const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in either language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) {\n return null;\n }\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) {\n continue;\n }\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) {\n continue;\n }\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) {\n return passthroughIngredient(line, options.language);\n }\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) {\n return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\n }\n\n return scaleSingleLine(line, options);\n}\n\n/**\n * Scale a line that offers a choice, one branch at a time.\n *\n * A cook follows one branch and ignores the other, so both have to carry the\n * same share of the recipe: a doubled line whose second branch still reads as\n * published hands whoever takes it half the ingredient. The two branches name\n * different things, and how far one stands for the other is the page's claim\n * rather than arithmetic, so such a line is never reported as exact.\n */\nfunction scaleBranchedLine(line: string, branch: Branch, options: ScaleOptions): ScaledIngredient {\n const head = scaleSingleLine(branch.head, options);\n if (head.scaling === \"unscaled\") {\n return { ...head, text: line.trim(), original: line };\n }\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) {\n return { text: published, rewritten: false };\n }\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) {\n return { text: published, rewritten: false };\n }\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\n/**\n * The equivalents beside a measure, written the way the line wrote them.\n *\n * A trailing restatement is printed after the item rather than beside the\n * amount, so it carries no label here.\n */\nfunction alternateLabel(\n count: number,\n style: string | null,\n texts: readonly string[],\n bracketed: string,\n): string {\n if (count === 0) {\n return \"\";\n }\n if (style === \"slash\") {\n return ` / ${texts.join(\" / \")}`;\n }\n if (style === \"trailing\") {\n return \"\";\n }\n return bracketed;\n}\n\n/**\n * What follows the amount: the partitive French puts between a measure and what\n * it measures, or the counted item itself, which stands straight after its\n * number in both languages and agrees with it.\n */\nfunction itemLabelFor(\n named: UnitInfo | null,\n item: string,\n counted: string,\n language: Language,\n): string {\n if (named) {\n return joinItem(item, language);\n }\n if (counted) {\n return ` ${counted}`;\n }\n return \"\";\n}\n\n/** What the answer says about a line beyond the figure it came to. */\ninterface LineOutcome {\n parsed: ParsedIngredient;\n bounds: readonly ScaledBound[];\n unit: UnitInfo | null;\n low: ScaledBound;\n clamped: ScaledBound | null;\n movedPrimary: boolean;\n movedAlternate: boolean;\n restated: boolean;\n collapsed: boolean;\n language: Language;\n carriesMore: boolean;\n}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * The reasons stack: a line can be clamped, carry a second quantity, collapse\n * its range and state an approximation, and each has to be said without\n * cancelling the others.\n */\nfunction noteForScaledLine(outcome: LineOutcome): string | undefined {\n const {\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n } = outcome;\n let note: string | undefined;\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));\n sentences.push(\n moved\n .map(\n (bound) =>\n `Rounded ${bound.amount > bound.exact ? \"up\" : \"down\"} from ` +\n `${asPublished(bound.raw, parsed.unit)}.`,\n )\n .join(\" \"),\n );\n } else if (movedAlternate) {\n // The amount itself came out exact, and only the equivalent beside it had\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (carriesMore) {\n sentences.push(\n \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\",\n );\n }\n\n if (parsed.capacity) {\n sentences.push(\n `${parsed.capacity} is what one container holds, so it stays as published and the count is ` +\n \"what the factor moved. Scaling this line is a matter of opening that many containers of \" +\n \"the size named.\",\n );\n }\n\n if (collapsed) {\n sentences.push(\"The page gave a range, and at this size both ends come to the same amount.\");\n }\n\n // Below what any scale shows, the arithmetic is right and the kitchen cannot\n // follow it. Saying so is the difference between an answer and a number.\n if (unit?.kind === \"measured\" && low.amount > 0 && low.amount < 0.05) {\n sentences.push(\n \"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye.\",\n );\n }\n\n // The page put the amount forward as loose, and multiplying it keeps it that\n // way: the answer is as approximate as the figure it came from.\n if (parsed.approximation) {\n sentences.push(\n \"The page gave this amount as an approximation, and the scaled figure is no firmer.\",\n );\n }\n\n if (sentences.length > 0) {\n note = sentences.join(\" \");\n }\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n note = withApproximateNote(parsed.unit, note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\n}\n\n/**\n * The line as it reads once scaled: the amount, its measure, what the container\n * holds, the equivalents beside it, and the item they belong to.\n *\n * The size word the page put in front of its measure goes back in front of it,\n * and the equivalents go back the way the line offered them, since a slash and\n * a bracket are two different ways of restating one quantity.\n */\nfunction renderScaledLine(\n parsed: ParsedIngredient,\n alternates: ReadonlyArray<{ text: string }>,\n unit: UnitInfo | null,\n shown: number,\n language: Language,\n): {\n named: UnitInfo | null;\n unitLabel: string;\n capacityLabel: string;\n altLabel: string;\n trailingLabel: string;\n itemLabel: string;\n} {\n const named = unit && !countsBarePieces(unit) ? unit : null;\n // The size word the page put in front of its measure goes back in front of\n // it: the page asked for a grosse pincée, and a pincée is not the same ask.\n const adjective =\n named && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}`\n : \"\";\n const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : \"\";\n const alternateTexts = alternates.map((entry) => entry.text);\n const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : \"\";\n const bracketed = alternates.length === 0 ? \"\" : ` (${introduced}${alternateTexts.join(\" / \")})`;\n // Equivalents go back where the line offered them: inside brackets beside the\n // amount, after a slash, or in the bracket the line closes on.\n const altLabel = alternateLabel(\n alternates.length,\n parsed.alternateStyle,\n alternateTexts,\n bracketed,\n );\n const trailingLabel = parsed.alternateStyle === \"trailing\" ? bracketed : \"\";\n // What a container holds is the page's figure and not the factor's, so it\n // goes back between the count and the container, exactly as published.\n const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : \"\";\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = parsed.capacity\n ? agreeCountedContainer(parsed.item, shown, language)\n : agreeWithAmount(parsed.item, shown, language);\n const itemLabel = itemLabelFor(named, parsed.item, counted, language);\n\n return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };\n}\n\n/** Why a line showing a figure came back as the page published it. */\nconst HELD_BACK_NOTE: Record<HeldBack, string> = {\n sizeQualifier:\n \"The figures here give the size of one item rather than how many, so the line is \" +\n \"left as published.\",\n itemSize:\n \"The measure standing behind the item gives the size of one of them rather than how many, \" +\n \"so the line is left as published. Serving more people is a matter of taking a bigger one, \" +\n \"and serving fewer a smaller one.\",\n containerSize:\n \"The measure here gives what one container holds rather than how many of them the recipe \" +\n \"uses, and the line states no count, so it is left as published. Scaling it is a matter of \" +\n \"opening that many containers of the size named.\",\n perPerson:\n \"This line already states an amount for one person, and the factor is what changes \" +\n \"how many people the recipe serves, so the line is left as published.\",\n duration:\n \"This line states a length of time rather than an amount of an ingredient, so it is left as \" +\n \"published. A rest, a proof or a bake takes as long for a large batch as for a small one, \" +\n \"and the factor has nothing to say about it.\",\n ambiguousDecimal:\n \"The comma in this number marks thousands in one convention and the decimal point in \" +\n \"another, and the line gives no sign which was meant, so it is left as published.\",\n};\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n note: parsed.heldBack\n ? HELD_BACK_NOTE[parsed.heldBack]\n : \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((bound) => !boundLandedExactly(bound));\n const movedAlternate = alternateBounds.some((bound) => !boundLandedExactly(bound));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote\n // them, and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\n const carriesMore = hasEmbeddedMeasure(parsed.item, language);\n // A further quantity beside a mass or a volume restates or qualifies it, and\n // it did not move, so the line as a whole is not the exact product of what\n // the page published. Beside a counted container the same figure gives the\n // size of one of them, which is a thing the factor must leave alone: \"2\n // boîtes de 400 g\" is exactly twice \"1 boîte de 400 g\".\n const leftover = carriesMore && parsed.unit?.kind === \"measured\";\n\n const low = primaryBounds[0];\n const high = primaryBounds[1] ?? null;\n const unit = primary.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(\n parsed,\n alternates,\n unit,\n shown,\n language,\n );\n const result: ScaledIngredient = {\n text: `${parsed.decoration ? `${parsed.decoration} ` : \"\"}${parsed.approximation ?? \"\"}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated || leftover ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n };\n\n const note = noteForScaledLine({\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n });\n if (note !== undefined) {\n result.note = note;\n }\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0];\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) {\n return low;\n }\n return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n const parsed = parseIngredient(line, language);\n\n const held = parsed.amount === null || parsed.heldBack !== null;\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: held ? \"unscaled\" : \"scaled\",\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n language: parsed.language,\n };\n if (parsed.heldBack) {\n result.note = HELD_BACK_NOTE[parsed.heldBack];\n } else if (parsed.amount === null) {\n result.note = \"No quantity given; adjust to taste.\";\n } else if (parsed.unit?.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, undefined);\n }\n return result;\n}\n\n/** An ingredient list returned unchanged, for when no scaling was requested. */\nexport function passthroughIngredients(\n lines: string[],\n language: LanguageChoice = \"auto\",\n): ScaledIngredient[] {\n return lines.map((line) => passthroughIngredient(line, language));\n}\n","/**\n * Reading what a page's own headings announce.\n *\n * A heading is the one statement a page makes about its own layout, and it is\n * the only evidence there is that a part of a recipe was published when nothing\n * was read out of that part. That matters because reading nothing is a fact\n * about this server, and only the page can say whether there was anything to\n * read.\n *\n * Both languages the pages are written in are recognised here, since a heading\n * belongs to a page rather than to the corpus it sits in.\n */\n\n/** The parts of a recipe a page announces with a heading of its own. */\nexport type PagePart = \"ingredients\" | \"method\";\n\nconst HEADINGS: Record<PagePart, RegExp> = {\n ingredients: /^\\s*(?:ingr[ée]dients?|ingredients?|garniture)\\b/i,\n method:\n /^\\s*(?:procedure|proc[ée]d[ée]|directions?|instructions?|method|preparation|pr[ée]paration|r[ée]alisation|steps?|[ée]tapes?)\\b/i,\n};\n\n/**\n * The first heading on the page announcing this part, when the page carries\n * one. Null covers two different things on purpose, and the caller separates\n * them: a page heading nothing of the kind, and a source that does not report\n * a page's headings at all.\n */\nexport function headingFor(part: PagePart, headings: readonly string[] | null): string | null {\n if (headings === null) {\n return null;\n }\n return headings.find((heading) => HEADINGS[part].test(heading)) ?? null;\n}\n","/** Schemas, error mapping and rendering shared by the tools. */\n\nimport { z } from \"zod\";\nimport { RecipesError } from \"../errors.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport type { RecipeRow, SourceReport } from \"../types.js\";\n\n/**\n * The text block is what many clients render, and some render nothing else, so\n * it has to answer on its own. This ceiling is what keeps a recipe with forty\n * ingredients from arriving as a wall of text.\n */\nexport const MAX_TEXT_CHARS = 2200;\n\nexport interface ToolResult {\n // The SDK's result type carries an index signature for protocol extensions.\n [key: string]: unknown;\n content: Array<{ type: \"text\"; text: string }>;\n structuredContent?: Record<string, unknown>;\n isError?: boolean;\n}\n\n/** One search row, carrying what it takes to pick a recipe out of a list. */\nexport const rowSchema = z.object({\n id: z\n .string()\n .describe(\"Pass this to get_recipe. It names its source, so nothing has to be guessed.\"),\n source: z.string().describe(\"Which source published this row.\"),\n source_name: z.string(),\n title: z.string(),\n url: z.string().describe(\"The recipe's own page. Show this when citing it.\"),\n image_url: z.string().nullable(),\n excerpt: z\n .string()\n .nullable()\n .describe(\"The gloss or matching passage the source offered, when it offered one.\"),\n});\n\n/**\n * What one source answered, or why it did not.\n *\n * Present for every source asked, including the ones that failed, because an\n * answer that quietly drops a source reads as a complete answer that found\n * less.\n */\nexport const reportSchema = z.object({\n source: z.string(),\n name: z.string(),\n status: z.enum([\"answered\", \"failed\"]).describe(\"Whether this source replied at all.\"),\n count: z.number().int().describe(\"Rows this source contributed to the answer.\"),\n reported_total: z\n .number()\n .int()\n .nullable()\n .describe(\"What the source said it saw. Null when it states no number at all.\"),\n reported_total_means: z\n .string()\n .nullable()\n .describe(\n \"What 'reported_total' counts on this source. Counts are never summed across sources.\",\n ),\n skipped: z\n .number()\n .int()\n .describe(\"Rows this source sent in a shape the server could not read, and left out.\"),\n cached: z.boolean().describe(\"Served from this server's short-lived in-memory cache.\"),\n preferred_by_name: z\n .boolean()\n .describe(\n \"Whether this source's rows were arranged so the ones whose title names the dish come first. An order over one source's own rows, never a score against another source.\",\n ),\n wordings: z\n .array(\n z.object({\n query: z.string().describe(\"What was sent to this source, or would have been.\"),\n derivation: z.string().describe(\"How this wording was derived from the question.\"),\n ran: z.boolean(),\n count: z\n .number()\n .int()\n .nullable()\n .describe(\"Rows this wording returned. Null when it did not run.\"),\n added: z\n .number()\n .int()\n .nullable()\n .describe(\"Rows it returned that no earlier wording had.\"),\n not_run_because: z.string().nullable().describe(\"Why it was held back. Null when it ran.\"),\n error: z\n .object({ code: z.string(), message: z.string(), hint: z.string().optional() })\n .nullable(),\n }),\n )\n .describe(\n \"Every wording this source was sent or held back from, in the order tried. A question written as a sentence is also sent in wordings derived from it, because these indexes answer the words they are handed. Counts here belong to one wording each and are never added.\",\n ),\n error: z\n .object({\n code: z.string(),\n message: z.string(),\n hint: z.string().optional(),\n })\n .nullable()\n .describe(\"Why this source did not answer. Null when it did.\"),\n read: z\n .object({\n status: z.enum([\"read\", \"failed\"]),\n error: z\n .object({ code: z.string(), message: z.string(), hint: z.string().optional() })\n .nullable(),\n })\n .nullable()\n .describe(\n \"What became of the row this source offered, once it was opened. Null when no row was opened, which is what a search that did not answer and a search holding nothing both come to. A search and a read are two moments, and this is the second one.\",\n ),\n});\n\n/** One ingredient line, scaled, in the shape every source is read into. */\nexport const ingredientSchema = z.object({\n text: z.string().describe(\"The line as it now reads.\"),\n original: z.string().describe(\"The line as published.\"),\n scaling: z\n .enum([\"scaled\", \"rounded\", \"unscaled\"])\n .describe(\n \"'scaled' means the arithmetic landed on the exact product, 'rounded' that something had to move for the line to stay usable or that the line carries a second quantity whose match is the page's claim, 'unscaled' that there is nothing to multiply.\",\n ),\n amount: z\n .number()\n .nullable()\n .describe(\"The scaled quantity, in 'unit'. Read the two together, never the number alone.\"),\n amount_max: z.number().nullable().describe(\"Upper bound when the line gives a range.\"),\n unit: z\n .string()\n .nullable()\n .describe(\"The unit 'amount' is in, which may differ from the page's.\"),\n language: z.enum([\"fr\", \"en\"]).describe(\"The language the line was read and rewritten in.\"),\n note: z.string().optional().describe(\"What happened to this line, when anything did.\"),\n});\n\nexport function toIngredientPayload(entry: ScaledIngredient): z.infer<typeof ingredientSchema> {\n return {\n text: entry.text,\n original: entry.original,\n scaling: entry.scaling,\n amount: entry.amount,\n amount_max: entry.amountMax,\n unit: entry.unit,\n language: entry.language,\n ...(entry.note ? { note: entry.note } : {}),\n };\n}\n\nexport function toRowPayload(row: RecipeRow): z.infer<typeof rowSchema> {\n return {\n id: row.id,\n source: row.source,\n source_name: row.sourceName,\n title: row.title,\n url: row.url,\n image_url: row.imageUrl,\n // An excerpt is a source's own prose, and a long one crowds out the rows it\n // was meant to help choose between.\n excerpt: row.excerpt === null ? null : truncate(row.excerpt, MAX_EXCERPT_CHARS),\n };\n}\n\n/** How much of a source's own prose a search row carries. */\nconst MAX_EXCERPT_CHARS = 200;\n\nexport function toReportPayload(report: SourceReport): z.infer<typeof reportSchema> {\n return {\n source: report.source,\n name: report.name,\n status: report.status,\n count: report.count,\n reported_total: report.reportedTotal,\n reported_total_means: report.reportedTotalMeans,\n skipped: report.skipped,\n cached: report.cached,\n preferred_by_name: report.preferredByName,\n wordings: report.wordings.map((attempt) => ({\n query: attempt.query,\n derivation: attempt.derivation,\n ran: attempt.ran,\n count: attempt.count,\n added: attempt.added,\n not_run_because: attempt.notRunBecause,\n error: attempt.error,\n })),\n error: report.error ?? null,\n // A search alone opens nothing, so only a tool that reads a row fills this\n // in, through `withRead`.\n read: null,\n };\n}\n\n/** What became of the row a source offered, added to that source's report. */\nexport function withRead(\n payload: z.infer<typeof reportSchema>,\n read: z.infer<typeof reportSchema>[\"read\"],\n): z.infer<typeof reportSchema> {\n return { ...payload, read };\n}\n\n/**\n * A sentence qualifying an answer.\n *\n * The text block has room for a limited number of them, so something gives when\n * an answer carries more, and a note the answer would mislead without says so\n * for itself. Whoever writes a note knows what it is for; deciding it later\n * from the words the note happens to use makes every rewording of a warning a\n * warning that can be dropped, and the notes most often reworded are the ones\n * written to correct a specific misreading.\n */\nexport type Note = string | EssentialNote;\n\nexport interface EssentialNote {\n text: string;\n /** The answer misleads without this sentence. */\n essential: true;\n}\n\n/** Mark a note the answer cannot be read safely without. */\nexport function mustKeep(text: string): EssentialNote {\n return { text, essential: true };\n}\n\nexport function noteText(note: Note): string {\n return typeof note === \"string\" ? note : note.text;\n}\n\nexport function noteTexts(notes: readonly Note[]): string[] {\n return notes.map(noteText);\n}\n\n/** Name the recipe a note is about, keeping what the note is worth. */\nexport function labelNote(label: string, note: Note): Note {\n if (typeof note === \"string\") {\n const plain: string = note;\n return `${label}: ${plain}`;\n }\n const said: string = note.text;\n return mustKeep(`${label}: ${said}`);\n}\n\n/**\n * Turn the per-source reports into sentences a reader can act on.\n *\n * A failed source is named with the reason, so an answer holding part of what\n * was asked for never reads as the whole of what exists.\n */\nexport function reportNotes(reports: SourceReport[]): Note[] {\n const notes: Note[] = [];\n\n const answered = reports.filter((report) => report.status === \"answered\");\n const failed = reports.filter((report) => report.status === \"failed\");\n\n for (const report of failed) {\n // What the rest of the answer holds depends on whether anything else\n // answered. Promising a caller that another source made up the difference\n // when none did is the failure this whole shape exists to prevent.\n const consolation =\n answered.length > 0\n ? `This answer holds what the other sources found, and says nothing about what ${report.name} holds.`\n : \"Nothing here is evidence about what it holds.\";\n notes.push(\n mustKeep(\n `${report.name} did not answer (${report.error?.code}): ${report.error?.message} ${consolation}`,\n ),\n );\n }\n\n for (const report of answered) {\n const sent = report.wordings.filter((attempt) => attempt.ran && attempt.error === null);\n\n if (report.count === 0) {\n notes.push(\n mustKeep(\n sent.length > 1\n ? `${report.name} answered and offered no row for any of the ${sent.length} wordings it was ` +\n `sent (${sent.map((attempt) => `\"${attempt.query}\"`).join(\", \")}). Each of those is a ` +\n \"statement about a wording; together they are the closest this server comes to saying \" +\n \"the corpus holds nothing. Try a main ingredient, or the dish's name in the language \" +\n \"that source publishes in.\"\n : `${report.name} answered and offered no row for this wording. That is a statement about ` +\n \"the wording as much as about the corpus: try the dish's name in another language, or a \" +\n \"main ingredient.\",\n ),\n );\n }\n if (sent.length > 1) {\n notes.push(\n `${report.name} was asked ${sent.length} wordings and these rows are their union: ${sent\n .map((attempt) => `\"${attempt.query}\" (${attempt.count ?? 0})`)\n .join(\", \")}. Those counts are per wording and are never added.`,\n );\n }\n if (report.preferredByName) {\n notes.push(\n `${report.name}'s rows are arranged with the ones naming the dish first, so a wording that ` +\n \"returned near-misses cannot crowd out what another wording found. That is an order over \" +\n \"one source's own rows and not a score against any other source.\",\n );\n }\n if (report.skipped > 0) {\n notes.push(\n mustKeep(\n `${report.name} sent ${report.skipped} row(s) in a shape this server could not read, and they were left out.`,\n ),\n );\n }\n if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {\n const forWording =\n sent.length > 1 ? ` That number belongs to \"${sent[0]?.query}\" alone.` : \"\";\n notes.push(\n `${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.${forWording}`,\n );\n } else {\n notes.push(\n `${report.name} states no total and offers no second page, so a short list here is not evidence that little exists.`,\n );\n }\n }\n\n if (answered.some((report) => report.mixesReferencePages && report.count > 0)) {\n const mixing = answered.filter((report) => report.mixesReferencePages && report.count > 0);\n notes.push(\n `${mixing.map((report) => report.name).join(\" and \")} files pages about an ingredient beside ` +\n \"recipes using it, so a row here can be a page about an ingredient rather than a recipe. \" +\n \"get_recipe says what it read off the page.\",\n );\n }\n\n if (answered.length > 1) {\n notes.push(\n \"Each count above measures something different, and they are never added together into one total.\",\n );\n }\n\n if (reports.some((report) => report.cached)) {\n notes.push(\"Part of this answer came from this server's short-lived in-memory cache.\");\n }\n\n return notes;\n}\n\n/**\n * Words this server writes at the start of one of its own lines.\n *\n * A caller has no way to tell one of these from the same words inside a title,\n * an ingredient or a step written by whoever published it, so a value opening\n * with one is indented before it is rendered. Spacing and case are allowed for,\n * because a forged line only has to look like one of these to a reader.\n */\nconst SERVER_MARKERS = /^(\\s*)(Note|Sources?|Hint|Ingredients|Steps|What differs)(\\s*:)/gim;\n\n/**\n * Keep text somebody else wrote out of the shape this server's own lines take.\n *\n * Applied to the foreign value rather than to the assembled block, because the\n * server writes those same words itself: indenting the whole block would indent\n * its own headings and leave the answer looking like quoted text.\n *\n * The structured output still carries the text exactly as it was published;\n * this is the rendered block only.\n */\nexport function indentMarkerLines(value: string): string {\n return value.replace(SERVER_MARKERS, \" $1$2$3\");\n}\n\n/**\n * Put a value somebody else wrote onto one of this server's own lines.\n *\n * Two things make that safe. A line terminator is turned into a space, because\n * a title or a licence carrying a newline can otherwise close the server's\n * sentence and open a whole section of its own: a forged \"Steps:\" block, a\n * forged search row with an address of its own, or a forged credit line. And an\n * image is defused, because most clients render this block as markdown and an\n * image tag fetches its address the moment it is drawn.\n *\n * Everything a source or a caller wrote passes through here on its way into the\n * text block, including into the notes and the credit, which are the lines a\n * reader trusts most.\n */\nexport function quoteForeign(value: string): string {\n return (\n value\n // Every line terminator Unicode recognises, so a value can occupy one line\n // and no more.\n .replace(/[\\r\\n\\u2028\\u2029\\u0085]+/g, \" \")\n // Control characters, which render as nothing and hide what follows them.\n .replace(/[\\u0000-\\u0008\\u000b-\\u001f\\u007f]/g, \"\")\n // An image tag fetches its address on render; escaping the mark leaves the\n // text readable and the request unmade.\n .replace(/!\\[/g, \"!\\\\[\")\n .replace(/\\s{2,}/g, \" \")\n .trim()\n // Now that it occupies one line, a value opening on one of the server's\n // own markers is the last way it could pass for a line the server wrote.\n .replace(SERVER_MARKERS, \" $1$2$3\")\n );\n}\n\n/**\n * How much of the block the notes may take.\n *\n * Enough for the sentences that qualify an answer, while leaving the answer\n * they qualify room to be one. Whatever is dropped is still in the structured\n * output, where a caller reading that instead loses nothing.\n */\nconst NOTE_BUDGET = Math.round(MAX_TEXT_CHARS * 0.55);\n\nexport interface OkOptions {\n notes?: readonly Note[];\n /** The line that credits whoever published what the answer holds. */\n credit?: string;\n}\n\n/**\n * The notes the block will carry, and the trailer they make with the credit.\n *\n * A long run of notes must not crowd out the answer it qualifies, so the last\n * note the answer reads correctly without goes first and the ones it misleads\n * without are still there when the room runs out. Whatever is dropped stays in\n * the structured output.\n */\nfunction buildTrailer(options: OkOptions): string {\n const kept: Note[] = [];\n const held = new Set<string>();\n for (const note of options.notes ?? []) {\n const text = noteText(note);\n if (held.has(text)) {\n continue;\n }\n held.add(text);\n kept.push(note);\n }\n\n const length = (): number => kept.map(noteText).join(\"\\n\").length;\n\n while (kept.length > 0 && length() > NOTE_BUDGET) {\n let victim = -1;\n for (let index = kept.length - 1; index >= 0; index -= 1) {\n if (typeof kept[index] === \"string\") {\n victim = index;\n break;\n }\n }\n // Room runs out with nothing but the sentences the answer misleads without.\n // Dropping one of those buys space by removing the warning the answer most\n // needed, so the body gives up its own room instead.\n if (victim === -1) {\n break;\n }\n kept.splice(victim, 1);\n }\n\n const credit = options.credit ?? \"Source: this server called none.\";\n return [...kept.map((note) => `Note: ${noteText(note)}`), credit].join(\"\\n\");\n}\n\n/**\n * Build a result whose text block ends with its notes and its credit.\n *\n * The body is cut to fit around the trailer rather than the whole block being\n * cut afterwards. Appending the credit and then truncating loses exactly the\n * credit, which is the one line that must survive.\n *\n * Notes qualify an answer: that a source failed, that a quantity was rounded,\n * that a count means one thing here and another there. A client that shows only\n * the text would otherwise present an unqualified answer, so they travel with\n * the credit.\n */\nexport function ok(\n structured: Record<string, unknown>,\n body: string,\n options: OkOptions = {},\n): ToolResult {\n const trailer = buildTrailer(options);\n const cut = \"\\n\\n[shortened; the full result is in the structured output]\";\n const room = MAX_TEXT_CHARS - `\\n\\n${trailer}`.length;\n const text =\n body.length <= room\n ? `${body}\\n\\n${trailer}`\n : `${truncate(body, Math.max(0, room - cut.length))}${cut}\\n\\n${trailer}`;\n\n return { content: [{ type: \"text\", text }], structuredContent: structured };\n}\n\n/**\n * Errors carry no structured payload: the SDK checks it against the tool's\n * declared output schema, and a failure does not fit that shape.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof RecipesError\n ? error\n : new RecipesError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${quoteForeign(known.message)}`];\n if (known.details.hint) {\n lines.push(`Hint: ${quoteForeign(known.details.hint)}`);\n }\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\n/**\n * How much room a body has once its notes and its credit are set aside.\n *\n * A tool rendering several things at once shares this out between them, so each\n * gets an opening rather than the first one filling the block and the rest\n * being cut away.\n */\nexport function roomForBody(options: OkOptions = {}): number {\n return Math.max(200, MAX_TEXT_CHARS - buildTrailer(options).length - 60);\n}\n\n/**\n * The opening of a list, and how much of it is not here.\n *\n * The sentences qualifying an answer keep their room, which leaves the result\n * lines as the thing that gives way when a block runs short. Giving way to\n * nothing at all, in silence, is what turns a hundred ingredients into a recipe\n * that appears to need none, so a few lines are kept whatever the room allows\n * and the caller is told how many are missing.\n */\nexport interface FittedLines {\n lines: string[];\n hidden: number;\n}\n\n/** Lines that were always going to be shown, however tight the block is. */\nconst ALWAYS_SHOWN = 3;\n\nexport function fitLines(lines: readonly string[], room: number): FittedLines {\n const kept: string[] = [];\n let used = 0;\n\n for (const line of lines) {\n if (kept.length >= ALWAYS_SHOWN && used + line.length > room) {\n break;\n }\n kept.push(line);\n used += line.length + 1;\n }\n\n return { lines: kept, hidden: lines.length - kept.length };\n}\n\n/** What a block says about the rows it had no room for. */\nexport function omittedLinesLine(shown: number, total: number, what: string): string {\n return `… ${shown} of ${total} ${what} shown here; all ${total} are in the structured output`;\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`;\n}\n\n/** A compact listing, carrying what it takes to pick one recipe out of many. */\nexport function renderRows(rows: z.infer<typeof rowSchema>[]): string {\n return rows\n .map((row, index) => {\n const head = `${index + 1}. ${quoteForeign(row.title)} · ${quoteForeign(row.source_name)} · id: ${row.id}`;\n // The address goes on its own line: a client that renders only text has\n // nothing else to cite from, and a model with an identifier and no link\n // will build one.\n return `${head}\\n ${quoteForeign(row.url)}`;\n })\n .join(\"\\n\");\n}\n\n/**\n * The credit line, naming the sources that actually contributed to this answer.\n *\n * A source that failed, or that was never asked, has published nothing here and\n * crediting it would say it had.\n */\nexport function creditLine(contributors: Array<{ attribution: string; url?: string }>): string {\n if (contributors.length === 0) {\n return \"No source contributed to this answer.\";\n }\n const names = contributors.map((entry) =>\n entry.url ? `${entry.attribution} — ${quoteForeign(entry.url)}` : entry.attribution,\n );\n return names.join(\"\\n\");\n}\n","/**\n * How a tool's arguments are declared, and what happens to one that is not.\n *\n * An argument this server does not declare is a question it cannot answer.\n * Reading it and dropping it produces an answer computed on the defaults, which\n * a caller reads as the answer to what they asked. So an undeclared argument is\n * refused, and the refusal names it and offers the declared name when one is\n * close enough to be the one that was meant.\n *\n * A caller branches on the code a refusal opens with, and arguments are refused\n * along two paths: the code of a tool writes its own refusals, and the schema\n * refuses on its own before that code runs. Both open with the same code here,\n * so a caller reading the vocabulary finds it whichever path refused.\n */\n\nimport { z } from \"zod\";\n\n/** The code a caller branches on when the arguments cannot produce a request. */\nconst INVALID_INPUT = \"invalid_input\";\n\n/** Zod's own wording. The error code is written in front of it. */\nconst defaultMessage = z.core.locales.en().localeError;\n\n/** Declare a tool's arguments, refusing anything outside the declaration. */\nexport function strictInput<Shape extends z.ZodRawShape>(shape: Shape) {\n const declared = Object.keys(shape);\n\n const refuse: z.core.$ZodErrorMap = (issue) =>\n issue.code === \"unrecognized_keys\"\n ? unknownArgumentMessage(issue.keys, declared)\n : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;\n\n for (const argument of Object.values(shape)) {\n carryTheCode(argument, refuse);\n }\n\n return z.strictObject(shape, { error: refuse });\n}\n\n/**\n * Route every refusal a declared argument can raise through one error map.\n *\n * A bound such as `min` raises its issue from the check that holds it, and the\n * issue names that check as the instance whose error map applies, so the map\n * reaches it only by being written onto the check itself. Zod offers no copy\n * that keeps a check's identity, hence the assignment. It is confined to\n * schemas built for a declaration and handed to this function, which is the\n * whole of what a tool module does with them.\n */\nfunction carryTheCode(schema: z.core.$ZodType, refuse: z.core.$ZodErrorMap): void {\n const definition = schema._zod.def as {\n error?: unknown;\n checks?: Array<{ _zod: { def: { error?: unknown } } }>;\n innerType?: z.core.$ZodType;\n };\n\n definition.error = refuse;\n for (const check of definition.checks ?? []) {\n check._zod.def.error = refuse;\n }\n\n // A default, an optional or any other wrapper answers for its own shape and\n // leaves the bounds to the type it wraps.\n if (definition.innerType) {\n carryTheCode(definition.innerType, refuse);\n }\n}\n\nfunction unknownArgumentMessage(keys: readonly string[], declared: readonly string[]): string {\n const named = keys\n .map((key) => {\n const near = nearestArgument(key, declared);\n return near ? `'${key}' (did you mean '${near}'?)` : `'${key}'`;\n })\n .join(\", \");\n\n return (\n `[${INVALID_INPUT}] Unknown ${keys.length > 1 ? \"arguments\" : \"argument\"} ${named}. ` +\n `This tool takes: ${declared.join(\", \")}.`\n );\n}\n\n/**\n * The declared name a caller most plausibly meant, when there is one.\n *\n * Three readings, ordered by how much each claims: the same name written\n * differently, a name that opens or closes the other, and a name a couple of\n * typing slips away. Anything further is left unnamed, because a suggestion\n * that misses sends a caller to an argument answering a different question.\n */\nfunction nearestArgument(key: string, declared: readonly string[]): string | undefined {\n const flatten = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n const flat = flatten(key);\n if (flat.length === 0) {\n return undefined;\n }\n\n const sameName = declared.find((name) => flatten(name) === flat);\n if (sameName) {\n return sameName;\n }\n\n // Either name may be the longer one: a caller can qualify a name this tool\n // keeps plain, or shorten one it spells out.\n const overlapping = declared.find((name) => {\n const other = flatten(name);\n const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];\n // Two characters in common say nothing; three start to.\n return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));\n });\n if (overlapping) {\n return overlapping;\n }\n\n let closest: string | undefined;\n let shortest = Number.POSITIVE_INFINITY;\n for (const name of declared) {\n const distance = editDistance(flat, flatten(name));\n if (distance < shortest) {\n shortest = distance;\n closest = name;\n }\n }\n\n // Up to a third of the name may differ. Past that the match is a guess.\n return shortest <= Math.max(1, Math.floor(flat.length / 3)) ? closest : undefined;\n}\n\n/**\n * Single-character insertions, deletions and substitutions between two words.\n *\n * One row of the matrix is kept at a time. The cell above comes from walking\n * that row, and the two other neighbours a cell needs, the one to its left and\n * the one diagonally above it, are carried forward from the step before, so\n * every value the walk uses is one it has just computed.\n */\nfunction editDistance(left: string, right: string): number {\n let previous = Array.from({ length: right.length + 1 }, (_, index) => index);\n let distance = right.length;\n\n for (let row = 1; row <= left.length; row += 1) {\n const current: number[] = [row];\n let diagonal = row - 1;\n let leftward = row;\n\n for (const [column, above] of previous.entries()) {\n if (column === 0) {\n continue;\n }\n const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);\n const cell = Math.min(substitution, above + 1, leftward + 1);\n current.push(cell);\n diagonal = above;\n leftward = cell;\n }\n\n previous = current;\n distance = leftward;\n }\n\n return distance;\n}\n","/**\n * get_recipe: read one recipe, from the source its identifier names.\n *\n * Only that source is called. Trying another after a miss would answer a\n * question about one corpus with somebody else's recipe of a different name,\n * which is a worse answer than the absence it replaced.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { buildRecipeView, recipeSchema, renderYield, SECTIONS } from \"./recipeView.js\";\nimport type { Section } from \"./recipeView.js\";\nimport { strictInput } from \"./arguments.js\";\nimport {\n creditLine,\n fitLines,\n noteTexts,\n ok,\n omittedLinesLine,\n quoteForeign,\n roomForBody,\n toToolError,\n} from \"./shared.js\";\nimport type { Note, ToolResult } from \"./shared.js\";\n\nexport const getRecipeDescription = [\n \"Read one recipe in full: its ingredients, its steps, what it yields, and whatever times, rating and nutrition its source publishes.\",\n \"'id' must come from search_recipes. It names the source, so this reads the right one without guessing; an identifier no source would have minted is refused, because sending it anywhere would answer about the wrong dish.\",\n \"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable is flagged rather than scaled.\",\n \"A page that states no number of servings comes back as published and says so, because dividing by a yield nobody wrote would answer for a number of people the page never claimed.\",\n \"A part this answer holds nothing for says which of two things happened: the page showed no sign of it, or this server failed to read what the page carries. An empty ingredient list is never evidence that an ingredient is absent from the dish.\",\n \"'sections' decides what comes back, and 'sections_omitted' names what was left out: a field belonging to an omitted section is empty because nobody asked for it, never because the page states nothing.\",\n \"A field a source does not publish is null, never zero. Credit the source and link the url when you repeat any of it.\",\n].join(\" \");\n\nexport const getRecipeInput = strictInput({\n id: z\n .string()\n .min(1)\n .max(500)\n .describe(\n \"From search_recipes, such as 'marmiton:44078', 'goodfood:recipes/carbonara' or 'cookbook:Cookbook:Carbonara'. Two sources address a recipe by a bare number, so spell an id with its source.\",\n ),\n servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Rescale to this many. Left out, the quantities come back as published.\"),\n sections: z\n .array(z.enum(SECTIONS))\n .default([\"ingredients\", \"steps\"])\n .describe(\"Which parts to return. A full recipe is a lot of text, so this defaults to two.\"),\n max_steps: z\n .number()\n .int()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Steps to return. The answer says how many more there are.\"),\n max_step_chars: z\n .number()\n .int()\n .min(80)\n .max(4000)\n .default(600)\n .describe(\"Characters kept per step. Raise it only if a step was cut mid-sentence.\"),\n});\n\nexport const getRecipeOutput = z.object({\n recipe: recipeSchema,\n id_read_as: z\n .string()\n .nullable()\n .describe(\"How a raw identifier was routed, when it was not spelled with its source.\"),\n notes: z.array(z.string()),\n});\n\nexport type GetRecipeArgs = z.infer<typeof getRecipeInput>;\n\nexport async function runGetRecipe(\n client: RecipesClient,\n args: GetRecipeArgs,\n): Promise<ToolResult> {\n try {\n const { recipe, cached, read } = await client.getRecipe(args.id);\n const view = buildRecipeView(recipe, {\n servings: args.servings ?? null,\n sections: args.sections as readonly Section[],\n maxSteps: args.max_steps,\n maxStepChars: args.max_step_chars,\n });\n\n const notes: Note[] = [...view.notes];\n if (read.inferred) {\n notes.unshift(\n `\"${quoteForeign(args.id)}\" was read as ${read.source.name}'s, because ${read.inferred}. ` +\n \"Spell an id with its source to leave nothing to infer.\",\n );\n }\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const payload = view.payload;\n const credit = creditLine([{ attribution: payload.attribution, url: payload.url }]);\n const head = [\n `${quoteForeign(payload.title)} · ${quoteForeign(payload.source_name)}`,\n quoteForeign(payload.url),\n renderYield(payload),\n ];\n\n const ingredientLines = payload.ingredients.map((entry) =>\n entry.scaling === \"unscaled\"\n ? `- ${quoteForeign(entry.text)} (no quantity)`\n : `- ${quoteForeign(entry.text)}`,\n );\n const stepLines = payload.steps.map((step, index) => `${index + 1}. ${quoteForeign(step)}`);\n\n // A long method and a long ingredient list share what the notes leave, so\n // neither of them fills the block and cuts the other away.\n const room = roomForBody({ notes, credit }) - head.join(\"\\n\").length;\n const parts = [ingredientLines, stepLines].filter((part) => part.length > 0);\n const share = parts.length === 0 ? 0 : room / parts.length;\n\n const lines = [...head];\n const ingredients = fitLines(ingredientLines, share);\n if (ingredientLines.length > 0) {\n lines.push(\"\", \"Ingredients:\", ...ingredients.lines);\n if (ingredients.hidden > 0) {\n lines.push(\n omittedLinesLine(ingredients.lines.length, ingredientLines.length, \"ingredient lines\"),\n );\n }\n }\n const steps = fitLines(stepLines, share);\n if (stepLines.length > 0) {\n lines.push(\"\", \"Steps:\", ...steps.lines);\n if (steps.hidden > 0) {\n lines.push(omittedLinesLine(steps.lines.length, stepLines.length, \"steps\"));\n }\n }\n\n return ok(\n { recipe: payload, id_read_as: read.inferred, notes: noteTexts(notes) },\n lines.join(\"\\n\"),\n { notes, credit },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * scale_ingredients: multiply a list a caller already holds.\n *\n * Nothing here touches the network. The list can come from any of the sources,\n * from a cookbook on a shelf or from a photograph of a card, and it can mix\n * French and English lines: each line is read in the language it is written in\n * and comes back written the same way.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport { scaleIngredients } from \"../recipe/scale.js\";\nimport type { LanguageChoice } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ingredientSchema, ok, quoteForeign, toIngredientPayload, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const scaleIngredientsDescription = [\n \"Multiply a list of ingredient lines, in French, in English, or in a list holding both.\",\n \"Give either 'factor', or 'from_servings' and 'to_servings' and the factor is worked out from them.\",\n \"Quantities land where a kitchen can follow them: an egg stays whole because half of one is not something a cook takes out of the shell, while anything that pours, weighs or cuts can halve, a spoonful shrinks into the smaller spoon before it is rounded, and a pinch keeps whatever size a hand gives it while its count is multiplied.\",\n \"Every line comes back with 'scaling': 'scaled' when the arithmetic landed on the exact product, 'rounded' when something had to move for the line to stay usable, 'unscaled' when the line carries nothing to multiply. A rounded line says what it was rounded from and in which direction.\",\n \"No quantity is converted between measuring systems: grams stay grams and cups stay cups, because a conversion changes what the recipe said.\",\n].join(\" \");\n\nexport const scaleIngredientsInput = strictInput({\n ingredients: z\n .array(z.string().max(500))\n .min(1)\n .max(200)\n .describe(\n \"The lines as written, one ingredient each, such as '200 g de farine' or '3 eggs'. One line, not a whole recipe.\",\n ),\n factor: z\n .number()\n .gt(0)\n .max(1000)\n .optional()\n .describe(\n \"What to multiply by. Give exactly one of: 'factor', or both 'from_servings' and 'to_servings'.\",\n ),\n from_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What the list serves now. Give it with 'to_servings', and without 'factor'.\"),\n to_servings: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"What it should serve. Give it with 'from_servings', and without 'factor'.\"),\n language: z\n .enum([\"auto\", \"fr\", \"en\"])\n .default(\"auto\")\n .describe(\n \"'auto' reads each line on its own, which is what a list holding both languages needs. Name a language to read every line that way.\",\n ),\n});\n\nexport const scaleIngredientsOutput = z.object({\n factor: z.number().describe(\"What every quantity was multiplied by.\"),\n language: z.string().describe(\"How the lines were read.\"),\n ingredients: z.array(ingredientSchema),\n scaled_count: z.number().int().describe(\"Lines whose arithmetic came out exact.\"),\n rounded_count: z.number().int().describe(\"Lines whose value moved to stay usable.\"),\n unscaled_count: z.number().int().describe(\"Lines carrying nothing that can be multiplied.\"),\n notes: z.array(z.string()),\n});\n\nexport type ScaleIngredientsArgs = z.infer<typeof scaleIngredientsInput>;\n\n/**\n * Read the factor off whichever pair of arguments the caller gave.\n *\n * The two ways of saying it are kept apart rather than merged, because a caller\n * who gives both and means different things by them has made a mistake worth\n * hearing about.\n */\nexport function resolveFactor(args: ScaleIngredientsArgs): number {\n const hasPair = args.from_servings !== undefined && args.to_servings !== undefined;\n\n if (args.factor !== undefined && hasPair) {\n throw invalidInput(\n \"Give either 'factor' or the pair of serving counts, not both.\",\n `factor=${args.factor} and ${args.from_servings}→${args.to_servings} would be two different multipliers.`,\n );\n }\n\n if (args.factor !== undefined) {\n return args.factor;\n }\n const { from_servings: from, to_servings: to } = args;\n if (from !== undefined && to !== undefined) {\n return to / from;\n }\n\n if (args.from_servings !== undefined || args.to_servings !== undefined) {\n throw invalidInput(\n \"Serving counts come in pairs: give both 'from_servings' and 'to_servings'.\",\n \"Or give 'factor' on its own.\",\n );\n }\n\n throw invalidInput(\n \"Nothing said how much to multiply by.\",\n \"Give 'factor', or both 'from_servings' and 'to_servings'.\",\n );\n}\n\n/**\n * The factor as a reader can check the quantities against.\n *\n * A pair of serving counts divides into a repeating decimal, so what is stated\n * is shortened. Shortening it to three places puts a small factor at zero, and\n * \"Scaled by 0\" beside quantities that are not zero contradicts the answer it\n * introduces; below that, the significant digits are what says what happened.\n */\nfunction statedFactor(factor: number): number {\n const shortened = Math.round(factor * 1000) / 1000;\n return shortened === 0 ? Number(factor.toPrecision(3)) : shortened;\n}\n\nexport function runScaleIngredients(args: ScaleIngredientsArgs): ToolResult {\n try {\n const factor = resolveFactor(args);\n const scaled = scaleIngredients(args.ingredients, {\n factor,\n language: args.language as LanguageChoice,\n });\n\n const counts = {\n scaled_count: scaled.filter((entry) => entry.scaling === \"scaled\").length,\n rounded_count: scaled.filter((entry) => entry.scaling === \"rounded\").length,\n unscaled_count: scaled.filter((entry) => entry.scaling === \"unscaled\").length,\n };\n\n const notes: string[] = [];\n if (counts.rounded_count > 0) {\n notes.push(\n `${counts.rounded_count} line(s) did not land on the exact product; each says in its own ` +\n \"note what happened to it.\",\n );\n }\n if (counts.unscaled_count > 0) {\n notes.push(\n `${counts.unscaled_count} line(s) carry no quantity to multiply and are repeated as given.`,\n );\n }\n if (args.language === \"auto\") {\n const languages = new Set(scaled.map((entry) => entry.language));\n if (languages.size > 1) {\n notes.push(\n \"This list holds lines in both languages, and each was read and rewritten in its own.\",\n );\n }\n }\n if (scaled.some((entry) => entry.note?.includes(\"approximate measure\"))) {\n notes.push(\n \"A pinch, a handful and their kind were multiplied by their count. The size of one stays \" +\n \"whatever the cook's hand gives it, and none of them was turned into grams or spoons.\",\n );\n }\n\n const body = scaled\n .map((entry, index) => `${index + 1}. ${quoteForeign(entry.text)}`)\n .join(\"\\n\");\n\n return ok(\n {\n factor: statedFactor(factor),\n language: args.language,\n ingredients: scaled.map(toIngredientPayload),\n ...counts,\n notes,\n },\n `Scaled by ${statedFactor(factor)}:\\n${body}`,\n { notes, credit: \"Source: the list you supplied. Scaled offline; nothing was fetched.\" },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_recipes: one question, every source, one list.\n *\n * The calls go out together, so asking them all costs about what asking one\n * costs. What each source said about its own results is reported beside the\n * rows, because those numbers count different things and adding them would\n * invent a figure no source published.\n */\n\nimport { z } from \"zod\";\nimport type { RecipesClient } from \"../sources/client.js\";\nimport { namesDish, readConditions } from \"../sources/wordings.js\";\nimport type { ConditionKind } from \"../sources/wordings.js\";\nimport type { SourceId } from \"../types.js\";\nimport {\n creditLine,\n mustKeep,\n noteTexts,\n ok,\n quoteForeign,\n renderRows,\n reportNotes,\n reportSchema,\n rowSchema,\n toReportPayload,\n toRowPayload,\n toToolError,\n} from \"./shared.js\";\nimport { strictInput } from \"./arguments.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nconst WHITESPACE = /\\s+/;\n\n/**\n * The order the rows are in, and what no order could be.\n *\n * No score orders the sources against each other, so the only order this server\n * can describe is the one it imposed.\n */\nfunction howTheRowsAreOrdered(contributed: ReadonlyArray<{ name: string }>): string {\n if (contributed.length > 1) {\n return \"One row from each source in turn, in the order each source returned them. No score orders them against each other.\";\n }\n if (contributed.length === 1) {\n return `Every row came from ${contributed[0]?.name}, in the order it returned them.`;\n }\n return \"No source contributed a row.\";\n}\n\n/**\n * What an empty answer amounts to.\n *\n * No source answering and every source holding nothing are different statements\n * about the world, and a caller cannot tell them apart from an empty list.\n */\nfunction nothingCameBack(answeredCount: number, query: string, alsoSent: string): string {\n if (answeredCount === 0) {\n return `No source answered for \"${query}\", so nothing here says whether such a recipe exists.`;\n }\n return `Nothing came back for \"${query}\".${alsoSent}`;\n}\n\nexport const searchRecipesDescription = [\n \"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.\",\n \"Each row carries the id get_recipe takes, and that id names the source it came from, so nothing has to be guessed afterwards.\",\n \"The sources are written in different languages and count their own results differently, so 'per_source' says what each one answered, what its own number means, and names any that failed. A short list is never evidence of what exists.\",\n \"Some sources keep recipes and reference pages together, so a row can be a page about an ingredient rather than a recipe using it. Only get_recipe can tell them apart, and it says what it read off the page.\",\n \"The query goes to each source's own search as free text. There is no filtering: a word naming a diet, a time or a calorie count matches only where that source's index happens to carry it.\",\n \"Ask in a whole sentence if that is the question. These indexes answer the words they are handed, so a sentence is also sent as the words naming the dish and as the dish word alone, and the rows are the union; 'per_source' lists every wording and what it returned.\",\n \"What the question says the recipe must not hold is set aside from those shorter wordings rather than searched for, and named back in the notes: a negation, an allergy stated as one, a diet named in one word, and the number of people at the table. No source filters on any of it, so open a row with get_recipe and read the ingredient list before calling it suitable.\",\n \"A condition is read with the food on whichever side of it the sentence put one, so 'allergique aux noix' and 'peanut allergy' both name the nut. Where a sentence puts a food on neither side, the notes say a condition was stated and that its food was not read, because naming the wrong word would hide a dish and search for the food being avoided at once.\",\n \"Rows are interleaved one source at a time rather than ranked, because the sources share no score that could order them against each other.\",\n].join(\" \");\n\nexport const searchRecipesInput = strictInput({\n query: z\n .string()\n .min(1)\n .max(200)\n .describe(\"A dish or an ingredient, in any of the languages the sources publish in.\"),\n limit_per_source: z\n .number()\n .int()\n .min(1)\n .max(25)\n .default(5)\n .describe(\"Rows to take from each source, so one source cannot fill the whole list.\"),\n sources: z\n .array(z.string())\n .optional()\n .describe(\n \"Sources to ask, by id. Left out, they are all asked, which is the point of this tool. The ids are the ones 'per_source' reports.\",\n ),\n fan_out: z\n .boolean()\n .default(true)\n .describe(\n \"Whether a question may also be sent in shorter wordings derived from it. On, because these indexes answer the words they are handed: a question written as a sentence comes back empty from a corpus holding several of the dish. Turn it off to send exactly the words typed; 'per_source' then names the wordings that were withheld.\",\n ),\n});\n\nexport const searchRecipesOutput = z.object({\n query: z.string(),\n results: z.array(rowSchema),\n result_count: z.number().int().describe(\"Rows in this answer, across every source.\"),\n per_source: z.array(reportSchema),\n order: z.string().describe(\"How the list was built, in words.\"),\n notes: z.array(z.string()),\n});\n\nexport type SearchRecipesArgs = z.infer<typeof searchRecipesInput>;\n\n/**\n * Beyond this many words a question is asking for conditions rather than naming\n * a dish, so the answer says that none of them were applied as conditions.\n */\nconst WORDS_BEFORE_SAYING_SO = 4;\n\nexport async function runSearchRecipes(\n client: RecipesClient,\n args: SearchRecipesArgs,\n): Promise<ToolResult> {\n try {\n const merged = await client.searchRecipes(\n args.query,\n args.limit_per_source,\n args.sources as readonly SourceId[] | undefined,\n { fanOut: args.fan_out },\n );\n\n const results = merged.rows.map(toRowPayload);\n const notes = reportNotes(merged.reports);\n\n const answered = merged.reports.filter((report) => report.status === \"answered\");\n const contributed = merged.reports.filter((report) => report.count > 0);\n\n // How the order was built depends on how many sources are in it. Describing\n // a merge that did not happen tells a caller the list means something it\n // does not.\n const order = howTheRowsAreOrdered(contributed);\n\n // What a question asks the dish to avoid is the part of it no source can\n // answer, and the part a reader is most harmed by getting wrong. A recipe\n // returned here may hold exactly the ingredient the person was avoiding, so\n // the conditions are named back rather than left to be assumed met.\n const conditions = readConditions(args.query);\n const named = (kind: ConditionKind): string =>\n conditions.conditions\n .filter((condition) => condition.kind === kind && condition.named !== null)\n .map((condition) => `\"${quoteForeign(condition.named as string)}\"`)\n .join(\" and \");\n // A condition whose food could not be read is still a condition the person\n // set, and the one thing an answer must not do with it is stay quiet.\n const unread = (kind: ConditionKind): boolean =>\n conditions.conditions.some(\n (condition) => condition.kind === kind && condition.named === null,\n );\n\n // An allergy is named as one. It is the same fact as an exclusion and not\n // the same stake: a row that turns out to carry the ingredient is a dish\n // somebody cannot eat.\n const allergies = named(\"allergy\");\n if (allergies !== \"\") {\n notes.push(\n mustKeep(\n `This question states an allergy to ${allergies}. No source filtered on it, and none can: ` +\n \"these are searches over what a page holds rather than over what it leaves out, so a row \" +\n \"here may be a recipe built on that ingredient. Open every row with get_recipe and read \" +\n \"the whole ingredient list before cooking from it.\",\n ),\n );\n }\n if (unread(\"allergy\")) {\n notes.push(\n mustKeep(\n \"This question states an allergy, and the words around it do not say plainly which food \" +\n \"it names, so no food was read from it and none is named here. Nothing was set aside \" +\n \"and no source filters on an allergy in any case: open every row with get_recipe and \" +\n \"read the whole ingredient list before cooking from it.\",\n ),\n );\n }\n\n const excluded = named(\"excluded\");\n if (excluded !== \"\") {\n notes.push(\n mustKeep(\n `This question asks for a recipe free of ${excluded}. No source filtered on that, and none ` +\n \"can: these are searches over what a page holds rather than over what it leaves out. \" +\n \"Open a row with get_recipe and check the ingredient list before calling it suitable.\",\n ),\n );\n }\n if (unread(\"excluded\")) {\n notes.push(\n mustKeep(\n \"This question says the recipe must leave something out, and the words around it do not \" +\n \"say plainly which food it names, so nothing was read from it and nothing is named \" +\n \"here. Open a row with get_recipe and read the ingredient list before calling it \" +\n \"suitable.\",\n ),\n );\n }\n\n const diets = named(\"diet\");\n if (diets !== \"\") {\n notes.push(\n mustKeep(\n `This question names a diet: ${diets}. A diet is a rule about what a recipe leaves out, and ` +\n \"no source filters on one, so the word was sent as text and matches only where a source's \" +\n \"index happens to carry it. Open a row with get_recipe and read the ingredient list \" +\n \"before calling it suitable.\",\n ),\n );\n }\n if (conditions.servings !== null) {\n notes.push(\n mustKeep(\n `This question asks for ${conditions.servings} servings. That is not something a search ` +\n \"matches, so it was not looked for. Pass 'servings' to get_recipe or compare_recipes to \" +\n \"rescale a recipe once one is chosen.\",\n ),\n );\n }\n\n // A long question reads like a request for filtering, and none is applied:\n // the words go to each source's own search as they were typed. A caller\n // told nothing would report \"desserts under 300 calories\" as a filtered\n // answer, when the number was matched as text or matched nothing at all.\n // This is said even where a condition was read by name, because the ones\n // read by name are the ones this server can recognise and a question can\n // carry others.\n if (args.query.trim().split(WHITESPACE).length > WORDS_BEFORE_SAYING_SO) {\n notes.push(\n mustKeep(\n \"This query went to each source as free text and no filter was applied to it. Anything the \" +\n \"question said the recipe must not contain went out as words like the rest of it, so read \" +\n \"the rows rather than assuming they meet it.\",\n ),\n );\n }\n\n // A source ranking a title on the letters it opens with answers \"chameau\"\n // with a chapeau and three châteaux. The rows are what the sources offered,\n // and a reader shown them without a word about it takes them for the dish.\n if (results.length > 0 && !results.some((row) => namesDish(row.title, args.query))) {\n notes.push(\n `No title here carries a word of \"${args.query}\". These rows are what the sources ranked ` +\n \"for that spelling, so read them as candidates to check rather than as recipes for the dish.\",\n );\n }\n\n // The one sentence that separates a wording every source answered from a\n // search that failed. Each source also says for itself that it offered no\n // row, and where there are several of those the per-source lines crowd the\n // block; this is the line the answer cannot be read safely without.\n if (results.length === 0 && answered.length === merged.reports.length) {\n notes.push(\n mustKeep(\n \"Every source answered and none holds anything under this wording. Try the dish's name in another language, or name a main ingredient instead.\",\n ),\n );\n }\n\n // A reader who can see what was actually sent can redo the search by hand,\n // and can tell a wording that found nothing from a corpus that holds\n // nothing. The words as asked are left out of this line: they are already\n // at the head of the answer.\n const derived = [\n ...new Set(\n merged.reports.flatMap((report) =>\n report.wordings\n .filter((attempt) => attempt.ran && attempt.query !== args.query.trim())\n .map((attempt) => attempt.query),\n ),\n ),\n ];\n const alsoSent =\n derived.length > 0\n ? `\\nAlso searched for: ${derived.map((wording) => `\"${quoteForeign(wording)}\"`).join(\", \")}.`\n : \"\";\n\n const body =\n results.length > 0\n ? `${results.length} recipes for \"${args.query}\":\\n${renderRows(results)}${alsoSent}`\n : nothingCameBack(answered.length, args.query, alsoSent);\n\n return ok(\n {\n query: args.query,\n results,\n result_count: results.length,\n per_source: merged.reports.map(toReportPayload),\n order,\n notes: noteTexts(notes),\n },\n body,\n {\n notes,\n credit: creditLine(\n contributed.map((report) => ({ attribution: `Source: ${report.name}` })),\n ),\n },\n );\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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ACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EA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","names":["process","SITE_URL","path","SITE_URL","REFERENCE","SITE_URL","REFERENCE","DOT_SEGMENT","LEADING_SLASH","asReference","meansFor","path","REFERENCE","count","z","z","VOCABULARY","text","count","text","fraction","text","heldBack","AL_ENDING","AUX_ENDING","EAUX_ENDING","EAU_ENDING","SIBILANT_LETTER","TRAILING_S_ANY_CASE","bounds","foldWord","text","count","text","text","names","z","z","z","names","z","z","z","z","resolveFactor","z","WHITESPACE","z","process"]}