mcp-recipes 1.0.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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +684 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3037 -0
- package/dist/index.js.map +1 -0
- package/dist/language-BgfmjwtF.d.ts +14 -0
- package/dist/recipe/scale.d.ts +86 -0
- package/dist/recipe/scale.js +1367 -0
- package/dist/recipe/scale.js.map +1 -0
- package/dist/sources/client.d.ts +448 -0
- package/dist/sources/client.js +697 -0
- package/dist/sources/client.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/language.ts","../../src/recipe/units.ts","../../src/recipe/quantity.ts","../../src/recipe/scale.ts"],"sourcesContent":["/**\n * Which language an ingredient line is written in.\n *\n * A list can hold French lines and English lines at once, because a cook\n * comparing two versions of a dish keeps both in front of them. The language\n * decides how a number is written, how a noun agrees with it, and which\n * vocabulary of measures applies, so it is settled per line rather than once\n * for a whole list.\n */\n\nexport type Language = \"fr\" | \"en\";\n\n/** What a caller may ask for: a language, or a reading taken per line. */\nexport type LanguageChoice = Language | \"auto\";\n\n/**\n * Structural words, which are the reliable signal.\n *\n * A line names its ingredient in whatever language the page is written in, and\n * an ingredient name travels: a French recipe asks for \"Mountain Dew\" and an\n * English one for \"crème fraîche\". The small words that hold a line together do\n * not travel, so \"de\", \"à\" and \"of\" say more about the line than the name of\n * the food does.\n */\nconst FRENCH_WORDS =\n /(?:^|[\\s(])(?:de|du|des|à|au|aux|la|le|les|un|une|quelques|dans|avec|sans|pour|selon|environ)(?=[\\s),.]|$)|\\bd'|\\bl'|\\bqu'/iu;\n\n/**\n * The bare article \"a\" is left out on purpose. French recipes abbreviate\n * \"cuillère à soupe\" as \"c a s\", dropping the accent, and counting that \"a\" as\n * English evidence would send a French line through the English vocabulary.\n */\nconst ENGLISH_WORDS =\n /(?:^|[\\s(])(?:of|an|the|and|with|without|for|into|about|each|per|plus|freshly|finely|roughly|coarsely|optional)(?=[\\s),.]|$)/iu;\n\n/** Letters English does not write, which no ingredient name borrows either. */\nconst FRENCH_LETTERS = /[àâäçéèêëîïôöùûüœ]/iu;\n\n/**\n * Foods each language names its own way.\n *\n * Weak evidence, and deliberately so: a name is the part of a line most likely\n * to be borrowed, and a structural word outranks any number of these. It is\n * enough to settle a line that carries nothing else, such as \"6 oeufs\", where\n * every other signal is silent.\n */\nconst FRENCH_FOODS =\n /\\b(?:oeufs?|œufs?|sel|poivre|sucres?|farine|beurre|lait|crème|creme|huile|ail|oignons?|fromage|pain|poulet|boeuf|bœuf|riz|pâtes|pates|jambon|citrons?|pommes?|vin|eau|persil|thym|miel|jaunes?|blancs?)\\b/iu;\n\nconst ENGLISH_FOODS =\n /\\b(?:eggs?|salt|pepper|sugar|flour|butter|milk|cream|oil|garlic|onions?|cheese|bread|chicken|beef|rice|pasta|ham|lemons?|apples?|wine|water|parsley|thyme|honey|yolks?|whites?)\\b/iu;\n\n/** Weight of one structural signal, which outranks a vocabulary match. */\nconst STRUCTURAL = 3;\n/** Weight of a measure only one of the two vocabularies carries. */\nconst VOCABULARY = 2;\n/** Weight of a food name, which is the signal a line borrows most readily. */\nconst NAME = 1;\n\nexport interface LanguageEvidence {\n language: Language;\n french: number;\n english: number;\n}\n\n/**\n * Read the language off one line.\n *\n * `frenchUnit` and `englishUnit` say whether each vocabulary recognised a\n * measure in the line. They count for less than a structural word, because a\n * French line can name an English measure in passing: \"1 dose (cup) de Mountain\n * Dew\" is French with an English gloss inside it, and the \"de\" is what settles\n * it.\n *\n * English is the answer when nothing points either way, which is what an\n * ingredient with no small words and no measure gets.\n */\nexport function readLanguage(\n line: string,\n evidence: { frenchUnit: boolean; englishUnit: boolean } = { frenchUnit: false, englishUnit: false },\n): LanguageEvidence {\n let french = 0;\n let english = 0;\n\n if (FRENCH_WORDS.test(line)) french += STRUCTURAL;\n if (FRENCH_LETTERS.test(line)) french += STRUCTURAL;\n if (ENGLISH_WORDS.test(line)) english += STRUCTURAL;\n if (evidence.frenchUnit) french += VOCABULARY;\n if (evidence.englishUnit) english += VOCABULARY;\n if (FRENCH_FOODS.test(line)) french += NAME;\n if (ENGLISH_FOODS.test(line)) english += NAME;\n\n return { language: french > english ? \"fr\" : \"en\", french, english };\n}\n\n/** The language a list is written in, when a caller wants one answer for all of it. */\nexport function readListLanguage(lines: string[]): Language {\n let french = 0;\n let english = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n }\n return french > english ? \"fr\" : \"en\";\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in both languages: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French line and an English line alike,\n * and they take no plural mark in either.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n};\n\nexport function unitKeys(language: Language): string[] {\n return UNIT_KEYS[language];\n}\n\nexport function lookupUnit(text: string, language: Language): UnitInfo | null {\n return VOCABULARY[language][normalizeUnitKey(text)] ?? null;\n}\n\n/**\n * Matches a number followed by a measure anywhere in a piece of text.\n *\n * Used to spot a quantity the parser did not take, such as the second half of\n * \"1 Tbsp vanilla sugar OR 1 tsp vanilla extract\", which would otherwise sit in\n * a scaled line still saying what the original said.\n */\nconst EMBEDDED: Record<Language, RegExp> = {\n fr: buildEmbedded(\"fr\"),\n en: buildEmbedded(\"en\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(text);\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a French measure named with a container or a gesture the vocabulary has\n * no entry for.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it: a bouchon, a poignée, a ramequin hold what they hold, and the recipe's\n * proportion lives in how many are asked for. French marks that grammatically,\n * by placing the noun between the amount and the partitive that introduces the\n * thing measured: \"un bouchon de rhum\", \"2 bouquets de persil\". A noun in that\n * position measures whatever follows it, so a container nobody thought to list\n * is read by the same rule as the ones that are, and the vocabulary only has to\n * carry the words whose plural or spelling the rule would get wrong.\n *\n * The amount has to come first. A line opening on the noun, as in \"beurre\n * pommade\", carries no quantity, and inventing one from the grammar would put a\n * number where the recipe wrote none.\n */\nexport function readPartitiveMeasure(text: string): { unit: UnitInfo; rest: string } | null {\n const match = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u.exec(text);\n if (!match) return null;\n\n const word = match[1]!;\n if (word.length < 3) return null;\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) return null;\n if (lookupUnit(word, \"fr\")) return null;\n\n const canonical = frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!/^[a-z]{3,}fuls?$/.test(key)) return null;\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (/eaux$/i.test(word)) return word.slice(0, -1);\n if (/aux$/i.test(word)) return `${word.slice(0, -3)}al`;\n if (/[aiou]s$/i.test(word)) return word;\n if (/s$/i.test(word) && word.length > 3) return word.slice(0, -1);\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 (/[sxz]$/i.test(word)) return word;\n if (/eau$/i.test(word)) return `${word}x`;\n if (/al$/i.test(word)) return `${word.slice(0, -2)}aux`;\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n};\n\n/** The everyday equivalence for an approximate measure, when there is a settled one. */\nexport function approximateEquivalent(unit: UnitInfo): string | null {\n return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;\n}\n\n/**\n * Ladders, used to keep a scaled amount at a human size.\n *\n * Multiplying a recipe by thirty is arithmetically fine and practically poor:\n * \"8335 g of sugar\" is correct, and nobody weighs eight thousand grams. Each\n * measured unit therefore knows the unit above and below it, so a large amount\n * climbs the ladder and a small one comes back down. Each system keeps its own\n * ladder, because converting between them changes what the recipe said.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n /** Which vocabulary the step's spelling comes from. */\n language: Language;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"kg\", per: 1000, language: \"en\" },\n ml: { to: \"l\", per: 1000, language: \"en\" },\n cl: { to: \"l\", per: 100, language: \"en\" },\n dl: { to: \"l\", per: 10, language: \"en\" },\n oz: { to: \"lb\", per: 16, language: \"en\" },\n ounce: { to: \"pound\", per: 16, language: \"en\" },\n \"fl oz\": { to: \"pint\", per: 20, language: \"en\" },\n pint: { to: \"quart\", per: 2, language: \"en\" },\n quart: { to: \"gallon\", per: 4, language: \"en\" },\n};\n\nconst DEMOTIONS: Record<string, UnitStep> = {\n // Spoons and cups hold a fixed volume, so a share of one is stated in the\n // smaller spoon rather than as a fraction no measuring set carries. The\n // spelling of the step matches the spelling of the unit it comes from, so a\n // line written in abbreviations stays in abbreviations, and a French line\n // stays in French.\n cup: { to: \"tablespoon\", per: 16, language: \"en\" },\n tablespoon: { to: \"teaspoon\", per: 3, language: \"en\" },\n tbsp: { to: \"tsp\", per: 3, language: \"en\" },\n \"cuillere a soupe\": { to: \"cuillere a cafe\", per: 3, language: \"fr\" },\n tasse: { to: \"cuillere a soupe\", per: 16, language: \"fr\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) return null;\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/.test(\n unit.canonical,\n );\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A doubtful word takes the half. Half an approximate measure is nearer the\n * truth than a whole one rounded up, and a cook reading \"1/2 sachet\" knows what\n * to do with it. A measure therefore answers `half`, apart from the few named\n * below, and the one thing that does not divide is decided from the item's own\n * name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A gousse and a tranche are already cut off something larger, and the board\n * that produced one takes a corner off it in the same gesture: a quarter of a\n * gousse d'ail is what a knife scrapes into a pan, a quarter of a tranche de\n * pain is a crouton.\n *\n * Each measure is listed in either language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|gousses?|tranches?|slices?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n */\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) break;\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) return unit.canonical;\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) return unit.canonical;\n return unit.plural ?? `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2]!]!;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = /^(\\d+)\\s*\\/\\s*(\\d+)/.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) return null;\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph]!, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (\n language === \"fr\" ? /^(\\d+(?:[.,]\\d+)?)/ : /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/\n ).exec(trimmed);\n if (decimal) {\n const written = decimal[1]!;\n const amount = Number(\n language === \"fr\" ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) return { amount, length: offset + decimal[0].length };\n }\n\n if (language === \"en\") {\n const written = parseWrittenFraction(trimmed);\n if (written) return { amount: written.amount, length: offset + written.length };\n }\n\n return null;\n}\n\n/** How many of the part a line names: \"two thirds\", \"a quarter\", \"half\". */\nconst WRITTEN_NUMERATORS: Record<string, number> = { a: 1, an: 1, one: 1, two: 2, three: 3 };\n\n/** What the part is a part of. */\nconst WRITTEN_DENOMINATORS: Record<string, number> = {\n half: 2,\n halves: 2,\n third: 3,\n thirds: 3,\n quarter: 4,\n quarters: 4,\n fourth: 4,\n fourths: 4,\n};\n\n/**\n * Read a fraction an English line spells out, as in \"half a bottle\" or \"two\n * thirds of a cup\".\n *\n * Recipes write the small fractions in words as readily as in figures, and a\n * line opening on one of them carries an amount like any other. The numerator\n * is optional because \"half\" alone is the common form.\n *\n * What follows decides whether the words are a quantity at all. A share of one\n * thing, \"half a lemon\" or \"two thirds of a cup\", states an amount of its own.\n * A share of a definite thing, \"half of the dough\", points back at an amount\n * stated elsewhere, and multiplying it would answer with a number that belongs\n * to another line.\n */\nfunction parseWrittenFraction(text: string): ParsedQuantity | null {\n const match =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i.exec(\n text,\n );\n if (!match) return null;\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[match[2]!.toLowerCase()];\n if (!numerator || !denominator) return null;\n\n const rest = text\n .slice(match[0].length)\n .replace(/^\\s*of\\s+/i, \"\")\n .trimStart();\n if (!/^an?\\s/i.test(rest) && !takeUnit(rest, \"en\").unit) return null;\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) return null;\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = language === \"fr\" ? /^\\s+(à|a|ou)\\s+/i : /^\\s+(to|or)\\s+/i;\n const separator = written.exec(after) ?? /^\\s*(–|—|-)\\s*/.exec(after);\n if (!separator) return null;\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) return null;\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1]!,\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets, as in \"450 g (1\n * pound)\", 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 the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/** Articles a French line writes where a digit would go, and what they count as. */\nconst FRENCH_ARTICLES: Record<string, number> = { un: 1, une: 1, quelques: 3 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(text: string, language: Language): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) continue;\n const unit = lookupUnit(key, language);\n if (!unit) continue;\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(/\\s+/);\n return { unit, rest: words.slice(wordCount).join(\" \") };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(/\\s+/);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) return { unit: load, rest: words.slice(1).join(\" \") };\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * A measure inside brackets can be stated in the other language's vocabulary: a\n * French page glosses grams in ounces, and an English page glosses cups in\n * millilitres. The line's own language is tried first, so a word both\n * vocabularies carry keeps the spelling and the plural of the line it sits in.\n */\nfunction takeUnitEitherLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) return first;\n return takeUnit(text, language === \"fr\" ? \"en\" : \"fr\");\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(/^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u, \"\").trimStart();\n const afterArticle = afterFigures.replace(/^(?:un|une|quelques|of\\s+an?|an?)\\s+/iu, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit }).language;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const text = line.trim();\n const language = choice === \"auto\" ? detectLanguage(text) : choice;\n\n const empty: ParsedIngredient = {\n original,\n language,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n };\n\n const range = parseLeadingRange(text, language);\n const article = range ? null : readArticle(text, language);\n const quantity = range ?? parseLeadingQuantity(text, language) ?? article;\n if (!quantity) return empty;\n\n let rest = text.slice(quantity.length).trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") rest = rest.replace(/^(?:of\\s+)?an?\\s+/i, \"\");\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) rest = multiplier.rest;\n const times = multiplier?.times ?? 1;\n\n const leading = takeLeadingUnit(rest, language, quantity === article);\n rest = leading.rest;\n\n const bracketed = takeAlternates(rest, language);\n rest = bracketed.rest;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) rest = slashed.rest;\n\n return {\n original,\n language,\n amount: quantity.amount * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates: slashed ? slashed.measures : bracketed.measures,\n alternateStyle: slashed ? \"slash\" : bracketed.measures.length > 0 ? \"bracket\" : null,\n item: stripItemLead(rest, language),\n articleWord: quantity === article ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (language === \"fr\") {\n const match = /^\\s*(un|une|quelques)\\b\\s*/i.exec(text);\n if (!match) return null;\n if (!counts(text.slice(match[0].length))) return null;\n const word = match[1]!;\n return { amount: FRENCH_ARTICLES[word.toLowerCase()]!, length: match[0].length, word };\n }\n\n const article = /^an?\\s+/i.exec(text);\n if (!article) return null;\n if (!counts(text.slice(article[0].length))) return null;\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = /^\\s*(\\p{L}+)\\s+/u.exec(text);\n if (!match) return null;\n\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(match[1]!)];\n if (times === undefined) return null;\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string } {\n const taken = takeUnit(text, language);\n if (taken.unit) return taken;\n\n if (language === \"fr\" && fromArticle) {\n const measure = readPartitiveMeasure(text);\n if (measure) return { unit: measure.unit, rest: measure.rest };\n }\n\n return taken;\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): string {\n if (language === \"fr\") {\n return text\n .replace(/^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i, \"\")\n .replace(/^(?:une|un)\\s+/i, \"\")\n .trim();\n }\n return text\n .replace(/^of\\s+/i, \"\")\n .replace(/^an?\\s+/i, \"\")\n .trim();\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): { measures: Measure[]; rest: string } {\n if (!text.startsWith(\"(\")) return { measures: [], rest: text };\n const close = text.indexOf(\")\");\n if (close < 0) return { measures: [], rest: text };\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, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) return { measures: [], rest: text };\n\n const after = takeUnitEitherLanguage(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") return { measures: [], rest: text };\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) return { measures: [], rest: text };\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 rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) break;\n\n const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) break;\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return language === \"fr\" ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) return \"\";\n if (Number.isInteger(amount)) return String(amount);\n if (options.fractions === false) return decimal(amount);\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: Array<[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) return whole > 0 ? `${whole} ${label}` : label;\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French and in English.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act\n * on. Multiplying every number by the factor is arithmetically correct and\n * practically useless: it produces \"2,4 oeufs\" and \"0.67 pinch of salt\" with\n * the same confidence as \"267 g flour\". Each line is therefore classified by\n * what its measure allows, and the classification travels with the result so\n * the caller can see what was computed and what was left alone.\n *\n * Leaving a line alone is a decision of the same weight. A quantity a recipe\n * states loosely still holds a share of the dish, and a leavening agent left at\n * one pinch for twenty-five servings is a recipe that does not rise.\n *\n * The two languages are read by one set of rules. A list holding French lines\n * and English lines comes back scaled by the same arithmetic, with each line\n * written the way its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n */\nfunction roundMeasured(value: number): number {\n if (value >= 100) return roundTo(value, 5);\n if (value >= 10) return roundTo(value, 1);\n if (value >= 1) return roundTo(value, 0.1);\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) return { value: 0, clamped: false };\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) return { value: floor, clamped: true };\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) return { value: floor, clamped: true };\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0]!;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) closest = candidate;\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) return { value: 0, clamped: false };\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]!;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) closest = candidate;\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) break;\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) return false;\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n}\n\ninterface ScaledMeasure {\n bounds: ScaledBound[];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n const raws = high === null ? [low * factor] : [low * factor, high * factor];\n const sources = high === null ? [low] : [low, high];\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : raws[0]!;\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: raws.map((raw, index) => {\n const exact = raw * ratio;\n const rounded = 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 ? sources[index]! * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) return inUnit(stepped.unit, stepped.ratio);\n\n const bounds = raws.map((raw, index) => {\n const ceiling = factor < 1 ? sources[index]! : 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 const bounds = raws.map((raw, index) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? sources[index]! : 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 * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in either language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genièvres?|genievres?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n \")\\\\b\",\n \"iu\",\n);\n\n/**\n * A juice, the one counted thing whose division stops at the half.\n *\n * Half the juice of a lemon is taken by squeezing half the fruit, which is a\n * step a recipe writes. A quarter of one has to be poured out and measured\n * back, and no recipe asks for that.\n *\n * It reads before the fruit, which a knife divides further on its own.\n */\nconst HALVED_ITEM = /\\b(?:jus|juices?)\\b/iu;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An egg comes out of its shell whole, and so does the yolk or the white a\n * recipe asks for on its own: half of one means beating it and weighing the\n * result, which no recipe asks for, and there is no way to keep the other half.\n * A count of them therefore lands on a whole number, whichever side of the half\n * the arithmetic fell on.\n *\n * This list is short on purpose, and a word joins it only when half of the\n * thing is genuinely not something a cook can measure out. Anything that is\n * poured, weighed or cut halves, and takes the half.\n *\n * Two more belong here for reasons the criterion cannot reach on its own:\n *\n * - a clou de girofle is a dried flower bud, dropped into the pot and fished\n * back out of it. Nothing about it is measured, so there is no half of one to\n * take;\n * - a zeste is what comes off one fruit in one go. A line asking for the zeste\n * of a citron is asking for all of it, and a share of a zeste names no amount\n * a cook stops at.\n */\nconst WHOLE_ITEM =\n /\\b(?:eggs?|yolks?|egg\\s+whites?|oeufs?|œufs?|jaunes?\\s+d['e]|clous?|zestes?|zests?)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and a quarter of\n * one is what a knife scrapes into a pan. A clove on its own is the dried\n * flower bud, the clou de girofle, dropped into the pot and fished back out of\n * it: nothing about it is measured, so there is no share of one to take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : /\\bcloves?\\b/i.test(item);\n if (!counted) return null;\n return /\\bgarlic\\b/i.test(item) ? \"quarter\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) return null;\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const clove = cloveDivisibility(unit, item);\n if (clove) return clove;\n if (unit && !countsBarePieces(unit)) return unitDivisibility(unit);\n const blanc = blancDivisibility(item);\n if (blanc) return blanc;\n if (WHOLE_ITEM.test(item)) return \"whole\";\n if (PORTION_SIZED_ITEM.test(item)) return \"whole\";\n if (HALVED_ITEM.test(item)) return \"half\";\n if (HALVED_CUT.test(item)) return \"half\";\n if (QUARTERED_MEASURE.test(item)) return \"quarter\";\n return QUARTERED_ITEM.test(item) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/** Keep the capitalisation the line used while looking the word up in lower case. */\nfunction matchCase(source: string, replacement: string): string {\n if (source[0] === source[0]?.toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {\n return replacement[0]!.toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) return word;\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) return matchCase(word, irregular);\n if (/(?:ch|sh|s|x|z)$/i.test(word)) return `${word}es`;\n if (/[^aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;\n if (/(?:[^f]f|fe)$/i.test(word)) return `${word.replace(/fe?$/i, \"\")}ves`;\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) return word;\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) return matchCase(word, irregular);\n if (/ies$/i.test(word) && word.length > 4) return `${word.slice(0, -3)}y`;\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (/(?:ch|sh|s|x|z)es$/i.test(word)) return word.slice(0, -2);\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (/ss$/i.test(word)) return word;\n if (/s$/i.test(word)) return word.slice(0, -1);\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) return item;\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = / of /i.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words[words.length - 1] ?? \"\";\n if (!/^[A-Za-z]+$/.test(last) || last.length <= 2) return item;\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) words[words.length - 1] = plural;\n else if (!wantsPlural && isPlural) words[words.length - 1] = singular;\n else return item;\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"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))) return null;\n if (wantsPlural === isPlural) return null;\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) return item;\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) return item;\n\n const wantsPlural = amount >= 2;\n const isPlural = /s$|eaux$|aux$/i.test(head);\n\n if (wantsPlural && !isPlural) {\n // Words ending in -s, -x or -z do not take a plural mark.\n if (/[sxz]$/i.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$/i.test(head)) words[0] = `${head}x`;\n else if (/al$/i.test(head)) words[0] = `${head.slice(0, -2)}aux`;\n else words[0] = `${head}s`;\n } else if (!wantsPlural && isPlural) {\n if (/eaux$/i.test(head)) words[0] = head.slice(0, -1);\n else if (/aux$/i.test(head)) words[0] = `${head.slice(0, -3)}al`;\n // \"ananas\", \"anis\", \"couscous\": the -s belongs to the singular.\n else if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n // The head stays as written.\n } else words[0] = head.slice(0, -1);\n }\n\n const last = words.length - 1;\n if (last > 0) {\n const adjective = agreeTrailingAdjective(words[last]!, wantsPlural);\n if (adjective) words[last] = adjective;\n }\n\n return words.join(\" \");\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n return language === \"fr\" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language): string {\n if (!item) return \"\";\n if (language === \"en\") return ` ${item}`;\n const elides = /^[aeiouàâäéèêëîïôöûü]/i.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in either language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) return null;\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) continue;\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) continue;\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) return passthroughIngredient(line, options.language);\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\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\") return { ...head, text: line.trim(), original: line };\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) return { text: published, rewritten: false };\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) return { text: published, rewritten: false };\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n note: \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((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\n // them, 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 const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const named = unit && !countsBarePieces(unit) ? unit : null;\n const unitLabel = named ? ` ${formatUnit(named, shown, language)}` : \"\";\n const alternateTexts = alternates.map((entry) => entry.text);\n // Equivalents go back the way the line offered them: inside brackets, or\n // after a slash beside the amount they restate.\n const altLabel =\n alternates.length === 0\n ? \"\"\n : parsed.alternateStyle === \"slash\"\n ? ` / ${alternateTexts.join(\" / \")}`\n : ` (${alternateTexts.join(\" / \")})`;\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = agreeWithAmount(parsed.item, shown, language);\n const itemLabel = named ? joinItem(parsed.item, language) : counted ? ` ${counted}` : \"\";\n\n const result: ScaledIngredient = {\n text: `${amountText}${unitLabel}${altLabel}${itemLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n };\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !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\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (hasEmbeddedMeasure(parsed.item, language)) {\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 if (sentences.length > 0) result.note = sentences.join(\" \");\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, result.note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n result.note = result.note ? `${read} ${result.note}` : read;\n }\n\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0]!;\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) return low;\n return /^[-–—]$/.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n const parsed = parseIngredient(line, language);\n\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: parsed.amount === null ? \"unscaled\" : \"scaled\",\n amount: parsed.amount,\n amountMax: parsed.amountMax,\n unit: parsed.unit?.canonical ?? null,\n language: parsed.language,\n };\n if (parsed.amount === null) result.note = \"No quantity given; adjust to taste.\";\n else if (parsed.unit?.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, undefined);\n }\n return result;\n}\n\n/** An ingredient list returned unchanged, for when no scaling was requested. */\nexport function passthroughIngredients(\n lines: string[],\n language: LanguageChoice = \"auto\",\n): ScaledIngredient[] {\n return lines.map((line) => passthroughIngredient(line, language));\n}\n"],"mappings":"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","names":["VOCABULARY","fraction","bounds"]}
|