mcp-marmiton 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/errors.ts","../src/marmiton/cache.ts","../src/marmiton/rateLimiter.ts","../src/marmiton/http.ts","../src/marmiton/parseRecipe.ts","../src/recipe/duration.ts","../src/marmiton/jsonld.ts","../src/marmiton/urls.ts","../src/marmiton/client.ts","../src/tools/getRecipe.ts","../src/recipe/units.ts","../src/recipe/quantity.ts","../src/recipe/scale.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/scaleIngredients.ts","../src/tools/searchRecipes.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for Marmiton over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails is no reason to stay\n // alive: the process leaves either way.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-marmiton] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\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\n/**\n * MARMITON asks third-party clients to identify themselves with an application\n * name, a version and a link to the project, so the default follows that format.\n */\nexport const DEFAULT_USER_AGENT = `mcp-marmiton v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * MARMITON publishes no rate limit and asks for nothing in return for a free\n * service. Self-imposed pacing is the whole of the politeness contract here, so\n * a value below this floor is refused and the default is used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-marmiton] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 200, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.MARMITON_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MARMITON_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `MARMITON_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Marmiton serves these pages for readers, so this client paces itself.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MARMITON_USER_AGENT?.trim();\n const rawLogLevel = env.MARMITON_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`MARMITON_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MARMITON_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MARMITON_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MARMITON_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MARMITON_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-marmiton] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so it\n * must not drift; the release checklist bumps all three together.\n */\nexport const PKG_VERSION = \"2.0.1\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-marmiton\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache are shared by all tools, so pacing\n * applies to the server as a whole rather than per tool.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { MarmitonClient } from \"./marmiton/client.js\";\nimport {\n getRecipeDescription,\n getRecipeInput,\n getRecipeOutputShape,\n runGetRecipe,\n} from \"./tools/getRecipe.js\";\nimport type { GetRecipeArgs } from \"./tools/getRecipe.js\";\nimport {\n runScaleIngredients,\n scaleIngredientsDescription,\n scaleIngredientsInput,\n scaleIngredientsOutputShape,\n} from \"./tools/scaleIngredients.js\";\nimport type { ScaleIngredientsArgs } from \"./tools/scaleIngredients.js\";\nimport {\n runSearchRecipes,\n searchRecipesDescription,\n searchRecipesInput,\n searchRecipesOutputShape,\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 fetchImpl?: typeof fetch;\n}\n\n/** This server only reads, so every tool is read-only. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\n/**\n * scale_ingredients does arithmetic on the arguments it was handed and contacts\n * nothing, so its world is closed: the same list and the same factor give the\n * same answer forever, whatever Marmiton publishes.\n */\nconst OFFLINE = { ...READ_ONLY, openWorldHint: false } as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new MarmitonClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-marmiton\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for looking up French recipes on Marmiton. No API key is needed. \" +\n \"Typical flow: search_recipes to find a recipe and its id, then get_recipe with that id, \" +\n \"passing 'servings' when the user wants a different number of people. \" +\n \"Do not rescale quantities yourself: get_recipe and scale_ingredients already round countable \" +\n \"items sensibly and flag what cannot be scaled, which is what stops answers like '2.4 eggs'. \" +\n \"Marmiton disallows paginating search results, so narrow the query instead of asking for more pages. \" +\n \"When you show a recipe to a user, credit it and link the source URL.\",\n },\n );\n\n server.registerTool(\n \"search_recipes\",\n {\n title: \"Search recipes\",\n description: searchRecipesDescription,\n inputSchema: searchRecipesInput,\n outputSchema: z.object(searchRecipesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchRecipes(client, args as SearchRecipesArgs),\n );\n\n server.registerTool(\n \"get_recipe\",\n {\n title: \"Get a recipe\",\n description: getRecipeDescription,\n inputSchema: getRecipeInput,\n outputSchema: z.object(getRecipeOutputShape),\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\",\n description: scaleIngredientsDescription,\n inputSchema: scaleIngredientsInput,\n outputSchema: z.object(scaleIngredientsOutputShape),\n annotations: OFFLINE,\n },\n async (args) => runScaleIngredients(args as ScaleIngredientsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, cache ${config.cacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule carried over from the sibling projects: a failure must never be\n * reported as an empty result. A model that sees \"no recipe found\" cannot tell\n * that apart from a genuine absence, and will confidently tell the user the\n * recipe does not exist.\n */\n\nexport type ErrorCode =\n | \"not_found\"\n | \"invalid_input\"\n | \"rate_limited\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class MarmitonError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"MarmitonError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-marmiton/issues\";\n\nexport function notFound(url: string, what: string): MarmitonError {\n return new MarmitonError(\"not_found\", `Marmiton has no recipe at ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_recipes to find a recipe and its id, then call this tool with that id.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): MarmitonError {\n return new MarmitonError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): MarmitonError {\n return new MarmitonError(\n \"rate_limited\",\n \"Marmiton is rate limiting this client. This does NOT mean the recipe does not exist.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MARMITON_MIN_INTERVAL_MS.\",\n },\n );\n}\n\nexport function parseFailure(url: string, what: string): MarmitonError {\n return new MarmitonError(\n \"parse_failure\",\n `The page loaded but the expected structured recipe data was not found (${what}). ` +\n \"Marmiton may have changed how it publishes it.\",\n { url, hint: `Please report this, with the recipe you asked for, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): MarmitonError {\n return new MarmitonError(\"network_error\", `Marmiton returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500 ? { hint: \"This is a problem on Marmiton's side. Try again shortly.\" } : {}),\n });\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same recipe inside one conversation do not hit Marmiton again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Marmiton publishes no rate limit and answers a burst by refusing rather than\n * by slowing down, so requests run one at a time with a floor on the gap between\n * starts. When the site does push back, the interval doubles and then decays\n * back down as requests succeed, which recovers faster than a fixed delay and\n * behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Marmiton answers with ordinary status codes, so this layer mostly translates\n * them. The one judgement call is on a body too small to be a real page, which is\n * how a site under load often refuses without saying so.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { MarmitonError, notFound, rateLimited, upstreamError } from \"../errors.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** A real Marmiton page weighs well over 100 KB. */\nconst MIN_PLAUSIBLE_HTML = 2000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * What one answer from the site amounts to.\n *\n * Three outcomes and no fourth: the page is usable, the address holds nothing\n * and never will, or the site is asking to be left alone for a while. Reading\n * this apart from the loop keeps the loop about attempts and waits, which is\n * the part where a mistake costs the site traffic it did not ask for.\n */\ntype Answer =\n | { kind: \"usable\" }\n | { kind: \"refused\"; error: MarmitonError }\n | {\n kind: \"again\";\n error: MarmitonError;\n waitMs: number | null;\n penalise: boolean;\n because: string;\n };\n\nfunction readAnswer(\n url: string,\n status: number,\n body: string,\n retryAfterMs: number | null,\n ownGuessMs: number,\n): Answer {\n if (status === 429 || status === 503 || status === 403) {\n return {\n kind: \"again\",\n error: rateLimited(url, retryAfterMs ?? ownGuessMs),\n waitMs: retryAfterMs,\n penalise: true,\n because: \"rate limited\",\n };\n }\n if (status === 404) {\n return { kind: \"refused\", error: notFound(url, \"that address\") };\n }\n if (status >= 500) {\n return {\n kind: \"again\",\n error: upstreamError(url, status),\n waitMs: null,\n penalise: false,\n because: `status ${status}`,\n };\n }\n if (status >= 400) {\n return { kind: \"refused\", error: upstreamError(url, status) };\n }\n\n const trimmed = body.trim();\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n // Too small to be a page: treat it as a refusal rather than parse it and\n // report an empty result.\n return {\n kind: \"again\",\n error: rateLimited(url, ownGuessMs),\n waitMs: null,\n penalise: true,\n because: \"implausibly short body\",\n };\n }\n\n return { kind: \"usable\" };\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The retry loop and its sleeps run inside a single limiter slot, so a queued\n * request cannot slip into the window the current one is backing away from.\n */\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: MarmitonError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"fr-FR,fr;q=0.9,en;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await response.text();\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = readAnswer(url, status, body, retryAfterMs, backoffDelay(attempt));\n if (verdict.kind === \"refused\") {\n throw verdict.error;\n }\n if (verdict.kind === \"again\") {\n if (verdict.penalise) {\n limiter.penalize();\n logger.info(`${verdict.because} on ${url}, interval now ${limiter.currentIntervalMs}ms`);\n }\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = verdict.waitMs;\n lastError = verdict.error;\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new MarmitonError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): MarmitonError {\n if (error instanceof MarmitonError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new MarmitonError(\"timeout\", \"Marmiton did not answer in time.\", {\n url,\n hint: \"Raise MARMITON_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new MarmitonError(\"network_error\", `Could not reach Marmiton: ${message}`, { url });\n}\n","/**\n * Turning Marmiton's JSON-LD into the domain types.\n *\n * The editorial `description` is deliberately not read here. Ingredient lists and\n * cooking steps are facts and procedures, and a public tool can hand them on with\n * a link to the source; the author's descriptive prose is their writing, and this\n * server has no business redistributing it.\n */\n\nimport process from \"node:process\";\nimport { parseFailure } from \"../errors.js\";\nimport { parseIsoDuration, parseYield } from \"../recipe/duration.js\";\nimport type { Nutrition, Rating, Recipe, RecipeSummary } from \"../types.js\";\nimport { asNumber, asString, asStringList, extractJsonLdNodes, findNodeOfType } from \"./jsonld.js\";\nimport { extractRecipeId, toAbsoluteUrl } from \"./urls.js\";\n\nexport interface ParseRecipeOptions {\n id: string;\n url: string;\n}\n\nexport function parseRecipePage(html: string, options: ParseRecipeOptions): Recipe {\n const nodes = extractJsonLdNodes(html);\n const node = findNodeOfType(nodes, \"Recipe\");\n\n // A page that loaded but carries no Recipe node means Marmiton changed how it\n // publishes its data. Reporting that as \"no recipe\" would be indistinguishable\n // from a genuine absence, so it has to be loud.\n if (!node) {\n throw parseFailure(options.url, \"no schema.org Recipe node in the page\");\n }\n\n const title = asString(node.name);\n const ingredients = asStringList(node.recipeIngredient);\n const steps = asStringList(node.recipeInstructions);\n\n if (!title) {\n throw parseFailure(options.url, \"the Recipe node has no name\");\n }\n if (ingredients.length === 0) {\n throw parseFailure(options.url, \"the Recipe node carries no ingredient list\");\n }\n\n return {\n id: options.id,\n title,\n url: asString(node.mainEntityOfPage) ?? options.url,\n imageUrl: asString(node.image),\n ingredients,\n steps,\n recipeYield: parseYield(node.recipeYield),\n prepMinutes: parseIsoDuration(node.prepTime),\n cookMinutes: parseIsoDuration(node.cookTime),\n totalMinutes: parseIsoDuration(node.totalTime),\n category: asString(node.recipeCategory),\n author: asString(node.author),\n rating: parseRating(node.aggregateRating),\n nutrition: parseNutrition(node.nutrition),\n };\n}\n\nfunction parseRating(value: unknown): Rating | null {\n if (typeof value !== \"object\" || value === null) {\n return null;\n }\n const record = value as Record<string, unknown>;\n const ratingValue = asNumber(record.ratingValue);\n if (ratingValue === null) {\n return null;\n }\n return {\n value: ratingValue,\n count: asNumber(record.ratingCount) ?? asNumber(record.reviewCount),\n best: asNumber(record.bestRating),\n };\n}\n\nfunction parseNutrition(value: unknown): Nutrition | null {\n if (typeof value !== \"object\" || value === null) {\n return null;\n }\n const record = value as Record<string, unknown>;\n const nutrition: Nutrition = {\n calories: asString(record.calories),\n protein: asString(record.proteinContent),\n fat: asString(record.fatContent),\n carbohydrate: asString(record.carbohydrateContent),\n fiber: asString(record.fiberContent),\n sodium: asString(record.sodiumContent),\n servingSize: asString(record.servingSize),\n };\n const hasAny = Object.values(nutrition).some((entry) => entry !== null);\n return hasAny ? nutrition : null;\n}\n\n/**\n * Search results, read from the ItemList the search page publishes.\n *\n * The page also lists more recipes in its HTML, but reaching them would mean\n * reintroducing CSS selectors and the breakage that comes with them. The\n * structured list is what the site offers machines, so that is what is used.\n */\nexport function parseSearchPage(html: string, url: string): RecipeSummary[] {\n const nodes = extractJsonLdNodes(html);\n const list = findNodeOfType(nodes, \"ItemList\");\n\n if (!list) {\n throw parseFailure(url, \"no schema.org ItemList node on the search page\");\n }\n\n const elements = list.itemListElement;\n if (!Array.isArray(elements)) {\n throw parseFailure(url, \"the ItemList node carries no itemListElement array\");\n }\n\n const results: RecipeSummary[] = [];\n let skipped = 0;\n\n for (const element of elements) {\n if (typeof element !== \"object\" || element === null) {\n skipped += 1;\n continue;\n }\n const record = element as Record<string, unknown>;\n const href = asString(record.url);\n const title = asString(record.name);\n const id = href ? extractRecipeId(href) : null;\n\n if (!(href && title && id)) {\n skipped += 1;\n continue;\n }\n\n results.push({ id, title, url: toAbsoluteUrl(href), imageUrl: asString(record.image) });\n }\n\n // Every entry failing while the list itself was present means the entry shape\n // changed, which is breakage rather than an empty search.\n if (results.length === 0 && elements.length > 0) {\n throw parseFailure(url, `${elements.length} list entries but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(`[mcp-marmiton] skipped ${skipped} unreadable search entries on ${url}\\n`);\n }\n\n return results;\n}\n","/**\n * ISO 8601 durations, which is how schema.org expresses cooking times.\n *\n * Marmiton writes \"PT25M\" for 25 minutes and \"PT1H30M\" for an hour and a half.\n */\n\nconst NUMBER_AND_REST = /^(\\d+(?:[.,]\\d+)?)\\s*(.*)$/;\n\nconst ISO_DURATION = /^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?$/;\n\n/** Minutes, or null when the value is absent or unreadable. */\nexport function parseIsoDuration(value: unknown): number | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const match = ISO_DURATION.exec(value.trim().toUpperCase());\n if (!match) {\n return null;\n }\n\n const [, days, hours, minutes, seconds] = match;\n const total =\n Number(days ?? 0) * 1440 +\n Number(hours ?? 0) * 60 +\n Number(minutes ?? 0) +\n Number(seconds ?? 0) / 60;\n\n // \"P\" alone parses but means nothing useful.\n if (total === 0 && !minutes && !hours && !days && !seconds) {\n return null;\n }\n return Math.round(total);\n}\n\n/** Minutes as \"1 h 30\" or \"25 min\", for the human-readable mirror. */\nexport function formatMinutes(minutes: number | null): string | null {\n if (minutes === null || !Number.isFinite(minutes) || minutes <= 0) {\n return null;\n }\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n if (hours === 0) {\n return `${rest} min`;\n }\n if (rest === 0) {\n return `${hours} h`;\n }\n return `${hours} h ${String(rest).padStart(2, \"0\")}`;\n}\n\nexport interface ParsedYield {\n /** Number of servings, or null when the text carries no number. */\n count: number | null;\n /** What is being counted: \"personnes\", \"pièces\", \"parts\". */\n unit: string | null;\n /** The original text, always preserved. */\n text: string;\n}\n\n/**\n * The yield as written, out of the three shapes schema.org allows for it: a\n * number, a string, or an array holding either.\n */\nfunction yieldText(value: unknown): string {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return String(value);\n }\n if (typeof value === \"string\") {\n return value.trim();\n }\n if (Array.isArray(value)) {\n return String(value[0] ?? \"\").trim();\n }\n return \"\";\n}\n\n/**\n * Read a schema.org recipeYield.\n *\n * Marmiton writes \"6 personnes\" but also \"15 pièces\", so the unit is kept: a\n * recipe yielding pieces cannot be described as serving people.\n */\nexport function parseYield(value: unknown): ParsedYield {\n const text = yieldText(value);\n if (!text) {\n return { count: null, unit: null, text: \"\" };\n }\n\n const match = NUMBER_AND_REST.exec(text);\n if (!match) {\n return { count: null, unit: null, text };\n }\n\n const [digits = \"\"] = match.slice(1);\n const count = Number(digits.replace(\",\", \".\"));\n const unit = (match[2] ?? \"\").trim() || null;\n return { count: Number.isFinite(count) ? count : null, unit, text };\n}\n","/**\n * JSON-LD extraction.\n *\n * Marmiton embeds a schema.org graph in every page, which is data the site\n * publishes deliberately for machines to read. Using it instead of CSS selectors\n * removes the whole class of breakage where a redesign renames a class and the\n * parser silently returns nothing.\n */\n\nconst LD_JSON_BLOCK = /<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n\n/** Every JSON-LD object on the page, flattened out of any `@graph` wrappers. */\nexport function extractJsonLdNodes(html: string): unknown[] {\n const nodes: unknown[] = [];\n LD_JSON_BLOCK.lastIndex = 0;\n\n for (let match = LD_JSON_BLOCK.exec(html); match !== null; match = LD_JSON_BLOCK.exec(html)) {\n const raw = match[1]?.trim();\n if (!raw) {\n continue;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A malformed block is skipped rather than fatal: pages carry several,\n // and one broken analytics blob must not hide the recipe.\n continue;\n }\n\n for (const node of flatten(parsed)) {\n nodes.push(node);\n }\n }\n\n return nodes;\n}\n\nfunction flatten(value: unknown): unknown[] {\n if (Array.isArray(value)) {\n return value.flatMap(flatten);\n }\n if (typeof value !== \"object\" || value === null) {\n return [];\n }\n\n const node = value as Record<string, unknown>;\n const graph = node[\"@graph\"];\n if (graph !== undefined) {\n return flatten(graph);\n }\n return [node];\n}\n\n/** True when a node declares the given schema.org type. */\nexport function hasType(node: unknown, type: string): boolean {\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n const declared = (node as Record<string, unknown>)[\"@type\"];\n if (typeof declared === \"string\") {\n return declared === type;\n }\n if (Array.isArray(declared)) {\n return declared.includes(type);\n }\n return false;\n}\n\nexport function findNodeOfType(nodes: unknown[], type: string): Record<string, unknown> | null {\n const found = nodes.find((node) => hasType(node, type));\n // The cast reads what the search found; putting it before the coalesce would\n // assert the node exists and leave the null branch unreachable.\n return found === undefined ? null : (found as Record<string, unknown>);\n}\n\n/** Read a string field, tolerating the array form schema.org allows. */\nexport function asString(value: unknown): string | null {\n if (typeof value === \"string\") {\n return value.trim() || null;\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n const found = asString(entry);\n if (found) {\n return found;\n }\n }\n }\n if (typeof value === \"object\" && value !== null) {\n // Fields such as `image` are often an ImageObject rather than a plain URL.\n const url = (value as Record<string, unknown>).url;\n if (typeof url === \"string\") {\n return url.trim() || null;\n }\n const name = (value as Record<string, unknown>).name;\n if (typeof name === \"string\") {\n return name.trim() || null;\n }\n }\n return null;\n}\n\nexport function asNumber(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\") {\n const parsed = Number(value.replace(\",\", \".\"));\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return null;\n}\n\n/** Read a list of strings, which is how ingredients and steps are expressed. */\nexport function asStringList(value: unknown): string[] {\n if (typeof value === \"string\") {\n return [value.trim()].filter(Boolean);\n }\n if (!Array.isArray(value)) {\n return [];\n }\n\n const out: string[] = [];\n for (const entry of value) {\n if (typeof entry === \"string\") {\n const text = entry.trim();\n if (text) {\n out.push(text);\n }\n continue;\n }\n if (typeof entry === \"object\" && entry !== null) {\n // HowToStep carries the sentence under `text`.\n const record = entry as Record<string, unknown>;\n const text = asString(record.text) ?? asString(record.name);\n if (text) {\n out.push(text);\n }\n }\n }\n return out;\n}\n","/** URL construction and validation for marmiton.org. */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.marmiton.org\";\n\n/** Recipe pages look like /recettes/recette_<slug>_<id>.aspx */\nexport const RECIPE_PATH_RE = /\\/recettes\\/recette[_-][a-z0-9%\\-.]*?_(\\d+)\\.aspx/i;\n\nconst ALLOWED_HOSTS = new Set([\"marmiton.org\", \"www.marmiton.org\"]);\n\n/**\n * Search URL.\n *\n * Only the `aqt` parameter is used. Marmiton's robots.txt disallows\n * `recherche.aspx?aqt=*page*` and `?aqt=*start*`, so this client never paginates\n * and the tool description says so rather than pretending the limit does not\n * exist.\n */\nexport function buildSearchUrl(query: string): string {\n const url = new URL(`${BASE_URL}/recettes/recherche.aspx`);\n url.searchParams.set(\"aqt\", query);\n return url.toString();\n}\n\n/**\n * Recipe URL from an id alone.\n *\n * Marmiton keys the page on the trailing id and ignores the slug, redirecting to\n * the canonical address whatever the slug says. It does require the slug to be\n * non-empty though: `recette__18588.aspx` answers 404 while `recette_r_18588.aspx`\n * redirects to the real page. Hence the placeholder, which lets a caller holding\n * only an id skip having to know the title.\n */\nexport function buildRecipeUrl(id: string): string {\n return `${BASE_URL}/recettes/recette_r_${id}.aspx`;\n}\n\n/** True only for marmiton.org, so a hostile URL cannot be used as a proxy. */\nexport function isMarmitonHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric recipe id from a Marmiton path or absolute URL. */\nexport function extractRecipeId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isMarmitonHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n const match = RECIPE_PATH_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/** Resolve the `id` / `url` pair accepted by get_recipe. `id` wins. */\nexport function resolveRecipeRef(input: { id?: string; url?: string }): {\n id: string;\n url: string;\n} {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a Marmiton recipe id.`,\n \"Ids are digits only, as returned by search_recipes.\",\n );\n }\n return { id: input.id, url: buildRecipeUrl(input.id) };\n }\n\n if (input.url) {\n if (!isMarmitonHost(input.url)) {\n throw invalidInput(\n \"Only marmiton.org URLs are accepted.\",\n \"Pass the url returned by search_recipes, or use the numeric id instead.\",\n );\n }\n const id = extractRecipeId(input.url);\n if (!id) {\n throw invalidInput(\n \"That Marmiton URL is not a recipe page.\",\n \"Recipe pages look like https://www.marmiton.org/recettes/recette_tarte-aux-pommes_18588.aspx\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * High-level Marmiton client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { MarmitonError } from \"../errors.js\";\nimport type { Recipe, RecipeSummary } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseRecipePage, parseSearchPage } from \"./parseRecipe.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveRecipeRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface MarmitonClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `MarmitonClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. Marmiton publishes no rate limit, so the pacing here is self-imposed politeness, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class MarmitonClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: MarmitonClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search recipes.\n *\n * One page only: Marmiton's robots.txt disallows the paginated form of this\n * URL, so there is no page parameter to pass.\n */\n async search(query: string): Promise<Outcome<RecipeSummary[]>> {\n const url = buildSearchUrl(query);\n try {\n return await this.fetchParsed(url, (html) => parseSearchPage(html, url));\n } catch (error) {\n // Marmiton answers a search matching no recipe with a 404 on the results\n // page. That is the site stating an absence, and handing it back as a\n // failed read would let a caller say Marmiton could not be reached when\n // Marmiton answered.\n if (error instanceof MarmitonError && error.code === \"not_found\") {\n return { data: [], cached: false };\n }\n throw error;\n }\n }\n\n async getRecipe(ref: { id?: string; url?: string }): Promise<Outcome<Recipe>> {\n const { id, url } = resolveRecipeRef(ref);\n return await this.fetchParsed(url, (html) => parseRecipePage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at Marmiton cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<Outcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n","/**\n * get_recipe: read one recipe, optionally scaled to a number of servings.\n */\n\nimport { z } from \"zod\";\nimport type { MarmitonClient } from \"../marmiton/client.js\";\nimport { formatMinutes } from \"../recipe/duration.js\";\nimport { isApproximateMeasure, passthroughIngredients, scaleIngredients } from \"../recipe/scale.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, scaledIngredientSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** The note a scaling that could not go lower leaves on an ingredient. */\nconst CLAMPED_UP = /clamped up/i;\n\nexport const getRecipeDescription = [\n \"Read one Marmiton recipe: ingredients, steps, times, category, rating and nutrition.\",\n \"Give the id returned by search_recipes, or a marmiton.org recipe URL.\",\n \"Set 'servings' to rescale the quantities. Each ingredient reports how it was handled: 'scaled' when the\",\n \"arithmetic came out exact, which a count of eggs, spoons or pinches reaches as readily as a mass in grams,\",\n \"'rounded' when the value had to be moved to stay usable, and 'unscaled' for lines carrying no quantity at all.\",\n \"Trust that flag rather than recomputing: the point of scaling here is to avoid answers like '2.4 eggs'.\",\n \"Recipes yielding pieces rather than servings are rescaled the same way; check 'yield' to see which.\",\n \"Always cite 'attribution' when showing a recipe to a user.\",\n].join(\" \");\n\nexport const getRecipeInput = strictInput({\n id: z\n .string()\n .regex(/^\\d+$/, \"Recipe ids are digits only.\")\n .optional()\n .describe(\"Marmiton recipe id, as returned by search_recipes. Preferred over 'url'.\"),\n url: z\n .string()\n .url()\n .optional()\n .describe(\n \"Full marmiton.org recipe URL. Only marmiton.org is accepted. Ignored when 'id' is given.\",\n ),\n servings: z\n .number()\n .positive()\n .max(500)\n .optional()\n .describe(\n \"Rescale the ingredients to this many servings. Omit to get the recipe exactly as published.\",\n ),\n});\n\nexport const getRecipeOutputShape = {\n id: z.string(),\n title: z.string(),\n url: z.string(),\n yield: z.object({\n original_count: z.number().nullable(),\n original_text: z.string(),\n requested: z.number().nullable(),\n unit: z.string().nullable().describe(\"What is being counted: 'personnes', 'pièces'.\"),\n factor: z.number().nullable().describe(\"Multiplier applied to the quantities.\"),\n }),\n ingredients: z.array(scaledIngredientSchema),\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.string().nullable(),\n rating: z\n .object({ value: z.number(), count: z.number().nullable(), best: z.number().nullable() })\n .nullable(),\n nutrition: z\n .object({\n calories: z.string().nullable(),\n protein: z.string().nullable(),\n fat: z.string().nullable(),\n carbohydrate: z.string().nullable(),\n fiber: z.string().nullable(),\n sodium: z.string().nullable(),\n serving_size: z.string().nullable(),\n })\n .nullable()\n .describe(\"Nutrition is given for the recipe as published, not rescaled.\"),\n attribution: z.string(),\n source: z.literal(\"marmiton.org\"),\n notes: z.array(z.string()),\n};\n\n/**\n * What a rescaled list owes its reader beyond the quantities themselves.\n *\n * Each sentence answers a question a cook would otherwise have to ask of every\n * line in turn: which figures moved, which ones stopped at a floor, which ones\n * are the cook's own judgement, and which ones were left alone.\n */\nfunction rescalingNotes(ingredients: readonly ScaledIngredient[], hasNutrition: boolean): string[] {\n const notes: string[] = [];\n\n // Lines that were actually moved, rather than lines that merely belong to the\n // roundable category: doubling a recipe lands every egg whole.\n const rounded = ingredients.filter(\n (entry) => entry.scaling === \"rounded\" && entry.adjusted,\n ).length;\n if (rounded > 0) {\n notes.push(`${rounded} quantity(ies) were rounded to remain usable; see the 'scaling' field.`);\n }\n\n const clamped = ingredients.filter((entry) => CLAMPED_UP.test(entry.note ?? \"\")).length;\n if (clamped > 0) {\n notes.push(\n `${clamped} quantity(ies) fell below the smallest amount worth measuring and were ` +\n \"clamped up, so their proportions no longer match the published recipe.\",\n );\n }\n\n if (ingredients.some(isApproximateMeasure)) {\n notes.push(\n \"Approximate measures such as a pinch or a handful had their count multiplied; the size \" +\n \"of one is yours to judge.\",\n );\n }\n\n if (ingredients.some((entry) => entry.scaling === \"unscaled\")) {\n notes.push(\"Some ingredients carry no usable quantity and were left as published.\");\n }\n\n if (hasNutrition) {\n notes.push(\"Nutrition values are for the recipe as published, not for the rescaled amounts.\");\n }\n\n return notes;\n}\n\nexport interface GetRecipeArgs {\n id?: string;\n url?: string;\n servings?: number;\n}\n\nexport async function runGetRecipe(\n client: MarmitonClient,\n args: GetRecipeArgs,\n): Promise<ToolResult> {\n try {\n const ref: { id?: string; url?: string } = {};\n if (args.id) {\n ref.id = args.id;\n } else if (args.url) {\n ref.url = args.url;\n }\n\n const { data, cached } = await client.getRecipe(ref);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const originalCount = data.recipeYield.count;\n let factor: number | null = null;\n let ingredients: ScaledIngredient[];\n\n if (args.servings === undefined) {\n ingredients = passthroughIngredients(data.ingredients);\n } else if (originalCount === null || originalCount <= 0) {\n // Without a published yield there is nothing to scale from, and guessing\n // would produce quantities that look authoritative and are not.\n ingredients = passthroughIngredients(data.ingredients);\n notes.push(\n `Marmiton does not state how many servings this recipe makes (\"${data.recipeYield.text}\"), ` +\n \"so the quantities are unchanged.\",\n );\n } else {\n factor = args.servings / originalCount;\n ingredients = scaleIngredients(data.ingredients, { factor });\n\n const unit = data.recipeYield.unit ?? \"portions\";\n notes.push(\n `Quantities rescaled from ${originalCount} to ${args.servings} ${unit} (factor ${\n Math.round(factor * 100) / 100\n }).`,\n );\n notes.push(...rescalingNotes(ingredients, data.nutrition !== null));\n }\n\n const attribution = `${data.title} — recette Marmiton — ${data.url}`;\n\n const structured = {\n id: data.id,\n title: data.title,\n url: data.url,\n yield: {\n original_count: originalCount,\n original_text: data.recipeYield.text,\n requested: args.servings ?? null,\n unit: data.recipeYield.unit,\n factor: factor === null ? null : Math.round(factor * 1000) / 1000,\n },\n ingredients,\n steps: data.steps,\n prep_minutes: data.prepMinutes,\n cook_minutes: data.cookMinutes,\n total_minutes: data.totalMinutes,\n category: data.category,\n author: data.author,\n rating: data.rating,\n nutrition: data.nutrition\n ? {\n calories: data.nutrition.calories,\n protein: data.nutrition.protein,\n fat: data.nutrition.fat,\n carbohydrate: data.nutrition.carbohydrate,\n fiber: data.nutrition.fiber,\n sodium: data.nutrition.sodium,\n serving_size: data.nutrition.servingSize,\n }\n : null,\n attribution,\n source: \"marmiton.org\" as const,\n notes,\n };\n\n const times = [\n data.prepMinutes ? `préparation ${formatMinutes(data.prepMinutes)}` : \"\",\n data.cookMinutes ? `cuisson ${formatMinutes(data.cookMinutes)}` : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n\n const header = [\n attribution,\n args.servings !== undefined && factor !== null\n ? `Pour ${args.servings} ${data.recipeYield.unit ?? \"portions\"} (recette prévue pour ${data.recipeYield.text}).`\n : `Recette pour ${data.recipeYield.text || \"portions non précisées\"}.`,\n times,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n // The flag answers \"why is this line unchanged\", which is only a question\n // when a rescaling was asked for. Without 'servings' it would read as a\n // statement about the ingredient itself.\n const rescaled = args.servings !== undefined && factor !== null;\n const ingredientLines = ingredients\n .map((entry) => {\n const flag = rescaled && entry.scaling === \"unscaled\" ? \" (non ajusté)\" : \"\";\n return `- ${entry.text}${flag}`;\n })\n .join(\"\\n\");\n\n const stepLines = data.steps.map((step, index) => `${index + 1}. ${step}`).join(\"\\n\");\n const body = [\n header,\n `Ingrédients:\\n${ingredientLines}`,\n stepLines ? `Préparation:\\n${stepLines}` : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return ok(structured, body, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * French cooking unit vocabulary and what scaling means for each.\n *\n * The distinction that matters is not metric versus imperial, it is what shape\n * the scaled number has to take. \"200 g\" doubled is \"400 g\". \"1 pincée\" doubled\n * is \"2 pincées\": the pinch keeps whatever size the cook's fingers give it, and\n * the count is what carries the recipe's proportion.\n */\n\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\nconst TRAILING_S = /s$/i;\nconst SIBILANT_ENDING = /[sxz]$/i;\nconst EAU_ENDING = /eau$/i;\nconst AL_ENDING = /al$/i;\nconst HOUSEHOLD_MEASURE = /^(cuillère à soupe|cuillère à café|verre|tasse|bol)$/;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls: each one has the size the cook gives it, so the\n * count scales in whole units and the unit itself is never converted.\n */\n | \"vague\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A metric symbol such as \"g\" or \"cl\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\", \"Cuillères\".\n */\nconst UNITS: Record<string, UnitInfo> = {\n // Mass\n g: { canonical: \"g\", kind: \"measured\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", symbol: true },\n mg: { canonical: \"mg\", kind: \"measured\", 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\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", symbol: true },\n\n // Volume\n ml: { canonical: \"ml\", kind: \"measured\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", 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 plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n \"c a s\": { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n cas: { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n \"c a c\": { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n cac: { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n verre: { canonical: \"verre\", kind: \"portioned\" },\n verres: { canonical: \"verre\", kind: \"portioned\" },\n bol: { canonical: \"bol\", kind: \"portioned\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\" },\n botte: { canonical: \"botte\", kind: \"portioned\" },\n bottes: { canonical: \"botte\", kind: \"portioned\" },\n boite: { canonical: \"boîte\", kind: \"portioned\" },\n boites: { canonical: \"boîte\", kind: \"portioned\" },\n pot: { canonical: \"pot\", kind: \"portioned\" },\n pots: { canonical: \"pot\", kind: \"portioned\" },\n brique: { canonical: \"brique\", kind: \"portioned\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\" },\n branche: { canonical: \"branche\", kind: \"portioned\" },\n branches: { canonical: \"branche\", kind: \"portioned\" },\n\n // Approximate by nature: the size of one is the cook's, the count is the\n // recipe's. See `readPartitiveMeasure` for what puts a word here.\n bouchon: { canonical: \"bouchon\", kind: \"vague\" },\n bouchons: { canonical: \"bouchon\", kind: \"vague\" },\n larme: { canonical: \"larme\", kind: \"vague\" },\n larmes: { canonical: \"larme\", kind: \"vague\" },\n doigt: { canonical: \"doigt\", kind: \"vague\" },\n doigts: { canonical: \"doigt\", kind: \"vague\" },\n nuage: { canonical: \"nuage\", kind: \"vague\" },\n nuages: { canonical: \"nuage\", kind: \"vague\" },\n louche: { canonical: \"louche\", kind: \"vague\" },\n louches: { canonical: \"louche\", kind: \"vague\" },\n lichette: { canonical: \"lichette\", kind: \"vague\" },\n lichettes: { canonical: \"lichette\", kind: \"vague\" },\n pointe: { canonical: \"pointe\", kind: \"vague\" },\n pointes: { canonical: \"pointe\", kind: \"vague\" },\n pincee: { canonical: \"pincée\", kind: \"vague\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"vague\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"vague\" },\n traits: { canonical: \"trait\", kind: \"vague\" },\n filet: { canonical: \"filet\", kind: \"vague\" },\n filets: { canonical: \"filet\", kind: \"vague\" },\n goutte: { canonical: \"goutte\", kind: \"vague\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"vague\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"vague\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"vague\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"vague\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"vague\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"vague\", plural: \"soupçons\" },\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so lookups survive French\n * spelling variants. Recipes write \"c. à soupe\" and \"c à s\" as readily as the\n * full \"cuillère à soupe\", and an unrecognised unit is worse than a wrong one:\n * the amount falls through to the countable branch and gets rounded as though a\n * 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 unit 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 \"cuillere a soupe\" wins over \"cuillere\". */\nexport const UNIT_KEYS = Object.keys(UNITS).sort((a, b) => b.length - a.length);\n\nexport function lookupUnit(text: string): UnitInfo | null {\n return UNITS[normalizeUnitKey(text)] ?? null;\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 measure a line names with a container or a gesture the vocabulary\n * above has 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\", \"une poignée de\n * roquette\". A noun in that position measures whatever follows it, so a\n * container nobody thought to list is read by the same rule as the ones that\n * are, and the vocabulary above only has to carry the words whose plural or\n * 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)) {\n return null;\n }\n\n const canonical = frenchSingular(word);\n return {\n unit: { canonical, kind: \"vague\", plural: frenchPlural(canonical) },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * The singular of a noun a line wrote in the plural, so the rewrite can put it\n * 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.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_ENDING.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 * Metric 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 de sucre\" 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.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000 },\n g: { to: \"kg\", per: 1000 },\n ml: { to: \"l\", per: 1000 },\n cl: { to: \"l\", per: 100 },\n dl: { to: \"l\", per: 10 },\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: a quarter\n // of a cuillère à soupe is three quarters of a cuillère à café, and that is\n // the one of the two a kitchen owns.\n \"cuillère à soupe\": { to: \"cuillere a cafe\", per: 3 },\n tasse: { to: \"cuillere a soupe\", per: 16 },\n\n kg: { to: \"g\", per: 1000 },\n l: { to: \"cl\", per: 100 },\n dl: { to: \"cl\", per: 10 },\n cl: { to: \"ml\", per: 10 },\n g: { to: \"mg\", per: 1000 },\n};\n\n/**\n * The unit one step down the metric ladder, with how many of it fit in one of\n * the current unit. Null at the bottom of a ladder, where there is nothing\n * smaller to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[unit.canonical];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An oeuf: half of one is not an amount a kitchen measures out. */\n | \"whole\"\n /** A boîte, une gousse d'ail, a feuille de gélatine: it splits in two, and no finer. */\n | \"half\"\n /** An oignon, a pomme: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How finely a unit divides, decided by what one of them holds rather than by\n * what holds it.\n *\n * The question is whether half of one is a quantity a cook can take: a boîte de\n * tomates is poured and the rest kept, a sachet de sucre vanillé is split by\n * eye, a feuille de gélatine is cut with scissors, a branche de thym is pinched\n * in two. Content that pours, weighs or cuts therefore divides, and the word for\n * the packaging settles nothing. What stays whole is what half of cannot be\n * measured out at all, and the egg is the case that names the rule: half of one\n * would have to be beaten and weighed, which is not what a recipe asks for. That\n * test belongs to the thing being counted, so it lives with the item in\n * `scale.ts`.\n *\n * A gesture keeps its own answer: half a pincée is a fraction of a hand, and the\n * count is the whole of what a pincée can say.\n *\n * A short list of measures goes one step further, to the quarter. See\n * `QUARTERED_MEASURE`.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"vague\") {\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 three answer the size question\n * differently. A pot de crème fraîche and a bouteille hold enough that a\n * quarter is still a portion someone serves and the rest still keeps: a quarter\n * of a pot is a couple of spoonfuls, a quarter of a bouteille is a glass. A\n * tranche is already cut off something larger, and the board that produced one\n * takes a corner off it in the same gesture: a quarter of a tranche de pain is\n * a crouton.\n *\n * The pattern is exported because a line can write the measure where the unit\n * goes, in \"1 pot de crème fraîche\", or inside the name of what it counts, in\n * \"1 petit pot de crème\". Both readings answer to the same list.\n */\nexport const QUARTERED_MEASURE = /\\b(pots?|bouteilles?|tranches?)\\b/;\n\n/**\n * Spoons, glasses and bowls: a portion of a fixed size, which a kitchen measures\n * out in the fractions printed on a measuring set rather than in halves alone.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\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 \"450 g à 1 kg\". The caller picks one bound to choose from, then\n * 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 */\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[current.canonical];\n if (up && amount * ratio >= up.per) {\n const target = lookupUnit(up.to);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * Matches a number followed by a unit anywhere in a piece of text.\n *\n * Used to spot a quantity this parser did not take, such as the second amount\n * of \"20 g de levure dissoute dans 1 cuillère à soupe d'eau\", which would\n * otherwise sit in a scaled line still saying what the original said.\n */\nconst EMBEDDED_MEASURE = new RegExp(\n `\\\\d[\\\\d.,/]*\\\\s*(?:${UNIT_KEYS.map((key) => key.replace(/ /g, \"\\\\s+\")).join(\"|\")})\\\\b`,\n \"i\",\n);\n\n/**\n * Whether a piece of text holds a number followed by a unit.\n *\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string): boolean {\n return EMBEDDED_MEASURE.test(normalizeUnitKey(text));\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 cuillères à\n * café\" where the page wrote \"4 pincées\" puts a figure on the page it never\n * claimed, and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\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 * Render a unit for a given amount, choosing singular or plural.\n *\n * French takes the plural from two onwards, so 1,5 stays singular: \"1,5 cuillère\n * à soupe\", not \"1,5 cuillères\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n if (amount < 2) {\n return unit.canonical;\n }\n if (unit.plural) {\n return unit.plural;\n }\n return `${unit.canonical}s`;\n}\n","/**\n * Parsing of French quantities out of free-text ingredient lines.\n *\n * Marmiton stores ingredients as plain strings: \"200 g de farine\", \"3 oeufs\",\n * \"0.5 oignon coupée en cubes\", \"sel\". There is no structured amount anywhere, so\n * everything downstream depends on reading these correctly.\n */\n\nimport type { UnitInfo } from \"./units.js\";\nimport { lookupUnit, normalizeUnitKey, readPartitiveMeasure, UNIT_KEYS } from \"./units.js\";\n\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst VAGUE_ARTICLE = /^\\s*(un|une|quelques)\\b\\s*/i;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst WHITESPACE = /\\s+/;\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DIGIT = /\\d/;\nconst TRAILING_S = /s$/;\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst LEADING_ARTICLE = /^(?:une|un)\\s+/i;\nconst RANGE_SEPARATOR = /^\\s*(à|a|ou|-|–|—|\\/)\\s*/;\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 appear in hand-written recipes. */\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 \"⅙\": 1 / 6,\n \"⅛\": 0.125,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\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 written with either a dot or a French comma.\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"coriandre\".\n */\nexport function parseLeadingQuantity(text: string): 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 = \"\", glyph = \"\"] = mixedGlyph.slice(1);\n const fraction = VULGAR_FRACTIONS[glyph];\n if (fraction !== undefined) {\n return { amount: Number(whole) + fraction, length: offset + mixedGlyph[0].length };\n }\n }\n\n const leading = trimmed[0];\n const bare = leading === undefined ? undefined : VULGAR_FRACTIONS[leading];\n if (bare !== undefined) {\n return { amount: bare, length: offset + 1 };\n }\n\n // \"1 1/2\" before \"1/2\" before \"1,5\", so the longest reading wins.\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const whole = Number(mixed[1]);\n const numerator = Number(mixed[2]);\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return { amount: whole + numerator / denominator, length: offset + mixed[0].length };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n if (denominator !== 0) {\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n }\n\n // French marks the decimal with a comma, so \"1,5 kg\" is a kilo and a half.\n const decimal = DECIMAL.exec(trimmed);\n if (decimal) {\n const [digits = \"\"] = decimal.slice(1);\n const amount = Number(digits.replace(\",\", \".\"));\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n return null;\n}\n\n/**\n * Articles a recipe writes where a digit would go, and what they count as.\n *\n * \"quelques\" names a small handful of them; three is the reading, and the word\n * travels with the result so a caller can see the amount came from a word.\n */\nconst ARTICLE_AMOUNTS: Record<string, number> = {\n un: 1,\n une: 1,\n quelques: 3,\n};\n\nexport interface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read a leading article standing in for a number, as in \"une pincée de sel\".\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 \"un sachet\" is\n * one sachet, while \"un oignon\" names a vegetable and no amount. Reading the\n * second as the first would multiply a number the line never wrote.\n */\nexport function parseLeadingArticle(text: string): ParsedArticle | null {\n const match = VAGUE_ARTICLE.exec(text);\n if (!match) {\n return null;\n }\n\n const rest = text.slice(match[0].length);\n if (!(matchLeadingUnit(rest, true) || readCountMultiplier(rest))) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const amount = ARTICLE_AMOUNTS[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n\n return { amount, length: match[0].length, word };\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 douzaine is twelve of whatever is being counted. \"2 douzaines d'escargots\"\n * therefore asks for twenty-four escargots, and the answer divides the way an\n * escargot does. Reading the word as a measure gives \"1 1/2 douzaine\", which is\n * not a count a kitchen works with, and it hands the question of divisibility to\n * a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\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\ninterface MatchedUnit {\n unit: UnitInfo;\n /** Unit text as the line wrote it, accents and all. */\n unitText: string;\n /** What follows the unit. */\n rest: string;\n}\n\n/**\n * Read a unit at the start of a line, longest spelling first, so \"cuillère à\n * soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item name.\n *\n * A noun the vocabulary does not carry can still be a measure, and the\n * partitive that follows it says so, which is what `readPartitiveMeasure`\n * reads. That reading is offered only where the line wrote an article instead\n * of a digit, because that is the position where a noun measures rather than\n * names: \"un bouchon de rhum\" asks for an amount, while \"1 piment de Cayenne\"\n * asks for a chilli whose variety happens to be introduced the same way. The\n * vocabulary is consulted first either way, so a word it holds keeps the kind\n * and the spelling it was given there.\n */\nfunction matchLeadingUnit(text: string, partitive = false): MatchedUnit | null {\n const normalized = normalizeUnitKey(text);\n for (const key of UNIT_KEYS) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key);\n if (!unit) {\n continue;\n }\n // Consume the same number of words from the original text, which may be\n // spelled with accents the normalized key has lost.\n const wordCount = key.split(\" \").length;\n const words = text.trim().split(WHITESPACE);\n return {\n unit,\n unitText: words.slice(0, wordCount).join(\" \"),\n rest: words.slice(wordCount).join(\" \"),\n };\n }\n\n const measure = partitive ? readPartitiveMeasure(text) : null;\n if (measure) {\n return {\n unit: measure.unit,\n unitText: text.trim().split(WHITESPACE)[0] ?? \"\",\n rest: measure.rest,\n };\n }\n\n return null;\n}\n\n/** One amount with its unit, 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 /** \"2 tranches de 2-cm\": the figure gives 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 /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"1,500,000 g\" grouped the way French never groups a number. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as Marmiton stores it. */\n original: string;\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 sign or word the page put before the amount to say it is loose, as in\n * the \"environ\" of \"environ 6 citrons\". Null when the page stated the amount\n * plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"grosse\"\n * of \"1 grosse pincée\". 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 \"2 à 3 gousses\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"2 à 3\" into the nonsense \"4 à 3\".\n */\n amountMax: number | null;\n /** The word or sign the range was written with: \"à\", \"-\", \"ou\". */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /** Raw unit text as written, kept so the rewrite can stay faithful. */\n unitText: string | null;\n /**\n * The same quantity restated in another system, which pages give in brackets:\n * \"450 g (1 livre)\". Left unscaled it would contradict the amount beside it,\n * so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets, as in \"450 g (1\n * livre)\", or after a slash, as in \"500 g / 1.1 lb\". The rewrite puts them\n * back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | null;\n /** What the amount and unit apply to, for example \"farine\" or \"oeufs\". */\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 number.\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 douzaines d'escargots\". 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 * A number grouped the way French never groups one.\n *\n * French marks the decimal with a comma and separates thousands with a space,\n * so a second comma group is a number written by some other convention.\n * Reading \"1,500,000\" as one and a half is wrong, and reading it as a million\n * and a half is a guess about a page that gave no sign. Neither is safe, so the\n * line goes back as published and says why.\n */\nconst AMBIGUOUS_COMMA = /^\\s*\\d+,\\d+,\\d/;\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);\n if (!size) {\n return false;\n }\n\n const measure = matchLeadingUnit(text.slice(size.length).trimStart());\n if (measure?.unit.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\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 = /\\bpar\\s+(?:personne|convive|part|tête)\\b/i;\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 = /^(?:~|≈|environ|approximativement|à peu près|a peu pres)\\s*/i;\n\n/**\n * Size words a recipe puts between the number and the measure.\n *\n * \"1 grosse pincée\" counts pincées 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 pincée 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 = 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/** Lowercase and strip accents, so \"générelle\" and \"generelle\" hit one entry. */\nfunction fold(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(text: string): { 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 = fold(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.has(folded) || MEASURE_ADJECTIVES.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 * How a line writes the equivalents beside its measure, when it writes any.\n *\n * \"125 g / 1 tasse\" states them with a slash and reads as one line; \"125 g\n * (1 tasse)\" puts them in brackets. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(slashed: boolean, bracketed: boolean): \"slash\" | \"bracket\" | null {\n if (slashed) {\n return \"slash\";\n }\n if (bracketed) {\n return \"bracket\";\n }\n return null;\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 petit piment oiseau\" the words that follow name the food itself, and\n * taking one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n text: string,\n fromArticle: boolean,\n): { unit: UnitInfo | null; unitText: string | null; adjective: string | null; rest: string } {\n const direct = matchLeadingUnit(text, fromArticle);\n const described = direct ? { adjective: null, rest: text } : takeMeasureAdjective(text);\n const behind = described.adjective ? matchLeadingUnit(described.rest, fromArticle) : null;\n const matched = direct ?? behind;\n\n return {\n unit: matched?.unit ?? null,\n unitText: matched?.unitText ?? null,\n adjective: behind ? described.adjective : null,\n rest: matched ? matched.rest : text,\n };\n}\n\n/**\n * Whether the figures on the line give the size of one item rather than how\n * many of them the recipe wants.\n *\n * Two ways a line says it. Without a measure, the words behind the count carry\n * the mass: \"2 poulets de 1,5 kg\". With one, \"une dinde de 3 kg\" writes the\n * noun where a measure stands and the partitive takes it for one. A noun the\n * vocabulary lists as a measure keeps counting, since a pot or a boîte is a\n * thing to buy more of; a noun read as a measure only for standing there names\n * the food itself, and a mass behind it is the size of one of them.\n */\nfunction statesSizeOfOne(unit: UnitInfo | null, unitText: string | null, item: string): boolean {\n if (!unit) {\n return statesItemSize(item);\n }\n const readForItsPosition = unitText !== null && lookupUnit(normalizeUnitKey(unitText)) === null;\n return readForItsPosition && isStatedSize(item);\n}\n\n/**\n * Split an ingredient line into amount, unit, bracketed equivalents and item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing unit is equally normal and means the item is counted, as in \"3 oeufs\".\n */\nexport function parseIngredient(line: string): ParsedIngredient {\n const original = line;\n const text = line.trim();\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n heldBack,\n approximation: null,\n measureAdjective: null,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n unitText: null,\n alternates: [],\n alternateStyle: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (AMBIGUOUS_COMMA.test(text)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX.exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated);\n const article = range ? null : parseLeadingArticle(stated);\n const quantity = range ?? parseLeadingQuantity(stated) ?? 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: \"2 tranches de 2-cm\" states a thickness.\n if (HYPHENATED_TAIL.test(stated.slice(quantity.length))) {\n return empty(\"sizeQualifier\");\n }\n\n let rest = stated.slice(quantity.length).trimStart();\n\n // \"2 douzaines d'escargots\" counts escargots, twelve to the douzaine, so the\n // multiplier is folded into the amount and the line goes on to be read as the\n // count of a thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, fromArticle);\n const { unit, unitText, adjective } = measure;\n rest = measure.rest;\n\n const bracketed = takeAlternates(rest);\n rest = bracketed.rest;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest);\n if (slashed) {\n rest = slashed.rest;\n }\n\n // \"200 g de farine\" reads better as item \"farine\" than \"de farine\".\n //\n // The article a partitive introduces goes with it: \"2/3 d'un flacon\" names a\n // share of one flacon, and once the share has been multiplied the count sits\n // where \"un\" stood. Leaving the article behind produces \"4 un flacon\", which\n // reads as broken text rather than as a quantity.\n const item = rest.replace(LEADING_PARTITIVE, \"\").replace(LEADING_ARTICLE, \"\").trim();\n\n if (statesSizeOfOne(unit, unitText, item)) {\n return empty(\"itemSize\");\n }\n\n return {\n original,\n heldBack: PER_PERSON.test(text) ? \"perPerson\" : null,\n approximation: loose ? loose[0] : null,\n measureAdjective: adjective,\n amount: quantity.amount * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit,\n unitText,\n alternates: slashed ? slashed.measures : bracketed.measures,\n alternateStyle: alternateStyleOf(slashed !== null, bracketed.measures.length > 0),\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 livre)\" 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 unit.\n * A bracket holding a remark, as in \"(bien mûres)\", stays in the item text\n * where it belongs, because scaling it would mean scaling prose.\n */\nfunction takeAlternates(text: string): { measures: Measure[]; rest: string } {\n if (!text.startsWith(\"(\")) {\n return { measures: [], rest: text };\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return { measures: [], rest: text };\n }\n\n const inside = text.slice(1, close);\n const parts = inside.split(\"/\").map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part);\n const quantity = range ?? parseLeadingQuantity(part);\n if (!quantity) {\n return { measures: [], rest: text };\n }\n\n const after = matchLeadingUnit(part.slice(quantity.length).trimStart());\n // A trailing word means the bracket is not purely a measure, as in\n // \"(2 cm d'épaisseur)\", so the whole group is left as prose.\n if (after?.rest.trim() !== \"\") {\n return { measures: [], rest: text };\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 { measures: [], rest: text };\n }\n return { measures, rest: text.slice(close + 1).trimStart() };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb de\n * flocons d'avoine\", 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 * unit is prose and stays in the item text.\n */\nfunction takeSlashAlternates(text: string): { 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);\n const quantity = range ?? parseLeadingQuantity(after);\n if (!quantity) {\n break;\n }\n\n const taken = matchLeadingUnit(after.slice(quantity.length).trimStart());\n if (!taken) {\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 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 \"2 à 3\", \"2-3\" or \"3 ou 4\".\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): ParsedRange | null {\n const low = parseLeadingQuantity(text);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n const separator = RANGE_SEPARATOR.exec(after);\n if (!separator) {\n return null;\n }\n // A slash between two numbers is a fraction, which parseLeadingQuantity has\n // already consumed if it was one.\n if (separator[1] === \"/\") {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length));\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\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 cuillère\" is how a kitchen\n * expresses it, \"0,33 cuillère\" 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.\n */\nexport function formatAmount(amount: number, options: FormatAmountOptions = {}): string {\n // French recipes write decimals with a comma. Two decimals is finer than any\n // kitchen resolves, and for anything smaller than that it is zero: a quantity\n // that survived being divided a thousandfold must not be handed back as none\n // of the ingredient, so below that point the significant digits are written.\n const decimal = (value: number) => {\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n return String(rounded).replace(\".\", \",\");\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.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act on.\n * Multiplying every number by the factor is arithmetically correct and practically\n * useless: it produces \"2,4 oeufs\" and \"0,67 pincée de sel\" with the same\n * confidence as \"267 g de farine\". Each line is therefore classified by what its\n * unit allows, and the classification travels with the result so the caller can\n * see what was computed and how.\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 pincée for twenty-five servings is a recipe that does not rise.\n */\n\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n hasEmbeddedMeasure,\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n demoteUnit,\n formatUnit,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\nconst EGG_WHITE = /\\bblancs? de? /;\nconst EGG_WHITE_NAMED = /\\bblancs? de? oeufs?\\b/;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /[sxz]$/i;\nconst EAU_ENDING = /eau$/i;\nconst AL_ENDING = /al$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst AUX_ENDING = /aux$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or a half, or a measurement was\n * rounded to what a scale can show.\n */\n | \"rounded\"\n /** Left as published: nothing on the line is the factor's to multiply. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\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 a\n * bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit of\n * \"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 scaling: ScalingKind;\n /**\n * Whether rounding moved the value away from the exact product. A line is\n * `rounded` either because of that or because a floor was reached, and this\n * says which of the two happened.\n */\n adjusted: boolean;\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 kilo as easily as a gram, and rounding 2,2 kg\n * to 2 would throw away a tenth of the ingredient.\n */\nfunction roundMeasured(value: number): number {\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\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 * Une crevette, une moule, une noisette, un grain de poivre, une baie de\n * genièvre, une étoile de badiane is already a portion on its own. A recipe\n * counts five, twelve, twenty of them, and a cook taking a share of that recipe\n * puts one fewer in the pan; cutting one in two is not a thing a kitchen does.\n * These land on a whole number.\n *\n * Un gigot, une baguette, un camembert, un ananas, un oignon, une pastèque, une\n * pintade sits at the other end: a recipe asks for one or for two, and the\n * share it wants out of one is decided by a knife. A quarter of one is a piece\n * someone serves, and what is left keeps.\n *\n * The lists are read on an item stripped of its accents, so \"échalote\" and\n * \"echalote\" hit the same entry.\n */\nconst PORTION_SIZED_ITEM =\n /\\b(crevettes?|gambas|langoustines?|moules?|noisettes?|grains?|genievres?|genevriers?|badianes?|anis)\\b/;\n\nconst QUARTERED_ITEM =\n /\\b(oignons?|echalotes?|pommes? de terre|pommes?|poires?|carottes?|citrons?|oranges?|tomates?|concombres?|courgettes?|aubergines?|courges?|potirons?|choux?|melons?|poivrons?|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pasteques?|gigots?|baguettes?|camemberts?|fromages?|chevres?|chorizos?|reblochons?|buches?|ananas|peches?|abricots?|laits?|poulets?|pintades?|rotis?)\\b/;\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 cuisse feeds one, and half of one\n * is the share a smaller recipe serves. Taking a quarter would name a piece no\n * one plates.\n *\n * It reads before the animal, whose name such a line carries alongside the cut.\n */\nconst HALVED_CUT = /\\b(cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/;\n\n/**\n * A jus, the one counted thing whose division stops at the half.\n *\n * Half the jus of a citron 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 = /\\bjus\\b/;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An oeuf comes out of its shell whole, and so does the jaune a recipe asks for\n * on its own: half of one would have to be beaten and weighed, which is not an\n * amount any recipe asks for and not one a cook can keep the rest of. A count of\n * them therefore lands on a whole number, whichever side of the half the\n * arithmetic fell on.\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 = /\\b(oeufs?|jaunes?|clous?|zestes?)\\b/;\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 */\nfunction blancDivisibility(key: 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 (!EGG_WHITE.test(key)) {\n return null;\n }\n return EGG_WHITE_NAMED.test(key) ? \"whole\" : \"half\";\n}\n\n/** How finely the thing a line counts can be divided. */\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n if (unit) {\n return unitDivisibility(unit);\n }\n const key = item\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/œ/g, \"oe\")\n .replace(/['’]/g, \" \");\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 * 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\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 that divides in two: half a boîte de tomates is a\n * real amount, and rounding it up to a whole adds a sixth of the tomatoes to a\n * recipe that asked for three boîtes.\n *\n * How finely the thing divides decides the floor. Under that floor the amount is\n * clamped up rather than shrunk towards nothing, which keeps the ingredient in\n * 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 than\n * 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 it\n // 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 the\n // 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, a glass or a bowl, which a kitchen measures out in halves and\n * in the 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 * cuillère à soupe is a spoon a kitchen owns, and there is nothing to gain by\n * calling it a cuillère à café et demie.\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\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\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}\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 * 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 * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"450 à 1000 g\" comes back as \"450 à\n * 1 kg\", where the two numbers are not in the same measure. Of the two, the\n * smaller end is the one a unit can ruin: choosing from the larger turns\n * \"450 à 1000 g\" into \"0,45 à 1 kg\", and pushed one step further it rounds the\n * small end away entirely. A large number in a small unit is merely long to\n * read.\n */\n const positive = ends.map((end) => end.raw).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: 1666 g rounded\n // as kilos is 1.7, and rounded as grams it is the 1.665 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 { amount: Math.min(usable, ceiling), exact, clamped: false, raw };\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 it\n // no longer fills.\n if (stepped.ratio !== 1) {\n const bounds = eachEnd(({ published, raw }) => {\n const exact = raw * stepped.ratio;\n const ceiling = factor < 1 ? published * stepped.ratio : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(exact, ceiling);\n return { amount: rounded.value, exact, clamped: rounded.clamped, raw };\n });\n return { bounds, unit: stepped.unit };\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 };\n });\n return { bounds, unit };\n }\n\n if (unit && unit.kind === \"vague\") {\n // A pincée, a poignée or a filet has the size the cook's hand gives it, so\n // the proportion of the recipe lives in how many are asked for. The count\n // therefore multiplies in whole units, and the measure stays in its own\n // vocabulary rather than being turned into grams or spoons, where published\n // equivalences span a fourfold range.\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, \"whole\", ceiling);\n return { amount: Math.min(rounded.value, ceiling), exact: raw, clamped: false, raw };\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 };\n });\n return { bounds, unit };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Make a counted item agree with its amount, 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, and\n * only its trailing \"s\": nouns already ending in -s, -x or -z are invariable in\n * the plural (\"ananas\", \"choux\"), and forcing one would be worse than leaving the\n * word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_NOUN`, because the ending settles nothing on\n * its own: \"jus\" and \"clous\" both end in -us, and the first is a singular where\n * the second is a plural of \"clou\".\n */\nfunction agreeWithAmount(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 const isPlural = PLURAL_ENDING.test(head);\n\n if (wantsPlural && !isPlural) {\n // Words ending in -s, -x or -z do not take a plural mark.\n if (SIBILANT_ENDING.test(head)) {\n // The head stays as written.\n }\n // \"morceau\" and \"bocal\" take -x and -aux where the ordinary noun takes -s.\n else if (EAU_ENDING.test(head)) {\n words[0] = `${head}x`;\n } else if (AL_ENDING.test(head)) {\n words[0] = `${head.slice(0, -2)}aux`;\n } else {\n words[0] = `${head}s`;\n }\n } else if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n words[0] = head.slice(0, -1);\n } else if (AUX_ENDING.test(head)) {\n words[0] = `${head.slice(0, -3)}al`;\n }\n // \"ananas\", \"anis\", \"couscous\": the -s belongs to the singular.\n else if (INVARIABLE_NOUN.has(foldWord(head))) {\n // The head stays as written.\n } else {\n words[0] = head.slice(0, -1);\n }\n }\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 * 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 */\nconst INVARIABLE_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 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 * 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 it takes the number the measure takes:\n * \"2 grosses pincées\". A word outside the declinable list stays as the recipe\n * wrote it, for the same reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number): string {\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\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\nfunction joinItem(item: string): string {\n if (!item) {\n return \"\";\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: \"100 g de beurre ou 2 oeufs\". */\nconst BRANCH_SEPARATOR = /\\s+ou\\s+/gi;\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 \"ou\" of a published\n * range such as \"2 ou 3 gousses d'ail\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"sucre ou cassonade\"\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 BRANCH_SEPARATOR.lastIndex = 0;\n for (let match = BRANCH_SEPARATOR.exec(text); match; match = BRANCH_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).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 pincée 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);\n }\n\n const text = line.trim();\n const branch = splitBranch(text, parseIngredient(text));\n if (branch) {\n return scaleBranchedLine(line, branch, options);\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);\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/** The equivalents beside a measure, written the way the line wrote them. */\nfunction alternateLabel(texts: readonly string[], restated: boolean): string {\n if (texts.length === 0) {\n return \"\";\n }\n return restated ? ` / ${texts.join(\" / \")}` : ` (${texts.join(\" / \")})`;\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 and agrees with it, as in \"1/3 oeuf\" and \"3 brioches\".\n */\nfunction itemLabelFor(unit: UnitInfo | null, item: string, counted: string): string {\n if (unit) {\n return joinItem(item);\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}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * A note is built rather than assembled at the call site because the reasons\n * stack: a line can be clamped, carry a second quantity, collapse its range and\n * state an approximation, and each of those has to be said without cancelling\n * 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 } = 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(Math.round(value * 1000) / 1000, { fractions: false })}${\n source ? ` ${formatUnit(source, value)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n // Name the floor this line actually landed on: how far one of the thing\n // divides is what sets it, so a sachet stops at a half where an oignon goes\n // to a quarter.\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000)}, the smallest amount worth ` +\n \"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 quantity.\n const moved = primaryBounds.filter((bound) => !landedExactly(bound.exact, bound.amount));\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 to\n // move. Saying \"rounded from 300 g\" when 300 g is exact would send a cook\n // 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 carry a second amount after the first, as in \"20 g de levure\n // dissoute dans 1 cuillère à soupe d'eau\". Only the amount the line opens with\n // is scaled, and one left at its published size contradicts it. This is said\n // whatever else happened to the line: a line that was also rounded is the one\n // where a stale second quantity is hardest to spot.\n if (hasEmbeddedMeasure(parsed.item)) {\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 (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 === \"vague\") {\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 \"douzaine\" 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)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\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 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 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);\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n original: parsed.original,\n text: parsed.original,\n amount: null,\n amountMax: null,\n unit: null,\n scaling: \"unscaled\",\n adjusted: false,\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));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((b) => !landedExactly(b.exact, b.amount));\n const movedAlternate = alternateBounds.some((b) => !landedExactly(b.exact, b.amount));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote them,\n // and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\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 // Mass and volume read as decimals; counted and spooned things read as\n // fractions.\n const asText = (value: number) => formatAmount(value, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 à\n // 1 gousse\" 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 // 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 unit && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown)}`\n : \"\";\n const unitLabel = unit ? `${adjective} ${formatUnit(unit, shown)}` : \"\";\n // Equivalents go back the way the line offered them: inside brackets, or\n // after a slash beside the amount they restate.\n const alternateTexts = alternates.map((entry) => entry.text);\n const altLabel = alternateLabel(alternateTexts, restated);\n // A measure needs the partitive French puts between it and what it measures.\n // A counted item stands straight after its number, and agrees with it:\n // \"1/3 oeuf\", \"3 brioches\".\n const counted = agreeWithAmount(parsed.item, shown);\n const itemLabel = itemLabelFor(unit, parsed.item, counted);\n\n const adjusted = movedPrimary || movedAlternate;\n const result: ScaledIngredient = {\n original: parsed.original,\n text: `${parsed.approximation ?? \"\"}${amountText}${unitLabel}${altLabel}${itemLabel}`.trim(),\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: unit?.canonical ?? null,\n scaling: adjusted || restated || clamped ? \"rounded\" : \"scaled\",\n adjusted,\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 });\n if (note !== undefined) {\n result.note = note;\n }\n\n return result;\n}\n\n/** Opening of the sentence an approximate measure carries. */\nconst APPROXIMATE_MEASURE_MARKER = \"is an approximate measure\";\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 pincées of\n * bicarbonate has said nothing about cuillères, and answering in cuillères\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} ${APPROXIMATE_MEASURE_MARKER}${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 * Whether this line was scaled as an approximate measure, so a caller can say\n * so once for a whole list instead of reading every note.\n */\nexport function isApproximateMeasure(entry: ScaledIngredient): boolean {\n return entry.note?.includes(APPROXIMATE_MEASURE_MARKER) ?? false;\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(measure: Measure, factor: number): { 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) => formatAmount(value, { 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)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3-4\" 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 to multiply is\n * `unscaled` and says why.\n */\nexport function passthroughIngredient(line: string): ScaledIngredient {\n const parsed = parseIngredient(line);\n const held = parsed.amount === null || parsed.heldBack !== null;\n\n const result: ScaledIngredient = {\n original: parsed.original,\n text: parsed.original,\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n scaling: held ? \"unscaled\" : \"scaled\",\n adjusted: false,\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 === \"vague\") {\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(lines: string[]): ScaledIngredient[] {\n return lines.map((line) => ({ ...passthroughIngredient(line), scaling: \"unscaled\" as const }));\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","/** Pieces shared by the three tools: schemas, error mapping, text mirrors. */\n\nimport { z } from \"zod\";\nimport { MarmitonError } from \"../errors.js\";\n\n/** Many MCP clients render only the text block, so it must read on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\nexport const scaledIngredientSchema = z.object({\n original: z.string().describe(\"The ingredient line exactly as Marmiton publishes it.\"),\n text: z.string().describe(\"The line after scaling, identical to 'original' when unscaled.\"),\n amount: z\n .number()\n .nullable()\n .describe(\n \"The scaled quantity, expressed in 'unit'. Read the two together: a large result is moved to a \" +\n \"bigger unit, so 200 g scaled tenfold reads as 2 kg, and the bare number shrinks while the \" +\n \"quantity grows.\",\n ),\n amountMax: z\n .number()\n .nullable()\n .describe(\n \"Upper bound when the line gives a range, as in '200 à 300 g', with 'amount' holding the lower \" +\n \"one. Null when the line states a single amount.\",\n ),\n unit: z\n .string()\n .nullable()\n .describe(\"The unit 'amount' is in, which may differ from the one the recipe used.\"),\n scaling: z\n .enum([\"scaled\", \"rounded\", \"unscaled\"])\n .describe(\n \"'scaled' means the arithmetic came out exact, which a count reaches as readily as a mass: one \" +\n \"pincée multiplied by six is six pincées. 'rounded' means the value had to be moved to stay \" +\n \"usable, to a whole or half unit or to a step a scale can show. 'unscaled' was left as \" +\n \"published, because nothing on the line is the factor's to multiply: the note says which of \" +\n \"the two it is.\",\n ),\n adjusted: z\n .boolean()\n .describe(\n \"True when rounding moved the value away from the exact product, which is what makes a line \" +\n \"'rounded' rather than 'scaled'.\",\n ),\n note: z\n .string()\n .optional()\n .describe(\n \"Why the line was rounded, clamped or left alone, including when the measure is approximate and \" +\n \"only its count was multiplied.\",\n ),\n});\n\nexport interface ToolResult {\n // The SDK's CallToolResult 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/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * The block ends with lines opening \"Note:\" and \"Source:\", and a caller has no\n * way to tell one of those from the same words inside a title, a quote or a\n * description written by whoever published it. Indenting a body line that\n * opens with one of those words keeps the two apart, and costs nothing: the\n * structured output still carries the text exactly as it was published.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:|Source:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with the notes.\n *\n * The notes are what qualifies the answer: that a quantity was clamped and no\n * longer holds its share, that the nutrition figures describe the recipe as\n * published rather than the amounts printed above them, that two ways of asking\n * for the same thing disagreed. Without them a client rendering only the text\n * reads an answer with nothing to qualify it.\n *\n * They are appended after the body is trimmed, so they survive a long\n * ingredient list.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof MarmitonError\n ? error\n : new MarmitonError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n","/**\n * scale_ingredients: rescale an arbitrary ingredient list, offline.\n *\n * This tool makes no network request. It exposes the quantity parser on its own,\n * so a list copied from anywhere, not only Marmiton, can be rescaled with the\n * same care about what is and is not safe to multiply.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport { isApproximateMeasure, scaleIngredients } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, scaledIngredientSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** The note a scaling that could not go lower leaves on an ingredient. */\nconst CLAMPED_UP = /clamped up/i;\n\nexport const scaleIngredientsDescription = [\n \"Rescale a list of ingredient lines to a different number of servings, without contacting any website.\",\n \"Give either 'factor' directly, or 'from_servings' and 'to_servings' and the factor is computed.\",\n \"Works on any French ingredient list, whatever its source, so it also serves a recipe the user pasted in.\",\n \"Quantities in grams or millilitres are multiplied and rounded to readable values; a countable thing lands on\",\n \"a half when half of one can be poured, weighed or cut, as a boîte, a sachet, a feuille de gélatine or a\",\n \"cuillère can, and on a whole one when it cannot, as an oeuf, a jaune or a blanc; approximate measures such\",\n \"as a pinch or a handful have\",\n \"their count multiplied in whole units and stay in their own vocabulary. A line writing an article where a\",\n \"digit would go, as in 'un bouchon de rhum' or 'une pincée de sel', is read as one of that measure. Lines\",\n \"carrying no quantity are returned untouched and flagged. Prefer this over doing the arithmetic yourself.\",\n].join(\" \");\n\nexport const scaleIngredientsInput = strictInput({\n ingredients: z\n .array(z.string().max(300))\n .min(1)\n .max(100)\n .describe(\"Ingredient lines, for example ['200 g de farine', '3 oeufs', 'sel'].\"),\n factor: z\n .number()\n .positive()\n .max(100)\n .optional()\n .describe(\"Multiplier to apply. Use this or the from/to pair.\"),\n from_servings: z\n .number()\n .positive()\n .max(500)\n .optional()\n .describe(\"How many servings the list is written for.\"),\n to_servings: z.number().positive().max(500).optional().describe(\"How many servings are wanted.\"),\n});\n\nexport const scaleIngredientsOutputShape = {\n factor: z.number(),\n ingredients: z.array(scaledIngredientSchema),\n scaled_count: z.number().int(),\n rounded_count: z\n .number()\n .int()\n .describe(\n \"Lines whose value rounding moved away from the exact product, not lines that could have been rounded.\",\n ),\n unscaled_count: z.number().int(),\n notes: z.array(z.string()),\n};\n\nexport interface ScaleIngredientsArgs {\n ingredients: string[];\n factor?: number;\n from_servings?: number;\n to_servings?: number;\n}\n\n/**\n * Print a factor without rounding it out of existence.\n *\n * Two decimals turn 0.001 into \"0\", which states that nothing was applied while\n * every quantity in the list was divided by a thousand.\n */\nfunction formatFactor(factor: number): string {\n return String(Number(factor.toPrecision(3)));\n}\n\nexport function runScaleIngredients(args: ScaleIngredientsArgs): ToolResult {\n try {\n let factor: number;\n const notes: string[] = [];\n\n if (args.factor !== undefined) {\n factor = args.factor;\n if (args.from_servings !== undefined || args.to_servings !== undefined) {\n notes.push(\n \"'factor' was given alongside 'from_servings'/'to_servings'. 'factor' was applied and the \" +\n \"pair ignored; send only one of the two to remove the ambiguity.\",\n );\n }\n } else if (args.from_servings !== undefined && args.to_servings !== undefined) {\n factor = args.to_servings / args.from_servings;\n } else {\n throw invalidInput(\n \"Provide either 'factor', or both 'from_servings' and 'to_servings'.\",\n \"For example from_servings=6 and to_servings=4, or factor=0.667.\",\n );\n }\n\n const ingredients = scaleIngredients(args.ingredients, { factor });\n const counts = {\n scaled: ingredients.filter((entry) => entry.scaling === \"scaled\").length,\n // Lines that were actually moved, rather than lines that merely belong to\n // the roundable category: at a factor of 100 every egg lands whole.\n rounded: ingredients.filter((entry) => entry.scaling === \"rounded\" && entry.adjusted).length,\n unscaled: ingredients.filter((entry) => entry.scaling === \"unscaled\").length,\n clamped: ingredients.filter((entry) => CLAMPED_UP.test(entry.note ?? \"\")).length,\n approximate: ingredients.filter(isApproximateMeasure).length,\n };\n\n if (counts.rounded > 0) {\n notes.push(\n `${counts.rounded} quantity(ies) were rounded to stay usable, rather than left as fractions.`,\n );\n }\n if (counts.unscaled > 0) {\n notes.push(\n `${counts.unscaled} line(s) carry no usable quantity and were returned unchanged; adjust to taste.`,\n );\n }\n if (counts.approximate > 0) {\n notes.push(\n `${counts.approximate} approximate measure(s) such as a pinch or a handful had their count ` +\n \"multiplied; the size of one is yours to judge.\",\n );\n }\n if (counts.clamped > 0) {\n notes.push(\n `${counts.clamped} quantity(ies) fell below the smallest amount worth measuring and were clamped up, ` +\n \"so their proportions no longer match the original recipe.\",\n );\n }\n\n const structured = {\n factor: Number(factor.toPrecision(3)),\n ingredients,\n scaled_count: counts.scaled,\n rounded_count: counts.rounded,\n unscaled_count: counts.unscaled,\n notes,\n };\n\n const lines = ingredients\n .map((entry) => {\n const flag = entry.scaling === \"unscaled\" ? \" (non ajusté)\" : \"\";\n return `- ${entry.text}${flag}`;\n })\n .join(\"\\n\");\n\n return ok(structured, `Facteur ${formatFactor(factor)}:\\n${lines}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_recipes: find recipes on Marmiton by free text.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport type { MarmitonClient } from \"../marmiton/client.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const searchRecipesDescription = [\n \"Search Marmiton for recipes by free text: a dish, an ingredient, or both, for example 'tarte aux pommes'\",\n \"or 'poulet curry coco'. Marmiton is a French site, so French terms work best.\",\n \"Returns candidate recipes with the id and URL needed to read one with get_recipe.\",\n \"One call returns one page of results. Marmiton's robots.txt disallows paginating search results, so this\",\n \"server does not, and there is no page parameter: narrow the query instead of asking for more pages.\",\n].join(\" \");\n\nexport const searchRecipesInput = strictInput({\n // Deliberately no min(1): an empty string would be rejected by the schema with\n // a protocol-level validation error, while a whitespace-only one would reach\n // the tool and come back as invalid_input. Letting both through to the same\n // check gives callers one error code for one problem.\n query: z\n .string()\n .max(200)\n .describe(\"What to search for, in French, for example 'tarte aux pommes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(30)\n .default(10)\n .describe(\"Maximum recipes to return from this page of results.\"),\n});\n\nexport const searchRecipesOutputShape = {\n query: z.string(),\n results: z.array(\n z.object({\n id: z.string().describe(\"Marmiton recipe id. Pass this to get_recipe.\"),\n title: z.string(),\n url: z.string(),\n image_url: z.string().nullable(),\n }),\n ),\n result_count: z.number().int(),\n total_available: z.number().int().describe(\"Recipes on this page before applying 'limit'.\"),\n source: z.literal(\"marmiton.org\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchRecipesArgs {\n query: string;\n limit: number;\n}\n\n/**\n * Whether a recipe title carries a word of the query.\n *\n * Marmiton matches on the opening letters, so \"chameau\" brings back a chapeau\n * and three châteaux: the rows are what the site ranked, and none of them names\n * the dish. Accents and punctuation are folded away so \"crêpes\" reads the same\n * as \"crepes\", and words of two letters are ignored, since an article says\n * nothing about the subject.\n */\nfunction titleCarries(title: string, query: string): boolean {\n const fold = (text: string) =>\n text\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/œ/g, \"oe\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \");\n\n const haystack = ` ${fold(title)} `;\n const words = fold(query)\n .split(\" \")\n .filter((word) => word.length > 2);\n if (words.length === 0) {\n return true;\n }\n return words.some((word) => haystack.includes(` ${word}`));\n}\n\nexport async function runSearchRecipes(\n client: MarmitonClient,\n args: SearchRecipesArgs,\n): Promise<ToolResult> {\n try {\n // Trimming happens before the emptiness check: a whitespace-only query is as\n // empty as a missing one, and letting it through would spend a request on a\n // search for nothing.\n const query = args.query.trim();\n if (!query) {\n throw invalidInput(\n \"'query' cannot be empty.\",\n 'Give a dish or an ingredient, for example query=\"tarte aux pommes\".',\n );\n }\n\n const { data, cached } = await client.search(query);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n if (data.length > args.limit) {\n notes.push(`This page holds ${data.length} recipes; showing the first ${args.limit}.`);\n }\n if (data.length === 0) {\n notes.push(\n \"Marmiton found no recipe for this query. Try French terms, or a broader wording. \" +\n \"Note that only one page of results is available.\",\n );\n }\n\n const results = data.slice(0, args.limit).map((recipe) => ({\n id: recipe.id,\n title: recipe.title,\n url: recipe.url,\n image_url: recipe.imageUrl,\n }));\n\n if (results.length > 0 && !results.some((recipe) => titleCarries(recipe.title, query))) {\n notes.push(\n `No title here carries a word of \"${query}\". Marmiton ranks a title on the letters it ` +\n \"opens with, so these rows are what the site offered for that spelling. Read them as \" +\n \"candidates to check rather than as recipes for the dish.\",\n );\n }\n\n const structured = {\n query,\n results,\n result_count: results.length,\n total_available: data.length,\n source: \"marmiton.org\" as const,\n notes,\n };\n\n const header =\n results.length > 0\n ? `${results.length} recette(s) pour \"${query}\":`\n : `Aucune recette trouvée pour \"${query}\".`;\n const list = results\n .map((recipe, index) => `${index + 1}. ${recipe.title} · id: ${recipe.id}`)\n .join(\"\\n\");\n\n return ok(structured, `${header}\\n${list}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","process","process","z","TRAILING_S","fraction","SIBILANT_ENDING","EAU_ENDING","AL_ENDING","EAUX_ENDING","AUX_ENDING","bounds","flatten","z","z","z","z","CLAMPED_UP","z","z","z","fold","z","process"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/version.ts","../src/server.ts","../src/errors.ts","../src/marmiton/cache.ts","../src/marmiton/rateLimiter.ts","../src/marmiton/http.ts","../src/marmiton/parseRecipe.ts","../src/recipe/duration.ts","../src/marmiton/jsonld.ts","../src/marmiton/urls.ts","../src/marmiton/client.ts","../src/tools/getRecipe.ts","../src/recipe/units.ts","../src/recipe/quantity.ts","../src/recipe/scale.ts","../src/tools/arguments.ts","../src/tools/shared.ts","../src/tools/scaleIngredients.ts","../src/tools/searchRecipes.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Entry point: an MCP server for Marmiton over stdio.\n */\n\nimport process from \"node:process\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { createServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const config = loadConfig();\n const logger = createLogger(config.logLevel);\n const server = createServer({ config, logger });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n const shutdown = () => {\n // The close is attempted, and a close that fails is no reason to stay\n // alive: the process leaves either way.\n server\n .close()\n .catch(() => undefined)\n .finally(() => process.exit(0));\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((error: unknown) => {\n process.stderr.write(\n `[mcp-marmiton] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\\n`,\n );\n process.exit(1);\n});\n","/**\n * Runtime configuration, read from environment variables.\n *\n * A bad value never crashes the process: an MCP server that dies at startup\n * because of a typo in a client config file is very hard to diagnose from the\n * host application, so invalid input is clamped and reported on stderr.\n */\n\nimport process from \"node:process\";\nimport { PKG_VERSION, REPO_URL } from \"./version.js\";\n\nexport type LogLevel = \"silent\" | \"error\" | \"info\" | \"debug\";\n\nexport interface Config {\n userAgent: string;\n minIntervalMs: number;\n timeoutMs: number;\n maxRetries: number;\n cacheTtlMs: number;\n cacheMaxEntries: number;\n /** The largest page this reader holds, in bytes. */\n maxBodyBytes: number;\n logLevel: LogLevel;\n}\n\n/**\n * MARMITON asks third-party clients to identify themselves with an application\n * name, a version and a link to the project, so the default follows that format.\n */\nexport const DEFAULT_USER_AGENT = `mcp-marmiton v${PKG_VERSION} (${REPO_URL})`;\n\nexport const DEFAULTS = {\n minIntervalMs: 1000,\n timeoutMs: 15_000,\n maxRetries: 3,\n cacheTtlMs: 15 * 60 * 1000,\n cacheMaxEntries: 200,\n // Recipe pages run to tens of kilobytes. A body an order of magnitude past\n // that is a page this reader has no reading for, and finding that out by\n // holding it costs the caller the memory and the site the transfer.\n maxBodyBytes: 8_000_000,\n logLevel: \"error\" as LogLevel,\n};\n\n/**\n * Floor on the request interval, enforced regardless of configuration.\n *\n * MARMITON publishes no rate limit and asks for nothing in return for a free\n * service. Self-imposed pacing is the whole of the politeness contract here, so\n * a value below this floor is refused and the default is used instead.\n */\nexport const MIN_ALLOWED_INTERVAL_MS = 500;\n\nconst LOG_LEVELS: LogLevel[] = [\"silent\", \"error\", \"info\", \"debug\"];\n\ninterface NumericRange {\n min: number;\n max: number;\n fallback: number;\n}\n\nfunction readNumber(name: string, env: NodeJS.ProcessEnv, range: NumericRange): number {\n const raw = env[name];\n if (raw === undefined || raw.trim() === \"\") {\n return range.fallback;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`${name}=\"${raw}\" is not a number, using ${range.fallback}`);\n return range.fallback;\n }\n\n // Out-of-range values fall back to the default rather than being clamped to\n // the nearest bound. Clamping turns a typo into a silent behaviour change:\n // -1 retries becomes \"never retry\", and -1 cache entries disables the cache\n // outright, both of which look like working configuration.\n const rounded = Math.round(parsed);\n if (rounded < range.min || rounded > range.max) {\n warn(\n `${name}=${raw} is outside the accepted range ${range.min}-${range.max} and was ignored; ` +\n `using ${range.fallback}`,\n );\n return range.fallback;\n }\n return rounded;\n}\n\nfunction warn(message: string): void {\n process.stderr.write(`[mcp-marmiton] ${message}\\n`);\n}\n\n/**\n * Read the request interval, refusing anything below the floor.\n *\n * A value under the floor falls back to the default rather than to the floor\n * itself: someone who set 0 was not asking for 200, they were asking for no\n * pacing at all, and the safe reading of that request is to ignore it.\n */\nfunction readInterval(env: NodeJS.ProcessEnv): number {\n const raw = env.MARMITON_MIN_INTERVAL_MS;\n if (raw === undefined || raw.trim() === \"\") {\n return DEFAULTS.minIntervalMs;\n }\n\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`MARMITON_MIN_INTERVAL_MS=\"${raw}\" is not a number, using ${DEFAULTS.minIntervalMs}ms`);\n return DEFAULTS.minIntervalMs;\n }\n\n const rounded = Math.round(parsed);\n if (rounded < MIN_ALLOWED_INTERVAL_MS) {\n warn(\n `MARMITON_MIN_INTERVAL_MS=${raw} is below the ${MIN_ALLOWED_INTERVAL_MS}ms floor and was ignored; ` +\n `using ${DEFAULTS.minIntervalMs}ms. Marmiton serves these pages for readers, so this client paces itself.`,\n );\n return DEFAULTS.minIntervalMs;\n }\n\n return Math.min(60_000, rounded);\n}\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const rawUserAgent = env.MARMITON_USER_AGENT?.trim();\n const rawLogLevel = env.MARMITON_LOG_LEVEL?.trim().toLowerCase();\n\n let logLevel = DEFAULTS.logLevel;\n if (rawLogLevel) {\n if (LOG_LEVELS.includes(rawLogLevel as LogLevel)) {\n logLevel = rawLogLevel as LogLevel;\n } else {\n warn(`MARMITON_LOG_LEVEL=\"${rawLogLevel}\" is unknown, using \"${DEFAULTS.logLevel}\"`);\n }\n }\n\n return {\n userAgent: rawUserAgent || DEFAULT_USER_AGENT,\n minIntervalMs: readInterval(env),\n timeoutMs: readNumber(\"MARMITON_TIMEOUT_MS\", env, {\n min: 1000,\n max: 120_000,\n fallback: DEFAULTS.timeoutMs,\n }),\n maxRetries: readNumber(\"MARMITON_MAX_RETRIES\", env, {\n min: 0,\n max: 10,\n fallback: DEFAULTS.maxRetries,\n }),\n cacheTtlMs: readNumber(\"MARMITON_CACHE_TTL_MS\", env, {\n min: 0,\n max: 24 * 60 * 60 * 1000,\n fallback: DEFAULTS.cacheTtlMs,\n }),\n cacheMaxEntries: readNumber(\"MARMITON_CACHE_MAX_ENTRIES\", env, {\n min: 0,\n max: 10_000,\n fallback: DEFAULTS.cacheMaxEntries,\n }),\n maxBodyBytes: readNumber(\"MARMITON_MAX_BODY_BYTES\", env, {\n min: 100_000,\n max: 64_000_000,\n fallback: DEFAULTS.maxBodyBytes,\n }),\n logLevel,\n };\n}\n\nconst LEVEL_RANK: Record<LogLevel, number> = { silent: 0, error: 1, info: 2, debug: 3 };\n\n/**\n * Logs go to stderr without exception. On a stdio transport, stdout carries the\n * protocol and any stray write there corrupts the session.\n */\nexport function createLogger(level: LogLevel) {\n const emit = (at: LogLevel, message: string) => {\n if (LEVEL_RANK[level] >= LEVEL_RANK[at]) {\n process.stderr.write(`[mcp-marmiton] ${message}\\n`);\n }\n };\n return {\n error: (message: string) => emit(\"error\", message),\n info: (message: string) => emit(\"info\", message),\n debug: (message: string) => emit(\"debug\", message),\n };\n}\n\nexport type Logger = ReturnType<typeof createLogger>;\n","/**\n * Kept in sync with package.json and server.json. The User-Agent embeds it, so it\n * must not drift; the release checklist bumps all three together.\n */\nexport const PKG_VERSION = \"2.2.0\";\nexport const REPO_URL = \"https://github.com/smeet666/mcp-marmiton\";\n","/**\n * MCP server wiring.\n *\n * One client, one rate limiter and one cache are shared by all tools, so pacing\n * applies to the server as a whole rather than per tool.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { Config, Logger } from \"./config.js\";\nimport { createLogger, loadConfig } from \"./config.js\";\nimport { MarmitonClient } from \"./marmiton/client.js\";\nimport {\n getRecipeDescription,\n getRecipeInput,\n getRecipeOutputShape,\n runGetRecipe,\n} from \"./tools/getRecipe.js\";\nimport type { GetRecipeArgs } from \"./tools/getRecipe.js\";\nimport {\n runScaleIngredients,\n scaleIngredientsDescription,\n scaleIngredientsInput,\n scaleIngredientsOutputShape,\n} from \"./tools/scaleIngredients.js\";\nimport type { ScaleIngredientsArgs } from \"./tools/scaleIngredients.js\";\nimport {\n runSearchRecipes,\n searchRecipesDescription,\n searchRecipesInput,\n searchRecipesOutputShape,\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 fetchImpl?: typeof fetch;\n}\n\n/** This server only reads, so every tool is read-only. */\nconst READ_ONLY = {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n} as const;\n\n/**\n * scale_ingredients does arithmetic on the arguments it was handed and contacts\n * nothing, so its world is closed: the same list and the same factor give the\n * same answer forever, whatever Marmiton publishes.\n */\nconst OFFLINE = { ...READ_ONLY, openWorldHint: false } as const;\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const config = options.config ?? loadConfig();\n const logger = options.logger ?? createLogger(config.logLevel);\n const client = new MarmitonClient({\n config,\n logger,\n ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),\n });\n\n const server = new McpServer(\n { name: \"mcp-marmiton\", version: PKG_VERSION },\n {\n instructions:\n \"Tools for looking up French recipes on Marmiton. No API key is needed. \" +\n \"Typical flow: search_recipes to find a recipe and its id, then get_recipe with that id, \" +\n \"passing 'servings' when the user wants a different number of people. \" +\n \"Do not rescale quantities yourself: get_recipe and scale_ingredients already round countable \" +\n \"items sensibly and flag what cannot be scaled, which is what stops answers like '2.4 eggs'. \" +\n \"Marmiton disallows paginating search results, so narrow the query instead of asking for more pages. \" +\n \"When you show a recipe to a user, credit it and link the source URL.\",\n },\n );\n\n server.registerTool(\n \"search_recipes\",\n {\n title: \"Search recipes\",\n description: searchRecipesDescription,\n inputSchema: searchRecipesInput,\n outputSchema: z.object(searchRecipesOutputShape),\n annotations: READ_ONLY,\n },\n async (args) => runSearchRecipes(client, args as SearchRecipesArgs),\n );\n\n server.registerTool(\n \"get_recipe\",\n {\n title: \"Get a recipe\",\n description: getRecipeDescription,\n inputSchema: getRecipeInput,\n outputSchema: z.object(getRecipeOutputShape),\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\",\n description: scaleIngredientsDescription,\n inputSchema: scaleIngredientsInput,\n outputSchema: z.object(scaleIngredientsOutputShape),\n annotations: OFFLINE,\n },\n async (args) => runScaleIngredients(args as ScaleIngredientsArgs),\n );\n\n logger.info(\n `ready: user-agent=\"${config.userAgent}\", min interval ${config.minIntervalMs}ms, cache ${config.cacheTtlMs}ms`,\n );\n\n return server;\n}\n","/**\n * Error taxonomy surfaced to the calling model.\n *\n * The rule carried over from the sibling projects: a failure must never be\n * reported as an empty result. A model that sees \"no recipe found\" cannot tell\n * that apart from a genuine absence, and will confidently tell the user the\n * recipe does not exist.\n */\n\nexport type ErrorCode =\n | \"not_found\"\n | \"invalid_input\"\n | \"rate_limited\"\n | \"parse_failure\"\n | \"network_error\"\n | \"timeout\";\n\nexport interface ErrorDetails {\n url?: string;\n status?: number;\n retryAfterMs?: number;\n hint?: string;\n}\n\nexport class MarmitonError extends Error {\n constructor(\n readonly code: ErrorCode,\n message: string,\n readonly details: ErrorDetails = {},\n ) {\n super(message);\n this.name = \"MarmitonError\";\n }\n}\n\nconst ISSUES_URL = \"https://github.com/smeet666/mcp-marmiton/issues\";\n\nexport function notFound(url: string, what: string): MarmitonError {\n return new MarmitonError(\"not_found\", `Marmiton has no recipe at ${what}.`, {\n url,\n status: 404,\n hint: \"Use search_recipes to find a recipe and its id, then call this tool with that id.\",\n });\n}\n\nexport function invalidInput(message: string, hint?: string): MarmitonError {\n return new MarmitonError(\"invalid_input\", message, hint ? { hint } : {});\n}\n\nexport function rateLimited(url: string, retryAfterMs: number): MarmitonError {\n return new MarmitonError(\n \"rate_limited\",\n \"Marmiton is rate limiting this client. This does NOT mean the recipe does not exist.\",\n {\n url,\n retryAfterMs,\n hint:\n `Wait about ${Math.ceil(retryAfterMs / 1000)} seconds, then call the same tool again with the ` +\n \"same arguments. If it keeps happening, raise MARMITON_MIN_INTERVAL_MS.\",\n },\n );\n}\n\nexport function parseFailure(url: string, what: string): MarmitonError {\n return new MarmitonError(\n \"parse_failure\",\n `The page loaded but the expected structured recipe data was not found (${what}). ` +\n \"Marmiton may have changed how it publishes it.\",\n { url, hint: `Please report this, with the recipe you asked for, at ${ISSUES_URL}` },\n );\n}\n\nexport function upstreamError(url: string, status: number): MarmitonError {\n return new MarmitonError(\"network_error\", `Marmiton returned HTTP ${status}.`, {\n url,\n status,\n ...(status >= 500 ? { hint: \"This is a problem on Marmiton's side. Try again shortly.\" } : {}),\n });\n}\n","/**\n * In-memory TTL + LRU cache.\n *\n * Nothing is written to disk. The cache exists so that repeated questions about\n * the same recipe inside one conversation do not hit Marmiton again, not to\n * build a local copy of the site.\n *\n * A Map iterates in insertion order, so re-inserting on every hit is enough to\n * make the first key the least recently used one.\n */\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\nexport class TtlLruCache<V> {\n private readonly store = new Map<string, Entry<V>>();\n\n constructor(\n private readonly maxEntries: number,\n private readonly ttlMs: number,\n ) {}\n\n get(key: string): V | undefined {\n const entry = this.store.get(key);\n if (!entry) {\n return undefined;\n }\n if (Date.now() >= entry.expiresAt) {\n this.store.delete(key);\n return undefined;\n }\n this.store.delete(key);\n this.store.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n if (this.maxEntries <= 0 || this.ttlMs <= 0) {\n return;\n }\n this.store.delete(key);\n this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });\n while (this.store.size > this.maxEntries) {\n const oldest = this.store.keys().next();\n if (oldest.done) {\n break;\n }\n this.store.delete(oldest.value);\n }\n }\n\n get size(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n}\n","/**\n * Serial request queue with an adaptive minimum interval.\n *\n * Marmiton publishes no rate limit and answers a burst by refusing rather than\n * by slowing down, so requests run one at a time with a floor on the gap between\n * starts. When the site does push back, the interval doubles and then decays\n * back down as requests succeed, which recovers faster than a fixed delay and\n * behaves better than a constant rate.\n */\n\nexport interface RateLimiterOptions {\n minIntervalMs: number;\n maxIntervalMs?: number;\n}\n\nconst DEFAULT_MAX_INTERVAL_MS = 10_000;\n\nexport class RateLimiter {\n private readonly baseIntervalMs: number;\n private readonly maxIntervalMs: number;\n private intervalMs: number;\n /** Tail of the queue: each task chains onto the previous one. */\n private tail: Promise<unknown> = Promise.resolve();\n private lastStart = 0;\n\n constructor(options: RateLimiterOptions) {\n this.baseIntervalMs = Math.max(0, options.minIntervalMs);\n this.maxIntervalMs = Math.max(\n this.baseIntervalMs,\n options.maxIntervalMs ?? DEFAULT_MAX_INTERVAL_MS,\n );\n this.intervalMs = this.baseIntervalMs;\n }\n\n get currentIntervalMs(): number {\n return this.intervalMs;\n }\n\n /** Queue a task. Tasks run in call order, one at a time. */\n schedule<T>(task: () => Promise<T>): Promise<T> {\n const run = this.tail.then(async () => task());\n // The queue must keep draining even when a task rejects.\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n return run;\n }\n\n /**\n * Wait for this request's slot, then claim it.\n *\n * Called once per upstream request rather than once per task, because a task\n * runs a whole retry chain: stamping only its start would let the next task\n * follow the chain's last request with no gap at all.\n */\n async beforeRequest(): Promise<void> {\n await this.waitForSlot();\n this.lastStart = Date.now();\n }\n\n /** Called after the site signals it is under load. */\n penalize(): void {\n const next = this.intervalMs === 0 ? 250 : this.intervalMs * 2;\n this.intervalMs = Math.min(this.maxIntervalMs, next);\n }\n\n /** Called after a success, so a single hiccup does not slow things down forever. */\n relax(): void {\n this.intervalMs = Math.max(this.baseIntervalMs, Math.floor(this.intervalMs * 0.75));\n }\n\n private async waitForSlot(): Promise<void> {\n if (this.intervalMs === 0 || this.lastStart === 0) {\n return;\n }\n const elapsed = Date.now() - this.lastStart;\n // Clamped to the interval: a clock stepped backwards, by NTP or a resumed\n // virtual machine, would otherwise make this wait for the size of the step,\n // and the queue is serial so every pending request would wait behind it.\n const remaining = Math.min(this.intervalMs, this.intervalMs - elapsed);\n if (remaining > 0) {\n await sleep(remaining);\n }\n }\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * HTTP layer: one GET, with backoff.\n *\n * Marmiton answers with ordinary status codes, so this layer mostly translates\n * them. The one judgement call is on a body too small to be a real page, which is\n * how a site under load often refuses without saying so.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport { MarmitonError, notFound, parseFailure, rateLimited, upstreamError } from \"../errors.js\";\nimport { type RateLimiter, sleep } from \"./rateLimiter.js\";\n\nconst CLOSING_HTML_TAG = /<\\/html>/i;\n\nconst BACKOFF_BASE_MS = 2000;\nconst BACKOFF_FACTOR = 2;\nconst BACKOFF_MAX_MS = 20_000;\n\n/** A real Marmiton page weighs well over 100 KB. */\nconst MIN_PLAUSIBLE_HTML = 2000;\n\n/** Exponential backoff with jitter, so parallel clients do not resynchronise. */\nexport function backoffDelay(attempt: number, random: () => number = Math.random): number {\n const capped = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * BACKOFF_FACTOR ** attempt);\n return Math.round(capped * (0.5 + random() * 0.5));\n}\n\nexport interface HttpDeps {\n config: Config;\n limiter: RateLimiter;\n logger: Logger;\n fetchImpl?: typeof fetch;\n}\n\n/**\n * What one answer from the site amounts to.\n *\n * Three outcomes and no fourth: the page is usable, the address holds nothing\n * and never will, or the site is asking to be left alone for a while. Reading\n * this apart from the loop keeps the loop about attempts and waits, which is\n * the part where a mistake costs the site traffic it did not ask for.\n */\ntype Answer =\n | { kind: \"usable\" }\n | { kind: \"refused\"; error: MarmitonError }\n | {\n kind: \"again\";\n error: MarmitonError;\n waitMs: number | null;\n penalise: boolean;\n because: string;\n };\n\nfunction readAnswer(\n url: string,\n status: number,\n body: string,\n retryAfterMs: number | null,\n ownGuessMs: number,\n): Answer {\n if (status === 429 || status === 503 || status === 403) {\n return {\n kind: \"again\",\n error: rateLimited(url, retryAfterMs ?? ownGuessMs),\n waitMs: retryAfterMs,\n penalise: true,\n because: \"rate limited\",\n };\n }\n if (status === 404) {\n return { kind: \"refused\", error: notFound(url, \"that address\") };\n }\n if (status >= 500) {\n return {\n kind: \"again\",\n error: upstreamError(url, status),\n waitMs: null,\n penalise: false,\n because: `status ${status}`,\n };\n }\n if (status >= 400) {\n return { kind: \"refused\", error: upstreamError(url, status) };\n }\n\n const trimmed = body.trim();\n if (trimmed.length < MIN_PLAUSIBLE_HTML && !CLOSING_HTML_TAG.test(trimmed)) {\n // Too small to be a page: treat it as a refusal rather than parse it and\n // report an empty result.\n return {\n kind: \"again\",\n error: rateLimited(url, ownGuessMs),\n waitMs: null,\n penalise: true,\n because: \"implausibly short body\",\n };\n }\n\n return { kind: \"usable\" };\n}\n\n/**\n * Fetch one page as HTML, retrying transient conditions.\n *\n * The retry loop and its sleeps run inside a single limiter slot, so a queued\n * request cannot slip into the window the current one is backing away from.\n */\n/**\n * The body, read in pieces and stopped at the size this reader holds.\n *\n * A deadline abandons a body that arrives slowly. One that arrives quickly and\n * large is never abandoned by it, and it lands in memory in one piece before\n * anything looks at it: a page of two hundred megabytes fits inside fifteen\n * seconds, and what it costs is the whole session rather than the one call.\n */\nasync function readBounded(response: Response, maxBytes: number, url: string): Promise<string> {\n const stream = response.body;\n // A response carrying no readable stream is read whole. Nothing arrives in\n // pieces to count, and the body still has to be waited for, so a caller\n // holding a deadline over this read keeps it.\n if (!stream) {\n return await response.text();\n }\n\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n const parts: string[] = [];\n let held = 0;\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n held += value.byteLength;\n if (held > maxBytes) {\n throw parseFailure(\n url,\n `the page carries more than the ${maxBytes} bytes this reads for one page`,\n );\n }\n parts.push(decoder.decode(value, { stream: true }));\n }\n } finally {\n await reader.cancel().catch(() => undefined);\n }\n\n parts.push(decoder.decode());\n return parts.join(\"\");\n}\n\nexport async function fetchHtml(url: string, deps: HttpDeps): Promise<string> {\n const { config, limiter, logger } = deps;\n const doFetch = deps.fetchImpl ?? fetch;\n\n return await limiter.schedule(async () => {\n let lastError: MarmitonError | undefined;\n\n // Set when the site says how long to stay away; it replaces our own guess\n // for the next attempt. Applied here rather than where it is read, so no\n // wait is ever served after the last attempt, when nobody would use it.\n let askedWaitMs: number | null = null;\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt += 1) {\n if (attempt > 0) {\n const delay = Math.min(askedWaitMs ?? backoffDelay(attempt - 1), BACKOFF_MAX_MS);\n askedWaitMs = null;\n logger.info(`retry ${attempt}/${config.maxRetries} in ${delay}ms for ${url}`);\n await sleep(delay);\n }\n\n let status: number;\n let body: string;\n let retryAfterMs: number | null = null;\n try {\n await limiter.beforeRequest();\n const response = await doFetch(url, {\n headers: {\n \"User-Agent\": config.userAgent,\n Accept: \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n \"Accept-Language\": \"fr-FR,fr;q=0.9,en;q=0.8\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(config.timeoutMs),\n });\n status = response.status;\n retryAfterMs = parseRetryAfter(response.headers.get(\"retry-after\"));\n body = await readBounded(response, config.maxBodyBytes, url);\n } catch (error) {\n lastError = asTransportError(error, url);\n logger.debug(`${lastError.code} for ${url}: ${lastError.message}`);\n continue;\n }\n\n const verdict = readAnswer(url, status, body, retryAfterMs, backoffDelay(attempt));\n if (verdict.kind === \"refused\") {\n throw verdict.error;\n }\n if (verdict.kind === \"again\") {\n if (verdict.penalise) {\n limiter.penalize();\n logger.info(`${verdict.because} on ${url}, interval now ${limiter.currentIntervalMs}ms`);\n }\n // A server that says when to come back knows better than our own guess.\n askedWaitMs = verdict.waitMs;\n lastError = verdict.error;\n continue;\n }\n\n limiter.relax();\n return body;\n }\n\n throw lastError ?? new MarmitonError(\"network_error\", `Could not fetch ${url}.`, { url });\n });\n}\n\n/** `Retry-After` carries either seconds or an HTTP date. */\nfunction parseRetryAfter(raw: string | null): number | null {\n if (!raw) {\n return null;\n }\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.round(seconds * 1000);\n }\n const when = Date.parse(raw);\n if (Number.isNaN(when)) {\n return null;\n }\n return Math.max(0, when - Date.now());\n}\n\nfunction asTransportError(error: unknown, url: string): MarmitonError {\n if (error instanceof MarmitonError) {\n return error;\n }\n const name = error instanceof Error ? error.name : \"\";\n if (name === \"TimeoutError\" || name === \"AbortError\") {\n return new MarmitonError(\"timeout\", \"Marmiton did not answer in time.\", {\n url,\n hint: \"Raise MARMITON_TIMEOUT_MS if this happens often on a slow connection.\",\n });\n }\n const message = error instanceof Error ? error.message : String(error);\n return new MarmitonError(\"network_error\", `Could not reach Marmiton: ${message}`, { url });\n}\n","/**\n * Turning Marmiton's JSON-LD into the domain types.\n *\n * The editorial `description` is deliberately not read here. Ingredient lists and\n * cooking steps are facts and procedures, and a public tool can hand them on with\n * a link to the source; the author's descriptive prose is their writing, and this\n * server has no business redistributing it.\n */\n\nimport process from \"node:process\";\nimport { parseFailure } from \"../errors.js\";\nimport { parseIsoDuration, parseYield } from \"../recipe/duration.js\";\nimport type { Nutrition, Rating, Recipe, RecipeSummary } from \"../types.js\";\nimport { asNumber, asString, asStringList, extractJsonLdNodes, findNodeOfType } from \"./jsonld.js\";\nimport { extractRecipeId, toAbsoluteUrl } from \"./urls.js\";\n\nexport interface ParseRecipeOptions {\n id: string;\n url: string;\n}\n\nexport function parseRecipePage(html: string, options: ParseRecipeOptions): Recipe {\n const nodes = extractJsonLdNodes(html);\n const node = findNodeOfType(nodes, \"Recipe\");\n\n // A page that loaded but carries no Recipe node means Marmiton changed how it\n // publishes its data. Reporting that as \"no recipe\" would be indistinguishable\n // from a genuine absence, so it has to be loud.\n if (!node) {\n throw parseFailure(options.url, \"no schema.org Recipe node in the page\");\n }\n\n const title = asString(node.name);\n const ingredients = asStringList(node.recipeIngredient);\n const steps = asStringList(node.recipeInstructions);\n\n if (!title) {\n throw parseFailure(options.url, \"the Recipe node has no name\");\n }\n if (ingredients.length === 0) {\n throw parseFailure(options.url, \"the Recipe node carries no ingredient list\");\n }\n\n return {\n id: options.id,\n title,\n url: asString(node.mainEntityOfPage) ?? options.url,\n imageUrl: asString(node.image),\n ingredients,\n steps,\n recipeYield: parseYield(node.recipeYield),\n prepMinutes: parseIsoDuration(node.prepTime),\n cookMinutes: parseIsoDuration(node.cookTime),\n totalMinutes: parseIsoDuration(node.totalTime),\n category: asString(node.recipeCategory),\n author: asString(node.author),\n rating: parseRating(node.aggregateRating),\n nutrition: parseNutrition(node.nutrition),\n };\n}\n\nfunction parseRating(value: unknown): Rating | null {\n if (typeof value !== \"object\" || value === null) {\n return null;\n }\n const record = value as Record<string, unknown>;\n const ratingValue = asNumber(record.ratingValue);\n if (ratingValue === null) {\n return null;\n }\n return {\n value: ratingValue,\n count: asNumber(record.ratingCount) ?? asNumber(record.reviewCount),\n best: asNumber(record.bestRating),\n };\n}\n\nfunction parseNutrition(value: unknown): Nutrition | null {\n if (typeof value !== \"object\" || value === null) {\n return null;\n }\n const record = value as Record<string, unknown>;\n const nutrition: Nutrition = {\n calories: asString(record.calories),\n protein: asString(record.proteinContent),\n fat: asString(record.fatContent),\n carbohydrate: asString(record.carbohydrateContent),\n fiber: asString(record.fiberContent),\n sodium: asString(record.sodiumContent),\n servingSize: asString(record.servingSize),\n };\n const hasAny = Object.values(nutrition).some((entry) => entry !== null);\n return hasAny ? nutrition : null;\n}\n\n/**\n * Search results, read from the ItemList the search page publishes.\n *\n * The page also lists more recipes in its HTML, but reaching them would mean\n * reintroducing CSS selectors and the breakage that comes with them. The\n * structured list is what the site offers machines, so that is what is used.\n */\nexport function parseSearchPage(html: string, url: string): RecipeSummary[] {\n const nodes = extractJsonLdNodes(html);\n const list = findNodeOfType(nodes, \"ItemList\");\n\n if (!list) {\n throw parseFailure(url, \"no schema.org ItemList node on the search page\");\n }\n\n const elements = list.itemListElement;\n if (!Array.isArray(elements)) {\n throw parseFailure(url, \"the ItemList node carries no itemListElement array\");\n }\n\n const results: RecipeSummary[] = [];\n let skipped = 0;\n\n for (const element of elements) {\n if (typeof element !== \"object\" || element === null) {\n skipped += 1;\n continue;\n }\n const record = element as Record<string, unknown>;\n const href = asString(record.url);\n const title = asString(record.name);\n const id = href ? extractRecipeId(href) : null;\n\n if (!(href && title && id)) {\n skipped += 1;\n continue;\n }\n\n results.push({ id, title, url: toAbsoluteUrl(href), imageUrl: asString(record.image) });\n }\n\n // Every entry failing while the list itself was present means the entry shape\n // changed, which is breakage rather than an empty search.\n if (results.length === 0 && elements.length > 0) {\n throw parseFailure(url, `${elements.length} list entries but none could be read`);\n }\n if (skipped > 0) {\n process.stderr.write(`[mcp-marmiton] skipped ${skipped} unreadable search entries on ${url}\\n`);\n }\n\n return results;\n}\n","/**\n * ISO 8601 durations, which is how schema.org expresses cooking times.\n *\n * Marmiton writes \"PT25M\" for 25 minutes and \"PT1H30M\" for an hour and a half.\n */\n\nconst NUMBER_AND_REST = /^(\\d+(?:[.,]\\d+)?)\\s*(.*)$/;\n\nconst ISO_DURATION = /^P(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?$/;\n\n/** Minutes, or null when the value is absent or unreadable. */\nexport function parseIsoDuration(value: unknown): number | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const match = ISO_DURATION.exec(value.trim().toUpperCase());\n if (!match) {\n return null;\n }\n\n const [, days, hours, minutes, seconds] = match;\n const total =\n Number(days ?? 0) * 1440 +\n Number(hours ?? 0) * 60 +\n Number(minutes ?? 0) +\n Number(seconds ?? 0) / 60;\n\n // \"P\" alone parses but means nothing useful.\n if (total === 0 && !minutes && !hours && !days && !seconds) {\n return null;\n }\n return Math.round(total);\n}\n\n/** Minutes as \"1 h 30\" or \"25 min\", for the human-readable mirror. */\nexport function formatMinutes(minutes: number | null): string | null {\n if (minutes === null || !Number.isFinite(minutes) || minutes <= 0) {\n return null;\n }\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n if (hours === 0) {\n return `${rest} min`;\n }\n if (rest === 0) {\n return `${hours} h`;\n }\n return `${hours} h ${String(rest).padStart(2, \"0\")}`;\n}\n\nexport interface ParsedYield {\n /** Number of servings, or null when the text carries no number. */\n count: number | null;\n /** What is being counted: \"personnes\", \"pièces\", \"parts\". */\n unit: string | null;\n /** The original text, always preserved. */\n text: string;\n}\n\n/**\n * The yield as written, out of the three shapes schema.org allows for it: a\n * number, a string, or an array holding either.\n */\nfunction yieldText(value: unknown): string {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return String(value);\n }\n if (typeof value === \"string\") {\n return value.trim();\n }\n if (Array.isArray(value)) {\n return String(value[0] ?? \"\").trim();\n }\n return \"\";\n}\n\n/**\n * Read a schema.org recipeYield.\n *\n * Marmiton writes \"6 personnes\" but also \"15 pièces\", so the unit is kept: a\n * recipe yielding pieces cannot be described as serving people.\n */\nexport function parseYield(value: unknown): ParsedYield {\n const text = yieldText(value);\n if (!text) {\n return { count: null, unit: null, text: \"\" };\n }\n\n const match = NUMBER_AND_REST.exec(text);\n if (!match) {\n return { count: null, unit: null, text };\n }\n\n const [digits = \"\"] = match.slice(1);\n const count = Number(digits.replace(\",\", \".\"));\n const unit = (match[2] ?? \"\").trim() || null;\n return { count: Number.isFinite(count) ? count : null, unit, text };\n}\n","/**\n * JSON-LD extraction.\n *\n * Marmiton embeds a schema.org graph in every page, which is data the site\n * publishes deliberately for machines to read. Using it instead of CSS selectors\n * removes the whole class of breakage where a redesign renames a class and the\n * parser silently returns nothing.\n */\n\nconst LD_JSON_BLOCK = /<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n\n/** Every JSON-LD object on the page, flattened out of any `@graph` wrappers. */\nexport function extractJsonLdNodes(html: string): unknown[] {\n const nodes: unknown[] = [];\n LD_JSON_BLOCK.lastIndex = 0;\n\n for (let match = LD_JSON_BLOCK.exec(html); match !== null; match = LD_JSON_BLOCK.exec(html)) {\n const raw = match[1]?.trim();\n if (!raw) {\n continue;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // A malformed block is skipped rather than fatal: pages carry several,\n // and one broken analytics blob must not hide the recipe.\n continue;\n }\n\n for (const node of flatten(parsed)) {\n nodes.push(node);\n }\n }\n\n return nodes;\n}\n\nfunction flatten(value: unknown): unknown[] {\n if (Array.isArray(value)) {\n return value.flatMap(flatten);\n }\n if (typeof value !== \"object\" || value === null) {\n return [];\n }\n\n const node = value as Record<string, unknown>;\n const graph = node[\"@graph\"];\n if (graph !== undefined) {\n return flatten(graph);\n }\n return [node];\n}\n\n/** True when a node declares the given schema.org type. */\nexport function hasType(node: unknown, type: string): boolean {\n if (typeof node !== \"object\" || node === null) {\n return false;\n }\n const declared = (node as Record<string, unknown>)[\"@type\"];\n if (typeof declared === \"string\") {\n return declared === type;\n }\n if (Array.isArray(declared)) {\n return declared.includes(type);\n }\n return false;\n}\n\nexport function findNodeOfType(nodes: unknown[], type: string): Record<string, unknown> | null {\n const found = nodes.find((node) => hasType(node, type));\n // The cast reads what the search found; putting it before the coalesce would\n // assert the node exists and leave the null branch unreachable.\n return found === undefined ? null : (found as Record<string, unknown>);\n}\n\n/** Read a string field, tolerating the array form schema.org allows. */\nexport function asString(value: unknown): string | null {\n if (typeof value === \"string\") {\n return value.trim() || null;\n }\n if (Array.isArray(value)) {\n for (const entry of value) {\n const found = asString(entry);\n if (found) {\n return found;\n }\n }\n }\n if (typeof value === \"object\" && value !== null) {\n // Fields such as `image` are often an ImageObject rather than a plain URL.\n const url = (value as Record<string, unknown>).url;\n if (typeof url === \"string\") {\n return url.trim() || null;\n }\n const name = (value as Record<string, unknown>).name;\n if (typeof name === \"string\") {\n return name.trim() || null;\n }\n }\n return null;\n}\n\nexport function asNumber(value: unknown): number | null {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n if (typeof value === \"string\") {\n const parsed = Number(value.replace(\",\", \".\"));\n if (Number.isFinite(parsed)) {\n return parsed;\n }\n }\n return null;\n}\n\n/** Read a list of strings, which is how ingredients and steps are expressed. */\nexport function asStringList(value: unknown): string[] {\n if (typeof value === \"string\") {\n return [value.trim()].filter(Boolean);\n }\n if (!Array.isArray(value)) {\n return [];\n }\n\n const out: string[] = [];\n for (const entry of value) {\n if (typeof entry === \"string\") {\n const text = entry.trim();\n if (text) {\n out.push(text);\n }\n continue;\n }\n if (typeof entry === \"object\" && entry !== null) {\n // HowToStep carries the sentence under `text`.\n const record = entry as Record<string, unknown>;\n const text = asString(record.text) ?? asString(record.name);\n if (text) {\n out.push(text);\n }\n }\n }\n return out;\n}\n","/** URL construction and validation for marmiton.org. */\n\nimport { invalidInput } from \"../errors.js\";\n\nconst ABSOLUTE_URL = /^https?:\\/\\//i;\nconst DIGITS_ONLY = /^\\d+$/;\n\nexport const BASE_URL = \"https://www.marmiton.org\";\n\n/** Recipe pages look like /recettes/recette_<slug>_<id>.aspx */\nexport const RECIPE_PATH_RE = /\\/recettes\\/recette[_-][a-z0-9%\\-.]*?_(\\d+)\\.aspx/i;\n\nconst ALLOWED_HOSTS = new Set([\"marmiton.org\", \"www.marmiton.org\"]);\n\n/**\n * Search URL.\n *\n * Only the `aqt` parameter is used. Marmiton's robots.txt disallows\n * `recherche.aspx?aqt=*page*` and `?aqt=*start*`, so this client never paginates\n * and the tool description says so rather than pretending the limit does not\n * exist.\n */\nexport function buildSearchUrl(query: string): string {\n const url = new URL(`${BASE_URL}/recettes/recherche.aspx`);\n url.searchParams.set(\"aqt\", query);\n return url.toString();\n}\n\n/**\n * Recipe URL from an id alone.\n *\n * Marmiton keys the page on the trailing id and ignores the slug, redirecting to\n * the canonical address whatever the slug says. It does require the slug to be\n * non-empty though: `recette__18588.aspx` answers 404 while `recette_r_18588.aspx`\n * redirects to the real page. Hence the placeholder, which lets a caller holding\n * only an id skip having to know the title.\n */\nexport function buildRecipeUrl(id: string): string {\n return `${BASE_URL}/recettes/recette_r_${id}.aspx`;\n}\n\n/** True only for marmiton.org, so a hostile URL cannot be used as a proxy. */\nexport function isMarmitonHost(rawUrl: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return false;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n return false;\n }\n return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());\n}\n\n/** Extract the numeric recipe id from a Marmiton path or absolute URL. */\nexport function extractRecipeId(hrefOrUrl: string): string | null {\n let path = hrefOrUrl;\n if (ABSOLUTE_URL.test(hrefOrUrl)) {\n if (!isMarmitonHost(hrefOrUrl)) {\n return null;\n }\n path = new URL(hrefOrUrl).pathname;\n }\n const match = RECIPE_PATH_RE.exec(path);\n return match?.[1] ?? null;\n}\n\n/** Resolve the `id` / `url` pair accepted by get_recipe. `id` wins. */\nexport function resolveRecipeRef(input: { id?: string; url?: string }): {\n id: string;\n url: string;\n} {\n if (input.id) {\n if (!DIGITS_ONLY.test(input.id)) {\n throw invalidInput(\n `\"${input.id}\" is not a Marmiton recipe id.`,\n \"Ids are digits only, as returned by search_recipes.\",\n );\n }\n return { id: input.id, url: buildRecipeUrl(input.id) };\n }\n\n if (input.url) {\n if (!isMarmitonHost(input.url)) {\n throw invalidInput(\n \"Only marmiton.org URLs are accepted.\",\n \"Pass the url returned by search_recipes, or use the numeric id instead.\",\n );\n }\n const id = extractRecipeId(input.url);\n if (!id) {\n throw invalidInput(\n \"That Marmiton URL is not a recipe page.\",\n \"Recipe pages look like https://www.marmiton.org/recettes/recette_tarte-aux-pommes_18588.aspx\",\n );\n }\n return { id, url: input.url };\n }\n\n throw invalidInput(\"Either 'id' or 'url' must be provided.\");\n}\n\nexport function toAbsoluteUrl(href: string): string {\n return new URL(href, BASE_URL).toString();\n}\n","/**\n * High-level Marmiton client.\n *\n * This module knows nothing about MCP, which keeps it testable against plain\n * strings and usable as a library through the `./client` export.\n */\n\nimport type { Config, Logger } from \"../config.js\";\nimport {\n DEFAULT_USER_AGENT,\n MIN_ALLOWED_INTERVAL_MS,\n createLogger,\n loadConfig,\n} from \"../config.js\";\nimport { MarmitonError } from \"../errors.js\";\nimport type { Recipe, RecipeSummary } from \"../types.js\";\nimport { TtlLruCache } from \"./cache.js\";\nimport { fetchHtml } from \"./http.js\";\nimport { parseRecipePage, parseSearchPage } from \"./parseRecipe.js\";\nimport { RateLimiter } from \"./rateLimiter.js\";\nimport { buildSearchUrl, resolveRecipeRef } from \"./urls.js\";\n\n/** The names a User-Agent carries when it passes traffic off as a browser. */\nconst BROWSER_IDENTITY = /mozilla\\/|applewebkit|chrome\\/|safari\\/|gecko/i;\n\nexport interface MarmitonClientOptions {\n config?: Config;\n logger?: Logger;\n fetchImpl?: typeof fetch;\n}\n\nexport interface Outcome<T> {\n data: T;\n /** True when served from the in-memory cache rather than the network. */\n cached: boolean;\n}\n\n/**\n * Apply the guarantees this project makes about its own traffic.\n *\n * The environment parser already enforces both, but `MarmitonClient` is published as a\n * library through the `./client` export and takes a caller-built config, so\n * without this the pacing floor and the honest identity are optional for anyone\n * importing it. Marmiton publishes no rate limit, so the pacing here is self-imposed politeness, and those promises hold on every path.\n *\n * A caller may still name their own application in the User-Agent. Passing the\n * traffic off as a browser is a different thing, and gets the project's own\n * identity appended so it stays attributable.\n */\nfunction withGuarantees(config: Config): Config {\n const userAgent = BROWSER_IDENTITY.test(config.userAgent)\n ? `${config.userAgent} ${DEFAULT_USER_AGENT}`\n : config.userAgent;\n return {\n ...config,\n userAgent,\n minIntervalMs: Math.max(MIN_ALLOWED_INTERVAL_MS, config.minIntervalMs),\n };\n}\n\nexport class MarmitonClient {\n private readonly config: Config;\n private readonly logger: Logger;\n private readonly limiter: RateLimiter;\n private readonly cache: TtlLruCache<unknown>;\n private readonly fetchImpl: typeof fetch | undefined;\n\n constructor(options: MarmitonClientOptions = {}) {\n this.config = withGuarantees(options.config ?? loadConfig());\n this.logger = options.logger ?? createLogger(this.config.logLevel);\n this.limiter = new RateLimiter({ minIntervalMs: this.config.minIntervalMs });\n this.cache = new TtlLruCache<unknown>(this.config.cacheMaxEntries, this.config.cacheTtlMs);\n this.fetchImpl = options.fetchImpl;\n }\n\n /**\n * Search recipes.\n *\n * One page only: Marmiton's robots.txt disallows the paginated form of this\n * URL, so there is no page parameter to pass.\n */\n async search(query: string): Promise<Outcome<RecipeSummary[]>> {\n const url = buildSearchUrl(query);\n try {\n return await this.fetchParsed(url, (html) => parseSearchPage(html, url));\n } catch (error) {\n // Marmiton answers a search matching no recipe with a 404 on the results\n // page. That is the site stating an absence, and handing it back as a\n // failed read would let a caller say Marmiton could not be reached when\n // Marmiton answered.\n if (error instanceof MarmitonError && error.code === \"not_found\") {\n return { data: [], cached: false };\n }\n throw error;\n }\n }\n\n async getRecipe(ref: { id?: string; url?: string }): Promise<Outcome<Recipe>> {\n const { id, url } = resolveRecipeRef(ref);\n return await this.fetchParsed(url, (html) => parseRecipePage(html, { id, url }));\n }\n\n /**\n * Fetch, parse, then cache. In that order: a page that could not be read is\n * never stored, so a bad minute at Marmiton cannot be replayed from memory for\n * the rest of the cache lifetime, leaving the tool unable to recover after\n * the site comes back.\n *\n * The cached value is the parsed result rather than the raw page, which also\n * keeps a few hundred kilobytes of markup per entry out of memory.\n */\n private async fetchParsed<T>(url: string, parse: (html: string) => T): Promise<Outcome<T>> {\n const hit = this.cache.get(url);\n if (hit !== undefined) {\n this.logger.debug(`cache hit ${url}`);\n return { data: hit as T, cached: true };\n }\n\n const html = await fetchHtml(url, {\n config: this.config,\n limiter: this.limiter,\n logger: this.logger,\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n\n const data = parse(html);\n this.cache.set(url, data);\n return { data, cached: false };\n }\n}\n","/**\n * get_recipe: read one recipe, optionally scaled to a number of servings.\n */\n\nimport { z } from \"zod\";\nimport type { MarmitonClient } from \"../marmiton/client.js\";\nimport { formatMinutes } from \"../recipe/duration.js\";\nimport { isApproximateMeasure, passthroughIngredients, scaleIngredients } from \"../recipe/scale.js\";\nimport type { ScaledIngredient } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, scaledIngredientSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** The note a scaling that could not go lower leaves on an ingredient. */\nconst CLAMPED_UP = /clamped up/i;\n\nexport const getRecipeDescription = [\n \"Read one Marmiton recipe: ingredients, steps, times, category, rating and nutrition.\",\n \"Give the id returned by search_recipes, or a marmiton.org recipe URL.\",\n \"Set 'servings' to rescale the quantities. Each ingredient reports how it was handled: 'scaled' when the\",\n \"arithmetic came out exact, which a count of eggs, spoons or pinches reaches as readily as a mass in grams,\",\n \"'rounded' when the value had to be moved to stay usable, and 'unscaled' for lines carrying no quantity at all.\",\n \"Trust that flag rather than recomputing: the point of scaling here is to avoid answers like '2.4 eggs'.\",\n \"Recipes yielding pieces rather than servings are rescaled the same way; check 'yield' to see which.\",\n \"Always cite 'attribution' when showing a recipe to a user.\",\n].join(\" \");\n\nexport const getRecipeInput = strictInput({\n id: z\n .string()\n .regex(/^\\d+$/, \"Recipe ids are digits only.\")\n .optional()\n .describe(\"Marmiton recipe id, as returned by search_recipes. Preferred over 'url'.\"),\n url: z\n .string()\n .url()\n .optional()\n .describe(\n \"Full marmiton.org recipe URL. Only marmiton.org is accepted. Ignored when 'id' is given.\",\n ),\n servings: z\n .number()\n .positive()\n .max(500)\n .optional()\n .describe(\n \"Rescale the ingredients to this many servings. Omit to get the recipe exactly as published.\",\n ),\n});\n\nexport const getRecipeOutputShape = {\n id: z.string(),\n title: z.string(),\n url: z.string(),\n yield: z.object({\n original_count: z.number().nullable(),\n original_text: z.string(),\n requested: z.number().nullable(),\n unit: z.string().nullable().describe(\"What is being counted: 'personnes', 'pièces'.\"),\n factor: z.number().nullable().describe(\"Multiplier applied to the quantities.\"),\n }),\n ingredients: z.array(scaledIngredientSchema),\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.string().nullable(),\n rating: z\n .object({ value: z.number(), count: z.number().nullable(), best: z.number().nullable() })\n .nullable(),\n nutrition: z\n .object({\n calories: z.string().nullable(),\n protein: z.string().nullable(),\n fat: z.string().nullable(),\n carbohydrate: z.string().nullable(),\n fiber: z.string().nullable(),\n sodium: z.string().nullable(),\n serving_size: z.string().nullable(),\n })\n .nullable()\n .describe(\"Nutrition is given for the recipe as published, not rescaled.\"),\n attribution: z.string(),\n source: z.literal(\"marmiton.org\"),\n notes: z.array(z.string()),\n};\n\n/**\n * What a rescaled list owes its reader beyond the quantities themselves.\n *\n * Each sentence answers a question a cook would otherwise have to ask of every\n * line in turn: which figures moved, which ones stopped at a floor, which ones\n * are the cook's own judgement, and which ones were left alone.\n */\nfunction rescalingNotes(ingredients: readonly ScaledIngredient[], hasNutrition: boolean): string[] {\n const notes: string[] = [];\n\n // Lines that were actually moved, rather than lines that merely belong to the\n // roundable category: doubling a recipe lands every egg whole.\n const rounded = ingredients.filter(\n (entry) => entry.scaling === \"rounded\" && entry.adjusted,\n ).length;\n if (rounded > 0) {\n notes.push(`${rounded} quantity(ies) were rounded to remain usable; see the 'scaling' field.`);\n }\n\n const clamped = ingredients.filter((entry) => CLAMPED_UP.test(entry.note ?? \"\")).length;\n if (clamped > 0) {\n notes.push(\n `${clamped} quantity(ies) fell below the smallest amount worth measuring and were ` +\n \"clamped up, so their proportions no longer match the published recipe.\",\n );\n }\n\n if (ingredients.some(isApproximateMeasure)) {\n notes.push(\n \"Approximate measures such as a pinch or a handful had their count multiplied; the size \" +\n \"of one is yours to judge.\",\n );\n }\n\n if (ingredients.some((entry) => entry.scaling === \"unscaled\")) {\n notes.push(\"Some ingredients carry no usable quantity and were left as published.\");\n }\n\n if (hasNutrition) {\n notes.push(\"Nutrition values are for the recipe as published, not for the rescaled amounts.\");\n }\n\n return notes;\n}\n\nexport interface GetRecipeArgs {\n id?: string;\n url?: string;\n servings?: number;\n}\n\nexport async function runGetRecipe(\n client: MarmitonClient,\n args: GetRecipeArgs,\n): Promise<ToolResult> {\n try {\n const ref: { id?: string; url?: string } = {};\n if (args.id) {\n ref.id = args.id;\n } else if (args.url) {\n ref.url = args.url;\n }\n\n const { data, cached } = await client.getRecipe(ref);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n\n const originalCount = data.recipeYield.count;\n let factor: number | null = null;\n let ingredients: ScaledIngredient[];\n\n if (args.servings === undefined) {\n ingredients = passthroughIngredients(data.ingredients);\n } else if (originalCount === null || originalCount <= 0) {\n // Without a published yield there is nothing to scale from, and guessing\n // would produce quantities that look authoritative and are not.\n ingredients = passthroughIngredients(data.ingredients);\n notes.push(\n `Marmiton does not state how many servings this recipe makes (\"${data.recipeYield.text}\"), ` +\n \"so the quantities are unchanged.\",\n );\n } else {\n factor = args.servings / originalCount;\n ingredients = scaleIngredients(data.ingredients, { factor });\n\n const unit = data.recipeYield.unit ?? \"portions\";\n notes.push(\n `Quantities rescaled from ${originalCount} to ${args.servings} ${unit} (factor ${\n Math.round(factor * 100) / 100\n }).`,\n );\n notes.push(...rescalingNotes(ingredients, data.nutrition !== null));\n }\n\n const attribution = `${data.title}, recette Marmiton (${data.url})`;\n\n const structured = {\n id: data.id,\n title: data.title,\n url: data.url,\n yield: {\n original_count: originalCount,\n original_text: data.recipeYield.text,\n requested: args.servings ?? null,\n unit: data.recipeYield.unit,\n factor: factor === null ? null : Math.round(factor * 1000) / 1000,\n },\n ingredients,\n steps: data.steps,\n prep_minutes: data.prepMinutes,\n cook_minutes: data.cookMinutes,\n total_minutes: data.totalMinutes,\n category: data.category,\n author: data.author,\n rating: data.rating,\n nutrition: data.nutrition\n ? {\n calories: data.nutrition.calories,\n protein: data.nutrition.protein,\n fat: data.nutrition.fat,\n carbohydrate: data.nutrition.carbohydrate,\n fiber: data.nutrition.fiber,\n sodium: data.nutrition.sodium,\n serving_size: data.nutrition.servingSize,\n }\n : null,\n attribution,\n source: \"marmiton.org\" as const,\n notes,\n };\n\n const times = [\n data.prepMinutes ? `préparation ${formatMinutes(data.prepMinutes)}` : \"\",\n data.cookMinutes ? `cuisson ${formatMinutes(data.cookMinutes)}` : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n\n const header = [\n attribution,\n args.servings !== undefined && factor !== null\n ? `Pour ${args.servings} ${data.recipeYield.unit ?? \"portions\"} (recette prévue pour ${data.recipeYield.text}).`\n : `Recette pour ${data.recipeYield.text || \"portions non précisées\"}.`,\n times,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n // The flag answers \"why is this line unchanged\", which is only a question\n // when a rescaling was asked for. Without 'servings' it would read as a\n // statement about the ingredient itself.\n const rescaled = args.servings !== undefined && factor !== null;\n const ingredientLines = ingredients\n .map((entry) => {\n const flag = rescaled && entry.scaling === \"unscaled\" ? \" (non ajusté)\" : \"\";\n return `- ${entry.text}${flag}`;\n })\n .join(\"\\n\");\n\n const stepLines = data.steps.map((step, index) => `${index + 1}. ${step}`).join(\"\\n\");\n const body = [\n header,\n `Ingrédients:\\n${ingredientLines}`,\n stepLines ? `Préparation:\\n${stepLines}` : \"\",\n ]\n .filter(Boolean)\n .join(\"\\n\\n\");\n\n return ok(structured, body, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * French cooking unit vocabulary and what scaling means for each.\n *\n * The distinction that matters is not metric versus imperial, it is what shape\n * the scaled number has to take. \"200 g\" doubled is \"400 g\". \"1 pincée\" doubled\n * is \"2 pincées\": the pinch keeps whatever size the cook's fingers give it, and\n * the count is what carries the recipe's proportion.\n */\n\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\nconst TRAILING_S = /s$/i;\nconst SIBILANT_ENDING = /[sxz]$/i;\nconst EAU_ENDING = /eau$/i;\nconst AL_ENDING = /al$/i;\nconst HOUSEHOLD_MEASURE = /^(cuillère à soupe|cuillère à café|verre|tasse|bol)$/;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls: each one has the size the cook gives it, so the\n * count scales in whole units and the unit itself is never converted.\n */\n | \"vague\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A metric symbol such as \"g\" or \"cl\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\", \"Cuillères\".\n */\nconst UNITS: Record<string, UnitInfo> = {\n // Mass\n g: { canonical: \"g\", kind: \"measured\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", symbol: true },\n mg: { canonical: \"mg\", kind: \"measured\", 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\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", symbol: true },\n\n // Volume\n ml: { canonical: \"ml\", kind: \"measured\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", 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 plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n \"c a s\": { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n cas: { canonical: \"cuillère à soupe\", kind: \"portioned\", plural: \"cuillères à soupe\" },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n \"c a c\": { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n cac: { canonical: \"cuillère à café\", kind: \"portioned\", plural: \"cuillères à café\" },\n verre: { canonical: \"verre\", kind: \"portioned\" },\n verres: { canonical: \"verre\", kind: \"portioned\" },\n bol: { canonical: \"bol\", kind: \"portioned\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\" },\n botte: { canonical: \"botte\", kind: \"portioned\" },\n bottes: { canonical: \"botte\", kind: \"portioned\" },\n boite: { canonical: \"boîte\", kind: \"portioned\" },\n boites: { canonical: \"boîte\", kind: \"portioned\" },\n pot: { canonical: \"pot\", kind: \"portioned\" },\n pots: { canonical: \"pot\", kind: \"portioned\" },\n brique: { canonical: \"brique\", kind: \"portioned\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\" },\n branche: { canonical: \"branche\", kind: \"portioned\" },\n branches: { canonical: \"branche\", kind: \"portioned\" },\n\n // Approximate by nature: the size of one is the cook's, the count is the\n // recipe's. See `readPartitiveMeasure` for what puts a word here.\n bouchon: { canonical: \"bouchon\", kind: \"vague\" },\n bouchons: { canonical: \"bouchon\", kind: \"vague\" },\n larme: { canonical: \"larme\", kind: \"vague\" },\n larmes: { canonical: \"larme\", kind: \"vague\" },\n doigt: { canonical: \"doigt\", kind: \"vague\" },\n doigts: { canonical: \"doigt\", kind: \"vague\" },\n nuage: { canonical: \"nuage\", kind: \"vague\" },\n nuages: { canonical: \"nuage\", kind: \"vague\" },\n louche: { canonical: \"louche\", kind: \"vague\" },\n louches: { canonical: \"louche\", kind: \"vague\" },\n lichette: { canonical: \"lichette\", kind: \"vague\" },\n lichettes: { canonical: \"lichette\", kind: \"vague\" },\n pointe: { canonical: \"pointe\", kind: \"vague\" },\n pointes: { canonical: \"pointe\", kind: \"vague\" },\n pincee: { canonical: \"pincée\", kind: \"vague\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"vague\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"vague\" },\n traits: { canonical: \"trait\", kind: \"vague\" },\n filet: { canonical: \"filet\", kind: \"vague\" },\n filets: { canonical: \"filet\", kind: \"vague\" },\n goutte: { canonical: \"goutte\", kind: \"vague\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"vague\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"vague\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"vague\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"vague\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"vague\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"vague\", plural: \"soupçons\" },\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so lookups survive French\n * spelling variants. Recipes write \"c. à soupe\" and \"c à s\" as readily as the\n * full \"cuillère à soupe\", and an unrecognised unit is worse than a wrong one:\n * the amount falls through to the countable branch and gets rounded as though a\n * 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 unit 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 \"cuillere a soupe\" wins over \"cuillere\". */\nexport const UNIT_KEYS = Object.keys(UNITS).sort((a, b) => b.length - a.length);\n\nexport function lookupUnit(text: string): UnitInfo | null {\n return UNITS[normalizeUnitKey(text)] ?? null;\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 measure a line names with a container or a gesture the vocabulary\n * above has 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\", \"une poignée de\n * roquette\". A noun in that position measures whatever follows it, so a\n * container nobody thought to list is read by the same rule as the ones that\n * are, and the vocabulary above only has to carry the words whose plural or\n * 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)) {\n return null;\n }\n\n const canonical = frenchSingular(word);\n return {\n unit: { canonical, kind: \"vague\", plural: frenchPlural(canonical) },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * The singular of a noun a line wrote in the plural, so the rewrite can put it\n * 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.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_ENDING.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 * Metric 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 de sucre\" 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.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000 },\n g: { to: \"kg\", per: 1000 },\n ml: { to: \"l\", per: 1000 },\n cl: { to: \"l\", per: 100 },\n dl: { to: \"l\", per: 10 },\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: a quarter\n // of a cuillère à soupe is three quarters of a cuillère à café, and that is\n // the one of the two a kitchen owns.\n \"cuillère à soupe\": { to: \"cuillere a cafe\", per: 3 },\n tasse: { to: \"cuillere a soupe\", per: 16 },\n\n kg: { to: \"g\", per: 1000 },\n l: { to: \"cl\", per: 100 },\n dl: { to: \"cl\", per: 10 },\n cl: { to: \"ml\", per: 10 },\n g: { to: \"mg\", per: 1000 },\n};\n\n/**\n * The unit one step down the metric ladder, with how many of it fit in one of\n * the current unit. Null at the bottom of a ladder, where there is nothing\n * smaller to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[unit.canonical];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An oeuf: half of one is not an amount a kitchen measures out. */\n | \"whole\"\n /** A boîte, une gousse d'ail, a feuille de gélatine: it splits in two, and no finer. */\n | \"half\"\n /** An oignon, a pomme: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How finely a unit divides, decided by what one of them holds rather than by\n * what holds it.\n *\n * The question is whether half of one is a quantity a cook can take: a boîte de\n * tomates is poured and the rest kept, a sachet de sucre vanillé is split by\n * eye, a feuille de gélatine is cut with scissors, a branche de thym is pinched\n * in two. Content that pours, weighs or cuts therefore divides, and the word for\n * the packaging settles nothing. What stays whole is what half of cannot be\n * measured out at all, and the egg is the case that names the rule: half of one\n * would have to be beaten and weighed, which is not what a recipe asks for. That\n * test belongs to the thing being counted, so it lives with the item in\n * `scale.ts`.\n *\n * A gesture keeps its own answer: half a pincée is a fraction of a hand, and the\n * count is the whole of what a pincée can say.\n *\n * A short list of measures goes one step further, to the quarter. See\n * `QUARTERED_MEASURE`.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"vague\") {\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 three answer the size question\n * differently. A pot de crème fraîche and a bouteille hold enough that a\n * quarter is still a portion someone serves and the rest still keeps: a quarter\n * of a pot is a couple of spoonfuls, a quarter of a bouteille is a glass. A\n * tranche is already cut off something larger, and the board that produced one\n * takes a corner off it in the same gesture: a quarter of a tranche de pain is\n * a crouton.\n *\n * The pattern is exported because a line can write the measure where the unit\n * goes, in \"1 pot de crème fraîche\", or inside the name of what it counts, in\n * \"1 petit pot de crème\". Both readings answer to the same list.\n */\nexport const QUARTERED_MEASURE = /\\b(pots?|bouteilles?|tranches?)\\b/;\n\n/**\n * Spoons, glasses and bowls: a portion of a fixed size, which a kitchen measures\n * out in the fractions printed on a measuring set rather than in halves alone.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\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 \"450 g à 1 kg\". The caller picks one bound to choose from, then\n * 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 */\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[current.canonical];\n if (up && amount * ratio >= up.per) {\n const target = lookupUnit(up.to);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * Matches a number followed by a unit anywhere in a piece of text.\n *\n * Used to spot a quantity this parser did not take, such as the second amount\n * of \"20 g de levure dissoute dans 1 cuillère à soupe d'eau\", which would\n * otherwise sit in a scaled line still saying what the original said.\n */\nconst EMBEDDED_MEASURE = new RegExp(\n `\\\\d[\\\\d.,/]*\\\\s*(?:${UNIT_KEYS.map((key) => key.replace(/ /g, \"\\\\s+\")).join(\"|\")})\\\\b`,\n \"i\",\n);\n\n/**\n * Whether a piece of text holds a number followed by a unit.\n *\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string): boolean {\n return EMBEDDED_MEASURE.test(normalizeUnitKey(text));\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 cuillères à\n * café\" where the page wrote \"4 pincées\" puts a figure on the page it never\n * claimed, and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\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 * Render a unit for a given amount, choosing singular or plural.\n *\n * French takes the plural from two onwards, so 1,5 stays singular: \"1,5 cuillère\n * à soupe\", not \"1,5 cuillères\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n if (amount < 2) {\n return unit.canonical;\n }\n if (unit.plural) {\n return unit.plural;\n }\n return `${unit.canonical}s`;\n}\n","/**\n * Parsing of French quantities out of free-text ingredient lines.\n *\n * Marmiton stores ingredients as plain strings: \"200 g de farine\", \"3 oeufs\",\n * \"0.5 oignon coupée en cubes\", \"sel\". There is no structured amount anywhere, so\n * everything downstream depends on reading these correctly.\n */\n\nimport type { UnitInfo } from \"./units.js\";\nimport { lookupUnit, normalizeUnitKey, readPartitiveMeasure, UNIT_KEYS } from \"./units.js\";\n\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst VAGUE_ARTICLE = /^\\s*(un|une|quelques)\\b\\s*/i;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst WHITESPACE = /\\s+/;\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DIGIT = /\\d/;\nconst TRAILING_S = /s$/;\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst LEADING_ARTICLE = /^(?:une|un)\\s+/i;\nconst RANGE_SEPARATOR = /^\\s*(à|a|ou|-|–|—|\\/)\\s*/;\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 appear in hand-written recipes. */\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 \"⅙\": 1 / 6,\n \"⅛\": 0.125,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\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 written with either a dot or a French comma.\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"coriandre\".\n */\nexport function parseLeadingQuantity(text: string): 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 = \"\", glyph = \"\"] = mixedGlyph.slice(1);\n const fraction = VULGAR_FRACTIONS[glyph];\n if (fraction !== undefined) {\n return { amount: Number(whole) + fraction, length: offset + mixedGlyph[0].length };\n }\n }\n\n const leading = trimmed[0];\n const bare = leading === undefined ? undefined : VULGAR_FRACTIONS[leading];\n if (bare !== undefined) {\n return { amount: bare, length: offset + 1 };\n }\n\n // \"1 1/2\" before \"1/2\" before \"1,5\", so the longest reading wins.\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const whole = Number(mixed[1]);\n const numerator = Number(mixed[2]);\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return { amount: whole + numerator / denominator, length: offset + mixed[0].length };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n if (denominator !== 0) {\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n }\n\n // French marks the decimal with a comma, so \"1,5 kg\" is a kilo and a half.\n const decimal = DECIMAL.exec(trimmed);\n if (decimal) {\n const [digits = \"\"] = decimal.slice(1);\n const amount = Number(digits.replace(\",\", \".\"));\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n return null;\n}\n\n/**\n * Articles a recipe writes where a digit would go, and what they count as.\n *\n * \"quelques\" names a small handful of them; three is the reading, and the word\n * travels with the result so a caller can see the amount came from a word.\n */\nconst ARTICLE_AMOUNTS: Record<string, number> = {\n un: 1,\n une: 1,\n quelques: 3,\n};\n\nexport interface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read a leading article standing in for a number, as in \"une pincée de sel\".\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 \"un sachet\" is\n * one sachet, while \"un oignon\" names a vegetable and no amount. Reading the\n * second as the first would multiply a number the line never wrote.\n */\nexport function parseLeadingArticle(text: string): ParsedArticle | null {\n const match = VAGUE_ARTICLE.exec(text);\n if (!match) {\n return null;\n }\n\n const rest = text.slice(match[0].length);\n if (!(matchLeadingUnit(rest, true) || readCountMultiplier(rest))) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const amount = ARTICLE_AMOUNTS[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n\n return { amount, length: match[0].length, word };\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 douzaine is twelve of whatever is being counted. \"2 douzaines d'escargots\"\n * therefore asks for twenty-four escargots, and the answer divides the way an\n * escargot does. Reading the word as a measure gives \"1 1/2 douzaine\", which is\n * not a count a kitchen works with, and it hands the question of divisibility to\n * a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\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\ninterface MatchedUnit {\n unit: UnitInfo;\n /** Unit text as the line wrote it, accents and all. */\n unitText: string;\n /** What follows the unit. */\n rest: string;\n}\n\n/**\n * Read a unit at the start of a line, longest spelling first, so \"cuillère à\n * soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item name.\n *\n * A noun the vocabulary does not carry can still be a measure, and the\n * partitive that follows it says so, which is what `readPartitiveMeasure`\n * reads. That reading is offered only where the line wrote an article instead\n * of a digit, because that is the position where a noun measures rather than\n * names: \"un bouchon de rhum\" asks for an amount, while \"1 piment de Cayenne\"\n * asks for a chilli whose variety happens to be introduced the same way. The\n * vocabulary is consulted first either way, so a word it holds keeps the kind\n * and the spelling it was given there.\n */\nfunction matchLeadingUnit(text: string, partitive = false): MatchedUnit | null {\n const normalized = normalizeUnitKey(text);\n for (const key of UNIT_KEYS) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key);\n if (!unit) {\n continue;\n }\n // Consume the same number of words from the original text, which may be\n // spelled with accents the normalized key has lost.\n const wordCount = key.split(\" \").length;\n const words = text.trim().split(WHITESPACE);\n return {\n unit,\n unitText: words.slice(0, wordCount).join(\" \"),\n rest: words.slice(wordCount).join(\" \"),\n };\n }\n\n const measure = partitive ? readPartitiveMeasure(text) : null;\n if (measure) {\n return {\n unit: measure.unit,\n unitText: text.trim().split(WHITESPACE)[0] ?? \"\",\n rest: measure.rest,\n };\n }\n\n return null;\n}\n\n/** One amount with its unit, 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 /** \"2 tranches de 2-cm\": the figure gives 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 /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"1,500,000 g\" grouped the way French never groups a number. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as Marmiton stores it. */\n original: string;\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 sign or word the page put before the amount to say it is loose, as in\n * the \"environ\" of \"environ 6 citrons\". Null when the page stated the amount\n * plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"grosse\"\n * of \"1 grosse pincée\". 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 \"2 à 3 gousses\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"2 à 3\" into the nonsense \"4 à 3\".\n */\n amountMax: number | null;\n /** The word or sign the range was written with: \"à\", \"-\", \"ou\". */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /** Raw unit text as written, kept so the rewrite can stay faithful. */\n unitText: string | null;\n /**\n * The same quantity restated in another system, which pages give in brackets:\n * \"450 g (1 livre)\". Left unscaled it would contradict the amount beside it,\n * so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets, as in \"450 g (1\n * livre)\", or after a slash, as in \"500 g / 1.1 lb\". The rewrite puts them\n * back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | null;\n /** What the amount and unit apply to, for example \"farine\" or \"oeufs\". */\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 number.\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 douzaines d'escargots\". 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 * A number grouped the way French never groups one.\n *\n * French marks the decimal with a comma and separates thousands with a space,\n * so a second comma group is a number written by some other convention.\n * Reading \"1,500,000\" as one and a half is wrong, and reading it as a million\n * and a half is a guess about a page that gave no sign. Neither is safe, so the\n * line goes back as published and says why.\n */\nconst AMBIGUOUS_COMMA = /^\\s*\\d+,\\d+,\\d/;\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);\n if (!size) {\n return false;\n }\n\n const measure = matchLeadingUnit(text.slice(size.length).trimStart());\n if (measure?.unit.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\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 = /\\bpar\\s+(?:personne|convive|part|tête)\\b/i;\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 = /^(?:~|≈|environ|approximativement|à peu près|a peu pres)\\s*/i;\n\n/**\n * Size words a recipe puts between the number and the measure.\n *\n * \"1 grosse pincée\" counts pincées 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 pincée 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 = 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/** Lowercase and strip accents, so \"générelle\" and \"generelle\" hit one entry. */\nfunction fold(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(text: string): { 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 = fold(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.has(folded) || MEASURE_ADJECTIVES.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 * How a line writes the equivalents beside its measure, when it writes any.\n *\n * \"125 g / 1 tasse\" states them with a slash and reads as one line; \"125 g\n * (1 tasse)\" puts them in brackets. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(slashed: boolean, bracketed: boolean): \"slash\" | \"bracket\" | null {\n if (slashed) {\n return \"slash\";\n }\n if (bracketed) {\n return \"bracket\";\n }\n return null;\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 petit piment oiseau\" the words that follow name the food itself, and\n * taking one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n text: string,\n fromArticle: boolean,\n): { unit: UnitInfo | null; unitText: string | null; adjective: string | null; rest: string } {\n const direct = matchLeadingUnit(text, fromArticle);\n const described = direct ? { adjective: null, rest: text } : takeMeasureAdjective(text);\n const behind = described.adjective ? matchLeadingUnit(described.rest, fromArticle) : null;\n const matched = direct ?? behind;\n\n return {\n unit: matched?.unit ?? null,\n unitText: matched?.unitText ?? null,\n adjective: behind ? described.adjective : null,\n rest: matched ? matched.rest : text,\n };\n}\n\n/**\n * Whether the figures on the line give the size of one item rather than how\n * many of them the recipe wants.\n *\n * Two ways a line says it. Without a measure, the words behind the count carry\n * the mass: \"2 poulets de 1,5 kg\". With one, \"une dinde de 3 kg\" writes the\n * noun where a measure stands and the partitive takes it for one. A noun the\n * vocabulary lists as a measure keeps counting, since a pot or a boîte is a\n * thing to buy more of; a noun read as a measure only for standing there names\n * the food itself, and a mass behind it is the size of one of them.\n */\nfunction statesSizeOfOne(unit: UnitInfo | null, unitText: string | null, item: string): boolean {\n if (!unit) {\n return statesItemSize(item);\n }\n const readForItsPosition = unitText !== null && lookupUnit(normalizeUnitKey(unitText)) === null;\n return readForItsPosition && isStatedSize(item);\n}\n\n/**\n * Split an ingredient line into amount, unit, bracketed equivalents and item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing unit is equally normal and means the item is counted, as in \"3 oeufs\".\n */\nexport function parseIngredient(line: string): ParsedIngredient {\n const original = line;\n const text = line.trim();\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n heldBack,\n approximation: null,\n measureAdjective: null,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n unitText: null,\n alternates: [],\n alternateStyle: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (AMBIGUOUS_COMMA.test(text)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX.exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated);\n const article = range ? null : parseLeadingArticle(stated);\n const quantity = range ?? parseLeadingQuantity(stated) ?? 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: \"2 tranches de 2-cm\" states a thickness.\n if (HYPHENATED_TAIL.test(stated.slice(quantity.length))) {\n return empty(\"sizeQualifier\");\n }\n\n let rest = stated.slice(quantity.length).trimStart();\n\n // \"2 douzaines d'escargots\" counts escargots, twelve to the douzaine, so the\n // multiplier is folded into the amount and the line goes on to be read as the\n // count of a thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, fromArticle);\n const { unit, unitText, adjective } = measure;\n rest = measure.rest;\n\n const bracketed = takeAlternates(rest);\n rest = bracketed.rest;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest);\n if (slashed) {\n rest = slashed.rest;\n }\n\n // \"200 g de farine\" reads better as item \"farine\" than \"de farine\".\n //\n // The article a partitive introduces goes with it: \"2/3 d'un flacon\" names a\n // share of one flacon, and once the share has been multiplied the count sits\n // where \"un\" stood. Leaving the article behind produces \"4 un flacon\", which\n // reads as broken text rather than as a quantity.\n const item = rest.replace(LEADING_PARTITIVE, \"\").replace(LEADING_ARTICLE, \"\").trim();\n\n if (statesSizeOfOne(unit, unitText, item)) {\n return empty(\"itemSize\");\n }\n\n return {\n original,\n heldBack: PER_PERSON.test(text) ? \"perPerson\" : null,\n approximation: loose ? loose[0] : null,\n measureAdjective: adjective,\n amount: quantity.amount * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit,\n unitText,\n alternates: slashed ? slashed.measures : bracketed.measures,\n alternateStyle: alternateStyleOf(slashed !== null, bracketed.measures.length > 0),\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 livre)\" 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 unit.\n * A bracket holding a remark, as in \"(bien mûres)\", stays in the item text\n * where it belongs, because scaling it would mean scaling prose.\n */\nfunction takeAlternates(text: string): { measures: Measure[]; rest: string } {\n if (!text.startsWith(\"(\")) {\n return { measures: [], rest: text };\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return { measures: [], rest: text };\n }\n\n const inside = text.slice(1, close);\n const parts = inside.split(\"/\").map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part);\n const quantity = range ?? parseLeadingQuantity(part);\n if (!quantity) {\n return { measures: [], rest: text };\n }\n\n const after = matchLeadingUnit(part.slice(quantity.length).trimStart());\n // A trailing word means the bracket is not purely a measure, as in\n // \"(2 cm d'épaisseur)\", so the whole group is left as prose.\n if (after?.rest.trim() !== \"\") {\n return { measures: [], rest: text };\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 { measures: [], rest: text };\n }\n return { measures, rest: text.slice(close + 1).trimStart() };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb de\n * flocons d'avoine\", 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 * unit is prose and stays in the item text.\n */\nfunction takeSlashAlternates(text: string): { 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);\n const quantity = range ?? parseLeadingQuantity(after);\n if (!quantity) {\n break;\n }\n\n const taken = matchLeadingUnit(after.slice(quantity.length).trimStart());\n if (!taken) {\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 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 \"2 à 3\", \"2-3\" or \"3 ou 4\".\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): ParsedRange | null {\n const low = parseLeadingQuantity(text);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n const separator = RANGE_SEPARATOR.exec(after);\n if (!separator) {\n return null;\n }\n // A slash between two numbers is a fraction, which parseLeadingQuantity has\n // already consumed if it was one.\n if (separator[1] === \"/\") {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length));\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\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 cuillère\" is how a kitchen\n * expresses it, \"0,33 cuillère\" 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.\n */\nexport function formatAmount(amount: number, options: FormatAmountOptions = {}): string {\n // French recipes write decimals with a comma. Two decimals is finer than any\n // kitchen resolves, and for anything smaller than that it is zero: a quantity\n // that survived being divided a thousandfold must not be handed back as none\n // of the ingredient, so below that point the significant digits are written.\n const decimal = (value: number) => {\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n return String(rounded).replace(\".\", \",\");\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.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act on.\n * Multiplying every number by the factor is arithmetically correct and practically\n * useless: it produces \"2,4 oeufs\" and \"0,67 pincée de sel\" with the same\n * confidence as \"267 g de farine\". Each line is therefore classified by what its\n * unit allows, and the classification travels with the result so the caller can\n * see what was computed and how.\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 pincée for twenty-five servings is a recipe that does not rise.\n */\n\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n hasEmbeddedMeasure,\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n demoteUnit,\n formatUnit,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\nconst EGG_WHITE = /\\bblancs? de? /;\nconst EGG_WHITE_NAMED = /\\bblancs? de? oeufs?\\b/;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /[sxz]$/i;\nconst EAU_ENDING = /eau$/i;\nconst AL_ENDING = /al$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst AUX_ENDING = /aux$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or a half, or a measurement was\n * rounded to what a scale can show.\n */\n | \"rounded\"\n /** Left as published: nothing on the line is the factor's to multiply. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\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 a\n * bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit of\n * \"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 scaling: ScalingKind;\n /**\n * Whether rounding moved the value away from the exact product. A line is\n * `rounded` either because of that or because a floor was reached, and this\n * says which of the two happened.\n */\n adjusted: boolean;\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 kilo as easily as a gram, and rounding 2,2 kg\n * to 2 would throw away a tenth of the ingredient.\n */\nfunction roundMeasured(value: number): number {\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\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 * Une crevette, une moule, une noisette, un grain de poivre, une baie de\n * genièvre, une étoile de badiane is already a portion on its own. A recipe\n * counts five, twelve, twenty of them, and a cook taking a share of that recipe\n * puts one fewer in the pan; cutting one in two is not a thing a kitchen does.\n * These land on a whole number.\n *\n * Un gigot, une baguette, un camembert, un ananas, un oignon, une pastèque, une\n * pintade sits at the other end: a recipe asks for one or for two, and the\n * share it wants out of one is decided by a knife. A quarter of one is a piece\n * someone serves, and what is left keeps.\n *\n * The lists are read on an item stripped of its accents, so \"échalote\" and\n * \"echalote\" hit the same entry.\n */\nconst PORTION_SIZED_ITEM =\n /\\b(crevettes?|gambas|langoustines?|moules?|noisettes?|grains?|genievres?|genevriers?|badianes?|anis)\\b/;\n\nconst QUARTERED_ITEM =\n /\\b(oignons?|echalotes?|pommes? de terre|pommes?|poires?|carottes?|citrons?|oranges?|tomates?|concombres?|courgettes?|aubergines?|courges?|potirons?|choux?|melons?|poivrons?|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pasteques?|gigots?|baguettes?|camemberts?|fromages?|chevres?|chorizos?|reblochons?|buches?|ananas|peches?|abricots?|laits?|poulets?|pintades?|rotis?)\\b/;\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 cuisse feeds one, and half of one\n * is the share a smaller recipe serves. Taking a quarter would name a piece no\n * one plates.\n *\n * It reads before the animal, whose name such a line carries alongside the cut.\n */\nconst HALVED_CUT = /\\b(cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/;\n\n/**\n * A jus, the one counted thing whose division stops at the half.\n *\n * Half the jus of a citron 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 = /\\bjus\\b/;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An oeuf comes out of its shell whole, and so does the jaune a recipe asks for\n * on its own: half of one would have to be beaten and weighed, which is not an\n * amount any recipe asks for and not one a cook can keep the rest of. A count of\n * them therefore lands on a whole number, whichever side of the half the\n * arithmetic fell on.\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 = /\\b(oeufs?|jaunes?|clous?|zestes?)\\b/;\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 */\nfunction blancDivisibility(key: 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 (!EGG_WHITE.test(key)) {\n return null;\n }\n return EGG_WHITE_NAMED.test(key) ? \"whole\" : \"half\";\n}\n\n/** How finely the thing a line counts can be divided. */\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n if (unit) {\n return unitDivisibility(unit);\n }\n const key = item\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/œ/g, \"oe\")\n .replace(/['’]/g, \" \");\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 * 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\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 that divides in two: half a boîte de tomates is a\n * real amount, and rounding it up to a whole adds a sixth of the tomatoes to a\n * recipe that asked for three boîtes.\n *\n * How finely the thing divides decides the floor. Under that floor the amount is\n * clamped up rather than shrunk towards nothing, which keeps the ingredient in\n * 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 than\n * 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 it\n // 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 the\n // 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, a glass or a bowl, which a kitchen measures out in halves and\n * in the 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 * cuillère à soupe is a spoon a kitchen owns, and there is nothing to gain by\n * calling it a cuillère à café et demie.\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\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\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}\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 * 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 * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"450 à 1000 g\" comes back as \"450 à\n * 1 kg\", where the two numbers are not in the same measure. Of the two, the\n * smaller end is the one a unit can ruin: choosing from the larger turns\n * \"450 à 1000 g\" into \"0,45 à 1 kg\", and pushed one step further it rounds the\n * small end away entirely. A large number in a small unit is merely long to\n * read.\n */\n const positive = ends.map((end) => end.raw).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: 1666 g rounded\n // as kilos is 1.7, and rounded as grams it is the 1.665 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 { amount: Math.min(usable, ceiling), exact, clamped: false, raw };\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 it\n // no longer fills.\n if (stepped.ratio !== 1) {\n const bounds = eachEnd(({ published, raw }) => {\n const exact = raw * stepped.ratio;\n const ceiling = factor < 1 ? published * stepped.ratio : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(exact, ceiling);\n return { amount: rounded.value, exact, clamped: rounded.clamped, raw };\n });\n return { bounds, unit: stepped.unit };\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 };\n });\n return { bounds, unit };\n }\n\n if (unit && unit.kind === \"vague\") {\n // A pincée, a poignée or a filet has the size the cook's hand gives it, so\n // the proportion of the recipe lives in how many are asked for. The count\n // therefore multiplies in whole units, and the measure stays in its own\n // vocabulary rather than being turned into grams or spoons, where published\n // equivalences span a fourfold range.\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, \"whole\", ceiling);\n return { amount: Math.min(rounded.value, ceiling), exact: raw, clamped: false, raw };\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 };\n });\n return { bounds, unit };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Make a counted item agree with its amount, 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, and\n * only its trailing \"s\": nouns already ending in -s, -x or -z are invariable in\n * the plural (\"ananas\", \"choux\"), and forcing one would be worse than leaving the\n * word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_NOUN`, because the ending settles nothing on\n * its own: \"jus\" and \"clous\" both end in -us, and the first is a singular where\n * the second is a plural of \"clou\".\n */\nfunction agreeWithAmount(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 const isPlural = PLURAL_ENDING.test(head);\n\n if (wantsPlural && !isPlural) {\n // Words ending in -s, -x or -z do not take a plural mark.\n if (SIBILANT_ENDING.test(head)) {\n // The head stays as written.\n }\n // \"morceau\" and \"bocal\" take -x and -aux where the ordinary noun takes -s.\n else if (EAU_ENDING.test(head)) {\n words[0] = `${head}x`;\n } else if (AL_ENDING.test(head)) {\n words[0] = `${head.slice(0, -2)}aux`;\n } else {\n words[0] = `${head}s`;\n }\n } else if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n words[0] = head.slice(0, -1);\n } else if (AUX_ENDING.test(head)) {\n words[0] = `${head.slice(0, -3)}al`;\n }\n // \"ananas\", \"anis\", \"couscous\": the -s belongs to the singular.\n else if (INVARIABLE_NOUN.has(foldWord(head))) {\n // The head stays as written.\n } else {\n words[0] = head.slice(0, -1);\n }\n }\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 * 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 */\nconst INVARIABLE_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 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 * 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 it takes the number the measure takes:\n * \"2 grosses pincées\". A word outside the declinable list stays as the recipe\n * wrote it, for the same reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number): string {\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\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\nfunction joinItem(item: string): string {\n if (!item) {\n return \"\";\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: \"100 g de beurre ou 2 oeufs\". */\nconst BRANCH_SEPARATOR = /\\s+ou\\s+/gi;\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 \"ou\" of a published\n * range such as \"2 ou 3 gousses d'ail\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"sucre ou cassonade\"\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 BRANCH_SEPARATOR.lastIndex = 0;\n for (let match = BRANCH_SEPARATOR.exec(text); match; match = BRANCH_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).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 pincée 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);\n }\n\n const text = line.trim();\n const branch = splitBranch(text, parseIngredient(text));\n if (branch) {\n return scaleBranchedLine(line, branch, options);\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);\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/** The equivalents beside a measure, written the way the line wrote them. */\nfunction alternateLabel(texts: readonly string[], restated: boolean): string {\n if (texts.length === 0) {\n return \"\";\n }\n return restated ? ` / ${texts.join(\" / \")}` : ` (${texts.join(\" / \")})`;\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 and agrees with it, as in \"1/3 oeuf\" and \"3 brioches\".\n */\nfunction itemLabelFor(unit: UnitInfo | null, item: string, counted: string): string {\n if (unit) {\n return joinItem(item);\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}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * A note is built rather than assembled at the call site because the reasons\n * stack: a line can be clamped, carry a second quantity, collapse its range and\n * state an approximation, and each of those has to be said without cancelling\n * 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 } = 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(Math.round(value * 1000) / 1000, { fractions: false })}${\n source ? ` ${formatUnit(source, value)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n // Name the floor this line actually landed on: how far one of the thing\n // divides is what sets it, so a sachet stops at a half where an oignon goes\n // to a quarter.\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000)}, the smallest amount worth ` +\n \"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 quantity.\n const moved = primaryBounds.filter((bound) => !landedExactly(bound.exact, bound.amount));\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 to\n // move. Saying \"rounded from 300 g\" when 300 g is exact would send a cook\n // 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 carry a second amount after the first, as in \"20 g de levure\n // dissoute dans 1 cuillère à soupe d'eau\". Only the amount the line opens with\n // is scaled, and one left at its published size contradicts it. This is said\n // whatever else happened to the line: a line that was also rounded is the one\n // where a stale second quantity is hardest to spot.\n if (hasEmbeddedMeasure(parsed.item)) {\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 (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 === \"vague\") {\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 \"douzaine\" 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)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\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 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 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);\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n original: parsed.original,\n text: parsed.original,\n amount: null,\n amountMax: null,\n unit: null,\n scaling: \"unscaled\",\n adjusted: false,\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));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((b) => !landedExactly(b.exact, b.amount));\n const movedAlternate = alternateBounds.some((b) => !landedExactly(b.exact, b.amount));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote them,\n // and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\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 // Mass and volume read as decimals; counted and spooned things read as\n // fractions.\n const asText = (value: number) => formatAmount(value, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 à\n // 1 gousse\" 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 // 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 unit && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown)}`\n : \"\";\n const unitLabel = unit ? `${adjective} ${formatUnit(unit, shown)}` : \"\";\n // Equivalents go back the way the line offered them: inside brackets, or\n // after a slash beside the amount they restate.\n const alternateTexts = alternates.map((entry) => entry.text);\n const altLabel = alternateLabel(alternateTexts, restated);\n // A measure needs the partitive French puts between it and what it measures.\n // A counted item stands straight after its number, and agrees with it:\n // \"1/3 oeuf\", \"3 brioches\".\n const counted = agreeWithAmount(parsed.item, shown);\n const itemLabel = itemLabelFor(unit, parsed.item, counted);\n\n const adjusted = movedPrimary || movedAlternate;\n const result: ScaledIngredient = {\n original: parsed.original,\n text: `${parsed.approximation ?? \"\"}${amountText}${unitLabel}${altLabel}${itemLabel}`.trim(),\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: unit?.canonical ?? null,\n scaling: adjusted || restated || clamped ? \"rounded\" : \"scaled\",\n adjusted,\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 });\n if (note !== undefined) {\n result.note = note;\n }\n\n return result;\n}\n\n/** Opening of the sentence an approximate measure carries. */\nconst APPROXIMATE_MEASURE_MARKER = \"is an approximate measure\";\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 pincées of\n * bicarbonate has said nothing about cuillères, and answering in cuillères\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} ${APPROXIMATE_MEASURE_MARKER}${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 * Whether this line was scaled as an approximate measure, so a caller can say\n * so once for a whole list instead of reading every note.\n */\nexport function isApproximateMeasure(entry: ScaledIngredient): boolean {\n return entry.note?.includes(APPROXIMATE_MEASURE_MARKER) ?? false;\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(measure: Measure, factor: number): { 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) => formatAmount(value, { 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)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3-4\" 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 to multiply is\n * `unscaled` and says why.\n */\nexport function passthroughIngredient(line: string): ScaledIngredient {\n const parsed = parseIngredient(line);\n const held = parsed.amount === null || parsed.heldBack !== null;\n\n const result: ScaledIngredient = {\n original: parsed.original,\n text: parsed.original,\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n scaling: held ? \"unscaled\" : \"scaled\",\n adjusted: false,\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 === \"vague\") {\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(lines: string[]): ScaledIngredient[] {\n return lines.map((line) => ({ ...passthroughIngredient(line), scaling: \"unscaled\" as const }));\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/**\n * Zod's own wording. The error code is written in front of it.\n *\n * An error map may answer with an object or with nothing, and the English\n * locale answers every issue with a string, which is the type given here.\n */\nconst defaultMessage = z.core.locales.en().localeError as (issue: z.core.$ZodRawIssue) => string;\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","/** Pieces shared by the three tools: schemas, error mapping, text mirrors. */\n\nimport { z } from \"zod\";\nimport { MarmitonError } from \"../errors.js\";\n\n/** Many MCP clients render only the text block, so it must read on its own. */\nexport const MAX_TEXT_MIRROR_CHARS = 2000;\n\nexport const scaledIngredientSchema = z.object({\n original: z.string().describe(\"The ingredient line exactly as Marmiton publishes it.\"),\n text: z.string().describe(\"The line after scaling, identical to 'original' when unscaled.\"),\n amount: z\n .number()\n .nullable()\n .describe(\n \"The scaled quantity, expressed in 'unit'. Read the two together: a large result is moved to a \" +\n \"bigger unit, so 200 g scaled tenfold reads as 2 kg, and the bare number shrinks while the \" +\n \"quantity grows.\",\n ),\n amountMax: z\n .number()\n .nullable()\n .describe(\n \"Upper bound when the line gives a range, as in '200 à 300 g', with 'amount' holding the lower \" +\n \"one. Null when the line states a single amount.\",\n ),\n unit: z\n .string()\n .nullable()\n .describe(\"The unit 'amount' is in, which may differ from the one the recipe used.\"),\n scaling: z\n .enum([\"scaled\", \"rounded\", \"unscaled\"])\n .describe(\n \"'scaled' means the arithmetic came out exact, which a count reaches as readily as a mass: one \" +\n \"pincée multiplied by six is six pincées. 'rounded' means the value had to be moved to stay \" +\n \"usable, to a whole or half unit or to a step a scale can show. 'unscaled' was left as \" +\n \"published, because nothing on the line is the factor's to multiply: the note says which of \" +\n \"the two it is.\",\n ),\n adjusted: z\n .boolean()\n .describe(\n \"True when rounding moved the value away from the exact product, which is what makes a line \" +\n \"'rounded' rather than 'scaled'.\",\n ),\n note: z\n .string()\n .optional()\n .describe(\n \"Why the line was rounded, clamped or left alone, including when the measure is approximate and \" +\n \"only its count was multiplied.\",\n ),\n});\n\nexport interface ToolResult {\n // The SDK's CallToolResult 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/**\n * Keep text from the site out of the shape this server's own lines take.\n *\n * The block ends with lines opening \"Note:\" and \"Source:\", and a caller has no\n * way to tell one of those from the same words inside a title, a quote or a\n * description written by whoever published it. Indenting a body line that\n * opens with one of those words keeps the two apart, and costs nothing: the\n * structured output still carries the text exactly as it was published.\n */\nfunction indentMarkerLines(body: string): string {\n return body.replace(/^(Note:|Source:)/gm, \" $1\");\n}\n\n/**\n * Build a result whose text block ends with the notes.\n *\n * The notes are what qualifies the answer: that a quantity was clamped and no\n * longer holds its share, that the nutrition figures describe the recipe as\n * published rather than the amounts printed above them, that two ways of asking\n * for the same thing disagreed. Without them a client rendering only the text\n * reads an answer with nothing to qualify it.\n *\n * They are appended after the body is trimmed, so they survive a long\n * ingredient list.\n */\nexport function ok(\n structured: Record<string, unknown>,\n text: string,\n notes: string[] = [],\n): ToolResult {\n const trailer = notes.map((note) => `Note: ${note}`).join(\"\\n\");\n const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);\n const body = truncate(indentMarkerLines(text), Math.max(0, budget));\n\n return {\n content: [{ type: \"text\", text: trailer ? `${body}\\n\\n${trailer}` : body }],\n structuredContent: structured,\n };\n}\n\n/**\n * Error results carry no structuredContent: the SDK validates it against the\n * tool's declared output schema, which an error payload does not satisfy.\n */\nexport function toToolError(error: unknown): ToolResult {\n const known =\n error instanceof MarmitonError\n ? error\n : new MarmitonError(\"network_error\", error instanceof Error ? error.message : String(error));\n\n const lines = [`[${known.code}] ${known.message}`];\n if (known.details.hint) {\n lines.push(`Hint: ${known.details.hint}`);\n }\n\n return { content: [{ type: \"text\", text: lines.join(\"\\n\") }], isError: true };\n}\n\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) {\n return text;\n }\n return `${text.slice(0, maxChars - 1).trimEnd()}…`;\n}\n","/**\n * scale_ingredients: rescale an arbitrary ingredient list, offline.\n *\n * This tool makes no network request. It exposes the quantity parser on its own,\n * so a list copied from anywhere, not only Marmiton, can be rescaled with the\n * same care about what is and is not safe to multiply.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport { isApproximateMeasure, scaleIngredients } from \"../recipe/scale.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, scaledIngredientSchema, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\n/** The note a scaling that could not go lower leaves on an ingredient. */\nconst CLAMPED_UP = /clamped up/i;\n\nexport const scaleIngredientsDescription = [\n \"Rescale a list of ingredient lines to a different number of servings, without contacting any website.\",\n \"Give either 'factor' directly, or 'from_servings' and 'to_servings' and the factor is computed.\",\n \"Works on any French ingredient list, whatever its source, so it also serves a recipe the user pasted in.\",\n \"Quantities in grams or millilitres are multiplied and rounded to readable values; a countable thing lands on\",\n \"a half when half of one can be poured, weighed or cut, as a boîte, a sachet, a feuille de gélatine or a\",\n \"cuillère can, and on a whole one when it cannot, as an oeuf, a jaune or a blanc; approximate measures such\",\n \"as a pinch or a handful have\",\n \"their count multiplied in whole units and stay in their own vocabulary. A line writing an article where a\",\n \"digit would go, as in 'un bouchon de rhum' or 'une pincée de sel', is read as one of that measure. Lines\",\n \"carrying no quantity are returned untouched and flagged. Prefer this over doing the arithmetic yourself.\",\n].join(\" \");\n\nexport const scaleIngredientsInput = strictInput({\n ingredients: z\n .array(z.string().max(300))\n .min(1)\n .max(100)\n .describe(\"Ingredient lines, for example ['200 g de farine', '3 oeufs', 'sel'].\"),\n factor: z\n .number()\n .positive()\n .max(100)\n .optional()\n .describe(\"Multiplier to apply. Use this or the from/to pair.\"),\n from_servings: z\n .number()\n .positive()\n .max(500)\n .optional()\n .describe(\"How many servings the list is written for.\"),\n to_servings: z.number().positive().max(500).optional().describe(\"How many servings are wanted.\"),\n});\n\nexport const scaleIngredientsOutputShape = {\n factor: z.number(),\n ingredients: z.array(scaledIngredientSchema),\n scaled_count: z.number().int(),\n rounded_count: z\n .number()\n .int()\n .describe(\n \"Lines whose value rounding moved away from the exact product, not lines that could have been rounded.\",\n ),\n unscaled_count: z.number().int(),\n notes: z.array(z.string()),\n};\n\nexport interface ScaleIngredientsArgs {\n ingredients: string[];\n factor?: number;\n from_servings?: number;\n to_servings?: number;\n}\n\n/**\n * Print a factor without rounding it out of existence.\n *\n * Two decimals turn 0.001 into \"0\", which states that nothing was applied while\n * every quantity in the list was divided by a thousand.\n */\nfunction formatFactor(factor: number): string {\n return String(Number(factor.toPrecision(3)));\n}\n\nexport function runScaleIngredients(args: ScaleIngredientsArgs): ToolResult {\n try {\n let factor: number;\n const notes: string[] = [];\n\n if (args.factor !== undefined) {\n factor = args.factor;\n if (args.from_servings !== undefined || args.to_servings !== undefined) {\n notes.push(\n \"'factor' was given alongside 'from_servings'/'to_servings'. 'factor' was applied and the \" +\n \"pair ignored; send only one of the two to remove the ambiguity.\",\n );\n }\n } else if (args.from_servings !== undefined && args.to_servings !== undefined) {\n factor = args.to_servings / args.from_servings;\n } else {\n throw invalidInput(\n \"Provide either 'factor', or both 'from_servings' and 'to_servings'.\",\n \"For example from_servings=6 and to_servings=4, or factor=0.667.\",\n );\n }\n\n const ingredients = scaleIngredients(args.ingredients, { factor });\n const counts = {\n scaled: ingredients.filter((entry) => entry.scaling === \"scaled\").length,\n // Lines that were actually moved, rather than lines that merely belong to\n // the roundable category: at a factor of 100 every egg lands whole.\n rounded: ingredients.filter((entry) => entry.scaling === \"rounded\" && entry.adjusted).length,\n unscaled: ingredients.filter((entry) => entry.scaling === \"unscaled\").length,\n clamped: ingredients.filter((entry) => CLAMPED_UP.test(entry.note ?? \"\")).length,\n approximate: ingredients.filter(isApproximateMeasure).length,\n };\n\n if (counts.rounded > 0) {\n notes.push(\n `${counts.rounded} quantity(ies) were rounded to stay usable, rather than left as fractions.`,\n );\n }\n if (counts.unscaled > 0) {\n notes.push(\n `${counts.unscaled} line(s) carry no usable quantity and were returned unchanged; adjust to taste.`,\n );\n }\n if (counts.approximate > 0) {\n notes.push(\n `${counts.approximate} approximate measure(s) such as a pinch or a handful had their count ` +\n \"multiplied; the size of one is yours to judge.\",\n );\n }\n if (counts.clamped > 0) {\n notes.push(\n `${counts.clamped} quantity(ies) fell below the smallest amount worth measuring and were clamped up, ` +\n \"so their proportions no longer match the original recipe.\",\n );\n }\n\n const structured = {\n factor: Number(factor.toPrecision(3)),\n ingredients,\n scaled_count: counts.scaled,\n rounded_count: counts.rounded,\n unscaled_count: counts.unscaled,\n notes,\n };\n\n const lines = ingredients\n .map((entry) => {\n const flag = entry.scaling === \"unscaled\" ? \" (non ajusté)\" : \"\";\n return `- ${entry.text}${flag}`;\n })\n .join(\"\\n\");\n\n return ok(structured, `Facteur ${formatFactor(factor)}:\\n${lines}`, notes);\n } catch (error) {\n return toToolError(error);\n }\n}\n","/**\n * search_recipes: find recipes on Marmiton by free text.\n */\n\nimport { z } from \"zod\";\nimport { invalidInput } from \"../errors.js\";\nimport type { MarmitonClient } from \"../marmiton/client.js\";\nimport { strictInput } from \"./arguments.js\";\nimport { ok, toToolError } from \"./shared.js\";\nimport type { ToolResult } from \"./shared.js\";\n\nexport const searchRecipesDescription = [\n \"Search Marmiton for recipes by free text: a dish, an ingredient, or both, for example 'tarte aux pommes'\",\n \"or 'poulet curry coco'. Marmiton is a French site, so French terms work best.\",\n \"Returns candidate recipes with the id and URL needed to read one with get_recipe.\",\n \"One call returns one page of results. Marmiton's robots.txt disallows paginating search results, so this\",\n \"server does not, and there is no page parameter: narrow the query instead of asking for more pages.\",\n].join(\" \");\n\nexport const searchRecipesInput = strictInput({\n // Deliberately no min(1): an empty string would be rejected by the schema with\n // a protocol-level validation error, while a whitespace-only one would reach\n // the tool and come back as invalid_input. Letting both through to the same\n // check gives callers one error code for one problem.\n query: z\n .string()\n .max(200)\n .describe(\"What to search for, in French, for example 'tarte aux pommes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(30)\n .default(10)\n .describe(\"Maximum recipes to return from this page of results.\"),\n});\n\nexport const searchRecipesOutputShape = {\n query: z.string(),\n results: z.array(\n z.object({\n id: z.string().describe(\"Marmiton recipe id. Pass this to get_recipe.\"),\n title: z.string(),\n url: z.string(),\n image_url: z.string().nullable(),\n }),\n ),\n result_count: z.number().int(),\n total_available: z.number().int().describe(\"Recipes on this page before applying 'limit'.\"),\n source: z.literal(\"marmiton.org\"),\n notes: z.array(z.string()),\n};\n\nexport interface SearchRecipesArgs {\n query: string;\n limit: number;\n}\n\n/**\n * Whether a recipe title carries a word of the query.\n *\n * Marmiton matches on the opening letters, so \"chameau\" brings back a chapeau\n * and three châteaux: the rows are what the site ranked, and none of them names\n * the dish. Accents and punctuation are folded away so \"crêpes\" reads the same\n * as \"crepes\", and words of two letters are ignored, since an article says\n * nothing about the subject.\n */\nfunction titleCarries(title: string, query: string): boolean {\n const fold = (text: string) =>\n text\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/œ/g, \"oe\")\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \" \");\n\n const haystack = ` ${fold(title)} `;\n const words = fold(query)\n .split(\" \")\n .filter((word) => word.length > 2);\n if (words.length === 0) {\n return true;\n }\n return words.some((word) => haystack.includes(` ${word}`));\n}\n\nexport async function runSearchRecipes(\n client: MarmitonClient,\n args: SearchRecipesArgs,\n): Promise<ToolResult> {\n try {\n // Trimming happens before the emptiness check: a whitespace-only query is as\n // empty as a missing one, and letting it through would spend a request on a\n // search for nothing.\n const query = args.query.trim();\n if (!query) {\n throw invalidInput(\n \"'query' cannot be empty.\",\n 'Give a dish or an ingredient, for example query=\"tarte aux pommes\".',\n );\n }\n\n const { data, cached } = await client.search(query);\n const notes: string[] = [];\n if (cached) {\n notes.push(\"Served from this server's short-lived in-memory cache.\");\n }\n if (data.length > args.limit) {\n notes.push(`This page holds ${data.length} recipes; showing the first ${args.limit}.`);\n }\n if (data.length === 0) {\n notes.push(\n \"Marmiton found no recipe for this query. Try French terms, or a broader wording. \" +\n \"Note that only one page of results is available.\",\n );\n }\n\n const results = data.slice(0, args.limit).map((recipe) => ({\n id: recipe.id,\n title: recipe.title,\n url: recipe.url,\n image_url: recipe.imageUrl,\n }));\n\n if (results.length > 0 && !results.some((recipe) => titleCarries(recipe.title, query))) {\n notes.push(\n `No title here carries a word of \"${query}\". Marmiton ranks a title on the letters it ` +\n \"opens with, so these rows are what the site offered for that spelling. Read them as \" +\n \"candidates to check rather than as recipes for the dish.\",\n );\n }\n\n const structured = {\n query,\n results,\n result_count: results.length,\n total_available: data.length,\n source: \"marmiton.org\" as const,\n notes,\n };\n\n const header =\n results.length > 0\n ? `${results.length} recette(s) pour \"${query}\":`\n : `Aucune recette trouvée pour \"${query}\".`;\n const list = results\n .map((recipe, index) => `${index + 1}. ${recipe.title} · id: ${recipe.id}`)\n .join(\"\\n\");\n\n return ok(structured, `${header}\\n${list}`);\n } catch (error) {\n return toToolError(error);\n }\n}\n"],"mappings":"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","names":["process","z","process","process","z","TRAILING_S","fraction","SIBILANT_ENDING","EAU_ENDING","AL_ENDING","EAUX_ENDING","AUX_ENDING","bounds","flatten","z","z","z","z","CLAMPED_UP","z","z","z","fold","z","process"]}