mcp-recipes 3.0.0 → 4.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 +70 -0
- package/PRIVACY.md +8 -6
- package/README.md +192 -103
- package/dist/index.js +1236 -249
- package/dist/index.js.map +1 -1
- package/dist/{language-BgfmjwtF.d.ts → language-B3VNNg7f.d.ts} +2 -2
- package/dist/recipe/scale.d.ts +13 -5
- package/dist/recipe/scale.js +616 -89
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +163 -9
- package/dist/sources/client.js +348 -50
- package/dist/sources/client.js.map +1 -1
- package/package.json +20 -17
- package/server.json +5 -5
package/dist/recipe/scale.js.map
CHANGED
|
@@ -1 +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 } = {\n frenchUnit: false,\n englishUnit: false,\n },\n): LanguageEvidence {\n let french = 0;\n let english = 0;\n\n if (FRENCH_WORDS.test(line)) {\n french += STRUCTURAL;\n }\n if (FRENCH_LETTERS.test(line)) {\n french += STRUCTURAL;\n }\n if (ENGLISH_WORDS.test(line)) {\n english += STRUCTURAL;\n }\n if (evidence.frenchUnit) {\n french += VOCABULARY;\n }\n if (evidence.englishUnit) {\n english += VOCABULARY;\n }\n if (FRENCH_FOODS.test(line)) {\n french += NAME;\n }\n if (ENGLISH_FOODS.test(line)) {\n english += NAME;\n }\n\n return { language: french > english ? \"fr\" : \"en\", french, english };\n}\n\n/** The language a list is written in, when a caller wants one answer for all of it. */\nexport function readListLanguage(lines: string[]): Language {\n let french = 0;\n let english = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n }\n return french > english ? \"fr\" : \"en\";\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst FUL_MEASURE = /^[a-z]{3,}fuls?$/;\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst HOUSEHOLD_MEASURE =\n /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in both languages: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French line and an English line alike,\n * and they take no plural mark in either.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n // A page glossing a metric weight names the pound, which French writes as a\n // livre: \"450 g (1 livre) de spaghetti\".\n livre: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n // A dish of the book standing as an ingredient of another: \"1 recipe Flaky\n // Pie Crust\". The count is what the word measures, so the plural mark belongs\n // on it and not on the name of the dish.\n recipe: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n recipes: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return (\n text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n};\n\nexport function unitKeys(language: Language): string[] {\n return UNIT_KEYS[language];\n}\n\nexport function lookupUnit(text: string, language: Language): UnitInfo | null {\n return VOCABULARY[language][normalizeUnitKey(text)] ?? null;\n}\n\n/**\n * Matches a number followed by a measure anywhere in a piece of text.\n *\n * Used to spot a quantity the parser did not take, such as the second half of\n * \"1 Tbsp vanilla sugar OR 1 tsp vanilla extract\", which would otherwise sit in\n * a scaled line still saying what the original said.\n */\nconst EMBEDDED: Record<Language, RegExp> = {\n fr: buildEmbedded(\"fr\"),\n en: buildEmbedded(\"en\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // A quantity keeps the same shape whether the measure stands against the\n // figure or behind the word introducing it: \"3/4 tasse\" and \"3/4 de tasse\"\n // are one quantity written twice. Reading only the first spelling leaves the\n // second sitting on a scaled line, still saying what the page said.\n const joiner = \"(?:(?:de|du|des|d|of|a|an|the)\\\\s+)?\";\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*${joiner}(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\n/**\n * Nouns naming what a food is sold in.\n *\n * A measure written in front of one of them gives what that container holds:\n * \"12 oz can\" is the size of the tin rather than an amount to weigh out, and\n * doubling the recipe means opening a second tin rather than finding a bigger\n * one.\n *\n * Written in either language, because what a word names has nothing to do with\n * which language names it.\n */\nexport const CONTAINER_NOUN =\n /^(?:cans?|tins?|jars?|packets?|packages?|packs?|box|boxes|bottles?|cartons?|bags?|tubs?|sachets?|blocks?|blocs?|boites?|pots?|briques?|bocaux|bocals?|barquettes?)\\b/i;\n\n/**\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a French line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(normalizeUnitKey(text));\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a French measure named with a container or a gesture the vocabulary has\n * no entry for.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it: a bouchon, a poignée, a ramequin hold what they hold, and the recipe's\n * proportion lives in how many are asked for. French marks that grammatically,\n * by placing the noun between the amount and the partitive that introduces the\n * thing measured: \"un bouchon de rhum\", \"2 bouquets de persil\". A noun in that\n * position measures whatever follows it, so a container nobody thought to list\n * is read by the same rule as the ones that are, and the vocabulary only has to\n * carry the words whose plural or spelling the rule would get wrong.\n *\n * The amount has to come first. A line opening on the noun, as in \"beurre\n * pommade\", carries no quantity, and inventing one from the grammar would put a\n * number where the recipe wrote none.\n */\nexport function readPartitiveMeasure(text: string): { unit: UnitInfo; rest: string } | null {\n const match = HEAD_BEFORE_DE.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n if (word.length < 3) {\n return null;\n }\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {\n return null;\n }\n if (lookupUnit(word, \"fr\")) {\n return null;\n }\n\n const canonical = frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!FUL_MEASURE.test(key)) {\n return null;\n }\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (EAUX_ENDING.test(word)) {\n return word.slice(0, -1);\n }\n if (AUX_ENDING.test(word)) {\n return `${word.slice(0, -3)}al`;\n }\n if (VOWEL_THEN_S.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/** The plural French writes for a noun, or the noun itself when it takes no mark. */\nfunction frenchPlural(word: string): string {\n if (SIBILANT_LETTER.test(word)) {\n return word;\n }\n if (EAU_ENDING.test(word)) {\n return `${word}x`;\n }\n if (AL_ENDING.test(word)) {\n return `${word.slice(0, -2)}aux`;\n }\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n};\n\n/** The everyday equivalence for an approximate measure, when there is a settled one. */\nexport function approximateEquivalent(unit: UnitInfo): string | null {\n return APPROXIMATE_EQUIVALENT[normalizeUnitKey(unit.canonical)] ?? null;\n}\n\n/**\n * Ladders, used to keep a scaled amount at a human size.\n *\n * Multiplying a recipe by thirty is arithmetically fine and practically poor:\n * \"8335 g of sugar\" is correct, and nobody weighs eight thousand grams. Each\n * measured unit therefore knows the unit above and below it, so a large amount\n * climbs the ladder and a small one comes back down. Each system keeps its own\n * ladder, because converting between them changes what the recipe said.\n */\ninterface UnitStep {\n /** Unit to switch to, and how many of the current unit it holds. */\n to: string;\n per: number;\n /** Which vocabulary the step's spelling comes from. */\n language: Language;\n}\n\nconst PROMOTIONS: Record<string, UnitStep> = {\n mg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"kg\", per: 1000, language: \"en\" },\n ml: { to: \"l\", per: 1000, language: \"en\" },\n cl: { to: \"l\", per: 100, language: \"en\" },\n dl: { to: \"l\", per: 10, language: \"en\" },\n oz: { to: \"lb\", per: 16, language: \"en\" },\n ounce: { to: \"pound\", per: 16, language: \"en\" },\n \"fl oz\": { to: \"pint\", per: 20, language: \"en\" },\n pint: { to: \"quart\", per: 2, language: \"en\" },\n quart: { to: \"gallon\", per: 4, language: \"en\" },\n};\n\nconst DEMOTIONS: Record<string, UnitStep> = {\n // Spoons and cups hold a fixed volume, so a share of one is stated in the\n // smaller spoon rather than as a fraction no measuring set carries. The\n // spelling of the step matches the spelling of the unit it comes from, so a\n // line written in abbreviations stays in abbreviations, and a French line\n // stays in French.\n cup: { to: \"tablespoon\", per: 16, language: \"en\" },\n tablespoon: { to: \"teaspoon\", per: 3, language: \"en\" },\n tbsp: { to: \"tsp\", per: 3, language: \"en\" },\n \"cuillere a soupe\": { to: \"cuillere a cafe\", per: 3, language: \"fr\" },\n tasse: { to: \"cuillere a soupe\", per: 16, language: \"fr\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A gesture keeps its own answer. A pincée, a poignée, a pinch is the amount a\n * hand produces in one go, and there is no half of a hand: the size of one is\n * the cook's and the count is the whole of what the measure can say, so the\n * count lands on a whole and the line reports that it moved.\n *\n * A doubtful word takes the half. A cook reading \"1/2 sachet\" knows what to do\n * with it, so a measure answers `half` apart from the few named below, and the\n * one thing that does not divide is decided from the item's own name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"approximate\") {\n return \"whole\";\n }\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A tranche is already cut off something larger, and the board that produced\n * one takes a corner off it in the same gesture: a quarter of a tranche de pain\n * is a crouton.\n *\n * Each measure is listed in either language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n *\n * Both directions ask whether the unit can hold the figure. A kitchen reads two\n * decimals and no more, so 2468 g written in kilos is 2.47 and the eight grams\n * are gone. A quantity the bigger unit cannot state stays where the page wrote\n * it, and a quantity the page's own unit cannot state walks down to the one that\n * can, so the same mass comes out the same however the page spelled it: \"1234 g\"\n * and \"1 kg 234\" are one quantity and answer with one figure.\n */\nexport function chooseReadableUnit(unit: UnitInfo, amount: number): ChosenUnit {\n if (unit.kind !== \"measured\" || !Number.isFinite(amount) || amount <= 0) {\n return { unit, ratio: 1 };\n }\n\n let current = unit;\n let ratio = 1;\n\n while (amount * ratio < 1) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per && writesExactly((amount * ratio) / up.per)) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n while (!writesExactly(amount * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/** Whether a figure survives being written with the two decimals a kitchen reads. */\nfunction writesExactly(value: number): boolean {\n return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) {\n return unit.canonical;\n }\n return unit.plural ?? `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice, LanguageEvidence } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n CONTAINER_NOUN,\n demoteUnit,\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nconst BRACKETED_TAIL = /\\s*(\\([^()]*\\))\\s*$/;\nconst LEADING_ARTICLE = /^(?:of\\s+)?an?\\s+/i;\nconst LEADING_ARTICLE_FRENCH = /^(?:une|un)\\s+/i;\nconst LEADING_INDEFINITE_WITH_SPACE = /^an?\\s+/i;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst OF_OPENING = /^of\\s+/i;\nconst OPENING_ARTICLE_FRENCH = /^\\s*(un|une)\\b\\s*/i;\nconst REPEATED_COMMA_GROUPS = /^\\s*\\d+,\\d+,\\d/;\nconst SLASH_OUTSIDE_A_FRACTION = /(?<!\\d)\\/|\\/(?!\\d)/;\n\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst DIGIT = /\\d/;\nconst ENGLISH_RANGE_JOINER = /^\\s+(to|or)\\s+/i;\nconst FRACTION_WORD =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i;\nconst FRENCH_RANGE_JOINER = /^\\s+(à|a|ou)\\s+/i;\nconst GROUPED_DECIMAL = /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/;\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_DASH = /^\\s*(–|—|-)\\s*/;\nconst LEADING_INDEFINITE = /^an?\\s/i;\nconst LEADING_OF = /^\\s*of\\s+/i;\nconst LEADING_QUANTITY = /^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst PARTITIVE_BEFORE_BRACKET = /^(?:de\\s+|du\\s+|des\\s+|d'|of\\s+)(?=\\()/i;\nconst PARTITIVE_OPENING = /^(?:de\\s|d'|du\\s|des\\s|,)/i;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst TRAILING_S = /s$/;\nconst VAGUE_ARTICLE = /^(?:un|une|quelques|of\\s+an?|an?)\\s+/iu;\nconst WHITESPACE = /\\s+/;\n\n/**\n * The measure the line leads with: the one standing straight after the amount,\n * or the one a size word stands in front of.\n */\nfunction leadingMeasure<T extends { unit: unknown }>(direct: T, behind: T | null): T {\n if (direct.unit) {\n return direct;\n }\n return behind?.unit ? behind : direct;\n}\n\n/**\n * How a line writes the equivalents beside its measure, when it writes any.\n *\n * Three shapes and three renderings: a slash beside the amount, a trailing\n * restatement, or a bracketed group. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(\n count: number,\n slashed: boolean,\n trailing: boolean,\n): \"slash\" | \"trailing\" | \"bracket\" | null {\n if (count === 0) {\n return null;\n }\n if (slashed) {\n return \"slash\";\n }\n if (trailing) {\n return \"trailing\";\n }\n return \"bracket\";\n}\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Named HTML entities a page writes where the character itself would do.\n *\n * The fractions are the ones that matter: a line reading \"3½ cups\" holds\n * three and a half cups, and a reader that does not decode it sees three and\n * carries the rest into the item name, where doubling the line loses half a cup\n * without saying so.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n frac12: \"\\u00bd\",\n frac13: \"\\u2153\",\n frac23: \"\\u2154\",\n frac14: \"\\u00bc\",\n frac34: \"\\u00be\",\n frac15: \"\\u2155\",\n frac18: \"\\u215b\",\n nbsp: \" \",\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n deg: \"\\u00b0\",\n times: \"\\u00d7\",\n minus: \"\\u2212\",\n ndash: \"\\u2013\",\n mdash: \"\\u2014\",\n};\n\n/**\n * Turn HTML entities back into the characters they stand for.\n *\n * A numeric entity names a code point directly, and the fraction glyphs a\n * recipe uses live well inside the range a page can write that way, so\n * \"⅔\" is two thirds and reads as such once decoded.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]+);/gi, (whole, hex: string) =>\n codePoint(Number.parseInt(hex, 16), whole),\n )\n .replace(/&#(\\d+);/g, (whole, digits: string) => codePoint(Number(digits), whole))\n .replace(\n /&([a-z][a-z0-9]*);/gi,\n (whole, name: string) => NAMED_ENTITIES[name.toLowerCase()] ?? whole,\n );\n}\n\n/** A code point a page named, or the entity as published when it names none. */\nfunction codePoint(value: number, published: string): string {\n if (!Number.isInteger(value) || value < 32 || value > 0x10ffff) {\n return published;\n }\n try {\n return String.fromCodePoint(value);\n } catch {\n return published;\n }\n}\n\n/**\n * Empty brackets, which a page leaves behind when it carried a conversion the\n * source did not render. They say nothing and belong in no answer.\n */\nconst EMPTY_BRACKETS = /\\(\\s*\\)/g;\n\n/**\n * Prepare a published line for reading: entities decoded, empty brackets\n * dropped, runs of spaces squeezed.\n */\nfunction readable(text: string): string {\n return decodeEntities(text).replace(EMPTY_BRACKETS, \" \").replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Signs and words a page puts before a number to say it is not exact.\n *\n * The number behind one of them is still a number, and reading none at all\n * hands back the line with a note saying it carries no quantity, which is\n * untrue. It is read, scaled and given back with the mark the page put on it,\n * so the answer stays as loose as the page was.\n */\nconst APPROXIMATION_PREFIX: Record<Language, RegExp> = {\n en: /^(?:~|\\u2248|about|approx\\.?|approximately|around|roughly)\\s*/i,\n fr: /^(?:~|\\u2248|environ|approximativement|\\u00e0 peu pr\\u00e8s|a peu pres)\\s*/i,\n};\n\n/**\n * Words that introduce what a container holds.\n *\n * \"1 pot de 500 g de miel\" counts pots and says how much honey is in one, so\n * the count is the recipe's to multiply. The partitive standing after the\n * measure is what marks the line as a container and its contents.\n */\nconst CONTAINER_CONTENTS = /^\\s*(?:de\\s|d'|du\\s|des\\s)/i;\n\n/**\n * Whether the measure standing behind an item gives the size of one of them,\n * as in \"1 dinde de 3 kg\".\n *\n * The count and the measure answer different questions: how many birds, and how\n * heavy one bird is. A cook serving half again as many people takes a heavier\n * bird, so the count belongs to the page rather than to the factor.\n *\n * Read on the item alone, which is what the line counts once its own measure\n * has been taken off it, so \"450 g (1 livre) de spaghetti\" never reaches here:\n * that line counts grams, and its bracket restates the same quantity.\n */\nfunction statesItemSize(item: string): boolean {\n const attached = ATTACHED_DE.exec(item);\n if (!attached) {\n return false;\n }\n\n const named = item.slice(0, attached.index).trim();\n if (!named || DIGIT.test(named)) {\n return false;\n }\n\n return isStatedSize(item.slice(attached.index + attached[0].length));\n}\n\n/** A mass or a volume standing on its own, with nothing it is the amount of. */\nfunction isStatedSize(text: string): boolean {\n const size = parseLeadingQuantity(text, \"fr\");\n if (!size) {\n return false;\n }\n\n const measure = takeUnit(text.slice(size.length).trimStart(), \"fr\");\n if (measure.unit?.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\n\n/**\n * A second measure a line writes straight after the first, both of them naming\n * one quantity: \"1 lb 4 oz beef\", \"2 kg 300 g de farine\", and the French\n * \"1 kg 500\", which leaves the smaller unit for the reader to supply.\n *\n * What joins the two members is the ladder: the second is written in the unit\n * the first steps down to, and it holds less than one of the first. A measure\n * from another ladder restates the quantity instead of continuing it, and a\n * number followed by a food names a second ingredient; neither is folded in\n * here.\n *\n * The share the second member adds is returned rather than the number itself,\n * so the quantity comes out as one amount in the unit the line opened with:\n * 1 lb 4 oz is 1.25 lb, and doubling it gives the 2.5 lb a butcher weighs.\n * Reading only the first member leaves the rest of the quantity sitting in the\n * item name, where doubling the line loses it.\n *\n * The two members can be joined by a sign or a word saying they add up, as in\n * \"2 c. à s. + 1 c. à c.\". The joiner is read wherever the same ladder relation\n * holds, since it states outright what juxtaposition leaves implicit.\n */\nfunction takeCompoundMember(\n text: string,\n unit: UnitInfo | null,\n language: Language,\n): { adds: number; rest: string } | null {\n if (!unit) {\n return null;\n }\n const step = demoteUnit(unit);\n if (!step) {\n return null;\n }\n\n const joiner = ADDS_UP.exec(text);\n const behindTheJoiner = joiner ? text.slice(joiner[0].length) : text;\n\n const second = parseLeadingQuantity(behindTheJoiner, language);\n if (!second || second.amount <= 0 || second.amount >= step.per) {\n return null;\n }\n\n const after = behindTheJoiner.slice(second.length);\n const measure = takeUnit(after.trimStart(), language);\n if (measure.unit) {\n if (measure.unit.canonical !== step.unit.canonical) {\n return null;\n }\n return { adds: second.amount / step.per, rest: measure.rest.trimStart() };\n }\n\n // The unwritten form: \"1 kg 500\" is a kilo and five hundred grams. It is read\n // only where a mass or a volume opened the line, and only where nothing but\n // the partitive follows the figure, so \"1 cup 2 eggs\" stays two eggs.\n if (unit.kind !== \"measured\") {\n return null;\n }\n if (!Number.isInteger(second.amount)) {\n return null;\n }\n const trailing = after.trimStart();\n if (trailing !== \"\" && !PARTITIVE_OPENING.test(trailing)) {\n return null;\n }\n\n return { adds: second.amount / step.per, rest: trailing };\n}\n\n/** How a line joins two measures that add up to one quantity. */\nconst ADDS_UP = /^\\s*(?:\\+|et|and|plus)\\s+/i;\n\n/**\n * A mark a page opens an ingredient line with, in front of the quantity.\n *\n * A bullet, a dash or a picture of the food is decoration: it says nothing\n * about how much, and a reader stopping at it answers that the line carries no\n * quantity, so a doubled recipe keeps the published amount of whatever the mark\n * stood in front of. It is set aside for reading and put back for writing.\n *\n * The signs that say an amount is loose are excluded, because those do carry a\n * claim about the figure behind them, and the mark has to be followed by space\n * so that a bracket or a decimal point opening a line is left where it is.\n */\nconst LEADING_DECORATION = /^(?:(?!~|≈|\\()[^\\p{L}\\p{N}\\s])+(?=\\s)/u;\n\n/** The mark a line opens with, or null when it opens on the quantity. */\nfunction takeDecoration(text: string): string | null {\n const match = LEADING_DECORATION.exec(text);\n return match ? match[0] : null;\n}\n\n/** Whether a word names something a food is sold in. */\nfunction namesContainer(word: string | undefined): boolean {\n return word !== undefined && CONTAINER_NOUN.test(normalizeUnitKey(word));\n}\n\n/**\n * Measures of time, in either language.\n *\n * An ingredient list carries lines that state a length rather than an amount:\n * a rest, a proof, a marinade, a bake. The factor says how much of the dish to\n * make, and how long a dough takes to rise is no part of that.\n */\nconst TIME_UNIT: Record<Language, RegExp> = {\n en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\\b/i,\n fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\\b/i,\n};\n\n/**\n * The letters a language glues to a figure to make it a rank: the \"er\" of\n * \"1er choix\", the \"e\" of \"2e couche\", the \"st\" of \"1st choice\".\n *\n * A rank names a position rather than an amount, so the line carries nothing to\n * multiply. The letters have to sit against the figure; a line that puts a\n * space between them wrote a number and then a word.\n */\nconst ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\\b/i;\n\n/**\n * A line that states its amount for one eater.\n *\n * The factor already says how many people the recipe is being made for, so\n * multiplying an amount that is per person applies it twice and asks for twice\n * as much on every plate.\n */\nconst PER_PERSON: Record<Language, RegExp> = {\n en: /\\bper\\s+(?:person|head|serving|guest|diner)\\b/i,\n fr: /\\bpar\\s+(?:personne|convive|part|t\\u00eate)\\b/i,\n};\n\n/**\n * Size and preparation words a recipe puts between the number and the measure.\n *\n * \"1 small handful\" counts handfuls and says how full one was. Reading the\n * adjective as the thing being counted loses the measure, and with it the fact\n * that a handful is held to no better than the hand: the line comes back as an\n * exact count of something the page never named.\n */\nconst MEASURE_ADJECTIVES: Record<Language, Set<string>> = {\n en: new Set([\n \"big\",\n \"generous\",\n \"good\",\n \"heaped\",\n \"heaping\",\n \"large\",\n \"level\",\n \"medium\",\n \"scant\",\n \"small\",\n ]),\n fr: new Set([\n \"beau\",\n \"belle\",\n \"bon\",\n \"bonne\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"petit\",\n \"petite\",\n ]),\n};\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(\n text: string,\n language: Language,\n): { adjective: string | null; rest: string } {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return { adjective: null, rest: text };\n }\n\n const [adjective = \"\"] = match.slice(1);\n const folded = normalizeUnitKey(adjective);\n // The word can be written in the plural where the count is, as in \"2 grosses\n // cuillères\", and the list carries the singular.\n const listed =\n MEASURE_ADJECTIVES[language].has(folded) ||\n MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, \"\"));\n if (!listed) {\n return { adjective: null, rest: text };\n }\n\n return { adjective, rest: text.slice(match[0].length) };\n}\n\n/**\n * A comma neither reading can account for.\n *\n * English groups thousands in threes and marks the decimal with a point;\n * French marks the decimal with a comma and never groups with one. A comma\n * followed by anything other than three digits is therefore not an English\n * number, and a second comma group is not a French one. Where the line gives no\n * sign which language it is in, \"1,500\" is fifteen hundred under one reading\n * and one and a half under the other, and choosing wrong is wrong by a factor\n * of a thousand. Neither is safe, so the line goes back as published and says\n * why.\n */\nconst COMMA_GROUPED = /^\\s*\\d{1,3}(?:,\\d{3})+(?!\\d)/;\nconst COMMA_DECIMAL = /^\\s*\\d+,\\d+/;\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2] ?? \"\"] ?? Number.NaN;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) {\n return null;\n }\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (language === \"fr\" ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);\n if (decimal) {\n const [written = \"\"] = decimal.slice(1);\n const amount = Number(\n language === \"fr\" ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n if (language === \"en\") {\n const written = parseWrittenFraction(trimmed);\n if (written) {\n return { amount: written.amount, length: offset + written.length };\n }\n }\n\n return null;\n}\n\n/** How many of the part a line names: \"two thirds\", \"a quarter\", \"half\". */\nconst WRITTEN_NUMERATORS: Record<string, number> = { a: 1, an: 1, one: 1, two: 2, three: 3 };\n\n/** What the part is a part of. */\nconst WRITTEN_DENOMINATORS: Record<string, number> = {\n half: 2,\n halves: 2,\n third: 3,\n thirds: 3,\n quarter: 4,\n quarters: 4,\n fourth: 4,\n fourths: 4,\n};\n\n/**\n * Read a fraction an English line spells out, as in \"half a bottle\" or \"two\n * thirds of a cup\".\n *\n * Recipes write the small fractions in words as readily as in figures, and a\n * line opening on one of them carries an amount like any other. The numerator\n * is optional because \"half\" alone is the common form.\n *\n * What follows decides whether the words are a quantity at all. A share of one\n * thing, \"half a lemon\" or \"two thirds of a cup\", states an amount of its own.\n * A share of a definite thing, \"half of the dough\", points back at an amount\n * stated elsewhere, and multiplying it would answer with a number that belongs\n * to another line.\n */\nfunction parseWrittenFraction(text: string): ParsedQuantity | null {\n const match = FRACTION_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[(match[2] ?? \"\").toLowerCase()];\n if (!(numerator && denominator)) {\n return null;\n }\n\n const rest = text.slice(match[0].length).replace(LEADING_OF, \"\").trimStart();\n if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, \"en\").unit)) {\n return null;\n }\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = language === \"fr\" ? FRENCH_RANGE_JOINER : ENGLISH_RANGE_JOINER;\n const separator = written.exec(after) ?? LEADING_DASH.exec(after);\n if (!separator) {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) {\n return null;\n }\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1] ?? \"\",\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\n/**\n * Why a line that shows a figure is still not the factor's to multiply.\n *\n * Each of these is a reading the parser can make and a scaling it must not do,\n * and they are kept apart from a line with no figure at all so the answer can\n * say which of the two it is looking at.\n */\nexport type HeldBack =\n /** \"4 to 5-pound roast\": the figures give the size of one, not how many. */\n | \"sizeQualifier\"\n /** \"1 dinde de 3 kg\": the measure behind the item weighs one of them. */\n | \"itemSize\"\n /** \"12 oz can tomatoes\": the measure gives what the tin holds, and no count is written. */\n | \"containerSize\"\n /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"2 h de repos\": the figure measures a length of time rather than an amount. */\n | \"duration\"\n /** \"1,500 g\" with nothing to say whether the comma groups or divides. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n /**\n * Why the figure on this line must not be multiplied, when there is such a\n * reason. Null for the ordinary line, whose amount is the factor's to scale.\n */\n heldBack: HeldBack | null;\n /**\n * The mark the page opened the line with, in front of the quantity, such as a\n * bullet or a picture of the food. It goes back where the page had it.\n */\n decoration: string | null;\n /**\n * The sign or word the page put before the amount to say it is loose, as in\n * the \"~\" of \"~1 cup water\". Null when the page stated the amount plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"small\"\n * of \"1 small handful\". It goes back in front of the measure so the answer\n * reads the way the page did.\n */\n measureAdjective: string | null;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets beside the amount, as\n * in \"450 g (1 pound)\", after a slash, as in \"500 g / 1.1 lb\", or in a\n * bracket closing the line, as in \"1 cup milk (240 ml)\". The rewrite puts\n * them back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | \"trailing\" | null;\n /**\n * The word the bracket opened on, as in the \"soit\" of \"(soit 3/4 de tasse)\".\n * Null when the line stated the equivalent without one.\n */\n alternateIntro: string | null;\n /**\n * A bracket giving what one container holds, as in \"1 (14 oz) can\", exactly\n * as the page wrote it. The count is the factor's to multiply and this figure\n * is not, so it goes back untouched.\n */\n capacity: string | null;\n /** What the amount and the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/**\n * Articles a French line writes where a digit would go, and what they count as.\n *\n * \"un\" and \"une\" are the number one written out, so a line using one has stated\n * a count. A word that says merely that there are several, \"quelques\" as much\n * as \"plusieurs\" or the English \"a few\", has stated none: any figure put behind\n * it is this server's reading rather than the page's quantity, and a reading\n * multiplied is a number nobody wrote.\n */\nconst FRENCH_ARTICLES: Record<string, number> = { un: 1, une: 1 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key, language);\n if (!unit) {\n continue;\n }\n const rest = afterKey(text, key);\n if (rest === null) {\n continue;\n }\n return { unit, rest };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(WHITESPACE);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) {\n return { unit: load, rest: words.slice(1).join(\" \") };\n }\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * What stands after the measure a key names, or null when the key does not line\n * up with the words the line is written in.\n *\n * How many words a key spells and how many words the line spends on it are\n * different numbers: normalising turns the abbreviation \"c.à.s\" into the three\n * words \"c a s\", and the line writes it as one. Consuming the key's own word\n * count would take two words of the ingredient with it, so the words the line\n * wrote are consumed one at a time until they normalise to the key.\n */\nfunction afterKey(text: string, key: string): string | null {\n const words = text.trim().split(WHITESPACE);\n for (let count = 1; count <= words.length; count += 1) {\n if (normalizeUnitKey(words.slice(0, count).join(\" \")) !== key) {\n continue;\n }\n return words.slice(count).join(\" \");\n }\n return null;\n}\n\n/**\n * A measure inside brackets can be stated in the other language's vocabulary: a\n * French page glosses grams in ounces, and an English page glosses cups in\n * millilitres. The line's own language is tried first, so a word both\n * vocabularies carry keeps the spelling and the plural of the line it sits in.\n */\nfunction takeUnitEitherLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) {\n return first;\n }\n return takeUnit(text, language === \"fr\" ? \"en\" : \"fr\");\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n return readEvidence(line).language;\n}\n\n/** The language a line reads as, with the weight each side gathered. */\nfunction readEvidence(line: string): LanguageEvidence {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(LEADING_QUANTITY, \"\").trimStart();\n const afterArticle = afterFigures.replace(VAGUE_ARTICLE, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit });\n}\n\n/**\n * The measure standing after the amount, and the size word in front of it.\n *\n * The adjective is only an adjective when a measure stands behind it. In\n * \"1 cleaned leek green\" the words that follow name the food itself, and taking\n * one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n rest: string,\n language: Language,\n fromArticle: boolean,\n): {\n leading: ReturnType<typeof takeLeadingUnit>;\n described: { adjective: string | null; rest: string };\n behind: ReturnType<typeof takeLeadingUnit> | null;\n} {\n const direct = takeLeadingUnit(rest, language, fromArticle);\n const described = direct.unit ? { adjective: null, rest } : takeMeasureAdjective(rest, language);\n const behind = described.adjective\n ? takeLeadingUnit(described.rest, language, fromArticle)\n : null;\n\n return { leading: leadingMeasure(direct, behind), described, behind };\n}\n\n/**\n * Whether the figure the line opens with counts anything, and what it is\n * instead when it does not.\n *\n * Three ways a figure is not a quantity: it is joined to a word by a hyphen and\n * describes one thing, it names a rank in an order, or it measures a length of\n * time, which belongs to the method rather than to the proportions. `false`\n * means the figure counts.\n */\nfunction whatTheFigureIsNot(\n behindFigure: string,\n couldBeARank: boolean,\n language: Language,\n): HeldBack | null | false {\n if (HYPHENATED_TAIL.test(behindFigure)) {\n return \"sizeQualifier\";\n }\n if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {\n return null;\n }\n if (TIME_UNIT[language].test(behindFigure.trimStart())) {\n return \"duration\";\n }\n return false;\n}\n\n/**\n * The equivalents a line writes beside its measure, in whichever of the three\n * shapes it wrote them.\n *\n * A bracketed group, a slash beside the amount, or a restatement trailing the\n * item: only one of them is read, because a line offering two would be stating\n * the same quantity three times.\n */\nfunction takeEquivalents(\n text: string,\n language: Language,\n): {\n rest: string;\n bracketed: ReturnType<typeof takeAlternates>;\n slashed: ReturnType<typeof takeSlashAlternates>;\n item: string;\n trailing: ReturnType<typeof takeTrailingAlternates>;\n group:\n | ReturnType<typeof takeTrailingAlternates>\n | Omit<ReturnType<typeof takeAlternates>, \"rest\">\n | null;\n} {\n const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text);\n const bracketed = takeAlternates(\n introducedBracket ? text.slice(introducedBracket[0].length) : text,\n language,\n );\n let rest = bracketed.measures.length > 0 ? bracketed.rest : text;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) {\n rest = slashed.rest;\n }\n\n let item = stripItemLead(rest, language);\n\n const trailing =\n bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;\n if (trailing) {\n item = trailing.item;\n }\n\n const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);\n // A measure inside a bracket beside a container gives what one of them holds,\n // as in \"1 (14 oz) can\". The recipe's proportion lives in how many tins are\n // opened, and a tin of twice the size is a tin no shop sells, so the figure\n // goes back exactly as the page wrote it.\n\n return { rest, item, bracketed, slashed, trailing, group };\n}\n\n/**\n * Whether the figures on the line give the size of something rather than how\n * many of it the recipe wants, and of what.\n *\n * Three ways a line says it. Without a measure, the words behind the count\n * carry the mass. With a partitive, \"une dinde de 3 kg\" writes the noun where a\n * measure stands and the partitive takes it for one — a noun the vocabulary\n * lists as a measure keeps counting, since a pot is a thing to buy more of,\n * while a noun read as a measure only for standing there names the food itself.\n * And \"12 oz can tomatoes\" qualifies the tin, leaving how many tins unwritten.\n */\nfunction whatTheFiguresSize(\n leading: { unit: UnitInfo | null; partitive: boolean },\n item: string,\n language: Language,\n): HeldBack | null {\n if (language === \"fr\" && !leading.unit && statesItemSize(item)) {\n return \"itemSize\";\n }\n\n // \"une dinde de 3 kg\" writes the noun where a measure stands, and the\n // partitive takes it for one. A noun the vocabulary lists as a measure keeps\n // counting, since a pot or a boîte is a thing to buy more of; a noun read as\n // a measure only for standing there names the food itself, and a mass behind\n // it is the size of one of them.\n if (language === \"fr\" && leading.partitive && isStatedSize(item)) {\n return \"itemSize\";\n }\n\n // \"12 oz can tomatoes\": the measure qualifies the tin, and how many tins the\n // recipe wants is not written anywhere on the line.\n if (leading.unit?.kind === \"measured\" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {\n return \"containerSize\";\n }\n\n return null;\n}\n\n/**\n * What the words behind the figure say the figure counts.\n *\n * \"two thirds of a cup\" names a share of one cup, and the measure stands behind\n * the preposition and the article that introduce it. \"2 dozen mushrooms\" counts\n * mushrooms, twelve to the dozen, so the multiplier is folded into the amount\n * and the line goes on to be read as the count of a thing it now is.\n */\nfunction whatTheCountIsOf(\n behindFigure: string,\n language: Language,\n): { rest: string; multiplier: ReturnType<typeof readCountMultiplier> } {\n let rest = behindFigure.trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") {\n rest = rest.replace(LEADING_ARTICLE, \"\");\n }\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n\n return { rest, multiplier };\n}\n\n/**\n * What a container holds, when the bracket beside it states a capacity rather\n * than an equivalent.\n *\n * \"2 boîtes (400 g)\" names two tins of that size, and the figure is the page's\n * own rather than the factor's, so it goes back between the count and the\n * container exactly as published. A bracket that restates the quantity in\n * another unit is a different thing and stays an equivalent.\n */\nfunction capacityOfTheContainer(\n group: { measures: ReadonlyArray<{ unit: UnitInfo | null }>; published: string } | null,\n measureName: string | undefined,\n item: string,\n): string | null {\n if (!group?.measures.every((measure) => measure.unit?.kind === \"measured\")) {\n return null;\n }\n if (!(namesContainer(measureName) || namesContainer(item))) {\n return null;\n }\n return group.published;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const marked = readable(line);\n const decoration = takeDecoration(marked);\n const text = decoration ? marked.slice(decoration.length).trimStart() : marked;\n const evidence = readEvidence(text);\n const language = choice === \"auto\" ? evidence.language : choice;\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n language,\n heldBack,\n decoration,\n approximation: null,\n measureAdjective: null,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n alternateIntro: null,\n capacity: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (unreadableComma(text, language, evidence)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX[language].exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated, language);\n const article = range ? null : readArticle(stated, language);\n const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;\n if (!quantity) {\n return empty(null);\n }\n\n // A figure joined to a word by a hyphen describes one thing rather than\n // counting things: \"4 to 5-pound roast\" is one roast that weighs that much.\n const notAnAmount = whatTheFigureIsNot(\n stated.slice(quantity.length),\n quantity !== article,\n language,\n );\n if (notAnAmount !== false) {\n return empty(notAnAmount);\n }\n\n const counted = whatTheCountIsOf(stated.slice(quantity.length), language);\n let rest = counted.rest;\n const multiplier = counted.multiplier;\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, language, fromArticle);\n const { leading, described, behind } = measure;\n rest = leading.rest;\n\n // A range gives two amounts, and a second member behind it would belong to\n // one of them without saying which.\n const compound = range ? null : takeCompoundMember(rest, leading.unit, language);\n if (compound) {\n rest = compound.rest;\n }\n const whole = quantity.amount + (compound?.adds ?? 0);\n\n // The partitive can stand between the measure and the bracket restating it,\n // as in \"150 g de (3/4 de tasse) de sucre\". It introduces the equivalent\n // rather than the food, so it is stepped over and the bracket read behind it.\n const equivalents = takeEquivalents(rest, language);\n const { slashed, trailing, group } = equivalents;\n rest = equivalents.rest;\n const item = equivalents.item;\n const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);\n\n // A counted thing whose size the line states: the number the line opens with\n // is one bird, and the measure behind it is what that bird weighs.\n const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);\n if (sizedRatherThanCounted !== null) {\n return empty(sizedRatherThanCounted);\n }\n\n const alternates = capacity === null ? (slashed?.measures ?? group?.measures ?? []) : [];\n\n return {\n original,\n language,\n heldBack: PER_PERSON[language].test(text) ? \"perPerson\" : null,\n decoration,\n approximation: loose ? loose[0] : null,\n measureAdjective: behind?.unit ? described.adjective : null,\n amount: whole * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates,\n alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),\n alternateIntro: capacity === null ? (group?.intro ?? null) : null,\n capacity,\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Whether the comma in a leading number is one this line cannot settle.\n *\n * A group of exactly three digits reads as thousands in English and as a\n * decimal in French, so it needs the line to say which language it is in; where\n * nothing does, both readings stand and they differ by a factor of a thousand.\n * A group of any other size is not an English number at all, and a second group\n * is not a French one.\n */\nfunction unreadableComma(text: string, language: Language, evidence: LanguageEvidence): boolean {\n if (COMMA_GROUPED.test(text)) {\n const settled = evidence.french !== evidence.english;\n return !settled;\n }\n if (language === \"en\") {\n return COMMA_DECIMAL.test(text);\n }\n return REPEATED_COMMA_GROUPS.test(text);\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (language === \"fr\") {\n const match = OPENING_ARTICLE_FRENCH.exec(text);\n if (!match) {\n return null;\n }\n if (!counts(text.slice(match[0].length))) {\n return null;\n }\n const [word = \"\"] = match.slice(1);\n const amount = FRENCH_ARTICLES[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n return { amount, length: match[0].length, word };\n }\n\n const article = LEADING_INDEFINITE_WITH_SPACE.exec(text);\n if (!article) {\n return null;\n }\n if (!counts(text.slice(article[0].length))) {\n return null;\n }\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];\n if (times === undefined) {\n return null;\n }\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string; partitive: boolean } {\n const taken = takeUnit(text, language);\n if (taken.unit) {\n return { ...taken, partitive: false };\n }\n\n if (language === \"fr\" && fromArticle) {\n const measure = readPartitiveMeasure(text);\n // A noun the vocabulary never listed, read as a measure for standing\n // between the article and the partitive.\n if (measure) {\n return { unit: measure.unit, rest: measure.rest, partitive: true };\n }\n }\n\n return { ...taken, partitive: false };\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): string {\n if (language === \"fr\") {\n return text.replace(LEADING_PARTITIVE, \"\").replace(LEADING_ARTICLE_FRENCH, \"\").trim();\n }\n return text.replace(OF_OPENING, \"\").replace(LEADING_INDEFINITE_WITH_SPACE, \"\").trim();\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): BracketedMeasures {\n const none: BracketedMeasures = { measures: [], intro: null, published: \"\", rest: text };\n if (!text.startsWith(\"(\")) {\n return none;\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return none;\n }\n\n const read = readBracket(text.slice(0, close + 1), language);\n if (!read) {\n return none;\n }\n return { ...read, rest: text.slice(close + 1).trimStart() };\n}\n\ninterface BracketedMeasures {\n measures: Measure[];\n /** What introduced the restatement, as the line wrote it. */\n intro: string | null;\n /** The bracket exactly as published, for a figure that must not be scaled. */\n published: string;\n rest: string;\n}\n\n/**\n * Words a page puts inside a bracket to announce that what follows restates the\n * quantity beside it rather than adding to it.\n */\nconst EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\\.?|approximately|or|ou|=)\\s*/i;\n\n/**\n * The partitive a line can put between a figure and the measure it names.\n *\n * \"3/4 de tasse\" and \"3/4 tasse\" are one quantity written two ways, and a\n * reader that only knows the second leaves the first sitting in prose beside an\n * amount that moved.\n */\nconst MEASURE_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+|of\\s+)/i;\n\n/** The measure a figure names, allowing for the partitive introducing it. */\nfunction takeMeasureAfterQuantity(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const direct = takeUnitEitherLanguage(text, language);\n if (direct.unit) {\n return direct;\n }\n\n const partitive = MEASURE_PARTITIVE.exec(text);\n if (!partitive) {\n return direct;\n }\n return takeUnitEitherLanguage(text.slice(partitive[0].length), language);\n}\n\n/**\n * Read a whole bracket as a group of equivalent measures, or nothing.\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", is prose, and scaling prose is not something a reader can check.\n */\nfunction readBracket(bracket: string, language: Language): Omit<BracketedMeasures, \"rest\"> | null {\n const inside = bracket.slice(1, -1);\n const intro = EQUIVALENT_INTRODUCER.exec(inside);\n const body = intro ? inside.slice(intro[0].length) : inside;\n // A slash separates two ways of stating the quantity, and it also writes a\n // fraction. One sitting between two digits belongs to the number, so\n // \"(3/4 de tasse)\" is one measure rather than a three and a four.\n const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) {\n return null;\n }\n\n const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") {\n return null;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: after.unit,\n });\n }\n\n if (measures.length === 0) {\n return null;\n }\n return { measures, intro: intro ? intro[0].trim() : null, published: bracket };\n}\n\n/**\n * Read a bracket a line closes on, as in \"150 g de sucre (soit 3/4 de tasse)\".\n *\n * A page states an equivalence wherever it reads best, and after the name of\n * the food is as common as beside the figure. Both say the same thing about the\n * same quantity, so both move with it: a doubled line whose closing bracket\n * still reads as published tells the cook that 300 g is three quarters of a\n * cup.\n */\nfunction takeTrailingAlternates(\n item: string,\n language: Language,\n): (Omit<BracketedMeasures, \"rest\"> & { item: string }) | null {\n const closing = BRACKETED_TAIL.exec(item);\n if (!closing) {\n return null;\n }\n\n const read = readBracket(closing[1] ?? \"\", language);\n if (!read) {\n return null;\n }\n\n const head = item.slice(0, closing.index).trim();\n // A bracket standing on its own is the quantity itself rather than a\n // restatement of one, and there would be nothing left for it to qualify.\n if (head === \"\") {\n return null;\n }\n\n return { ...read, item: head };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) {\n break;\n }\n\n const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) {\n break;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return language === \"fr\" ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) {\n return \"\";\n }\n if (Number.isInteger(amount)) {\n return String(amount);\n }\n if (options.fractions === false) {\n return decimal(amount);\n }\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: [number, string][] = [\n [0.25, \"1/4\"],\n [1 / 3, \"1/3\"],\n [0.5, \"1/2\"],\n [2 / 3, \"2/3\"],\n [0.75, \"3/4\"],\n ];\n for (const [value, label] of known) {\n if (Math.abs(rest - value) < 0.02) {\n return whole > 0 ? `${whole} ${label}` : label;\n }\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French and in English.\n *\n * The guiding rule is that a scaled quantity must be something a cook can act\n * on. Multiplying every number by the factor is arithmetically correct and\n * practically useless: it produces \"2,4 oeufs\" and \"0.67 pinch of salt\" with\n * the same confidence as \"267 g flour\". Each line is therefore classified by\n * what its measure allows, and the classification travels with the result so\n * the caller can see what was computed and what was left alone.\n *\n * Leaving a line alone is a decision of the same weight. A quantity a recipe\n * states loosely still holds a share of the dish, and a leavening agent left at\n * one pinch for twenty-five servings is a recipe that does not rise.\n *\n * The two languages are read by one set of rules. A list holding French lines\n * and English lines comes back scaled by the same arithmetic, with each line\n * written the way its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst CLOVE = /\\bcloves?\\b/i;\nconst CONSONANT_THEN_Y = /[^aeiou]y$/i;\nconst DOUBLE_S_ENDING = /ss$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst F_ENDING = /(?:[^f]f|fe)$/i;\nconst F_OR_FE_ENDING = /fe?$/i;\nconst GARLIC = /\\bgarlic\\b/i;\nconst IES_ENDING = /ies$/i;\nconst LETTER = /\\p{L}/u;\nconst LETTERS_ONLY = /^[A-Za-z]+$/;\nconst OF_JOINER = / of /i;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\nconst X_ENDING = /x$/i;\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n *\n * A whole number is already what a scale shows, so it is left where the\n * arithmetic put it: 1234 g doubled is 2468 g, and a step of five grams would\n * report 2470 for a product that needed no rounding at all.\n */\nfunction roundMeasured(value: number): number {\n if (Number.isInteger(value)) {\n return value;\n }\n if (value >= 100) {\n return roundTo(value, 5);\n }\n if (value >= 10) {\n return roundTo(value, 1);\n }\n if (value >= 1) {\n return roundTo(value, 0.1);\n }\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) {\n return { value: floor, clamped: true };\n }\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) {\n return { value: floor, clamped: true };\n }\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0] ?? floor;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n if (value < 1) {\n const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(\n (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION),\n );\n let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) {\n return false;\n }\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\n/**\n * Whether a bound came out as the exact product, judged in the finer of the two\n * units involved.\n *\n * An absolute tolerance only means what it says at one scale. Read in kilos, a\n * hundredth is ten grams, so a quantity moved from 2468 g to 2470 g passes as\n * exact for being 0.002 kg away; read in grams, the same gap is two grams and\n * the answer is that the value moved.\n */\nfunction boundLandedExactly(bound: ScaledBound): boolean {\n const fine = Math.max(bound.ratio, 1);\n return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n /** How many of the unit that came back fit in one of the unit the recipe wrote. */\n ratio: number;\n}\n\ninterface ScaledMeasure {\n /**\n * One bound for a single measure, two for a range, in that order. The shape\n * says so, because every reader takes the first without checking: a measure\n * that scaled to nothing is not a thing this can produce.\n */\n bounds: [ScaledBound, ...ScaledBound[]];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n /** What the page published at one end of the measure, and what it scales to. */\n interface End {\n published: number;\n raw: number;\n }\n\n const ends: [End, ...End[]] =\n high === null\n ? [{ published: low, raw: low * factor }]\n : [\n { published: low, raw: low * factor },\n { published: high, raw: high * factor },\n ];\n\n /** Read both ends, keeping the shape every caller of the bounds relies on. */\n const eachEnd = <T>(read: (end: End) => T): [T, ...T[]] => [\n read(ends[0]),\n ...ends.slice(1).map(read),\n ];\n\n const raws = ends.map((end) => end.raw);\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : low * factor;\n\n /** Both bounds share one unit, and each keeps the precision that unit affords. */\n const inUnit = (target: UnitInfo, ratio: number): ScaledMeasure => ({\n bounds: eachEnd(({ published, raw }) => {\n const exact = raw * ratio;\n // The rounding happens in the smaller of the two units, so moving to a\n // bigger one never throws away precision the page wrote: 1500 g rounded\n // as kilos is 2, and rounded as grams it is the 1.5 kg a scale shows.\n const rounded =\n ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);\n // At the bottom of a ladder, keep what precision is left rather than\n // deleting the ingredient.\n const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;\n // Rounding to a step of five grams above a hundred can round upwards, and\n // a recipe being made smaller must never come out asking for more than\n // the page published.\n const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n ratio,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) {\n return inUnit(stepped.unit, stepped.ratio);\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(raw, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, divisibility, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n}\n\n/**\n * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in either language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genievres?|genevriers?|badianes?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n \")\\\\b\",\n \"iu\",\n);\n\n/**\n * A juice, the one counted thing whose division stops at the half.\n *\n * Half the juice of a lemon is taken by squeezing half the fruit, which is a\n * step a recipe writes. A quarter of one has to be poured out and measured\n * back, and no recipe asks for that.\n *\n * It reads before the fruit, which a knife divides further on its own.\n */\nconst HALVED_ITEM = /\\b(?:jus|juices?)\\b/iu;\n\n/**\n * Things a kitchen takes one of or none of.\n *\n * An egg comes out of its shell whole, and so does the yolk or the white a\n * recipe asks for on its own: half of one means beating it and weighing the\n * result, which no recipe asks for, and there is no way to keep the other half.\n * A count of them therefore lands on a whole number, whichever side of the half\n * the arithmetic fell on.\n *\n * This list is short on purpose, and a word joins it only when half of the\n * thing is genuinely not something a cook can measure out. Anything that is\n * poured, weighed or cut halves, and takes the half.\n *\n * Two more belong here for reasons the criterion cannot reach on its own:\n *\n * - a clou de girofle is a dried flower bud, dropped into the pot and fished\n * back out of it. Nothing about it is measured, so there is no half of one to\n * take;\n * - a zeste is what comes off one fruit in one go. A line asking for the zeste\n * of a citron is asking for all of it, and a share of a zeste names no amount\n * a cook stops at.\n */\nconst WHOLE_ITEM =\n /\\b(?:eggs?|yolks?|egg\\s+whites?|oeufs?|œufs?|jaunes?\\s+d['e]|clous?|zestes?|zests?)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and the share of\n * one a recipe asks for is the half a knife makes of it. A clove on its own is\n * the dried flower bud, the clou de girofle, dropped into the pot and fished\n * back out of it: nothing about it is measured, so there is no share of one to\n * take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : CLOVE.test(item);\n if (!counted) {\n return null;\n }\n return GARLIC.test(item) ? \"half\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) {\n return null;\n }\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\n/**\n * Accents removed and the ligature spelled out, so \"échalote\" and \"echalote\"\n * hit one entry.\n *\n * A word boundary sits between an ASCII letter and a non-letter and nowhere\n * else, so a pattern opening on \"é\" never matches at the start of a word:\n * folding the item is what lets the lists below be written once, in plain\n * letters.\n */\nfunction foldItem(item: string): string {\n return item\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\u0153/g, \"oe\")\n .replace(/\\u0152/g, \"OE\");\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const key = foldItem(item);\n const clove = cloveDivisibility(unit, key);\n if (clove) {\n return clove;\n }\n if (unit && !countsBarePieces(unit)) {\n return unitDivisibility(unit);\n }\n const blanc = blancDivisibility(key);\n if (blanc) {\n return blanc;\n }\n if (WHOLE_ITEM.test(key)) {\n return \"whole\";\n }\n if (PORTION_SIZED_ITEM.test(key)) {\n return \"whole\";\n }\n if (HALVED_ITEM.test(key)) {\n return \"half\";\n }\n if (HALVED_CUT.test(key)) {\n return \"half\";\n }\n if (QUARTERED_MEASURE.test(key)) {\n return \"quarter\";\n }\n return QUARTERED_ITEM.test(key) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/**\n * Keep the capitalisation the line used while looking the word up in lower\n * case.\n *\n * A page writes its ingredients in whichever case it likes, and the shape of a\n * word is the page's rather than this server's: a list set in capitals reads as\n * a list somebody typed over when one line of it comes back in lower case.\n * Three shapes carry: all capitals, one leading capital, and everything else,\n * which keeps the replacement as the vocabulary spells it.\n */\nfunction matchCase(source: string, replacement: string): string {\n const hasLetter = LETTER.test(source);\n if (hasLetter && source === source.toUpperCase()) {\n return replacement.toUpperCase();\n }\n const first = source.slice(0, 1);\n if (\n first !== \"\" &&\n first === first.toUpperCase() &&\n source.slice(1) === source.slice(1).toLowerCase()\n ) {\n return (replacement[0] ?? \"\").toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (SIBILANT_ENDING.test(word)) {\n return `${word}es`;\n }\n if (CONSONANT_THEN_Y.test(word)) {\n return `${word.slice(0, -1)}ies`;\n }\n if (F_ENDING.test(word)) {\n return `${word.replace(F_OR_FE_ENDING, \"\")}ves`;\n }\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (IES_ENDING.test(word) && word.length > 4) {\n return `${word.slice(0, -3)}y`;\n }\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (SIBILANT_PLURAL.test(word)) {\n return word.slice(0, -2);\n }\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (DOUBLE_S_ENDING.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word)) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = OF_JOINER.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words.at(-1) ?? \"\";\n if (!LETTERS_ONLY.test(last) || last.length <= 2) {\n return item;\n }\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) {\n words[words.length - 1] = plural;\n } else if (!wantsPlural && isPlural) {\n words[words.length - 1] = singular;\n } else {\n return item;\n }\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\n/**\n * French nouns in -ou whose plural takes an -x.\n *\n * The ending settles nothing on its own: \"clou\" and \"trou\" take the ordinary\n * -s, and only these few take the -x. A cabbage is the one a recipe counts, and\n * the others are here because the list is the rule.\n */\nconst FRENCH_OU_PLURAL_IN_X = new Set([\n \"bijou\",\n \"caillou\",\n \"chou\",\n \"genou\",\n \"hibou\",\n \"joujou\",\n \"pou\",\n]);\n\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"mur\",\n \"mure\",\n \"vert\",\n \"verte\",\n \"rouge\",\n \"jaune\",\n \"noir\",\n \"noire\",\n \"blanc\",\n \"blanche\",\n \"rond\",\n \"ronde\",\n \"hache\",\n \"hachee\",\n \"coupe\",\n \"coupee\",\n \"rape\",\n \"rapee\",\n \"pele\",\n \"pelee\",\n \"epluche\",\n \"epluchee\",\n \"denoyaute\",\n \"denoyautee\",\n \"emince\",\n \"emincee\",\n]);\n\n/** Lowercase and strip accents, so \"entière\" and \"entiere\" hit the same entry. */\nfunction foldWord(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The trailing adjective agreed with the count, or null when it is left alone. */\nfunction agreeTrailingAdjective(word: string, wantsPlural: boolean): string | null {\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\");\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return null;\n }\n if (wantsPlural === isPlural) {\n return null;\n }\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * The head of a French noun phrase, put in the number its count asks for.\n *\n * The marks are not uniform: \"morceau\" and \"chou\" take -x where an ordinary\n * noun takes -s, \"bocal\" takes -aux, a word ending in -s, -x or -z takes no\n * mark at all, and \"ananas\" or \"couscous\" carry their -s in the singular. A\n * head already in the number wanted is left as written.\n */\nfunction agreeFrenchHead(\n head: string,\n number: { wantsPlural: boolean; isPlural: boolean; ouPlural: boolean },\n): string {\n const { wantsPlural, isPlural, ouPlural } = number;\n\n if (wantsPlural && !isPlural) {\n if (SIBILANT_LETTER.test(head)) {\n return head;\n }\n if (EAU_ENDING.test(head)) {\n return `${head}x`;\n }\n if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) {\n return `${head}x`;\n }\n if (AL_ENDING.test(head)) {\n return `${head.slice(0, -2)}aux`;\n }\n return `${head}s`;\n }\n\n if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n return head.slice(0, -1);\n }\n if (AUX_ENDING.test(head)) {\n return `${head.slice(0, -3)}al`;\n }\n if (ouPlural) {\n return head.slice(0, -1);\n }\n if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n return head;\n }\n return head.slice(0, -1);\n }\n\n return head;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) {\n return item;\n }\n\n const wantsPlural = amount >= 2;\n // A final -x marks the plural of the few nouns in -ou that take one, and it\n // belongs to the singular of everything else (\"prix\", \"houx\").\n const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));\n const isPlural = PLURAL_ENDING.test(head) || ouPlural;\n\n words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });\n\n const last = words.length - 1;\n const trailing = last > 0 ? words[last] : undefined;\n if (trailing !== undefined) {\n const adjective = agreeTrailingAdjective(trailing, wantsPlural);\n if (adjective) {\n words[last] = adjective;\n }\n }\n\n return words.join(\" \");\n}\n\n/**\n * Agree the adjective a line put in front of its measure, as in the \"grosse\" of\n * \"1 grosse pincée\".\n *\n * The word qualifies the measure, so in French it takes the number the measure\n * takes: \"2 grosses pincées\". English adjectives do not decline, and a French\n * word outside the declinable list stays as the recipe wrote it, for the same\n * reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number, language: Language): string {\n if (language === \"en\") {\n return word;\n }\n\n const wantsPlural = amount >= 2;\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\") && !AGREEABLE_ADJECTIVES.has(folded);\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return word;\n }\n if (wantsPlural === isPlural) {\n return word;\n }\n return wantsPlural ? `${singular}s` : singular;\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n return language === \"fr\" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * Agree the container a line counts, which is the word the item opens with.\n *\n * \"2 (14 oz) cans tomatoes\" counts tins and names what is in them, so the\n * number belongs to the tin. English otherwise agrees the last word before a\n * comma, which would put the plural mark on the tomatoes and leave the tin\n * reading as one.\n */\nfunction agreeCountedContainer(item: string, amount: number, language: Language): string {\n if (language === \"fr\") {\n return agreeInFrench(item, amount);\n }\n const space = item.indexOf(\" \");\n if (space < 0) {\n return agreeInEnglish(item, amount);\n }\n return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language): string {\n if (!item) {\n return \"\";\n }\n if (language === \"en\") {\n return ` ${item}`;\n }\n const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in either language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) {\n return null;\n }\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) {\n continue;\n }\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) {\n continue;\n }\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) {\n return passthroughIngredient(line, options.language);\n }\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) {\n return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\n }\n\n return scaleSingleLine(line, options);\n}\n\n/**\n * Scale a line that offers a choice, one branch at a time.\n *\n * A cook follows one branch and ignores the other, so both have to carry the\n * same share of the recipe: a doubled line whose second branch still reads as\n * published hands whoever takes it half the ingredient. The two branches name\n * different things, and how far one stands for the other is the page's claim\n * rather than arithmetic, so such a line is never reported as exact.\n */\nfunction scaleBranchedLine(line: string, branch: Branch, options: ScaleOptions): ScaledIngredient {\n const head = scaleSingleLine(branch.head, options);\n if (head.scaling === \"unscaled\") {\n return { ...head, text: line.trim(), original: line };\n }\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) {\n return { text: published, rewritten: false };\n }\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) {\n return { text: published, rewritten: false };\n }\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\n/**\n * The equivalents beside a measure, written the way the line wrote them.\n *\n * A trailing restatement is printed after the item rather than beside the\n * amount, so it carries no label here.\n */\nfunction alternateLabel(\n count: number,\n style: string | null,\n texts: readonly string[],\n bracketed: string,\n): string {\n if (count === 0) {\n return \"\";\n }\n if (style === \"slash\") {\n return ` / ${texts.join(\" / \")}`;\n }\n if (style === \"trailing\") {\n return \"\";\n }\n return bracketed;\n}\n\n/**\n * What follows the amount: the partitive French puts between a measure and what\n * it measures, or the counted item itself, which stands straight after its\n * number in both languages and agrees with it.\n */\nfunction itemLabelFor(\n named: UnitInfo | null,\n item: string,\n counted: string,\n language: Language,\n): string {\n if (named) {\n return joinItem(item, language);\n }\n if (counted) {\n return ` ${counted}`;\n }\n return \"\";\n}\n\n/** What the answer says about a line beyond the figure it came to. */\ninterface LineOutcome {\n parsed: ParsedIngredient;\n bounds: readonly ScaledBound[];\n unit: UnitInfo | null;\n low: ScaledBound;\n clamped: ScaledBound | null;\n movedPrimary: boolean;\n movedAlternate: boolean;\n restated: boolean;\n collapsed: boolean;\n language: Language;\n carriesMore: boolean;\n}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * The reasons stack: a line can be clamped, carry a second quantity, collapse\n * its range and state an approximation, and each has to be said without\n * cancelling the others.\n */\nfunction noteForScaledLine(outcome: LineOutcome): string | undefined {\n const {\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n } = outcome;\n let note: string | undefined;\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));\n sentences.push(\n moved\n .map(\n (bound) =>\n `Rounded ${bound.amount > bound.exact ? \"up\" : \"down\"} from ` +\n `${asPublished(bound.raw, parsed.unit)}.`,\n )\n .join(\" \"),\n );\n } else if (movedAlternate) {\n // The amount itself came out exact, and only the equivalent beside it had\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (carriesMore) {\n sentences.push(\n \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\",\n );\n }\n\n if (parsed.capacity) {\n sentences.push(\n `${parsed.capacity} is what one container holds, so it stays as published and the count is ` +\n \"what the factor moved. Scaling this line is a matter of opening that many containers of \" +\n \"the size named.\",\n );\n }\n\n if (collapsed) {\n sentences.push(\"The page gave a range, and at this size both ends come to the same amount.\");\n }\n\n // Below what any scale shows, the arithmetic is right and the kitchen cannot\n // follow it. Saying so is the difference between an answer and a number.\n if (unit?.kind === \"measured\" && low.amount > 0 && low.amount < 0.05) {\n sentences.push(\n \"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye.\",\n );\n }\n\n // The page put the amount forward as loose, and multiplying it keeps it that\n // way: the answer is as approximate as the figure it came from.\n if (parsed.approximation) {\n sentences.push(\n \"The page gave this amount as an approximation, and the scaled figure is no firmer.\",\n );\n }\n\n if (sentences.length > 0) {\n note = sentences.join(\" \");\n }\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n note = withApproximateNote(parsed.unit, note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\n}\n\n/**\n * The line as it reads once scaled: the amount, its measure, what the container\n * holds, the equivalents beside it, and the item they belong to.\n *\n * The size word the page put in front of its measure goes back in front of it,\n * and the equivalents go back the way the line offered them, since a slash and\n * a bracket are two different ways of restating one quantity.\n */\nfunction renderScaledLine(\n parsed: ParsedIngredient,\n alternates: ReadonlyArray<{ text: string }>,\n unit: UnitInfo | null,\n shown: number,\n language: Language,\n): {\n named: UnitInfo | null;\n unitLabel: string;\n capacityLabel: string;\n altLabel: string;\n trailingLabel: string;\n itemLabel: string;\n} {\n const named = unit && !countsBarePieces(unit) ? unit : null;\n // The size word the page put in front of its measure goes back in front of\n // it: the page asked for a grosse pincée, and a pincée is not the same ask.\n const adjective =\n named && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}`\n : \"\";\n const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : \"\";\n const alternateTexts = alternates.map((entry) => entry.text);\n const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : \"\";\n const bracketed = alternates.length === 0 ? \"\" : ` (${introduced}${alternateTexts.join(\" / \")})`;\n // Equivalents go back where the line offered them: inside brackets beside the\n // amount, after a slash, or in the bracket the line closes on.\n const altLabel = alternateLabel(\n alternates.length,\n parsed.alternateStyle,\n alternateTexts,\n bracketed,\n );\n const trailingLabel = parsed.alternateStyle === \"trailing\" ? bracketed : \"\";\n // What a container holds is the page's figure and not the factor's, so it\n // goes back between the count and the container, exactly as published.\n const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : \"\";\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = parsed.capacity\n ? agreeCountedContainer(parsed.item, shown, language)\n : agreeWithAmount(parsed.item, shown, language);\n const itemLabel = itemLabelFor(named, parsed.item, counted, language);\n\n return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };\n}\n\n/** Why a line showing a figure came back as the page published it. */\nconst HELD_BACK_NOTE: Record<HeldBack, string> = {\n sizeQualifier:\n \"The figures here give the size of one item rather than how many, so the line is \" +\n \"left as published.\",\n itemSize:\n \"The measure standing behind the item gives the size of one of them rather than how many, \" +\n \"so the line is left as published. Serving more people is a matter of taking a bigger one, \" +\n \"and serving fewer a smaller one.\",\n containerSize:\n \"The measure here gives what one container holds rather than how many of them the recipe \" +\n \"uses, and the line states no count, so it is left as published. Scaling it is a matter of \" +\n \"opening that many containers of the size named.\",\n perPerson:\n \"This line already states an amount for one person, and the factor is what changes \" +\n \"how many people the recipe serves, so the line is left as published.\",\n duration:\n \"This line states a length of time rather than an amount of an ingredient, so it is left as \" +\n \"published. A rest, a proof or a bake takes as long for a large batch as for a small one, \" +\n \"and the factor has nothing to say about it.\",\n ambiguousDecimal:\n \"The comma in this number marks thousands in one convention and the decimal point in \" +\n \"another, and the line gives no sign which was meant, so it is left as published.\",\n};\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n note: parsed.heldBack\n ? HELD_BACK_NOTE[parsed.heldBack]\n : \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((bound) => !boundLandedExactly(bound));\n const movedAlternate = alternateBounds.some((bound) => !boundLandedExactly(bound));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote\n // them, and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\n const carriesMore = hasEmbeddedMeasure(parsed.item, language);\n // A further quantity beside a mass or a volume restates or qualifies it, and\n // it did not move, so the line as a whole is not the exact product of what\n // the page published. Beside a counted container the same figure gives the\n // size of one of them, which is a thing the factor must leave alone: \"2\n // boîtes de 400 g\" is exactly twice \"1 boîte de 400 g\".\n const leftover = carriesMore && parsed.unit?.kind === \"measured\";\n\n const low = primaryBounds[0];\n const high = primaryBounds[1] ?? null;\n const unit = primary.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(\n parsed,\n alternates,\n unit,\n shown,\n language,\n );\n const result: ScaledIngredient = {\n text: `${parsed.decoration ? `${parsed.decoration} ` : \"\"}${parsed.approximation ?? \"\"}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated || leftover ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n };\n\n const note = noteForScaledLine({\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n });\n if (note !== undefined) {\n result.note = note;\n }\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0];\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) {\n return low;\n }\n return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n const parsed = parseIngredient(line, language);\n\n const held = parsed.amount === null || parsed.heldBack !== null;\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: held ? \"unscaled\" : \"scaled\",\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n language: parsed.language,\n };\n if (parsed.heldBack) {\n result.note = HELD_BACK_NOTE[parsed.heldBack];\n } else if (parsed.amount === null) {\n result.note = \"No quantity given; adjust to taste.\";\n } else if (parsed.unit?.kind === \"approximate\") {\n result.note = withApproximateNote(parsed.unit, undefined);\n }\n return result;\n}\n\n/** An ingredient list returned unchanged, for when no scaling was requested. */\nexport function passthroughIngredients(\n lines: string[],\n language: LanguageChoice = \"auto\",\n): ScaledIngredient[] {\n return lines.map((line) => passthroughIngredient(line, language));\n}\n"],"mappings":"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","names":["VOCABULARY","fraction","AL_ENDING","AUX_ENDING","EAUX_ENDING","EAU_ENDING","SIBILANT_LETTER","TRAILING_S_ANY_CASE","bounds"]}
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/equipment.ts","../../src/recipe/language.ts","../../src/recipe/units.ts","../../src/recipe/quantity.ts","../../src/recipe/scale.ts"],"sourcesContent":["/**\n * Whether a line names a tool.\n *\n * Some sites write what a recipe is cooked with inside the list they write the\n * ingredients in, so a mould, a tin or an air fryer arrives in the same list as\n * the chicken. Multiplying such a line asks for six air fryers, which is the\n * one wrong answer a scaler can give that a reader has no way to catch: the\n * arithmetic is right and the sentence is absurd.\n *\n * The reading is done on the head noun, which is what the line is about, after\n * the quantity and the article in front of it have been taken off. A word that\n * names a tool in one reading and a food in another is settled by what follows\n * it: \"papel de hornear\" is baking paper and \"papel de arroz\" is rice paper, so\n * a tool word is only taken as one when nothing behind it names the food.\n */\n\n/** Whatever stands before the noun the line is about. */\nconst LEADING_QUANTITY = /^\\s*(?:\\d+(?:[.,/]\\d+)?|[¼½¾⅓⅔⅛])\\s*/u;\nconst LEADING_ARTICLE = /^(?:un|una|unos|unas|une|des|del|de|the|an?|le|la|los|las)\\s+/iu;\nconst WORD_SPLIT = /[\\s,;:()]+/u;\n\n/**\n * Nouns that name a tool.\n *\n * One list for the three languages, because a word naming a tool in one of them\n * names nothing at all in the others, so nothing is gained by keeping them\n * apart and a line whose language was read one way still reaches the right\n * answer.\n *\n * A word this vocabulary already carries as a measure stays out. A \"cuillère\",\n * a \"cazo\" and a \"louche\" are what a recipe measures with, and reading either\n * as a tool would leave every spoonful in the list unscaled.\n */\nconst TOOL_NOUNS = new Set([\n // Spanish\n \"freidora\",\n \"airfryer\",\n \"batidora\",\n \"licuadora\",\n \"amasadora\",\n \"thermomix\",\n \"robot\",\n \"olla\",\n \"paellera\",\n \"cazuela\",\n \"sarten\",\n \"molde\",\n \"rejilla\",\n \"brocha\",\n \"pincel\",\n \"espatula\",\n \"varilla\",\n \"varillas\",\n \"colador\",\n \"escurridor\",\n \"tamiz\",\n \"mortero\",\n \"rodillo\",\n \"boquilla\",\n \"termometro\",\n \"temporizador\",\n \"microondas\",\n \"vaporera\",\n \"parrilla\",\n \"cuchillo\",\n \"pelador\",\n \"rallador\",\n \"exprimidor\",\n \"pinza\",\n \"pinzas\",\n \"cuenco\",\n \"recipiente\",\n \"batidor\",\n \"prensa\",\n \"palillo\",\n \"palillos\",\n \"brocheta\",\n \"brochetas\",\n // French\n \"poele\",\n \"cocotte\",\n \"sauteuse\",\n \"saladier\",\n \"fouet\",\n \"spatule\",\n \"passoire\",\n \"mixeur\",\n \"mandoline\",\n \"pinceau\",\n \"casserole\",\n // English\n \"skillet\",\n \"saucepan\",\n \"whisk\",\n \"spatula\",\n \"sieve\",\n \"colander\",\n \"grater\",\n \"blender\",\n \"processor\",\n \"ramekin\",\n \"thermometer\",\n \"peeler\",\n]);\n\n/**\n * Nouns that name a tool in one reading and food in another, with the words\n * that settle which.\n *\n * \"1 tin tomatoes\" is food and \"1 baking tin\" is the tin it bakes in; \"papel de\n * arroz\" is eaten and \"papel de hornear\" is not. Such a noun is read as a tool\n * only where one of its own words stands beside it, which is the word before it\n * in English and the word after it in French and Spanish.\n */\nconst AMBIGUOUS_TOOL_NOUNS: Record<string, RegExp> = {\n papel: /^(?:hornear|horno|aluminio|film|vegetal|absorbente|cocina|parchment|baking)$/,\n papier: /^(?:cuisson|sulfurise|aluminium|film)$/,\n vaso: /^(?:medidor|batidora|thermomix)$/,\n cuchara: /^(?:madera|silicona|helado)$/,\n manga: /^(?:pastelera)$/,\n tabla: /^(?:cortar|madera)$/,\n plancha: /^(?:electrica|hierro)$/,\n horno: /^(?:precalentado)$/,\n bol: /^(?:grande|mediano|pequeno|hondo|cristal|acero)$/,\n plato: /^(?:hondo|llano|grande)$/,\n bandeja: /^(?:horno|hornear|pastelera)$/,\n fuente: /^(?:horno|hornear|cristal)$/,\n film: /^(?:transparente|plastico|alimentario)$/,\n moule: /^(?:tarte|manque|cake|silicone|charniere)$/,\n plat: /^(?:four|gratin|cuisson)$/,\n plaque: /^(?:four|cuisson|patisserie)$/,\n planche: /^(?:decouper|bois)$/,\n tin: /^(?:baking|cake|loaf|roasting|flan|sandwich)$/,\n pan: /^(?:frying|baking|roasting|cake|loaf|sauce|griddle)$/,\n dish: /^(?:baking|roasting|ovenproof|gratin)$/,\n tray: /^(?:baking|roasting|oven)$/,\n sheet: /^(?:baking|greaseproof|parchment)$/,\n bowl: /^(?:mixing|large|small)$/,\n mould: /^(?:cake|jelly|silicone)$/,\n mold: /^(?:cake|jelly|silicone)$/,\n paper: /^(?:baking|greaseproof|parchment)$/,\n foil: /^(?:aluminium|aluminum|kitchen)$/,\n board: /^(?:chopping|cutting|wooden)$/,\n};\n\n/**\n * What follows a partitive after a tool noun, when the line is still about the\n * tool.\n *\n * French and Spanish measure by the container as readily as they name it: \"une\n * casserole de lait\" is an amount of milk, and \"1 pincel de silicona\" is the\n * brush. What stands after the partitive is what tells them apart, so a tool\n * noun followed by one takes a word from here or the line is read as an amount\n * of whatever follows. A size counts: a mould given in centimetres is a mould.\n */\nconst QUALIFIES_A_TOOL =\n /^(?:aire|hornear|horno|cocina|madera|silicona|silicone|barro|cristal|acero|hierro|aluminio|aluminium|plastico|papel|cortar|decouper|bois|varillas|mano|repostera|pasteleria|pastelera|tarte|manque|four|cuisson|gratin|patisserie|baking|frying|roasting|chopping|cutting|mixing|greaseproof|parchment|nonstick|cake|loaf|wooden|\\d+|cm|mm|l|ml)$/;\n\n/** The partitive a Romance line puts between a container and what it holds. */\nconst PARTITIVE = /^(?:de|del|du|des|d|a|au|à)$/i;\n\n/** Accents removed, so a list written in plain letters answers for every spelling. */\nfunction fold(word: string): string {\n return word.normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\").replace(/œ/g, \"oe\").toLowerCase();\n}\n\n/**\n * Whether a set holds the word, in the number the line wrote it in.\n *\n * The lists are written in the singular, and a recipe asks for two moulds as\n * readily as for one. Both plural marks the three languages use are undone,\n * \"-s\" and the \"-es\" Spanish writes after a consonant.\n */\nfunction holds(vocabulary: Set<string>, word: string): boolean {\n if (vocabulary.has(word)) {\n return true;\n }\n if (word.endsWith(\"es\") && vocabulary.has(word.slice(0, -2))) {\n return true;\n }\n return word.endsWith(\"s\") && vocabulary.has(word.slice(0, -1));\n}\n\n/**\n * Whether the line is about a tool.\n *\n * The noun the line is about stands at its head in French and Spanish and at\n * the end of the group that opens an English line, so both positions are read;\n * the second only counts where the word in front of it is one this vocabulary\n * knows, which keeps \"2 carottes râpées\" from being read as a grater.\n *\n * A tool noun followed by a partitive measures whatever comes after it unless\n * that word qualifies the tool: \"une casserole de lait\" is an amount of milk,\n * and \"1 pincel de silicona\" is the brush.\n */\nexport function isEquipmentLine(line: string): boolean {\n const head = line.replace(LEADING_QUANTITY, \"\").replace(LEADING_ARTICLE, \"\").trim();\n const words = head.split(WORD_SPLIT).filter((word) => word !== \"\");\n const first = words[0];\n if (first === undefined) {\n return false;\n }\n\n const folded = fold(first);\n if (holds(TOOL_NOUNS, folded)) {\n return notMeasuringSomethingElse(words);\n }\n\n const ambiguous = qualifierFor(folded);\n if (ambiguous) {\n return ambiguous.test(wordBeside(words, 0));\n }\n\n // English writes the qualifier first, so the noun of \"baking tin\" is the\n // second word. It is read only behind a word that qualifies a tool, which is\n // what keeps an ordinary line naming its preparation out of this.\n const second = words[1];\n if (second !== undefined && QUALIFIES_A_TOOL.test(folded)) {\n const behind = qualifierFor(fold(second));\n return behind ? behind.test(folded) : holds(TOOL_NOUNS, fold(second));\n }\n\n return false;\n}\n\n/** The qualifier pattern for an ambiguous noun, in the number the line wrote it. */\nfunction qualifierFor(word: string): RegExp | undefined {\n return (\n AMBIGUOUS_TOOL_NOUNS[word] ??\n (word.endsWith(\"es\") ? AMBIGUOUS_TOOL_NOUNS[word.slice(0, -2)] : undefined) ??\n (word.endsWith(\"s\") ? AMBIGUOUS_TOOL_NOUNS[word.slice(0, -1)] : undefined)\n );\n}\n\n/** The word at `index`, folded, or an empty string where the line ends. */\nfunction wordAt(words: string[], index: number): string {\n const word = words[index];\n return word === undefined ? \"\" : fold(word);\n}\n\n/**\n * The word that qualifies the noun at `index`, with the partitive stepped over.\n *\n * A Romance line writes the qualifier behind the partitive, \"papel de hornear\",\n * and an adjective without one, \"sartén antiadherente\". Both stand in the same\n * place once the partitive is passed.\n */\nfunction wordBeside(words: string[], index: number): string {\n const next = wordAt(words, index + 1);\n return PARTITIVE.test(next) ? wordAt(words, index + 2) : next;\n}\n\n/**\n * Whether the line names the tool itself rather than an amount measured by it.\n *\n * A partitive behind the noun opens onto what is being measured, so the word\n * after it has to qualify the tool for the line to still be about the tool.\n * Anything else, an adjective or nothing at all, leaves the line about the tool.\n */\nfunction notMeasuringSomethingElse(words: string[]): boolean {\n const after = words[1];\n if (after === undefined || !PARTITIVE.test(fold(after))) {\n return true;\n }\n return QUALIFIES_A_TOOL.test(wordAt(words, 2));\n}\n\n/** What the answer says about a line it left alone for naming a tool. */\nexport const EQUIPMENT_NOTE =\n \"This line names a tool, so it was left as published: \" +\n \"a recipe made for more people uses the same one.\";\n","/**\n * Which language an ingredient line is written in.\n *\n * A list can hold lines in more than one language 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\" | \"es\";\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 \"du\", \"aux\" and \"of\" say more about the line than the name of\n * the food does.\n *\n * Only the words one language alone writes stand here. French and Spanish\n * share \"de\", \"la\", \"un\" and \"en\", and counting a shared word for one of them\n * would read every Spanish line as French.\n */\nconst FRENCH_WORDS =\n /(?:^|[\\s(])(?:du|des|à|au|aux|le|les|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/**\n * Spanish is written with its own articles and its own past participles, and a\n * recipe line spends most of its words on the second: \"picado\", \"troceado\" and\n * \"rallado\" say how the ingredient arrives in the pan.\n */\nconst SPANISH_WORDS =\n /(?:^|[\\s(])(?:del|el|los|las|una|unos|unas|con|sin|para|al|y|según|aproximadamente|picad[oa]s?|tro?cead[oa]s?|rallad[oa]s?|molid[oa]s?|cortad[oa]s?|pelad[oa]s?|opcional)(?=[\\s),.]|$)/iu;\n\n/**\n * The words French and Spanish both write.\n *\n * They tell the two apart from English and say nothing between them, so they\n * count for both and cancel out, leaving whatever else the line carries to\n * decide.\n */\nconst ROMANCE_WORDS = /(?:^|[\\s(])(?:de|la|un|en)(?=[\\s),.]|$)/iu;\n\n/** Letters only French writes, which no Spanish or English word borrows. */\nconst FRENCH_LETTERS = /[àâçèêëîïôùûœ]/iu;\n\n/** Letters only Spanish writes. */\nconst SPANISH_LETTERS = /[áíóúñ¿¡]/iu;\n\n/** Letters both write, which place a line in neither. */\nconst ROMANCE_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 *\n * A name two of the languages spell alike is in none of these lists. Spanish\n * writes \"pasta\" as English does and \"miel\" as French does, and either would\n * settle a line for the wrong side.\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|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\nconst SPANISH_FOODS =\n /\\b(?:huevos?|sal|pimienta|azúcar|azucar|harina|mantequilla|leche|nata|aceite|ajos?|cebollas?|queso|pollo|ternera|cerdo|arroz|jamón|jamon|limones|limón|limon|manzanas?|patatas?|vino|agua|perejil|tomillo|yemas?|claras?)\\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 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 spanish: number;\n}\n\n/** Whether each vocabulary recognised a measure standing in the line. */\nexport interface UnitEvidence {\n frenchUnit: boolean;\n englishUnit: boolean;\n spanishUnit: boolean;\n}\n\nconst NO_UNIT: UnitEvidence = { frenchUnit: false, englishUnit: false, spanishUnit: false };\n\n/**\n * Read the language off one line.\n *\n * The unit flags say whether each vocabulary recognised a measure in the line.\n * They count for less than a structural word, because a French line can name an\n * English measure in passing: \"1 dose (cup) de Mountain Dew\" is French with an\n * English gloss inside it, and the \"de\" is what settles it.\n *\n * English is the answer when nothing points anywhere, which is what an\n * ingredient with no small words and no measure gets. Between French and\n * Spanish an equal score can only come from evidence the two share, and shared\n * evidence is written the same way in both, so it is read as French.\n */\nexport function readLanguage(line: string, evidence: UnitEvidence = NO_UNIT): LanguageEvidence {\n let french = 0;\n let english = 0;\n let spanish = 0;\n\n if (ROMANCE_WORDS.test(line)) {\n french += STRUCTURAL;\n spanish += STRUCTURAL;\n }\n if (ROMANCE_LETTERS.test(line)) {\n french += STRUCTURAL;\n spanish += STRUCTURAL;\n }\n if (FRENCH_WORDS.test(line)) {\n french += STRUCTURAL;\n }\n if (FRENCH_LETTERS.test(line)) {\n french += STRUCTURAL;\n }\n if (ENGLISH_WORDS.test(line)) {\n english += STRUCTURAL;\n }\n if (SPANISH_WORDS.test(line)) {\n spanish += STRUCTURAL;\n }\n if (SPANISH_LETTERS.test(line)) {\n spanish += STRUCTURAL;\n }\n if (evidence.frenchUnit) {\n french += VOCABULARY;\n }\n if (evidence.englishUnit) {\n english += VOCABULARY;\n }\n if (evidence.spanishUnit) {\n spanish += VOCABULARY;\n }\n if (FRENCH_FOODS.test(line)) {\n french += NAME;\n }\n if (ENGLISH_FOODS.test(line)) {\n english += NAME;\n }\n if (SPANISH_FOODS.test(line)) {\n spanish += NAME;\n }\n\n return { language: winner(french, english, spanish), french, english, spanish };\n}\n\nfunction winner(french: number, english: number, spanish: number): Language {\n if (french >= spanish && french > english) {\n return \"fr\";\n }\n if (spanish > english) {\n return \"es\";\n }\n return \"en\";\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 let spanish = 0;\n for (const line of lines) {\n const evidence = readLanguage(line);\n french += evidence.french;\n english += evidence.english;\n spanish += evidence.spanish;\n }\n return winner(french, english, spanish);\n}\n","/**\n * Cooking measures in French and in English, and what scaling means for each.\n *\n * What matters about a measure is how far its number divides before it stops\n * naming something a cook can produce.\n * Doubling \"200 g\" gives \"400 g\", to the tenth of a gram. Doubling \"1 pincée\"\n * gives \"2 pincées\", which is the whole of what a pinch can say: the count\n * carries the quantity, and the size of one pinch is the hand's business.\n */\n\nimport type { Language } from \"./language.js\";\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst FUL_MEASURE = /^[a-z]{3,}fuls?$/;\nconst HEAD_BEFORE_DE = /^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')/u;\nconst HOUSEHOLD_MEASURE =\n /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse|taza|cucharada|cucharadita|cda|cdta)$/;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_THEN_S = /[aiou]s$/i;\n/** Spanish nouns already carrying their -s in the singular: \"análisis\", \"virus\". */\nconst INVARIABLE_SPANISH = /(?:is|us)$/i;\nconst ES_ENDING = /(?:[^aeiouáéíóú]es)$/i;\nconst SPANISH_VOWEL_ENDING = /[aeiouáéíóú]$/i;\nconst Z_ENDING = /z$/i;\n\nexport type UnitKind =\n /** Mass or volume: scales continuously and cleanly. */\n | \"measured\"\n /** Spoons, cups, cloves, sachets: scales, but only to sensible fractions. */\n | \"portioned\"\n /**\n * Pinches, dashes, handfuls, pincées, poignées: a real amount, held to no\n * better than the hand that produces it. The count is multiplied and lands on\n * a whole one, and the line says the measure is approximate.\n */\n | \"approximate\";\n\nexport type UnitSystem = \"metric\" | \"imperial\" | \"none\";\n\nexport interface UnitInfo {\n /** Canonical singular form, used when rewriting the ingredient line. */\n canonical: string;\n kind: UnitKind;\n system: UnitSystem;\n /** Plural form when it is not simply the singular plus an \"s\". */\n plural?: string;\n /** A symbol such as \"g\" or \"ml\", which never takes a plural mark. */\n symbol?: true;\n}\n\n/**\n * Measures written the same way in every language: the metric symbols.\n *\n * \"g\", \"kg\", \"ml\" and \"l\" are read off a French, an English and a Spanish line\n * alike, and they take no plural mark in any of them.\n */\nconst SHARED_UNITS: Record<string, UnitInfo> = {\n mg: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n g: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gr: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kg: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n ml: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n cl: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n dl: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n l: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litre: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litres: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n};\n\n/**\n * Keys are matched lowercased and accent-stripped, so a single entry covers\n * \"cuillere\", \"cuillère\" and \"Cuillères\".\n */\nconst FRENCH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramme: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogrammes: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitre: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitres: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n // A page glossing a metric weight names the pound, which French writes as a\n // livre: \"450 g (1 livre) de spaghetti\".\n livre: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n livres: { canonical: \"livre\", kind: \"measured\", system: \"imperial\", plural: \"livres\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n \"cuillere a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuilleres a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a soupe\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"c a s\": {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n cas: {\n canonical: \"cuillère à soupe\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à soupe\",\n },\n \"cuillere a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"cuilleres a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a cafe\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n \"c a c\": {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n cac: {\n canonical: \"cuillère à café\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cuillères à café\",\n },\n verre: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n verres: { canonical: \"verre\", kind: \"portioned\", system: \"none\" },\n bol: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n bols: { canonical: \"bol\", kind: \"portioned\", system: \"none\" },\n tasse: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n tasses: { canonical: \"tasse\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n gousse: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n gousses: { canonical: \"gousse\", kind: \"portioned\", system: \"none\" },\n tranche: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n tranches: { canonical: \"tranche\", kind: \"portioned\", system: \"none\" },\n botte: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n bottes: { canonical: \"botte\", kind: \"portioned\", system: \"none\" },\n boite: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n boites: { canonical: \"boîte\", kind: \"portioned\", system: \"none\", plural: \"boîtes\" },\n pot: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n pots: { canonical: \"pot\", kind: \"portioned\", system: \"none\" },\n brique: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n briques: { canonical: \"brique\", kind: \"portioned\", system: \"none\" },\n feuille: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n feuilles: { canonical: \"feuille\", kind: \"portioned\", system: \"none\" },\n branche: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n branches: { canonical: \"branche\", kind: \"portioned\", system: \"none\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readPartitiveMeasure` explains what else lands here.\n bouchon: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n bouchons: { canonical: \"bouchon\", kind: \"approximate\", system: \"none\" },\n larme: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n larmes: { canonical: \"larme\", kind: \"approximate\", system: \"none\" },\n doigt: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n doigts: { canonical: \"doigt\", kind: \"approximate\", system: \"none\" },\n nuage: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n nuages: { canonical: \"nuage\", kind: \"approximate\", system: \"none\" },\n louche: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n louches: { canonical: \"louche\", kind: \"approximate\", system: \"none\" },\n lichette: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n lichettes: { canonical: \"lichette\", kind: \"approximate\", system: \"none\" },\n pointe: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pointes: { canonical: \"pointe\", kind: \"approximate\", system: \"none\" },\n pincee: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n pincees: { canonical: \"pincée\", kind: \"approximate\", system: \"none\", plural: \"pincées\" },\n trait: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n traits: { canonical: \"trait\", kind: \"approximate\", system: \"none\" },\n filet: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n filets: { canonical: \"filet\", kind: \"approximate\", system: \"none\" },\n goutte: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n gouttes: { canonical: \"goutte\", kind: \"approximate\", system: \"none\", plural: \"gouttes\" },\n poignee: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n poignees: { canonical: \"poignée\", kind: \"approximate\", system: \"none\", plural: \"poignées\" },\n // \"noix\" carries its own plural mark already.\n noix: { canonical: \"noix\", kind: \"approximate\", system: \"none\", plural: \"noix\" },\n soupcon: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n soupcons: { canonical: \"soupçon\", kind: \"approximate\", system: \"none\", plural: \"soupçons\" },\n};\n\n/**\n * Keys are matched lowercased with dots dropped, so a single entry covers\n * \"Tbsp\", \"tbsp.\" and \"TBSP\".\n */\nconst ENGLISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n milligram: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n milligrams: { canonical: \"mg\", kind: \"measured\", system: \"metric\", symbol: true },\n gram: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grams: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramme: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n grammes: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogram: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilograms: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliter: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n milliliters: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitre: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n millilitres: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n liter: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n liters: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n\n // Imperial mass\n oz: { canonical: \"oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n ounce: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n ounces: { canonical: \"ounce\", kind: \"measured\", system: \"imperial\", plural: \"ounces\" },\n lb: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n lbs: { canonical: \"lb\", kind: \"measured\", system: \"imperial\", symbol: true },\n pound: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n pounds: { canonical: \"pound\", kind: \"measured\", system: \"imperial\", plural: \"pounds\" },\n\n // Imperial volume\n \"fl oz\": { canonical: \"fl oz\", kind: \"measured\", system: \"imperial\", symbol: true },\n \"fluid ounce\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n \"fluid ounces\": {\n canonical: \"fluid ounce\",\n kind: \"measured\",\n system: \"imperial\",\n plural: \"fluid ounces\",\n },\n pint: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n pints: { canonical: \"pint\", kind: \"measured\", system: \"imperial\", plural: \"pints\" },\n quart: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n quarts: { canonical: \"quart\", kind: \"measured\", system: \"imperial\", plural: \"quarts\" },\n gallon: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n gallons: { canonical: \"gallon\", kind: \"measured\", system: \"imperial\", plural: \"gallons\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n tsp: { canonical: \"tsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n teaspoon: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n teaspoons: { canonical: \"teaspoon\", kind: \"portioned\", system: \"imperial\", plural: \"teaspoons\" },\n tbsp: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tbs: { canonical: \"Tbsp\", kind: \"portioned\", system: \"imperial\", symbol: true },\n tablespoon: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n tablespoons: {\n canonical: \"tablespoon\",\n kind: \"portioned\",\n system: \"imperial\",\n plural: \"tablespoons\",\n },\n cup: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n cups: { canonical: \"cup\", kind: \"portioned\", system: \"imperial\", plural: \"cups\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n can: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n cans: { canonical: \"can\", kind: \"portioned\", system: \"none\" },\n jar: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n jars: { canonical: \"jar\", kind: \"portioned\", system: \"none\" },\n packet: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n packets: { canonical: \"packet\", kind: \"portioned\", system: \"none\" },\n package: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n packages: { canonical: \"package\", kind: \"portioned\", system: \"none\" },\n sachet: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n sachets: { canonical: \"sachet\", kind: \"portioned\", system: \"none\" },\n clove: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n cloves: { canonical: \"clove\", kind: \"portioned\", system: \"none\" },\n slice: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n slices: { canonical: \"slice\", kind: \"portioned\", system: \"none\" },\n stick: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n sticks: { canonical: \"stick\", kind: \"portioned\", system: \"none\" },\n stalk: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n stalks: { canonical: \"stalk\", kind: \"portioned\", system: \"none\" },\n sprig: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n sprigs: { canonical: \"sprig\", kind: \"portioned\", system: \"none\" },\n bunch: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n bunches: { canonical: \"bunch\", kind: \"portioned\", system: \"none\", plural: \"bunches\" },\n head: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n heads: { canonical: \"head\", kind: \"portioned\", system: \"none\" },\n sheet: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n sheets: { canonical: \"sheet\", kind: \"portioned\", system: \"none\" },\n leaf: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n leaves: { canonical: \"leaf\", kind: \"portioned\", system: \"none\", plural: \"leaves\" },\n ea: { canonical: \"ea\", kind: \"portioned\", system: \"none\", symbol: true },\n // A dish of the book standing as an ingredient of another: \"1 recipe Flaky\n // Pie Crust\". The count is what the word measures, so the plural mark belongs\n // on it and not on the name of the dish.\n recipe: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n recipes: { canonical: \"recipe\", kind: \"portioned\", system: \"none\", plural: \"recipes\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n // `readContainerLoad` explains what else lands here.\n capful: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n capfuls: { canonical: \"capful\", kind: \"approximate\", system: \"none\", plural: \"capfuls\" },\n glug: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n glugs: { canonical: \"glug\", kind: \"approximate\", system: \"none\" },\n dollop: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n dollops: { canonical: \"dollop\", kind: \"approximate\", system: \"none\" },\n squeeze: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n squeezes: { canonical: \"squeeze\", kind: \"approximate\", system: \"none\" },\n sprinkle: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkles: { canonical: \"sprinkle\", kind: \"approximate\", system: \"none\" },\n sprinkling: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n sprinklings: { canonical: \"sprinkling\", kind: \"approximate\", system: \"none\" },\n grating: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n gratings: { canonical: \"grating\", kind: \"approximate\", system: \"none\" },\n twist: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n twists: { canonical: \"twist\", kind: \"approximate\", system: \"none\" },\n smidgen: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n smidgens: { canonical: \"smidgen\", kind: \"approximate\", system: \"none\" },\n pinch: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n pinches: { canonical: \"pinch\", kind: \"approximate\", system: \"none\", plural: \"pinches\" },\n dash: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n dashes: { canonical: \"dash\", kind: \"approximate\", system: \"none\", plural: \"dashes\" },\n splash: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n splashes: { canonical: \"splash\", kind: \"approximate\", system: \"none\", plural: \"splashes\" },\n drizzle: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n drizzles: { canonical: \"drizzle\", kind: \"approximate\", system: \"none\" },\n handful: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n handfuls: { canonical: \"handful\", kind: \"approximate\", system: \"none\", plural: \"handfuls\" },\n drop: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n drops: { canonical: \"drop\", kind: \"approximate\", system: \"none\" },\n knob: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n knobs: { canonical: \"knob\", kind: \"approximate\", system: \"none\" },\n};\n\n/**\n * Spanish measures.\n *\n * The spoon vocabulary is where a Spanish list differs most from the other\n * two: \"cucharada\" and \"cucharadita\" are the tablespoon and the teaspoon, and\n * a page writes them as \"cda\" and \"cdta\" as readily as in full. The countable\n * packagings, \"lata\", \"sobre\", \"loncha\", carry the divisibility a cook can\n * follow rather than a plain multiplication.\n */\nconst SPANISH_UNITS: Record<string, UnitInfo> = {\n ...SHARED_UNITS,\n\n gramo: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n gramos: { canonical: \"g\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramo: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n kilogramos: { canonical: \"kg\", kind: \"measured\", system: \"metric\", symbol: true },\n litro: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n litros: { canonical: \"l\", kind: \"measured\", system: \"metric\", symbol: true },\n mililitro: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n mililitros: { canonical: \"ml\", kind: \"measured\", system: \"metric\", symbol: true },\n decilitro: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n decilitros: { canonical: \"dl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitro: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n centilitros: { canonical: \"cl\", kind: \"measured\", system: \"metric\", symbol: true },\n // A page glossing a metric weight names the pound and the ounce, which\n // Spanish writes in full.\n libra: { canonical: \"libra\", kind: \"measured\", system: \"imperial\", plural: \"libras\" },\n libras: { canonical: \"libra\", kind: \"measured\", system: \"imperial\", plural: \"libras\" },\n onza: { canonical: \"onza\", kind: \"measured\", system: \"imperial\", plural: \"onzas\" },\n onzas: { canonical: \"onza\", kind: \"measured\", system: \"imperial\", plural: \"onzas\" },\n\n // Spoons and cups: real measures, but only in sensible fractions.\n cucharada: { canonical: \"cucharada\", kind: \"portioned\", system: \"none\", plural: \"cucharadas\" },\n cucharadas: { canonical: \"cucharada\", kind: \"portioned\", system: \"none\", plural: \"cucharadas\" },\n cda: { canonical: \"cda\", kind: \"portioned\", system: \"none\", symbol: true },\n cdas: { canonical: \"cda\", kind: \"portioned\", system: \"none\", symbol: true },\n \"c s\": { canonical: \"cucharada\", kind: \"portioned\", system: \"none\", plural: \"cucharadas\" },\n cucharadita: {\n canonical: \"cucharadita\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cucharaditas\",\n },\n cucharaditas: {\n canonical: \"cucharadita\",\n kind: \"portioned\",\n system: \"none\",\n plural: \"cucharaditas\",\n },\n cdta: { canonical: \"cdta\", kind: \"portioned\", system: \"none\", symbol: true },\n cdtas: { canonical: \"cdta\", kind: \"portioned\", system: \"none\", symbol: true },\n \"c p\": { canonical: \"cucharadita\", kind: \"portioned\", system: \"none\", plural: \"cucharaditas\" },\n taza: { canonical: \"taza\", kind: \"portioned\", system: \"none\", plural: \"tazas\" },\n tazas: { canonical: \"taza\", kind: \"portioned\", system: \"none\", plural: \"tazas\" },\n vaso: { canonical: \"vaso\", kind: \"portioned\", system: \"none\" },\n vasos: { canonical: \"vaso\", kind: \"portioned\", system: \"none\" },\n\n // Packaging and natural units: countable, so they round to whole things.\n sobre: { canonical: \"sobre\", kind: \"portioned\", system: \"none\" },\n sobres: { canonical: \"sobre\", kind: \"portioned\", system: \"none\" },\n diente: { canonical: \"diente\", kind: \"portioned\", system: \"none\" },\n dientes: { canonical: \"diente\", kind: \"portioned\", system: \"none\" },\n lata: { canonical: \"lata\", kind: \"portioned\", system: \"none\", plural: \"latas\" },\n latas: { canonical: \"lata\", kind: \"portioned\", system: \"none\", plural: \"latas\" },\n bote: { canonical: \"bote\", kind: \"portioned\", system: \"none\" },\n botes: { canonical: \"bote\", kind: \"portioned\", system: \"none\" },\n tarro: { canonical: \"tarro\", kind: \"portioned\", system: \"none\" },\n tarros: { canonical: \"tarro\", kind: \"portioned\", system: \"none\" },\n paquete: { canonical: \"paquete\", kind: \"portioned\", system: \"none\" },\n paquetes: { canonical: \"paquete\", kind: \"portioned\", system: \"none\" },\n loncha: { canonical: \"loncha\", kind: \"portioned\", system: \"none\", plural: \"lonchas\" },\n lonchas: { canonical: \"loncha\", kind: \"portioned\", system: \"none\", plural: \"lonchas\" },\n rebanada: { canonical: \"rebanada\", kind: \"portioned\", system: \"none\", plural: \"rebanadas\" },\n rebanadas: { canonical: \"rebanada\", kind: \"portioned\", system: \"none\", plural: \"rebanadas\" },\n rodaja: { canonical: \"rodaja\", kind: \"portioned\", system: \"none\", plural: \"rodajas\" },\n rodajas: { canonical: \"rodaja\", kind: \"portioned\", system: \"none\", plural: \"rodajas\" },\n hoja: { canonical: \"hoja\", kind: \"portioned\", system: \"none\", plural: \"hojas\" },\n hojas: { canonical: \"hoja\", kind: \"portioned\", system: \"none\", plural: \"hojas\" },\n rama: { canonical: \"rama\", kind: \"portioned\", system: \"none\", plural: \"ramas\" },\n ramas: { canonical: \"rama\", kind: \"portioned\", system: \"none\", plural: \"ramas\" },\n ramita: { canonical: \"ramita\", kind: \"portioned\", system: \"none\", plural: \"ramitas\" },\n ramitas: { canonical: \"ramita\", kind: \"portioned\", system: \"none\", plural: \"ramitas\" },\n manojo: { canonical: \"manojo\", kind: \"portioned\", system: \"none\" },\n manojos: { canonical: \"manojo\", kind: \"portioned\", system: \"none\" },\n unidad: { canonical: \"unidad\", kind: \"portioned\", system: \"none\", plural: \"unidades\" },\n unidades: { canonical: \"unidad\", kind: \"portioned\", system: \"none\", plural: \"unidades\" },\n\n // Held to no better than a hand: the count scales, the size of one does not.\n pizca: { canonical: \"pizca\", kind: \"approximate\", system: \"none\", plural: \"pizcas\" },\n pizcas: { canonical: \"pizca\", kind: \"approximate\", system: \"none\", plural: \"pizcas\" },\n punado: { canonical: \"puñado\", kind: \"approximate\", system: \"none\", plural: \"puñados\" },\n punados: { canonical: \"puñado\", kind: \"approximate\", system: \"none\", plural: \"puñados\" },\n chorro: { canonical: \"chorro\", kind: \"approximate\", system: \"none\" },\n chorros: { canonical: \"chorro\", kind: \"approximate\", system: \"none\" },\n chorrito: { canonical: \"chorrito\", kind: \"approximate\", system: \"none\" },\n chorritos: { canonical: \"chorrito\", kind: \"approximate\", system: \"none\" },\n gota: { canonical: \"gota\", kind: \"approximate\", system: \"none\", plural: \"gotas\" },\n gotas: { canonical: \"gota\", kind: \"approximate\", system: \"none\", plural: \"gotas\" },\n cazo: { canonical: \"cazo\", kind: \"approximate\", system: \"none\" },\n cazos: { canonical: \"cazo\", kind: \"approximate\", system: \"none\" },\n cucharon: { canonical: \"cucharón\", kind: \"approximate\", system: \"none\", plural: \"cucharones\" },\n cucharones: { canonical: \"cucharón\", kind: \"approximate\", system: \"none\", plural: \"cucharones\" },\n toque: { canonical: \"toque\", kind: \"approximate\", system: \"none\" },\n toques: { canonical: \"toque\", kind: \"approximate\", system: \"none\" },\n};\n\nconst VOCABULARY: Record<Language, Record<string, UnitInfo>> = {\n fr: FRENCH_UNITS,\n en: ENGLISH_UNITS,\n es: SPANISH_UNITS,\n};\n\n/**\n * Lowercase, strip accents and drop abbreviation dots, so a lookup survives the\n * spellings a recipe actually uses. Recipes write \"c. à soupe\" and \"Tbsp.\" as\n * readily as the full words, and an unrecognised measure is worse than a wrong\n * one: the amount falls through to the countable branch and gets rounded as\n * though a spoonful were an indivisible object.\n */\nexport function normalizeUnitKey(text: string): string {\n return (\n text\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/\\./g, \" \")\n // A recipe that does not know how many it will be writes the plural mark in\n // brackets: \"4 cuillère(s) à soupe\". The measure is the word without it.\n .replace(/\\((?:s|x|es)\\)/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n}\n\n/** Longest keys first, so \"cuillère à soupe\" wins over \"cuillère\". */\nfunction orderedKeys(vocabulary: Record<string, UnitInfo>): string[] {\n return Object.keys(vocabulary).sort(\n (a, b) => b.split(\" \").length - a.split(\" \").length || b.length - a.length,\n );\n}\n\nconst UNIT_KEYS: Record<Language, string[]> = {\n fr: orderedKeys(FRENCH_UNITS),\n en: orderedKeys(ENGLISH_UNITS),\n es: orderedKeys(SPANISH_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 es: buildEmbedded(\"es\"),\n};\n\nfunction buildEmbedded(language: Language): RegExp {\n const keys = UNIT_KEYS[language].map((key) => key.replace(/ /g, \"\\\\s+\"));\n // A quantity keeps the same shape whether the measure stands against the\n // figure or behind the word introducing it: \"3/4 tasse\" and \"3/4 de tasse\"\n // are one quantity written twice. Reading only the first spelling leaves the\n // second sitting on a scaled line, still saying what the page said.\n const joiner = \"(?:(?:de|du|des|d|of|a|an|the)\\\\s+)?\";\n // The digits and the whitespace are kept in separate pieces that cannot both\n // match a space. Letting them overlap makes the engine try every way of\n // splitting a run of spaces between them, which turns a long line into\n // seconds of work for an answer that was always going to be no.\n return new RegExp(`\\\\d[\\\\d.,/]*\\\\s*${joiner}(?:${keys.join(\"|\")})\\\\b`, \"i\");\n}\n\n/**\n * Nouns naming what a food is sold in.\n *\n * A measure written in front of one of them gives what that container holds:\n * \"12 oz can\" is the size of the tin rather than an amount to weigh out, and\n * doubling the recipe means opening a second tin rather than finding a bigger\n * one.\n *\n * Written in every language, because what a word names has nothing to do with\n * which language names it.\n */\nexport const CONTAINER_NOUN =\n /^(?:cans?|tins?|jars?|packets?|packages?|packs?|box|boxes|bottles?|cartons?|bags?|tubs?|sachets?|blocks?|blocs?|boites?|pots?|briques?|bocaux|bocals?|barquettes?)\\b/i;\n\n/**\n * The text is normalized first, because the vocabulary is keyed without accents\n * and a French line writes \"cuillère à soupe\" with them.\n */\nexport function hasEmbeddedMeasure(text: string, language: Language): boolean {\n return EMBEDDED[language].test(normalizeUnitKey(text));\n}\n\n/**\n * Words that stand where a measure would and name no container: \"un peu de\n * sel\" states that there is some salt, and multiplying it says nothing.\n */\nconst NOT_A_MEASURE = new Set([\n \"peu\",\n \"beaucoup\",\n \"plus\",\n \"moins\",\n \"assez\",\n \"trop\",\n \"autant\",\n \"tant\",\n \"moitie\",\n \"quart\",\n \"tiers\",\n \"reste\",\n \"melange\",\n \"ensemble\",\n]);\n\n/**\n * Read a measure named with a container or a gesture the vocabulary has no\n * 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 chorrito, a ramequin hold what they hold, and the\n * recipe's proportion lives in how many are asked for. French and Spanish both\n * mark that grammatically, by placing the noun between the amount and the\n * partitive that introduces the thing measured: \"un bouchon de rhum\",\n * \"2 bouquets de persil\", \"un chorrito de aceite\". A noun in that position\n * measures whatever follows it, so a container nobody thought to list is read\n * by the same rule as the ones that are, and the vocabulary only has to carry\n * 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(\n text: string,\n language: Language,\n): { unit: UnitInfo; rest: string } | null {\n const match = HEAD_BEFORE_DE.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n if (word.length < 3) {\n return null;\n }\n if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {\n return null;\n }\n if (lookupUnit(word, language)) {\n return null;\n }\n\n const canonical = language === \"es\" ? spanishSingular(word) : frenchSingular(word);\n return {\n unit: {\n canonical,\n kind: \"approximate\",\n system: \"none\",\n plural: language === \"es\" ? spanishPlural(canonical) : frenchPlural(canonical),\n },\n rest: text.slice(match[0].length),\n };\n}\n\n/**\n * Read an English measure named after the thing that holds it: a capful, a\n * spoonful, a jarful.\n *\n * What makes a measure approximate is that its size belongs to whoever pours\n * it rather than to a standard, and English marks that in the word itself. The\n * suffix -ful means \"as much as one of these holds\", and what a cap, a bowl or\n * a handful holds is whatever the one in the kitchen holds. Any noun built that\n * way is therefore a measure of the same family as a pinch, which is why the\n * suffix is read as a rule: a container nobody thought to list is understood\n * the first time it appears, and the vocabulary only has to carry the gestures\n * whose name says nothing about their size either.\n */\nexport function readContainerLoad(word: string): UnitInfo | null {\n const key = normalizeUnitKey(word);\n // At least three letters name the container, which keeps \"awful\" out of the\n // kitchen.\n if (!FUL_MEASURE.test(key)) {\n return null;\n }\n\n const canonical = key.endsWith(\"fuls\") ? key.slice(0, -1) : key;\n return { canonical, kind: \"approximate\", system: \"none\", plural: `${canonical}s` };\n}\n\n/**\n * The singular of a French noun a line wrote in the plural, so the rewrite can\n * put it back in either number.\n *\n * \"ananas\", \"jus\" and \"anis\" carry their -s in the singular, and \"morceaux\"\n * comes from \"morceau\", so the ending decides rather than the last letter\n * alone.\n */\nfunction frenchSingular(word: string): string {\n if (EAUX_ENDING.test(word)) {\n return word.slice(0, -1);\n }\n if (AUX_ENDING.test(word)) {\n return `${word.slice(0, -3)}al`;\n }\n if (VOWEL_THEN_S.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/**\n * The singular of a Spanish noun a line wrote in the plural.\n *\n * Spanish marks the plural with \"-s\" after a vowel and \"-es\" after a consonant,\n * so both endings are undone. A noun already carrying its \"-s\" in the singular,\n * such as \"análisis\", ends in \"-is\" or \"-us\" and is left alone.\n */\nexport function spanishSingular(word: string): string {\n if (INVARIABLE_SPANISH.test(word)) {\n return word;\n }\n if (ES_ENDING.test(word) && word.length > 4) {\n return word.slice(0, -2);\n }\n if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/** The plural Spanish writes for a noun: \"-s\" after a vowel, \"-es\" after a consonant. */\nexport function spanishPlural(word: string): string {\n if (INVARIABLE_SPANISH.test(word)) {\n return word;\n }\n if (SPANISH_VOWEL_ENDING.test(word)) {\n return `${word}s`;\n }\n if (Z_ENDING.test(word)) {\n return `${word.slice(0, -1)}ces`;\n }\n return `${word}es`;\n}\n\n/** The plural French writes for a noun, or the noun itself when it takes no mark. */\nfunction frenchPlural(word: string): string {\n if (SIBILANT_LETTER.test(word)) {\n return word;\n }\n if (EAU_ENDING.test(word)) {\n return `${word}x`;\n }\n if (AL_ENDING.test(word)) {\n return `${word.slice(0, -2)}aux`;\n }\n return `${word}s`;\n}\n\n/**\n * What a kitchen usually takes each approximate measure to be.\n *\n * Offered as words for a note, never as the quantity: writing \"2 teaspoons\"\n * where the page wrote \"4 pinches\" puts a figure on the page it never claimed,\n * and the cook is the one holding the pinch.\n */\nconst APPROXIMATE_EQUIVALENT: Record<string, string> = {\n capful: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n glug: \"commonly taken as about two tablespoons poured free-hand\",\n dollop: \"commonly taken as about a heaped tablespoon\",\n pinch: \"commonly taken as about half a teaspoon\",\n dash: \"commonly taken as about an eighth of a teaspoon\",\n splash: \"commonly taken as about a tablespoon\",\n handful: \"commonly taken as about a quarter of a cup\",\n knob: \"commonly taken as about a tablespoon of butter\",\n drizzle: \"commonly taken as about a teaspoon poured in a thin line\",\n pincee: \"commonly taken as about half a teaspoon\",\n poignee: \"commonly taken as about a quarter of a cup\",\n bouchon: \"commonly taken as about a tablespoon, the size of a bottle cap\",\n goutte: \"commonly taken as a single drop\",\n filet: \"commonly taken as about a teaspoon poured in a thin line\",\n noix: \"commonly taken as about a tablespoon of butter\",\n louche: \"commonly taken as about half a cup\",\n soupcon: \"commonly taken as the smallest amount a spoon tip carries\",\n pizca: \"commonly taken as about half a teaspoon\",\n punado: \"commonly taken as about a quarter of a cup\",\n chorrito: \"commonly taken as about a teaspoon poured in a thin line\",\n chorro: \"commonly taken as about a tablespoon poured free-hand\",\n gota: \"commonly taken as a single drop\",\n cucharon: \"commonly taken as about half a cup\",\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 cucharada: { to: \"cucharadita\", per: 3, language: \"es\" },\n cda: { to: \"cdta\", per: 3, language: \"es\" },\n taza: { to: \"cucharada\", per: 16, language: \"es\" },\n\n kg: { to: \"g\", per: 1000, language: \"en\" },\n g: { to: \"mg\", per: 1000, language: \"en\" },\n l: { to: \"cl\", per: 100, language: \"en\" },\n dl: { to: \"cl\", per: 10, language: \"en\" },\n cl: { to: \"ml\", per: 10, language: \"en\" },\n lb: { to: \"oz\", per: 16, language: \"en\" },\n pound: { to: \"ounce\", per: 16, language: \"en\" },\n gallon: { to: \"quart\", per: 4, language: \"en\" },\n quart: { to: \"pint\", per: 2, language: \"en\" },\n pint: { to: \"fl oz\", per: 20, language: \"en\" },\n};\n\n/**\n * The unit one step down the ladder, with how many of it fit in one of the\n * current unit. Null at the bottom of a ladder, where there is nothing smaller\n * to express the amount in.\n */\nexport function demoteUnit(unit: UnitInfo): { unit: UnitInfo; per: number } | null {\n const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];\n if (!step) {\n return null;\n }\n const target = lookupUnit(step.to, step.language);\n return target ? { unit: target, per: step.per } : null;\n}\n\n/**\n * Spoons and cups: a portion, and at the same time a fixed volume. The volume\n * is what lets a share of one be restated in a smaller spoon.\n */\nexport function isSpoonMeasure(unit: UnitInfo): boolean {\n return HOUSEHOLD_MEASURE.test(unit.canonical);\n}\n\n/** How finely a kitchen can divide one of a counted thing. */\nexport type Divisibility =\n /** An egg: half of one is not something a cook takes out of the shell. */\n | \"whole\"\n /** A clove, a tin, a sachet: half of it is a quantity a kitchen can take. */\n | \"half\"\n /** An onion, an apple: a knife takes it to quarters. */\n | \"quarter\";\n\n/**\n * How far one of a measure divides.\n *\n * A measure divides as far as half of what it holds stays a quantity a kitchen\n * can take out. Almost always it does: what a tin, a jar, a sachet, a bottle or\n * a carton holds is\n * poured, weighed or spooned, so half a tin of tomatoes is half a tin of\n * tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of\n * thyme are cut with a knife. A handful is halved by taking less.\n *\n * What does not divide is what has no half a cook can measure out. An egg is\n * the case: half of one means beating it and weighing the result, which no\n * recipe asks for, and the same holds for a yolk and a white on their own. That\n * is a fact about the contents, so it is decided where the item is named rather\n * than here.\n *\n * A gesture keeps its own answer. A pincée, a poignée, a pinch is the amount a\n * hand produces in one go, and there is no half of a hand: the size of one is\n * the cook's and the count is the whole of what the measure can say, so the\n * count lands on a whole and the line reports that it moved.\n *\n * A doubtful word takes the half. A cook reading \"1/2 sachet\" knows what to do\n * with it, so a measure answers `half` apart from the few named below, and the\n * one thing that does not divide is decided from the item's own name.\n */\nexport function unitDivisibility(unit: UnitInfo): Divisibility {\n if (unit.kind === \"approximate\") {\n return \"whole\";\n }\n return QUARTERED_MEASURE.test(unit.canonical) ? \"quarter\" : \"half\";\n}\n\n/**\n * Measures a cook takes a quarter of.\n *\n * The half is as far as the criterion goes on its own, because that is the\n * share most measures give up by eye. These answer the size question\n * differently, in two ways. A pot de crème fraîche, a bouteille, a jar and a\n * block hold enough that a quarter is still a portion someone serves and the\n * rest still keeps: a quarter of a pot is a couple of spoonfuls, a quarter of a\n * bouteille is a glass, a quarter of a block of tofu is a piece cut on a board.\n * A tranche is already cut off something larger, and the board that produced\n * one takes a corner off it in the same gesture: a quarter of a tranche de pain\n * is a crouton.\n *\n * Each measure is listed in every language, because how far one of them\n * divides has nothing to do with the words a page uses for it.\n *\n * The pattern is exported because any of these words can stand where the\n * measure goes or inside the name of what is counted, and both readings answer\n * to the same list.\n */\nexport const QUARTERED_MEASURE =\n /\\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?|botes?|tarros?|botellas?|bloques?|lonchas?|rebanadas?|rodajas?)\\b/i;\n\n/**\n * True for a measure that counts pieces without saying anything about them.\n *\n * \"ea\" is short for \"each\": it announces that the figure is a count of objects\n * and names no measure of any one of them. How far the count divides therefore\n * belongs to the thing standing beside it, and the marker itself has nothing to\n * say in a line a cook reads.\n */\nexport function countsBarePieces(unit: UnitInfo): boolean {\n return unit.canonical === \"ea\";\n}\n\nexport interface ChosenUnit {\n unit: UnitInfo;\n /** What to multiply an amount in the original unit by to express it in this one. */\n ratio: number;\n}\n\n/**\n * Choose the unit a cook would actually write a quantity in, and say how to get\n * there.\n *\n * A ratio rather than a converted number, because a range has two bounds and\n * they have to end up in the same unit: converting each on its own gives the\n * unreadable \"13 oz to 1.5 pounds\". The caller picks one bound to choose from,\n * then applies the ratio to both.\n *\n * Demotion repeats while the amount is under one, so a quantity divided a\n * thousandfold walks all the way down its ladder instead of rounding away.\n * Promotion takes one step, at a full unit of the step above, so 999 g stays\n * grams and 1000 g becomes a kilo.\n *\n * Both directions ask whether the unit can hold the figure. A kitchen reads two\n * decimals and no more, so 2468 g written in kilos is 2.47 and the eight grams\n * are gone. A quantity the bigger unit cannot state stays where the page wrote\n * it, and a quantity the page's own unit cannot state walks down to the one that\n * can, so the same mass comes out the same however the page spelled it: \"1234 g\"\n * and \"1 kg 234\" are one quantity and answer with one figure.\n */\nexport function chooseReadableUnit(unit: UnitInfo, amount: number): ChosenUnit {\n if (unit.kind !== \"measured\" || !Number.isFinite(amount) || amount <= 0) {\n return { unit, ratio: 1 };\n }\n\n let current = unit;\n let ratio = 1;\n\n while (amount * ratio < 1) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n const up = PROMOTIONS[normalizeUnitKey(current.canonical)];\n if (up && amount * ratio >= up.per && writesExactly((amount * ratio) / up.per)) {\n const target = lookupUnit(up.to, up.language);\n if (target) {\n ratio /= up.per;\n current = target;\n }\n }\n\n while (!writesExactly(amount * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/** Whether a figure survives being written with the two decimals a kitchen reads. */\nfunction writesExactly(value: number): boolean {\n return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;\n}\n\n/**\n * Render a measure for a given amount, choosing singular or plural.\n *\n * The threshold differs by language. English marks the plural above one, so 1.5\n * is plural: \"1.5 cups\". French takes it from two onwards, so 1,5 stays\n * singular: \"1,5 cuillère à soupe\".\n */\nexport function formatUnit(unit: UnitInfo, amount: number, language: Language): string {\n if (unit.symbol) {\n return unit.canonical;\n }\n // French keeps the singular through \"1,5\", where English and Spanish take\n // the plural on anything above one.\n const singular = language === \"fr\" ? amount < 2 : amount <= 1;\n if (singular) {\n return unit.canonical;\n }\n if (unit.plural) {\n return unit.plural;\n }\n return language === \"es\" ? spanishPlural(unit.canonical) : `${unit.canonical}s`;\n}\n","/**\n * Reading quantities out of ingredient lines, in French and in English.\n *\n * Recipes are written as prose rather than as data: \"200 g de farine\",\n * \"450 g (1 pound) spaghetti\", \"3 ¼ cups quick oats\", \"sel\". Everything\n * downstream depends on reading these correctly, so the parser is deliberate\n * about what it recognises and returns nothing rather than guessing.\n */\n\nimport type { Language, LanguageChoice, LanguageEvidence } from \"./language.js\";\nimport { readLanguage } from \"./language.js\";\nimport type { UnitInfo } from \"./units.js\";\nimport {\n CONTAINER_NOUN,\n demoteUnit,\n lookupUnit,\n normalizeUnitKey,\n readContainerLoad,\n readPartitiveMeasure,\n unitKeys,\n} from \"./units.js\";\n\nconst BRACKETED_TAIL = /\\s*(\\([^()]*\\))\\s*$/;\nconst LEADING_ARTICLE = /^(?:of\\s+)?an?\\s+/i;\nconst LEADING_ARTICLE_FRENCH = /^(?:une|un)\\s+/i;\nconst LEADING_INDEFINITE_WITH_SPACE = /^an?\\s+/i;\nconst LEADING_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+)/i;\nconst OF_OPENING = /^of\\s+/i;\nconst OPENING_ARTICLE_FRENCH = /^\\s*(un|une)\\b\\s*/i;\nconst OPENING_ARTICLE_SPANISH = /^\\s*(un|una)\\b\\s*/i;\nconst LEADING_ARTICLE_SPANISH = /^(?:una|un)\\s+/i;\nconst REPEATED_COMMA_GROUPS = /^\\s*\\d+,\\d+,\\d/;\nconst SLASH_OUTSIDE_A_FRACTION = /(?<!\\d)\\/|\\/(?!\\d)/;\n\nconst ATTACHED_DE = /\\s+de\\s+(?=\\d)/i;\nconst DECIMAL = /^(\\d+(?:[.,]\\d+)?)/;\nconst DIGIT = /\\d/;\nconst ENGLISH_RANGE_JOINER = /^\\s+(to|or)\\s+/i;\nconst FRACTION_WORD =\n /^(?:(a|an|one|two|three)[\\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\\b/i;\nconst FRENCH_RANGE_JOINER = /^\\s+(à|a|ou)\\s+/i;\nconst SPANISH_RANGE_JOINER = /^\\s+(a|o)\\s+/i;\n/**\n * A fraction a Spanish line spells out: \"medio limón\", \"tres cuartos de taza\".\n *\n * \"medio\" and \"media\" agree with what they halve and carry no numerator of\n * their own, so the numerator is only read on the parts that take one. The word\n * boundary is what keeps \"medialuna\" out: a croissant is not half a moon.\n */\nconst SPANISH_FRACTION_WORD =\n /^(?:medi[oa]s?\\b|(?:(un|una|dos|tres)\\s+)?(cuartos|cuarto|tercios|tercio|mitad(?:es)?)\\b)/i;\nconst GROUPED_DECIMAL = /^(\\d{1,3}(?:,\\d{3})+(?:\\.\\d+)?|\\d+(?:\\.\\d+)?)/;\n\n/**\n * Which languages write a decimal comma.\n *\n * English groups thousands with the comma it never uses as a decimal mark, so\n * \"1,500 g\" is fifteen hundred grams there and one and a half elsewhere. The\n * two readings are incompatible, and the language is what chooses between them.\n */\nfunction writesDecimalComma(language: Language): boolean {\n return language !== \"en\";\n}\n\n/**\n * Which languages introduce what is measured with a partitive.\n *\n * French and Spanish both write \"un chorro de aceite\" and \"un bouchon de rhum\",\n * placing the measure between the article and the thing measured. English puts\n * the preposition the other way round and is read by its own rules.\n */\nfunction usesPartitiveDe(language: Language): boolean {\n return language !== \"en\";\n}\n\n/** The article each Romance language opens a line with in place of the figure one. */\nconst OPENING_ARTICLE_ROMANCE: Record<Language, RegExp> = {\n en: OPENING_ARTICLE_FRENCH,\n fr: OPENING_ARTICLE_FRENCH,\n es: OPENING_ARTICLE_SPANISH,\n};\n\n/** The same article, for stripping off the front of an item name. */\nconst ROMANCE_ARTICLE: Record<Language, RegExp> = {\n en: LEADING_ARTICLE_FRENCH,\n fr: LEADING_ARTICLE_FRENCH,\n es: LEADING_ARTICLE_SPANISH,\n};\n\n/** The words each language joins the two ends of a range with. */\nconst RANGE_JOINER: Record<Language, RegExp> = {\n en: ENGLISH_RANGE_JOINER,\n fr: FRENCH_RANGE_JOINER,\n es: SPANISH_RANGE_JOINER,\n};\n\n/**\n * The vocabularies tried after a line's own, in the order they are tried.\n *\n * A gloss inside brackets is written in whichever language the page reached\n * for, so the order decides only which reading wins when two vocabularies spell\n * a measure alike. The nearer language comes first: a French page glossing in\n * Spanish is likelier than one glossing in English, and the reverse for Spanish.\n */\nconst OTHER_LANGUAGES: Record<Language, readonly Language[]> = {\n en: [\"fr\", \"es\"],\n fr: [\"es\", \"en\"],\n es: [\"fr\", \"en\"],\n};\nconst HYPHENATED_TAIL = /^-\\p{L}/u;\nconst LEADING_DASH = /^\\s*(–|—|-)\\s*/;\nconst LEADING_INDEFINITE = /^an?\\s/i;\nconst LEADING_OF = /^\\s*of\\s+/i;\nconst LEADING_QUANTITY = /^[\\s\\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\\-–—]+/u;\nconst LEADING_WORD = /^\\s*(\\p{L}+)\\s+/u;\nconst MIXED_FRACTION = /^(\\d+)\\s+(\\d+)\\s*\\/\\s*(\\d+)/;\nconst PARTITIVE_BEFORE_BRACKET = /^(?:de\\s+|du\\s+|des\\s+|d'|of\\s+)(?=\\()/i;\nconst PARTITIVE_OPENING = /^(?:de\\s|d'|du\\s|des\\s|,)/i;\nconst SIMPLE_FRACTION = /^(\\d+)\\s*\\/\\s*(\\d+)/;\nconst TRAILING_S = /s$/;\nconst VAGUE_ARTICLE = /^(?:une|unas|unos|una|un|quelques|of\\s+an?|an?)\\s+/iu;\nconst WHITESPACE = /\\s+/;\n\n/**\n * The measure the line leads with: the one standing straight after the amount,\n * or the one a size word stands in front of.\n */\nfunction leadingMeasure<T extends { unit: unknown }>(direct: T, behind: T | null): T {\n if (direct.unit) {\n return direct;\n }\n return behind?.unit ? behind : direct;\n}\n\n/**\n * How a line writes the equivalents beside its measure, when it writes any.\n *\n * Three shapes and three renderings: a slash beside the amount, a trailing\n * restatement, or a bracketed group. The reading is kept rather than the\n * rendering being guessed back from the measures later.\n */\nfunction alternateStyleOf(\n count: number,\n slashed: boolean,\n trailing: boolean,\n): \"slash\" | \"trailing\" | \"bracket\" | null {\n if (count === 0) {\n return null;\n }\n if (slashed) {\n return \"slash\";\n }\n if (trailing) {\n return \"trailing\";\n }\n return \"bracket\";\n}\n\nexport interface ParsedQuantity {\n amount: number;\n /** Characters consumed from the start of the line. */\n length: number;\n}\n\n/** Unicode vulgar fractions, which hand-written recipes use freely. */\nconst VULGAR_FRACTIONS: Record<string, number> = {\n \"½\": 0.5,\n \"⅓\": 1 / 3,\n \"⅔\": 2 / 3,\n \"¼\": 0.25,\n \"¾\": 0.75,\n \"⅕\": 0.2,\n \"⅖\": 0.4,\n \"⅗\": 0.6,\n \"⅘\": 0.8,\n \"⅙\": 1 / 6,\n \"⅚\": 5 / 6,\n \"⅛\": 0.125,\n \"⅜\": 0.375,\n \"⅝\": 0.625,\n \"⅞\": 0.875,\n};\n\nconst VULGAR_CLASS = Object.keys(VULGAR_FRACTIONS).join(\"\");\n\n/**\n * Named HTML entities a page writes where the character itself would do.\n *\n * The fractions are the ones that matter: a line reading \"3½ cups\" holds\n * three and a half cups, and a reader that does not decode it sees three and\n * carries the rest into the item name, where doubling the line loses half a cup\n * without saying so.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n frac12: \"\\u00bd\",\n frac13: \"\\u2153\",\n frac23: \"\\u2154\",\n frac14: \"\\u00bc\",\n frac34: \"\\u00be\",\n frac15: \"\\u2155\",\n frac18: \"\\u215b\",\n nbsp: \" \",\n amp: \"&\",\n lt: \"<\",\n gt: \">\",\n quot: '\"',\n apos: \"'\",\n deg: \"\\u00b0\",\n times: \"\\u00d7\",\n minus: \"\\u2212\",\n ndash: \"\\u2013\",\n mdash: \"\\u2014\",\n};\n\n/**\n * Turn HTML entities back into the characters they stand for.\n *\n * A numeric entity names a code point directly, and the fraction glyphs a\n * recipe uses live well inside the range a page can write that way, so\n * \"⅔\" is two thirds and reads as such once decoded.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]+);/gi, (whole, hex: string) =>\n codePoint(Number.parseInt(hex, 16), whole),\n )\n .replace(/&#(\\d+);/g, (whole, digits: string) => codePoint(Number(digits), whole))\n .replace(\n /&([a-z][a-z0-9]*);/gi,\n (whole, name: string) => NAMED_ENTITIES[name.toLowerCase()] ?? whole,\n );\n}\n\n/** A code point a page named, or the entity as published when it names none. */\nfunction codePoint(value: number, published: string): string {\n if (!Number.isInteger(value) || value < 32 || value > 0x10ffff) {\n return published;\n }\n try {\n return String.fromCodePoint(value);\n } catch {\n return published;\n }\n}\n\n/**\n * Empty brackets, which a page leaves behind when it carried a conversion the\n * source did not render. They say nothing and belong in no answer.\n */\nconst EMPTY_BRACKETS = /\\(\\s*\\)/g;\n\n/**\n * Prepare a published line for reading: entities decoded, empty brackets\n * dropped, runs of spaces squeezed.\n */\nfunction readable(text: string): string {\n return decodeEntities(text).replace(EMPTY_BRACKETS, \" \").replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Signs and words a page puts before a number to say it is not exact.\n *\n * The number behind one of them is still a number, and reading none at all\n * hands back the line with a note saying it carries no quantity, which is\n * untrue. It is read, scaled and given back with the mark the page put on it,\n * so the answer stays as loose as the page was.\n */\nconst APPROXIMATION_PREFIX: Record<Language, RegExp> = {\n en: /^(?:~|\\u2248|about|approx\\.?|approximately|around|roughly)\\s*/i,\n fr: /^(?:~|\\u2248|environ|approximativement|\\u00e0 peu pr\\u00e8s|a peu pres)\\s*/i,\n es: /^(?:~|≈|unos|unas|aproximadamente|alrededor de|más o menos|mas o menos|como)\\s*/i,\n};\n\n/**\n * Words that introduce what a container holds.\n *\n * \"1 pot de 500 g de miel\" counts pots and says how much honey is in one, so\n * the count is the recipe's to multiply. The partitive standing after the\n * measure is what marks the line as a container and its contents.\n */\nconst CONTAINER_CONTENTS = /^\\s*(?:de\\s|d'|du\\s|des\\s)/i;\n\n/**\n * Whether the measure standing behind an item gives the size of one of them,\n * as in \"1 dinde de 3 kg\".\n *\n * The count and the measure answer different questions: how many birds, and how\n * heavy one bird is. A cook serving half again as many people takes a heavier\n * bird, so the count belongs to the page rather than to the factor.\n *\n * Read on the item alone, which is what the line counts once its own measure\n * has been taken off it, so \"450 g (1 livre) de spaghetti\" never reaches here:\n * that line counts grams, and its bracket restates the same quantity.\n */\nfunction statesItemSize(item: string, language: Language): boolean {\n const attached = ATTACHED_DE.exec(item);\n if (!attached) {\n return false;\n }\n\n const named = item.slice(0, attached.index).trim();\n if (!named || DIGIT.test(named)) {\n return false;\n }\n\n return isStatedSize(item.slice(attached.index + attached[0].length), language);\n}\n\n/** A mass or a volume standing on its own, with nothing it is the amount of. */\nfunction isStatedSize(text: string, language: Language): boolean {\n const size = parseLeadingQuantity(text, language);\n if (!size) {\n return false;\n }\n\n const measure = takeUnit(text.slice(size.length).trimStart(), language);\n if (measure.unit?.kind !== \"measured\") {\n return false;\n }\n\n return !CONTAINER_CONTENTS.test(measure.rest);\n}\n\n/**\n * A second measure a line writes straight after the first, both of them naming\n * one quantity: \"1 lb 4 oz beef\", \"2 kg 300 g de farine\", and the French\n * \"1 kg 500\", which leaves the smaller unit for the reader to supply.\n *\n * What joins the two members is the ladder: the second is written in the unit\n * the first steps down to, and it holds less than one of the first. A measure\n * from another ladder restates the quantity instead of continuing it, and a\n * number followed by a food names a second ingredient; neither is folded in\n * here.\n *\n * The share the second member adds is returned rather than the number itself,\n * so the quantity comes out as one amount in the unit the line opened with:\n * 1 lb 4 oz is 1.25 lb, and doubling it gives the 2.5 lb a butcher weighs.\n * Reading only the first member leaves the rest of the quantity sitting in the\n * item name, where doubling the line loses it.\n *\n * The two members can be joined by a sign or a word saying they add up, as in\n * \"2 c. à s. + 1 c. à c.\". The joiner is read wherever the same ladder relation\n * holds, since it states outright what juxtaposition leaves implicit.\n */\nfunction takeCompoundMember(\n text: string,\n unit: UnitInfo | null,\n language: Language,\n): { adds: number; rest: string } | null {\n if (!unit) {\n return null;\n }\n const step = demoteUnit(unit);\n if (!step) {\n return null;\n }\n\n const joiner = ADDS_UP.exec(text);\n const behindTheJoiner = joiner ? text.slice(joiner[0].length) : text;\n\n const second = parseLeadingQuantity(behindTheJoiner, language);\n if (!second || second.amount <= 0 || second.amount >= step.per) {\n return null;\n }\n\n const after = behindTheJoiner.slice(second.length);\n const measure = takeUnit(after.trimStart(), language);\n if (measure.unit) {\n if (measure.unit.canonical !== step.unit.canonical) {\n return null;\n }\n return { adds: second.amount / step.per, rest: measure.rest.trimStart() };\n }\n\n // The unwritten form: \"1 kg 500\" is a kilo and five hundred grams. It is read\n // only where a mass or a volume opened the line, and only where nothing but\n // the partitive follows the figure, so \"1 cup 2 eggs\" stays two eggs.\n if (unit.kind !== \"measured\") {\n return null;\n }\n if (!Number.isInteger(second.amount)) {\n return null;\n }\n const trailing = after.trimStart();\n if (trailing !== \"\" && !PARTITIVE_OPENING.test(trailing)) {\n return null;\n }\n\n return { adds: second.amount / step.per, rest: trailing };\n}\n\n/** How a line joins two measures that add up to one quantity. */\nconst ADDS_UP = /^\\s*(?:\\+|et|and|plus)\\s+/i;\n\n/**\n * A mark a page opens an ingredient line with, in front of the quantity.\n *\n * A bullet, a dash or a picture of the food is decoration: it says nothing\n * about how much, and a reader stopping at it answers that the line carries no\n * quantity, so a doubled recipe keeps the published amount of whatever the mark\n * stood in front of. It is set aside for reading and put back for writing.\n *\n * The signs that say an amount is loose are excluded, because those do carry a\n * claim about the figure behind them, and the mark has to be followed by space\n * so that a bracket or a decimal point opening a line is left where it is.\n */\nconst LEADING_DECORATION = /^(?:(?!~|≈|\\()[^\\p{L}\\p{N}\\s])+(?=\\s)/u;\n\n/** The mark a line opens with, or null when it opens on the quantity. */\nfunction takeDecoration(text: string): string | null {\n const match = LEADING_DECORATION.exec(text);\n return match ? match[0] : null;\n}\n\n/** Whether a word names something a food is sold in. */\nfunction namesContainer(word: string | undefined): boolean {\n return word !== undefined && CONTAINER_NOUN.test(normalizeUnitKey(word));\n}\n\n/**\n * Measures of time, in every language.\n *\n * An ingredient list carries lines that state a length rather than an amount:\n * a rest, a proof, a marinade, a bake. The factor says how much of the dish to\n * make, and how long a dough takes to rise is no part of that.\n *\n * Every vocabulary is tried on every line, because a duration is not a quantity\n * whichever language names it, and a line whose language was read one way and\n * whose hours were written the other would come back with its resting time\n * doubled.\n */\nconst TIME_UNIT: Record<Language, RegExp> = {\n en: /^(?:h|hr|hrs|hours?|mins?|minutes?|secs?|seconds?|days?|nights?|weeks?)\\b/i,\n fr: /^(?:h|mn|heures?|mins?|minutes?|secs?|secondes?|jours?|nuits?|semaines?)\\b/i,\n es: /^(?:h|hs|horas?|mins?|minutos?|segs?|segundos?|días?|dias?|noches?|semanas?)\\b/i,\n};\n\n/** Whether a line states its amount for one eater, in any of the vocabularies. */\nfunction statesAnAmountPerEater(text: string): boolean {\n return Object.values(PER_PERSON).some((pattern) => pattern.test(text));\n}\n\n/** Whether a figure is followed by a length of time in any of the vocabularies. */\nfunction namesALengthOfTime(text: string): boolean {\n return Object.values(TIME_UNIT).some((pattern) => pattern.test(text));\n}\n\n/**\n * The letters a language glues to a figure to make it a rank: the \"er\" of\n * \"1er choix\", the \"e\" of \"2e couche\", the \"st\" of \"1st choice\".\n *\n * A rank names a position rather than an amount, so the line carries nothing to\n * multiply. The letters have to sit against the figure; a line that puts a\n * space between them wrote a number and then a word.\n */\nconst ORDINAL_SUFFIX = /^(?:ers?|[eè]res?|[eè]mes?|es?|st|nd|rd|th)\\b/i;\n\n/**\n * A line that states its amount for one eater.\n *\n * The factor already says how many people the recipe is being made for, so\n * multiplying an amount that is per person applies it twice and asks for twice\n * as much on every plate.\n *\n * Every vocabulary is tried on every line, because an amount per eater is not\n * one to multiply whichever language says so, and a line whose language was\n * read one way and whose \"par personne\" was written the other would come back\n * with twice as much on every plate.\n */\nconst PER_PERSON: Record<Language, RegExp> = {\n en: /\\bper\\s+(?:person|head|serving|guest|diner)\\b/i,\n fr: /\\bpar\\s+(?:personne|convive|part|t\\u00eate)\\b/i,\n es: /\\bpor\\s+(?:persona|comensal|ración|racion)\\b/i,\n};\n\n/**\n * Size and preparation words a recipe puts between the number and the measure.\n *\n * \"1 small handful\" counts handfuls and says how full one was. Reading the\n * adjective as the thing being counted loses the measure, and with it the fact\n * that a handful is held to no better than the hand: the line comes back as an\n * exact count of something the page never named.\n */\nconst MEASURE_ADJECTIVES: Record<Language, Set<string>> = {\n en: new Set([\n \"big\",\n \"generous\",\n \"good\",\n \"heaped\",\n \"heaping\",\n \"large\",\n \"level\",\n \"medium\",\n \"scant\",\n \"small\",\n ]),\n fr: new Set([\n \"beau\",\n \"belle\",\n \"bon\",\n \"bonne\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"petit\",\n \"petite\",\n ]),\n es: new Set([\n \"buen\",\n \"buena\",\n \"colmada\",\n \"colmado\",\n \"escasa\",\n \"escaso\",\n \"generosa\",\n \"generoso\",\n \"gran\",\n \"grande\",\n \"mediana\",\n \"mediano\",\n \"pequeña\",\n \"pequeño\",\n \"rasa\",\n \"raso\",\n ]),\n};\n\n/** The adjective a line put in front of its measure, and what stands after it. */\nfunction takeMeasureAdjective(\n text: string,\n language: Language,\n): { adjective: string | null; rest: string } {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return { adjective: null, rest: text };\n }\n\n const [adjective = \"\"] = match.slice(1);\n const folded = normalizeUnitKey(adjective);\n // The word can be written in the plural where the count is, as in \"2 grosses\n // cuillères\", and the list carries the singular.\n const listed =\n MEASURE_ADJECTIVES[language].has(folded) ||\n MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, \"\"));\n if (!listed) {\n return { adjective: null, rest: text };\n }\n\n return { adjective, rest: text.slice(match[0].length) };\n}\n\n/**\n * A comma neither reading can account for.\n *\n * English groups thousands in threes and marks the decimal with a point;\n * French marks the decimal with a comma and never groups with one. A comma\n * followed by anything other than three digits is therefore not an English\n * number, and a second comma group is not a French one. Where the line gives no\n * sign which language it is in, \"1,500\" is fifteen hundred under one reading\n * and one and a half under the other, and choosing wrong is wrong by a factor\n * of a thousand. Neither is safe, so the line goes back as published and says\n * why.\n */\nconst COMMA_GROUPED = /^\\s*\\d{1,3}(?:,\\d{3})+(?!\\d)/;\nconst COMMA_DECIMAL = /^\\s*\\d+,\\d+/;\n\n/**\n * Read a leading amount.\n *\n * Handles, in order of precedence: a whole number followed by a fraction, in\n * either the glyph form \"3 ¼\" or the written form \"1 1/2\"; a bare fraction; a\n * bare glyph; and a decimal. French writes the decimal with a comma, so \"1,5\"\n * is one and a half there and two separate readings in English, where a comma\n * groups thousands.\n *\n * Returns null when the line does not start with a number, which is the normal\n * case for \"sel\" or \"Freshly ground pepper\".\n */\nexport function parseLeadingQuantity(text: string, language: Language): ParsedQuantity | null {\n const trimmed = text.trimStart();\n const offset = text.length - trimmed.length;\n\n // \"3 ¼\" and \"3¼\" before the bare \"3\", so the longest reading wins.\n const mixedGlyph = new RegExp(`^(\\\\d+)\\\\s*([${VULGAR_CLASS}])`).exec(trimmed);\n if (mixedGlyph) {\n const whole = Number(mixedGlyph[1]);\n const fraction = VULGAR_FRACTIONS[mixedGlyph[2] ?? \"\"] ?? Number.NaN;\n return { amount: whole + fraction, length: offset + mixedGlyph[0].length };\n }\n\n const mixed = MIXED_FRACTION.exec(trimmed);\n if (mixed) {\n const denominator = Number(mixed[3]);\n if (denominator !== 0) {\n return {\n amount: Number(mixed[1]) + Number(mixed[2]) / denominator,\n length: offset + mixed[0].length,\n };\n }\n }\n\n const fraction = SIMPLE_FRACTION.exec(trimmed);\n if (fraction) {\n const denominator = Number(fraction[2]);\n // A denominator of zero is not a quantity. Reading the numerator alone\n // would leave \"/0\" in the item name and scale a number nobody wrote.\n if (denominator === 0) {\n return null;\n }\n return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };\n }\n\n const glyph = trimmed[0];\n if (glyph && glyph in VULGAR_FRACTIONS) {\n return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };\n }\n\n // English groups thousands with the comma it never uses as a decimal mark, so\n // \"1,500 g\" is fifteen hundred grams. Reading the digits up to the comma and\n // stopping there answers 1 for a line that said 1500, and leaves \",500 g\"\n // behind in the item name.\n const decimal = (writesDecimalComma(language) ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);\n if (decimal) {\n const [written = \"\"] = decimal.slice(1);\n const amount = Number(\n writesDecimalComma(language) ? written.replace(\",\", \".\") : written.replace(/,/g, \"\"),\n );\n if (Number.isFinite(amount)) {\n return { amount, length: offset + decimal[0].length };\n }\n }\n\n const written =\n language === \"es\" ? parseSpanishFraction(trimmed) : parseWrittenFraction(trimmed, language);\n if (written) {\n 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> = {\n a: 1,\n an: 1,\n one: 1,\n two: 2,\n three: 3,\n un: 1,\n una: 1,\n dos: 2,\n tres: 3,\n};\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 cuarto: 4,\n cuartos: 4,\n tercio: 3,\n tercios: 3,\n mitad: 2,\n mitades: 2,\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, language: Language): ParsedQuantity | null {\n if (language !== \"en\") {\n return null;\n }\n const match = FRACTION_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n const denominator = WRITTEN_DENOMINATORS[(match[2] ?? \"\").toLowerCase()];\n if (!(numerator && denominator)) {\n return null;\n }\n\n const rest = text.slice(match[0].length).replace(LEADING_OF, \"\").trimStart();\n if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, \"en\").unit)) {\n return null;\n }\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\n/**\n * Read a fraction a Spanish line spells out, as in \"medio limón\" or \"tres\n * cuartos de taza\".\n *\n * Spanish writes the halves in words far more readily than in figures, and a\n * line opening on one carries an amount like any other. \"medio\" and \"media\"\n * stand directly in front of what they halve; the other parts take the\n * partitive, \"un cuarto de cebolla\".\n *\n * What follows decides whether the words are a quantity at all. \"media\" with\n * nothing behind it names no share of anything, and a line that only names a\n * food or a measure is what states an amount.\n */\nfunction parseSpanishFraction(text: string): ParsedQuantity | null {\n const match = SPANISH_FRACTION_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;\n // \"medio\" and \"media\" name the half on their own, with no part word behind.\n const denominator = match[2] ? WRITTEN_DENOMINATORS[match[2].toLowerCase()] : 2;\n if (!(numerator && denominator)) {\n return null;\n }\n\n const rest = text.slice(match[0].length).replace(LEADING_PARTITIVE, \"\").trimStart();\n if (rest === \"\") {\n return null;\n }\n\n return { amount: numerator / denominator, length: match[0].length };\n}\n\nexport interface ParsedRange extends ParsedQuantity {\n /** Upper bound. `amount` carries the lower one. */\n max: number;\n /** How the range was written, so the rewrite can keep the same shape. */\n separator: string;\n}\n\n/**\n * Read a leading range such as \"225–500\", \"3-4\", \"2 to 3\" or \"2 à 3\".\n *\n * Recipes use ranges where the exact amount is the cook's call, and both bounds\n * describe the same quantity. Reading only the first one is worse than reading\n * neither: the second number survives unscaled into the answer and contradicts\n * it.\n *\n * A descending pair is not a range. \"1/2 3\" is two amounts the parser has no\n * business joining, and a dash between two numbers is a range only when the\n * second is the larger.\n */\nexport function parseLeadingRange(text: string, language: Language): ParsedRange | null {\n const low = parseLeadingQuantity(text, language);\n if (!low) {\n return null;\n }\n\n const after = text.slice(low.length);\n // A written separator needs whitespace around it, so \"5 tomatoes\" is not read\n // as \"5 to\" followed by an unreadable second bound.\n const written = RANGE_JOINER[language];\n const separator = written.exec(after) ?? LEADING_DASH.exec(after);\n if (!separator) {\n return null;\n }\n\n const high = parseLeadingQuantity(after.slice(separator[0].length), language);\n if (!high || high.amount <= low.amount) {\n return null;\n }\n\n return {\n amount: low.amount,\n max: high.amount,\n separator: separator[1] ?? \"\",\n length: low.length + separator[0].length + high.length,\n };\n}\n\n/** One amount with its measure, as the line wrote it. */\nexport interface Measure {\n amount: number;\n /** Upper bound when the measure is a range, null otherwise. */\n amountMax: number | null;\n /** The word or sign a range was written with. */\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n}\n\n/**\n * Why a line that shows a figure is still not the factor's to multiply.\n *\n * Each of these is a reading the parser can make and a scaling it must not do,\n * and they are kept apart from a line with no figure at all so the answer can\n * say which of the two it is looking at.\n */\nexport type HeldBack =\n /** \"4 to 5-pound roast\": the figures give the size of one, not how many. */\n | \"sizeQualifier\"\n /** \"1 dinde de 3 kg\": the measure behind the item weighs one of them. */\n | \"itemSize\"\n /** \"12 oz can tomatoes\": the measure gives what the tin holds, and no count is written. */\n | \"containerSize\"\n /** \"2 pommes de terre par personne\": the amount is already stated for one eater. */\n | \"perPerson\"\n /** \"2 h de repos\": the figure measures a length of time rather than an amount. */\n | \"duration\"\n /** \"1,500 g\" with nothing to say whether the comma groups or divides. */\n | \"ambiguousDecimal\";\n\nexport interface ParsedIngredient {\n /** The line exactly as it was given. */\n original: string;\n /** The language the line was read in. */\n language: Language;\n /**\n * Why the figure on this line must not be multiplied, when there is such a\n * reason. Null for the ordinary line, whose amount is the factor's to scale.\n */\n heldBack: HeldBack | null;\n /**\n * The mark the page opened the line with, in front of the quantity, such as a\n * bullet or a picture of the food. It goes back where the page had it.\n */\n decoration: string | null;\n /**\n * The sign or word the page put before the amount to say it is loose, as in\n * the \"~\" of \"~1 cup water\". Null when the page stated the amount plainly.\n */\n approximation: string | null;\n /**\n * A size word standing between the number and the measure, as in the \"small\"\n * of \"1 small handful\". It goes back in front of the measure so the answer\n * reads the way the page did.\n */\n measureAdjective: string | null;\n /**\n * Whether the line wrote a partitive between its measure and what it measures.\n *\n * French and Spanish write one where the page chose to, \"6 cucharadas de\n * aceite\", and leave it out where they did not, \"2 tazas arroz\". The rewrite\n * puts back what the page had rather than deciding for it.\n */\n itemPartitive: boolean;\n /**\n * Whether that size word stood behind the measure rather than in front of it.\n *\n * Spanish writes \"1 cucharada colmada\" where French writes \"1 grosse pincée\",\n * and the rewrite has to put the word back where the page had it.\n */\n measureAdjectiveFollows: boolean;\n amount: number | null;\n /**\n * Upper bound when the line gives a range, as in \"225–500 g\". Null for a\n * single amount. `amount` holds the lower bound, so the two must be scaled\n * together: multiplying only one turns \"225–500\" into the nonsense \"450–500\".\n */\n amountMax: number | null;\n rangeSeparator: string | null;\n unit: UnitInfo | null;\n /**\n * The same quantity restated in another system, which recipes give in\n * brackets: \"450 g (1 pound)\". Left unscaled it would contradict the amount\n * beside it, so it is parsed and scaled with the rest.\n */\n alternates: Measure[];\n /**\n * How the line introduced its equivalents: in brackets beside the amount, as\n * in \"450 g (1 pound)\", after a slash, as in \"500 g / 1.1 lb\", or in a\n * bracket closing the line, as in \"1 cup milk (240 ml)\". The rewrite puts\n * them back the way the line offered them.\n */\n alternateStyle: \"bracket\" | \"slash\" | \"trailing\" | null;\n /**\n * The word the bracket opened on, as in the \"soit\" of \"(soit 3/4 de tasse)\".\n * Null when the line stated the equivalent without one.\n */\n alternateIntro: string | null;\n /**\n * A bracket giving what one container holds, as in \"1 (14 oz) can\", exactly\n * as the page wrote it. The count is the factor's to multiply and this figure\n * is not, so it goes back untouched.\n */\n capacity: string | null;\n /** What the amount and the measure apply to, such as \"farine\" or \"egg yolks\". */\n item: string;\n /**\n * The article the amount was read from, as in \"une\" in \"une pincée de sel\".\n * Null when the line wrote a figure.\n */\n articleWord: string | null;\n /**\n * How many things one of the word the line counted with stands for, as in the\n * twelve of \"2 dozen mushrooms\". Null when the line counted the things\n * themselves. `amount` already holds the product, so this is what says where\n * the figure came from.\n */\n countMultiplier: number | null;\n}\n\n/**\n * Articles a French line writes where a digit would go, and what they count as.\n *\n * \"un\" and \"une\" are the number one written out, so a line using one has stated\n * a count. A word that says merely that there are several, \"quelques\" as much\n * as \"plusieurs\" or the English \"a few\", has stated none: any figure put behind\n * it is this server's reading rather than the page's quantity, and a reading\n * multiplied is a number nobody wrote.\n */\n/**\n * The articles that stand for the figure one.\n *\n * French and Spanish each write two of them, and \"un\" is the one they share.\n * The plural forms Spanish writes, \"unos\" and \"unas\", stand for a vague several\n * rather than for one, and are read as an approximation elsewhere.\n */\nconst ROMANCE_ARTICLES: Record<string, number> = { un: 1, une: 1, una: 1 };\n\n/**\n * Take a measure off the front of `text`, longest spelling first, so \"cuillère\n * à soupe\" is not read as \"cuillère\" with \"à soupe\" spilling into the item\n * name, and \"fluid ounce\" is not read as \"ounce\" with \"fluid\" left dangling.\n */\nexport function takeUnit(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const normalized = normalizeUnitKey(text);\n for (const key of unitKeys(language)) {\n if (normalized !== key && !normalized.startsWith(`${key} `)) {\n continue;\n }\n const unit = lookupUnit(key, language);\n if (!unit) {\n continue;\n }\n const rest = afterKey(text, key);\n if (rest === null) {\n continue;\n }\n return { unit, rest };\n }\n\n if (language === \"en\") {\n const words = text.trimStart().split(WHITESPACE);\n const load = words[0] ? readContainerLoad(words[0]) : null;\n if (load) {\n return { unit: load, rest: words.slice(1).join(\" \") };\n }\n }\n\n return { unit: null, rest: text };\n}\n\n/**\n * What stands after the measure a key names, or null when the key does not line\n * up with the words the line is written in.\n *\n * How many words a key spells and how many words the line spends on it are\n * different numbers: normalising turns the abbreviation \"c.à.s\" into the three\n * words \"c a s\", and the line writes it as one. Consuming the key's own word\n * count would take two words of the ingredient with it, so the words the line\n * wrote are consumed one at a time until they normalise to the key.\n */\nfunction afterKey(text: string, key: string): string | null {\n const words = text.trim().split(WHITESPACE);\n for (let count = 1; count <= words.length; count += 1) {\n if (normalizeUnitKey(words.slice(0, count).join(\" \")) !== key) {\n continue;\n }\n return words.slice(count).join(\" \");\n }\n return null;\n}\n\n/**\n * A measure inside brackets can be stated in another 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 several\n * vocabularies carry keeps the spelling and the plural of the line it sits in;\n * the others are tried in the order `OTHER_LANGUAGES` declares.\n */\nfunction takeUnitAnyLanguage(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const first = takeUnit(text, language);\n if (first.unit) {\n return first;\n }\n for (const other of OTHER_LANGUAGES[language]) {\n const taken = takeUnit(text, other);\n if (taken.unit) {\n return taken;\n }\n }\n return first;\n}\n\n/**\n * Decide which language a line is written in, using the small words that hold\n * it together and whether each vocabulary recognises the measure it names.\n */\nexport function detectLanguage(line: string): Language {\n return readEvidence(line).language;\n}\n\n/** The language a line reads as, with the weight each side gathered. */\nfunction readEvidence(line: string): LanguageEvidence {\n const text = line.trim();\n // Whatever a leading figure, fraction or article occupies, so the probe looks\n // at the position a measure would stand in.\n const afterFigures = text.replace(LEADING_QUANTITY, \"\").trimStart();\n const afterArticle = afterFigures.replace(VAGUE_ARTICLE, \"\");\n\n const frenchUnit = takeUnit(afterArticle, \"fr\").unit !== null;\n const englishUnit = takeUnit(afterArticle, \"en\").unit !== null;\n const spanishUnit = takeUnit(afterArticle, \"es\").unit !== null;\n\n return readLanguage(text, { frenchUnit, englishUnit, spanishUnit });\n}\n\n/**\n * The measure standing after the amount, and the size word in front of it.\n *\n * The adjective is only an adjective when a measure stands behind it. In\n * \"1 cleaned leek green\" the words that follow name the food itself, and taking\n * one off would hand back a line the page never wrote.\n */\nfunction readMeasure(\n rest: string,\n language: Language,\n fromArticle: boolean,\n): {\n leading: ReturnType<typeof takeLeadingUnit>;\n described: { adjective: string | null; rest: string };\n behind: ReturnType<typeof takeLeadingUnit> | null;\n /** Whether the size word stood behind the measure rather than in front of it. */\n follows: boolean;\n} {\n const direct = takeLeadingUnit(rest, language, fromArticle);\n if (direct.unit) {\n // Spanish puts the size word behind the measure it qualifies, where French\n // and English put it in front: \"1 cucharada colmada\" is a heaped spoonful,\n // and reading \"colmada\" as what is being measured loses the measure.\n const following = language === \"es\" ? takeMeasureAdjective(direct.rest, language) : null;\n if (following?.adjective) {\n return {\n leading: { ...direct, rest: following.rest },\n described: { adjective: following.adjective, rest: direct.rest },\n behind: direct,\n follows: true,\n };\n }\n return { leading: direct, described: { adjective: null, rest }, behind: null, follows: false };\n }\n\n const described = takeMeasureAdjective(rest, language);\n const behind = described.adjective\n ? takeLeadingUnit(described.rest, language, fromArticle)\n : null;\n\n return { leading: leadingMeasure(direct, behind), described, behind, follows: false };\n}\n\n/**\n * Whether the figure the line opens with counts anything, and what it is\n * instead when it does not.\n *\n * Three ways a figure is not a quantity: it is joined to a word by a hyphen and\n * describes one thing, it names a rank in an order, or it measures a length of\n * time, which belongs to the method rather than to the proportions. `false`\n * means the figure counts.\n */\nfunction whatTheFigureIsNot(behindFigure: string, couldBeARank: boolean): HeldBack | null | false {\n if (HYPHENATED_TAIL.test(behindFigure)) {\n return \"sizeQualifier\";\n }\n if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {\n return null;\n }\n if (namesALengthOfTime(behindFigure.trimStart())) {\n return \"duration\";\n }\n return false;\n}\n\n/**\n * The equivalents a line writes beside its measure, in whichever of the three\n * shapes it wrote them.\n *\n * A bracketed group, a slash beside the amount, or a restatement trailing the\n * item: only one of them is read, because a line offering two would be stating\n * the same quantity three times.\n */\nfunction takeEquivalents(\n text: string,\n language: Language,\n): {\n rest: string;\n bracketed: ReturnType<typeof takeAlternates>;\n slashed: ReturnType<typeof takeSlashAlternates>;\n item: string;\n /** Whether the line wrote a partitive between its measure and what it measures. */\n partitive: boolean;\n trailing: ReturnType<typeof takeTrailingAlternates>;\n group:\n | ReturnType<typeof takeTrailingAlternates>\n | Omit<ReturnType<typeof takeAlternates>, \"rest\">\n | null;\n} {\n const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text);\n const bracketed = takeAlternates(\n introducedBracket ? text.slice(introducedBracket[0].length) : text,\n language,\n );\n let rest = bracketed.measures.length > 0 ? bracketed.rest : text;\n\n const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);\n if (slashed) {\n rest = slashed.rest;\n }\n\n const lead = stripItemLead(rest, language);\n let item = lead.item;\n\n const trailing =\n bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;\n if (trailing) {\n item = trailing.item;\n }\n\n const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);\n // A measure inside a bracket beside a container gives what one of them holds,\n // as in \"1 (14 oz) can\". The recipe's proportion lives in how many tins are\n // opened, and a tin of twice the size is a tin no shop sells, so the figure\n // goes back exactly as the page wrote it.\n\n return { rest, item, partitive: lead.partitive, bracketed, slashed, trailing, group };\n}\n\n/**\n * Whether the figures on the line give the size of something rather than how\n * many of it the recipe wants, and of what.\n *\n * Three ways a line says it. Without a measure, the words behind the count\n * carry the mass. With a partitive, \"une dinde de 3 kg\" writes the noun where a\n * measure stands and the partitive takes it for one — a noun the vocabulary\n * lists as a measure keeps counting, since a pot is a thing to buy more of,\n * while a noun read as a measure only for standing there names the food itself.\n * And \"12 oz can tomatoes\" qualifies the tin, leaving how many tins unwritten.\n */\nfunction whatTheFiguresSize(\n leading: { unit: UnitInfo | null; partitive: boolean },\n item: string,\n language: Language,\n): HeldBack | null {\n if (usesPartitiveDe(language) && !leading.unit && statesItemSize(item, language)) {\n return \"itemSize\";\n }\n\n // \"une dinde de 3 kg\" writes the noun where a measure stands, and the\n // partitive takes it for one. A noun the vocabulary lists as a measure keeps\n // counting, since a pot or a boîte is a thing to buy more of; a noun read as\n // a measure only for standing there names the food itself, and a mass behind\n // it is the size of one of them.\n if (usesPartitiveDe(language) && leading.partitive && isStatedSize(item, language)) {\n return \"itemSize\";\n }\n\n // \"12 oz can tomatoes\": the measure qualifies the tin, and how many tins the\n // recipe wants is not written anywhere on the line.\n if (leading.unit?.kind === \"measured\" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {\n return \"containerSize\";\n }\n\n return null;\n}\n\n/**\n * What the words behind the figure say the figure counts.\n *\n * \"two thirds of a cup\" names a share of one cup, and the measure stands behind\n * the preposition and the article that introduce it. \"2 dozen mushrooms\" counts\n * mushrooms, twelve to the dozen, so the multiplier is folded into the amount\n * and the line goes on to be read as the count of a thing it now is.\n */\nfunction whatTheCountIsOf(\n behindFigure: string,\n language: Language,\n): { rest: string; multiplier: ReturnType<typeof readCountMultiplier> } {\n let rest = behindFigure.trimStart();\n // \"two thirds of a cup\" names a share of one cup, and the measure stands\n // behind the preposition and the article that introduce it.\n if (language === \"en\") {\n rest = rest.replace(LEADING_ARTICLE, \"\");\n }\n\n // \"2 dozen mushrooms\" counts mushrooms, twelve to the dozen, so the multiplier\n // is folded into the amount and the line goes on to be read as the count of a\n // thing it now is.\n const multiplier = readCountMultiplier(rest);\n if (multiplier) {\n rest = multiplier.rest;\n }\n\n return { rest, multiplier };\n}\n\n/**\n * What a container holds, when the bracket beside it states a capacity rather\n * than an equivalent.\n *\n * \"2 boîtes (400 g)\" names two tins of that size, and the figure is the page's\n * own rather than the factor's, so it goes back between the count and the\n * container exactly as published. A bracket that restates the quantity in\n * another unit is a different thing and stays an equivalent.\n */\nfunction capacityOfTheContainer(\n group: { measures: ReadonlyArray<{ unit: UnitInfo | null }>; published: string } | null,\n measureName: string | undefined,\n item: string,\n): string | null {\n if (!group?.measures.every((measure) => measure.unit?.kind === \"measured\")) {\n return null;\n }\n if (!(namesContainer(measureName) || namesContainer(item))) {\n return null;\n }\n return group.published;\n}\n\n/**\n * Split an ingredient line into amount, measure, bracketed equivalents and\n * item.\n *\n * A missing amount is normal and not an error: many lines are just \"sel\". A\n * missing measure is equally normal and means the item is counted, as in\n * \"5 egg yolks\".\n */\nexport function parseIngredient(line: string, choice: LanguageChoice = \"auto\"): ParsedIngredient {\n const original = line;\n const marked = readable(line);\n const decoration = takeDecoration(marked);\n const text = decoration ? marked.slice(decoration.length).trimStart() : marked;\n const evidence = readEvidence(text);\n const language = choice === \"auto\" ? evidence.language : choice;\n\n const empty = (heldBack: HeldBack | null): ParsedIngredient => ({\n original,\n language,\n heldBack,\n decoration,\n approximation: null,\n measureAdjective: null,\n measureAdjectiveFollows: false,\n itemPartitive: false,\n amount: null,\n amountMax: null,\n rangeSeparator: null,\n unit: null,\n alternates: [],\n alternateStyle: null,\n alternateIntro: null,\n capacity: null,\n item: text,\n articleWord: null,\n countMultiplier: null,\n });\n\n if (unreadableComma(text, language, evidence)) {\n return empty(\"ambiguousDecimal\");\n }\n\n const loose = APPROXIMATION_PREFIX[language].exec(text);\n const stated = loose ? text.slice(loose[0].length) : text;\n\n const range = parseLeadingRange(stated, language);\n const article = range ? null : readArticle(stated, language);\n const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;\n if (!quantity) {\n return empty(null);\n }\n\n // A figure joined to a word by a hyphen describes one thing rather than\n // counting things: \"4 to 5-pound roast\" is one roast that weighs that much.\n const notAnAmount = whatTheFigureIsNot(stated.slice(quantity.length), quantity !== article);\n if (notAnAmount !== false) {\n return empty(notAnAmount);\n }\n\n const counted = whatTheCountIsOf(stated.slice(quantity.length), language);\n let rest = counted.rest;\n const multiplier = counted.multiplier;\n const times = multiplier?.times ?? 1;\n\n const fromArticle = quantity === article;\n const measure = readMeasure(rest, language, fromArticle);\n const { leading, described, behind } = measure;\n rest = leading.rest;\n\n // A range gives two amounts, and a second member behind it would belong to\n // one of them without saying which.\n const compound = range ? null : takeCompoundMember(rest, leading.unit, language);\n if (compound) {\n rest = compound.rest;\n }\n const whole = quantity.amount + (compound?.adds ?? 0);\n\n // The partitive can stand between the measure and the bracket restating it,\n // as in \"150 g de (3/4 de tasse) de sucre\". It introduces the equivalent\n // rather than the food, so it is stepped over and the bracket read behind it.\n const equivalents = takeEquivalents(rest, language);\n const { slashed, trailing, group } = equivalents;\n rest = equivalents.rest;\n const item = equivalents.item;\n const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);\n\n // A counted thing whose size the line states: the number the line opens with\n // is one bird, and the measure behind it is what that bird weighs.\n const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);\n if (sizedRatherThanCounted !== null) {\n return empty(sizedRatherThanCounted);\n }\n\n const alternates = capacity === null ? (slashed?.measures ?? group?.measures ?? []) : [];\n\n return {\n original,\n language,\n heldBack: statesAnAmountPerEater(text) ? \"perPerson\" : null,\n decoration,\n approximation: loose ? loose[0] : null,\n measureAdjective: behind?.unit ? described.adjective : null,\n measureAdjectiveFollows: measure.follows,\n itemPartitive: equivalents.partitive,\n amount: whole * times,\n amountMax: range === null ? null : range.max * times,\n rangeSeparator: range?.separator ?? null,\n unit: leading.unit,\n alternates,\n alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),\n alternateIntro: capacity === null ? (group?.intro ?? null) : null,\n capacity,\n item,\n articleWord: fromArticle ? (article?.word ?? null) : null,\n countMultiplier: multiplier?.times ?? null,\n };\n}\n\n/**\n * Whether the comma in a leading number is one this line cannot settle.\n *\n * A group of exactly three digits reads as thousands in English and as a\n * decimal in French, so it needs the line to say which language it is in; where\n * nothing does, both readings stand and they differ by a factor of a thousand.\n * A group of any other size is not an English number at all, and a second group\n * is not a French one.\n */\nfunction unreadableComma(text: string, language: Language, evidence: LanguageEvidence): boolean {\n if (COMMA_GROUPED.test(text)) {\n // Settled means one language outscored every other. Two tied at the top\n // leave the comma readable two ways, whichever pair they are.\n const scores = [evidence.french, evidence.english, evidence.spanish];\n const top = Math.max(...scores);\n const settled = scores.filter((score) => score === top).length === 1;\n return !settled;\n }\n if (language === \"en\") {\n return COMMA_DECIMAL.test(text);\n }\n return REPEATED_COMMA_GROUPS.test(text);\n}\n\ninterface ParsedArticle extends ParsedQuantity {\n /** The article as the line wrote it. */\n word: string;\n}\n\n/**\n * Read the article a line uses in place of the figure one, as in \"une pincée de\n * sel\" or \"a pinch of salt\".\n *\n * The article counts as a quantity only when a measure follows it, because that\n * is where it stands for a number: \"une pincée\" is one pinch and \"a pinch\" is\n * one pinch, while \"un oignon\" and \"a ripe apple\" name a vegetable and a fruit\n * and no amount at all. Reading the second as the first would multiply a number\n * the line never wrote.\n */\nfunction readArticle(text: string, language: Language): ParsedArticle | null {\n /** A measure or a multiplier is what makes the article stand for a number. */\n const counts = (rest: string) =>\n takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;\n\n if (usesPartitiveDe(language)) {\n const match = OPENING_ARTICLE_ROMANCE[language].exec(text);\n if (!match) {\n return null;\n }\n if (!counts(text.slice(match[0].length))) {\n return null;\n }\n const [word = \"\"] = match.slice(1);\n const amount = ROMANCE_ARTICLES[word.toLowerCase()];\n if (amount === undefined) {\n return null;\n }\n return { amount, length: match[0].length, word };\n }\n\n const article = LEADING_INDEFINITE_WITH_SPACE.exec(text);\n if (!article) {\n return null;\n }\n if (!counts(text.slice(article[0].length))) {\n return null;\n }\n return { amount: 1, length: article[0].length, word: article[0].trim() };\n}\n\n/**\n * Words that say how many things a number stands for, rather than how much of\n * something one of them holds.\n *\n * A dozen, and a douzaine, is twelve of whatever is being counted. \"2 dozen\n * mushrooms\" therefore asks for twenty-four mushrooms, and the answer divides\n * the way a mushroom does. Reading the word as a measure gives \"1 1/2 dozen\",\n * which is not a count a kitchen works with, and it hands the question of\n * divisibility to a word that names no food.\n */\nconst COUNT_MULTIPLIERS: Record<string, number> = {\n dozen: 12,\n dozens: 12,\n douzaine: 12,\n douzaines: 12,\n};\n\n/** The multiplier a line opens with, and what stands after it. */\nfunction readCountMultiplier(text: string): { times: number; rest: string } | null {\n const match = LEADING_WORD.exec(text);\n if (!match) {\n return null;\n }\n\n const [word = \"\"] = match.slice(1);\n const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];\n if (times === undefined) {\n return null;\n }\n return { times, rest: text.slice(match[0].length) };\n}\n\n/**\n * Take the measure a line opens with, allowing for the ones no vocabulary can\n * list.\n *\n * French names a measure by the container followed by the partitive that\n * introduces what is measured, and that reading is offered only where the line\n * wrote an article instead of a digit: \"un bouchon de rhum\" asks for an amount,\n * while \"1 piment de Cayenne\" asks for a chilli whose variety happens to be\n * introduced the same way.\n */\nfunction takeLeadingUnit(\n text: string,\n language: Language,\n fromArticle: boolean,\n): { unit: UnitInfo | null; rest: string; partitive: boolean } {\n const taken = takeUnit(text, language);\n if (taken.unit) {\n return { ...taken, partitive: false };\n }\n\n if (usesPartitiveDe(language) && fromArticle) {\n const measure = readPartitiveMeasure(text, language);\n // A noun the vocabulary never listed, read as a measure for standing\n // between the article and the partitive.\n if (measure) {\n return { unit: measure.unit, rest: measure.rest, partitive: true };\n }\n }\n\n return { ...taken, partitive: false };\n}\n\n/**\n * Drop the preposition and the article that stand between a measure and what it\n * measures.\n *\n * \"200 g de farine\" reads better as item \"farine\" than \"de farine\", and\n * \"2 heads of garlic\" names the same thing as \"2 heads garlic\".\n *\n * The article goes with the preposition. \"2/3 d'un flacon\" names a share of one\n * flacon, and once the share has been multiplied the count sits where \"un\"\n * stood. Leaving the article behind produces \"4 un flacon\", which reads as\n * broken text rather than as a quantity.\n */\nfunction stripItemLead(text: string, language: Language): { item: string; partitive: boolean } {\n if (usesPartitiveDe(language)) {\n const partitive = LEADING_PARTITIVE.test(text);\n return {\n item: text.replace(LEADING_PARTITIVE, \"\").replace(ROMANCE_ARTICLE[language], \"\").trim(),\n partitive,\n };\n }\n return {\n item: text.replace(OF_OPENING, \"\").replace(LEADING_INDEFINITE_WITH_SPACE, \"\").trim(),\n partitive: false,\n };\n}\n\n/**\n * Read a bracketed group of equivalent measures, as in \"(1 pound)\" or\n * \"(500 g / 1.1 lb)\".\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", stays in the item text where it belongs, because scaling it would\n * mean scaling prose.\n */\nfunction takeAlternates(text: string, language: Language): BracketedMeasures {\n const none: BracketedMeasures = { measures: [], intro: null, published: \"\", rest: text };\n if (!text.startsWith(\"(\")) {\n return none;\n }\n const close = text.indexOf(\")\");\n if (close < 0) {\n return none;\n }\n\n const read = readBracket(text.slice(0, close + 1), language);\n if (!read) {\n return none;\n }\n return { ...read, rest: text.slice(close + 1).trimStart() };\n}\n\ninterface BracketedMeasures {\n measures: Measure[];\n /** What introduced the restatement, as the line wrote it. */\n intro: string | null;\n /** The bracket exactly as published, for a figure that must not be scaled. */\n published: string;\n rest: string;\n}\n\n/**\n * Words a page puts inside a bracket to announce that what follows restates the\n * quantity beside it rather than adding to it.\n */\nconst EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\\.?|approximately|or|ou|=)\\s*/i;\n\n/**\n * The partitive a line can put between a figure and the measure it names.\n *\n * \"3/4 de tasse\" and \"3/4 tasse\" are one quantity written two ways, and a\n * reader that only knows the second leaves the first sitting in prose beside an\n * amount that moved.\n */\nconst MEASURE_PARTITIVE = /^(?:de\\s+la\\s+|de\\s+l'|d'|de\\s+|du\\s+|des\\s+|of\\s+)/i;\n\n/** The measure a figure names, allowing for the partitive introducing it. */\nfunction takeMeasureAfterQuantity(\n text: string,\n language: Language,\n): { unit: UnitInfo | null; rest: string } {\n const direct = takeUnitAnyLanguage(text, language);\n if (direct.unit) {\n return direct;\n }\n\n const partitive = MEASURE_PARTITIVE.exec(text);\n if (!partitive) {\n return direct;\n }\n return takeUnitAnyLanguage(text.slice(partitive[0].length), language);\n}\n\n/**\n * Read a whole bracket as a group of equivalent measures, or nothing.\n *\n * The group is only taken when every part of it reads as an amount with a\n * measure. A bracket holding a remark, as in \"(the riper the better)\" or\n * \"(cup)\", is prose, and scaling prose is not something a reader can check.\n */\nfunction readBracket(bracket: string, language: Language): Omit<BracketedMeasures, \"rest\"> | null {\n const inside = bracket.slice(1, -1);\n const intro = EQUIVALENT_INTRODUCER.exec(inside);\n const body = intro ? inside.slice(intro[0].length) : inside;\n // A slash separates two ways of stating the quantity, and it also writes a\n // fraction. One sitting between two digits belongs to the number, so\n // \"(3/4 de tasse)\" is one measure rather than a three and a four.\n const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());\n const measures: Measure[] = [];\n\n for (const part of parts) {\n const range = parseLeadingRange(part, language);\n const quantity = range ?? parseLeadingQuantity(part, language);\n if (!quantity) {\n return null;\n }\n\n const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);\n // A trailing word means the bracket is not purely a measure, as in\n // \"(1-inch pieces)\", so the whole group is left as prose.\n if (!after.unit || after.rest.trim() !== \"\") {\n return null;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: after.unit,\n });\n }\n\n if (measures.length === 0) {\n return null;\n }\n return { measures, intro: intro ? intro[0].trim() : null, published: bracket };\n}\n\n/**\n * Read a bracket a line closes on, as in \"150 g de sucre (soit 3/4 de tasse)\".\n *\n * A page states an equivalence wherever it reads best, and after the name of\n * the food is as common as beside the figure. Both say the same thing about the\n * same quantity, so both move with it: a doubled line whose closing bracket\n * still reads as published tells the cook that 300 g is three quarters of a\n * cup.\n */\nfunction takeTrailingAlternates(\n item: string,\n language: Language,\n): (Omit<BracketedMeasures, \"rest\"> & { item: string }) | null {\n const closing = BRACKETED_TAIL.exec(item);\n if (!closing) {\n return null;\n }\n\n const read = readBracket(closing[1] ?? \"\", language);\n if (!read) {\n return null;\n }\n\n const head = item.slice(0, closing.index).trim();\n // A bracket standing on its own is the quantity itself rather than a\n // restatement of one, and there would be nothing left for it to qualify.\n if (head === \"\") {\n return null;\n }\n\n return { ...read, item: head };\n}\n\n/**\n * Read equivalents a line states after a slash, as in \"500 g / 1.1 lb rolled\n * oats\", where the item follows the last of them.\n *\n * Both figures name one quantity, so both have to move together: a doubled\n * line reading \"1 kg / 1.1 lb\" gives two answers a factor of two apart for the\n * same ingredient. A slash followed by anything other than an amount and a\n * measure is prose and stays in the item text.\n */\nfunction takeSlashAlternates(\n text: string,\n language: Language,\n): { measures: Measure[]; rest: string } | null {\n const measures: Measure[] = [];\n let rest = text;\n\n while (rest.startsWith(\"/\")) {\n const after = rest.slice(1).trimStart();\n const range = parseLeadingRange(after, language);\n const quantity = range ?? parseLeadingQuantity(after, language);\n if (!quantity) {\n break;\n }\n\n const taken = takeUnitAnyLanguage(after.slice(quantity.length).trimStart(), language);\n if (!taken.unit) {\n break;\n }\n\n measures.push({\n amount: quantity.amount,\n amountMax: range?.max ?? null,\n rangeSeparator: range?.separator ?? null,\n unit: taken.unit,\n });\n rest = taken.rest.trimStart();\n }\n\n return measures.length > 0 ? { measures, rest } : null;\n}\n\nexport interface FormatAmountOptions {\n /**\n * Whether to snap near-fractions to 1/4, 1/3, 1/2, 2/3 and 3/4.\n *\n * True for things a cook counts or spoons out: \"1/3 cup\" is how a kitchen\n * expresses it, \"0.33 cup\" is not. False for mass and volume, which are\n * decimal by nature: nobody weighs \"8 1/3 kg\" of sugar, they weigh 8.33 kg.\n */\n fractions?: boolean;\n}\n\n/**\n * Render an amount the way a recipe would write it, with the decimal mark the\n * language uses.\n */\nexport function formatAmount(\n amount: number,\n language: Language,\n options: FormatAmountOptions = {},\n): string {\n const decimal = (value: number) => {\n // Two decimals is finer than any kitchen resolves, and for anything smaller\n // than that it is zero. A quantity that survived being divided a\n // thousandfold must not be handed back as none of the ingredient, so below\n // that point the significant digits are what gets written.\n const rounded =\n value !== 0 && Math.abs(value) < 0.01\n ? Number(value.toPrecision(2))\n : Math.round(value * 100) / 100;\n const rendered = String(rounded);\n return writesDecimalComma(language) ? rendered.replace(\".\", \",\") : rendered;\n };\n\n if (!Number.isFinite(amount)) {\n return \"\";\n }\n if (Number.isInteger(amount)) {\n return String(amount);\n }\n if (options.fractions === false) {\n return decimal(amount);\n }\n\n const whole = Math.floor(amount);\n const rest = amount - whole;\n const known: [number, string][] = [\n [0.25, \"1/4\"],\n [1 / 3, \"1/3\"],\n [0.5, \"1/2\"],\n [2 / 3, \"2/3\"],\n [0.75, \"3/4\"],\n ];\n for (const [value, label] of known) {\n if (Math.abs(rest - value) < 0.02) {\n return whole > 0 ? `${whole} ${label}` : label;\n }\n }\n\n return decimal(amount);\n}\n","/**\n * Scaling ingredient quantities, in French, in English and in Spanish.\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 languages are read by one set of rules. A list holding lines in more than\n * one comes back scaled by the same arithmetic, with each line written the way\n * its own language writes numbers, plurals and measures.\n */\n\nimport type { Language, LanguageChoice } from \"./language.js\";\nimport { EQUIPMENT_NOTE, isEquipmentLine } from \"./equipment.js\";\nimport { detectLanguage, formatAmount, parseIngredient } from \"./quantity.js\";\nimport type { HeldBack, Measure, ParsedIngredient } from \"./quantity.js\";\nimport type { Divisibility, UnitInfo } from \"./units.js\";\nimport {\n QUARTERED_MEASURE,\n approximateEquivalent,\n chooseReadableUnit,\n countsBarePieces,\n demoteUnit,\n formatUnit,\n hasEmbeddedMeasure,\n isSpoonMeasure,\n spanishPlural,\n spanishSingular,\n unitDivisibility,\n} from \"./units.js\";\n\nconst DASH_ONLY = /^[-–—]$/;\n\nconst AL_ENDING = /al$/i;\nconst AUX_ENDING = /aux$/i;\nconst CLOVE = /\\bcloves?\\b/i;\nconst CONSONANT_THEN_Y = /[^aeiou]y$/i;\nconst DOUBLE_S_ENDING = /ss$/i;\nconst EAUX_ENDING = /eaux$/i;\nconst EAU_ENDING = /eau$/i;\nconst F_ENDING = /(?:[^f]f|fe)$/i;\nconst F_OR_FE_ENDING = /fe?$/i;\nconst GARLIC = /\\bgarlic\\b/i;\nconst IES_ENDING = /ies$/i;\nconst LETTER = /\\p{L}/u;\nconst LETTERS_ONLY = /^[A-Za-z]+$/;\n/** A word of letters in any of the alphabets these languages write. */\nconst WORD_LETTERS = /^\\p{L}+$/u;\nconst OF_JOINER = / of /i;\nconst PLURAL_ENDING = /s$|eaux$|aux$/i;\nconst SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;\nconst SIBILANT_LETTER = /[sxz]$/i;\nconst SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;\nconst TRAILING_S_ANY_CASE = /s$/i;\nconst VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;\nconst X_ENDING = /x$/i;\n\nexport type { Language, LanguageChoice } from \"./language.js\";\n\nexport type ScalingKind =\n /** The arithmetic was exact. */\n | \"scaled\"\n /**\n * A countable item was moved to a whole or half unit, or a measurement was\n * demoted to a smaller unit to stay usable.\n */\n | \"rounded\"\n /** The line carries nothing that can be multiplied. */\n | \"unscaled\";\n\nexport interface ScaledIngredient {\n /** The line after scaling, identical to `original` when unscaled. */\n text: string;\n /** The line as published. */\n original: string;\n scaling: ScalingKind;\n /**\n * The scaled quantity, expressed in `unit`, and the lower bound when the line\n * gives a range.\n *\n * Read it together with `unit`, never on its own: a large result is moved to\n * a bigger unit, so scaling \"200 g\" by ten gives an amount of 2 with a unit\n * of \"kg\". The bare number can therefore shrink while the quantity grows.\n */\n amount: number | null;\n /** Upper bound when the line gives a range, null otherwise. */\n amountMax: number | null;\n /** The unit `amount` is in, which may differ from the one the recipe used. */\n unit: string | null;\n /** The language the line was read and rewritten in. */\n language: Language;\n /**\n * Whether the line names a tool.\n *\n * Some sites write what a recipe is cooked with inside the list they write\n * the ingredients in, so a mould or an air fryer arrives among the food. Such\n * a line is never multiplied: a recipe made for more people uses the same one.\n */\n isEquipment: boolean;\n /** Why the line was rounded, clamped or left alone. */\n note?: string;\n}\n\n/**\n * A tool line, as published, with the reason it was left alone.\n *\n * The language is still read, so an answer can say which vocabulary the line\n * was understood in, and no quantity is reported: the number a tool line\n * carries counts tools.\n */\nfunction equipmentIngredient(line: string, language: LanguageChoice = \"auto\"): ScaledIngredient {\n return {\n text: line.trim(),\n original: line,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language: language === \"auto\" ? detectLanguage(line) : language,\n isEquipment: true,\n note: EQUIPMENT_NOTE,\n };\n}\n\n/** Round to a step, keeping two decimals at most. */\nfunction roundTo(value: number, step: number): number {\n return Math.round(Math.round(value / step) * step * 100) / 100;\n}\n\n/**\n * Round a measured amount to something a kitchen scale can show.\n *\n * Large amounts do not need fine precision and small ones do, so the step grows\n * with the value rather than being fixed. The step stays a tenth in the single\n * digits because a unit can be a pound as easily as a gram, and rounding 2.2 lb\n * to 2 would throw away a tenth of the ingredient.\n *\n * A whole number is already what a scale shows, so it is left where the\n * arithmetic put it: 1234 g doubled is 2468 g, and a step of five grams would\n * report 2470 for a product that needed no rounding at all.\n */\nfunction roundMeasured(value: number): number {\n if (Number.isInteger(value)) {\n return value;\n }\n if (value >= 100) {\n return roundTo(value, 5);\n }\n if (value >= 10) {\n return roundTo(value, 1);\n }\n if (value >= 1) {\n return roundTo(value, 0.1);\n }\n return Math.round(value * 100) / 100;\n}\n\n/** Below this there is nothing a kitchen can measure out of a spoonful. */\nconst SMALLEST_USABLE_FRACTION = 0.25;\n\n/** The smallest share of one thing that is still worth putting in a bowl. */\nconst SMALLEST_USABLE: Record<Divisibility, number> = {\n whole: 1,\n half: 0.5,\n quarter: 0.25,\n};\n\n/** True when a number is a whole or a half, to the last bit of precision. */\nfunction isHalfStep(value: number): boolean {\n return Math.abs(value * 2 - Math.round(value * 2)) < 1e-9;\n}\n\n/** Two decimals, which is finer than any kitchen resolves. */\nfunction trim(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\ninterface CountableResult {\n value: number;\n /** The floor was hit, so this line no longer holds its share of the recipe. */\n clamped: boolean;\n}\n\n/**\n * Round a counted thing to an amount a kitchen produces.\n *\n * A count lands on a whole. The one exception is a share that comes out on a\n * half by itself, for a thing a knife can halve: half a clove of garlic is a\n * real amount, and rounding it up to a whole adds a fifth of the garlic to a\n * recipe that asked for five cloves.\n *\n * How finely the thing divides decides the floor. Under that floor the amount\n * is clamped up rather than shrunk towards nothing, which keeps the ingredient\n * in the recipe at the cost of its proportion, and the caller is told through\n * `clamped`. The ceiling stops a shrinking recipe from ever asking for more\n * than it started with.\n */\nfunction roundCountable(\n value: number,\n divisibility: Divisibility,\n ceiling: number,\n): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n const floor = SMALLEST_USABLE[divisibility];\n\n if (divisibility !== \"whole\" && value >= floor && isHalfStep(value)) {\n return { value: trim(value), clamped: false };\n }\n\n if (divisibility === \"whole\") {\n // Below the halfway mark the nearest whole is none, and dropping the\n // ingredient is worse than overstating it, so the line keeps one and says\n // it no longer holds its share.\n if (value < 0.5) {\n return { value: floor, clamped: true };\n }\n return { value: Math.round(value), clamped: false };\n }\n\n if (value < floor) {\n return { value: floor, clamped: true };\n }\n\n if (value < 1) {\n // A knife takes a vegetable to quarters and thirds; anything else offers\n // the half it can be split on.\n const steps = divisibility === \"quarter\" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];\n const candidates = steps.filter(\n (candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor),\n );\n let closest = candidates[0] ?? floor;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: false };\n }\n\n return { value: Math.round(value), clamped: false };\n}\n\n/**\n * Round a spoon or a cup, which a kitchen measures out in halves and in the\n * fractions printed on a measuring set.\n */\nfunction roundSpoon(value: number, ceiling: number): CountableResult {\n if (value <= 0) {\n return { value: 0, clamped: false };\n }\n\n if (value < 1) {\n const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(\n (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION),\n );\n let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;\n for (const candidate of candidates) {\n if (Math.abs(value - candidate) < Math.abs(value - closest)) {\n closest = candidate;\n }\n }\n return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };\n }\n\n return { value: roundTo(value, 0.5), clamped: false };\n}\n\n/**\n * Walk a spoon or a cup down to the smaller spoon while the amount sits under\n * one, so a share is stated in a measure that exists.\n *\n * An amount already on a whole or a half stays where the line put it: half a\n * tablespoon is a spoon a kitchen owns, and there is nothing to gain by calling\n * it a teaspoon and a half.\n */\nfunction stepDownSpoon(unit: UnitInfo, reference: number): { unit: UnitInfo; ratio: number } {\n let current = unit;\n let ratio = 1;\n\n while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {\n const step = demoteUnit(current);\n if (!step) {\n break;\n }\n ratio *= step.per;\n current = step.unit;\n }\n\n return { unit: current, ratio };\n}\n\n/**\n * How close a result has to be to the exact product to be called exact.\n *\n * Two tests, because one of them alone is wrong at some scale. An absolute gap\n * of a hundredth is beneath what a kitchen resolves at ordinary sizes, and at a\n * hundredth of a millilitre it is the whole quantity: 0.006 rounded to 0.01 sits\n * inside the absolute gap while being two thirds larger than what was asked for.\n * A share of half a percent catches that without calling ordinary rounding\n * inexact.\n */\nconst EXACT_WITHIN = 0.01;\nconst EXACT_SHARE = 0.005;\n\nfunction landedExactly(exact: number, amount: number): boolean {\n const gap = Math.abs(exact - amount);\n if (gap > EXACT_WITHIN) {\n return false;\n }\n return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;\n}\n\n/**\n * Whether a bound came out as the exact product, judged in the finer of the two\n * units involved.\n *\n * An absolute tolerance only means what it says at one scale. Read in kilos, a\n * hundredth is ten grams, so a quantity moved from 2468 g to 2470 g passes as\n * exact for being 0.002 kg away; read in grams, the same gap is two grams and\n * the answer is that the value moved.\n */\nfunction boundLandedExactly(bound: ScaledBound): boolean {\n const fine = Math.max(bound.ratio, 1);\n return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));\n}\n\nexport interface ScaleOptions {\n /** Multiplier applied to the quantities. */\n factor: number;\n /**\n * Which language to read the lines in. Left out, each line is read on its\n * own, so a list holding both comes back written the way each line was.\n */\n language?: LanguageChoice;\n}\n\ninterface ScaledBound {\n amount: number;\n /** The exact product, expressed in the unit that came back. */\n exact: number;\n clamped: boolean;\n /** The exact product in the unit the recipe wrote, for a readable note. */\n raw: number;\n /** How many of the unit that came back fit in one of the unit the recipe wrote. */\n ratio: number;\n}\n\ninterface ScaledMeasure {\n /**\n * One bound for a single measure, two for a range, in that order. The shape\n * says so, because every reader takes the first without checking: a measure\n * that scaled to nothing is not a thing this can produce.\n */\n bounds: [ScaledBound, ...ScaledBound[]];\n /** The unit every bound is expressed in, which both ends of a range share. */\n unit: UnitInfo | null;\n}\n\n/**\n * Scale one measure, both ends of a range together.\n *\n * The unit is chosen once, from the smaller end of a range, and applied to\n * every bound.\n *\n * A measurement walks down to a smaller unit before it is rounded, so a\n * quantity divided a thousandfold never rounds to zero and states that the\n * recipe needs none of it.\n */\nfunction scaleMeasure(\n low: number,\n high: number | null,\n unit: UnitInfo | null,\n factor: number,\n divisibility: Divisibility,\n): ScaledMeasure {\n /** What the page published at one end of the measure, and what it scales to. */\n interface End {\n published: number;\n raw: number;\n }\n\n const ends: [End, ...End[]] =\n high === null\n ? [{ published: low, raw: low * factor }]\n : [\n { published: low, raw: low * factor },\n { published: high, raw: high * factor },\n ];\n\n /** Read both ends, keeping the shape every caller of the bounds relies on. */\n const eachEnd = <T>(read: (end: End) => T): [T, ...T[]] => [\n read(ends[0]),\n ...ends.slice(1).map(read),\n ];\n\n const raws = ends.map((end) => end.raw);\n /**\n * The unit is chosen from the smaller end of a range.\n *\n * Both ends have to share one unit, or \"½ to 1 pound\" comes back as \"13 oz to\n * 1.5 pounds\", where the second number reads smaller than the first. Of the\n * two, the smaller end is the one a unit can ruin: choosing from the larger\n * turns \"450 to 1000 g\" into \"0.45 to 1 kg\", and pushed one step further it\n * rounds the small end away entirely. A large number in a small unit is\n * merely long to read.\n */\n const positive = raws.filter((raw) => raw > 0);\n const reference = positive.length > 0 ? Math.min(...positive) : low * factor;\n\n /** Both bounds share one unit, and each keeps the precision that unit affords. */\n const inUnit = (target: UnitInfo, ratio: number): ScaledMeasure => ({\n bounds: eachEnd(({ published, raw }) => {\n const exact = raw * ratio;\n // The rounding happens in the smaller of the two units, so moving to a\n // bigger one never throws away precision the page wrote: 1500 g rounded\n // as kilos is 2, and rounded as grams it is the 1.5 kg a scale shows.\n const rounded =\n ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);\n // At the bottom of a ladder, keep what precision is left rather than\n // deleting the ingredient.\n const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;\n // Rounding to a step of five grams above a hundred can round upwards, and\n // a recipe being made smaller must never come out asking for more than\n // the page published.\n const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;\n return {\n amount: Math.min(usable, ceiling),\n exact,\n clamped: false,\n raw,\n ratio,\n };\n }),\n unit: target,\n });\n\n if (unit && unit.kind === \"measured\") {\n const chosen = chooseReadableUnit(unit, reference);\n return inUnit(chosen.unit, chosen.ratio);\n }\n\n if (unit && isSpoonMeasure(unit)) {\n const stepped = stepDownSpoon(unit, reference);\n // A share stated in the smaller spoon is a measurement, and keeps the\n // precision of one rather than being snapped to the fractions of a spoon\n // it no longer fills.\n if (stepped.ratio !== 1) {\n return inUnit(stepped.unit, stepped.ratio);\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundSpoon(raw, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n }\n\n const bounds = eachEnd(({ published, raw }) => {\n // Scaling down must never end up asking for more than the recipe did.\n const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;\n const rounded = roundCountable(raw, divisibility, ceiling);\n return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };\n });\n return { bounds, unit };\n}\n\n/**\n * How finely a counted thing divides, decided by the size of one of them\n * against what a recipe puts in.\n *\n * `PORTION_SIZED_ITEM` and `QUARTERED_ITEM` are the two ends of that one\n * comparison, and each entry earns its place by where the food falls on it.\n *\n * A shrimp, a mussel, a hazelnut, a peppercorn, a juniper berry, a star anise\n * is already a portion on its own. A recipe counts five, twelve, twenty of\n * them, and a cook taking a share of that recipe puts one fewer in the pan;\n * cutting one in two is not a thing a kitchen does. These land on a whole\n * number.\n *\n * A leg of lamb, a baguette, a camembert, a pineapple, an onion, a watermelon,\n * a guinea fowl sits at the other end: a recipe asks for one or for two, and\n * the share it wants out of one is decided by a knife. A quarter of one is a\n * piece someone serves, and what is left keeps.\n *\n * Both lists carry each food in every language, because where a food falls on\n * that comparison has nothing to do with the words a page uses for it.\n */\nconst PORTION_SIZED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"shrimps?|prawns?|langoustines?|mussels?|hazelnuts?|peppercorns?|junipers?|grains?|anise\" +\n // The same foods, as a French line names them.\n \"|crevettes?|gambas|moules?|noisettes?|genievres?|genevriers?|badianes?|anis\" +\n \")\\\\b\",\n \"iu\",\n);\n\nconst QUARTERED_ITEM = new RegExp(\n \"\\\\b(?:\" +\n \"onions?|shallots?|potatoes|potato|carrots?|apples?|pears?|lemons?|limes?|oranges?\" +\n \"|tomato(?:es)?|cucumbers?|courgettes?|zucchinis?|aubergines?|eggplants?|squash(?:es)?\" +\n \"|pumpkins?|cabbages?|melons?|watermelons?|peppers?|beets?|turnips?|parsnips?\" +\n \"|leeks?|bananas?|mango(?:e?s)?|avocados?\" +\n \"|legs? of lamb|lamb legs?|baguettes?|camemberts?|cheeses?|chorizos?|pineapples?\" +\n \"|peach(?:es)?|apricots?|milk|chickens?|guinea fowls?|roasts?\" +\n // The same produce, as a French line names it.\n \"|oignons?|échalotes?|echalotes?|pommes? de terre|carottes?|pommes?|poires?|citrons?\" +\n \"|tomates?|concombres?|courgettes?|aubergines?|potirons?|choux?|chou|melons?|poivrons?\" +\n \"|betteraves?|navets?|panais|poireaux?|bananes?|mangues?|avocats?|pastèques?|pasteques?\" +\n \"|gigots?|fromages?|chèvres?|chevres?|ananas|pêches?|peches?|abricots?|laits?\" +\n \"|poulets?|pintades?|reblochons?|bûches?|buches?|rôtis?|rotis?\" +\n // The same produce, as a Spanish line names it.\n \"|cebollas?|cebolletas?|chalotas?|patatas?|papas?|zanahorias?|manzanas?|peras?\" +\n \"|limones|limón|limon|limas?|naranjas?|tomates?|pepinos?|calabacines|calabacín|calabacin\" +\n \"|berenjenas?|calabazas?|coles|col|melones|melón|melon|sandías?|sandias?|pimientos?\" +\n \"|remolachas?|nabos?|chirivías?|chirivias?|puerros?|plátanos?|platanos?|mangos?\" +\n \"|aguacates?|piñas?|pinas?|melocotones|melocotón|melocoton|albaricoques?|duraznos?\" +\n \"|quesos?|pollos?|asados?|barras? de pan\" +\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?|huevos?|yemas?\\s+de|claras?\\s+de|clavos?\\s+de)\\b/iu;\n\n/**\n * A piece carved off a bird or off a joint, which stops at the half.\n *\n * The whole animal divides by the knife that portions it, and one of these is\n * already the portion that knife produced: a blanc or a cuisse feeds one, and\n * half of one is the share a smaller recipe serves. Taking a quarter would name\n * a piece no one plates.\n *\n * It reads before the animal, and before the fruit or the vegetable a line\n * often names beside the meat, so that neither answers for the cut.\n */\nconst HALVED_CUT =\n /\\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\\b/iu;\n\n/**\n * How far a \"clove\" divides, when a line counts one.\n *\n * The word names two foods that answer the question in opposite ways. A clove\n * of garlic is a wedge broken off a bulb, the gousse d'ail, and the share of\n * one a recipe asks for is the half a knife makes of it. A clove on its own is\n * the dried flower bud, the clou de girofle, dropped into the pot and fished\n * back out of it: nothing about it is measured, so there is no share of one to\n * take.\n *\n * Garlic named in the line is what separates the two, and that is how the great\n * majority of lines writing the word say which food they mean. The French words\n * need none of this, each naming one food and one only.\n *\n * The question is asked only where the clove is the thing being counted. A head\n * of garlic that mentions its cloves is a head, and divides as one.\n *\n * Null when the line counts no clove at all.\n */\nfunction cloveDivisibility(unit: UnitInfo | null, item: string): Divisibility | null {\n const counted = unit ? unit.canonical === \"clove\" : CLOVE.test(item);\n if (!counted) {\n return null;\n }\n return GARLIC.test(item) ? \"half\" : \"whole\";\n}\n\n/**\n * How far a \"blanc\" divides, when a line names one.\n *\n * The word covers two foods that answer the question in opposite ways. The\n * white of an oeuf goes with the oeuf and the jaune: half of one would have to\n * be beaten and weighed. A blanc de poulet or de dinde is a piece of meat, and\n * half of one is a portion a knife cuts and a fridge keeps.\n *\n * Deciding the word here rather than letting the line fall through is what\n * keeps the fruit or the vegetable such a line often names beside the meat from\n * answering for it.\n *\n * Null when the line names no blanc at all.\n */\nconst BLANC_OF = /\\bblancs?\\s+d(?:e\\s|['’])/iu;\nconst BLANC_OF_EGG = /\\bblancs?\\s+d(?:e\\s|['’])\\s*(?:oeufs?|œufs?)\\b/iu;\n\nfunction blancDivisibility(item: string): Divisibility | null {\n // The noun is the one followed by what it is the blanc of. \"vin blanc\" and\n // \"oignon blanc\" use the same letters as a colour and count as neither.\n if (!BLANC_OF.test(item)) {\n return null;\n }\n return BLANC_OF_EGG.test(item) ? \"whole\" : \"half\";\n}\n\n/**\n * Accents removed and the ligature spelled out, so \"échalote\" and \"echalote\"\n * hit one entry.\n *\n * A word boundary sits between an ASCII letter and a non-letter and nowhere\n * else, so a pattern opening on \"é\" never matches at the start of a word:\n * folding the item is what lets the lists below be written once, in plain\n * letters.\n */\nfunction foldItem(item: string): string {\n return item\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/\\u0153/g, \"oe\")\n .replace(/\\u0152/g, \"OE\");\n}\n\nfunction divisibilityOf(unit: UnitInfo | null, item: string): Divisibility {\n const key = foldItem(item);\n const clove = cloveDivisibility(unit, key);\n if (clove) {\n return clove;\n }\n if (unit && !countsBarePieces(unit)) {\n return unitDivisibility(unit);\n }\n const blanc = blancDivisibility(key);\n if (blanc) {\n return blanc;\n }\n if (WHOLE_ITEM.test(key)) {\n return \"whole\";\n }\n if (PORTION_SIZED_ITEM.test(key)) {\n return \"whole\";\n }\n if (HALVED_ITEM.test(key)) {\n return \"half\";\n }\n if (HALVED_CUT.test(key)) {\n return \"half\";\n }\n if (QUARTERED_MEASURE.test(key)) {\n return \"quarter\";\n }\n return QUARTERED_ITEM.test(key) ? \"quarter\" : \"half\";\n}\n\n/* -------------------------------------------------------------------------- */\n/* Agreement between a number and the thing it counts */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Names of food that read the same whatever the number.\n *\n * Some are mass nouns a recipe counts in spoons rather than in units, and some\n * are plurals already. An -s added to one of them names a thing no shop sells.\n */\nconst INVARIABLE_ITEM = new Set([\n \"anise\",\n \"ananas\",\n \"asparagus\",\n \"bacon\",\n \"basil\",\n \"beef\",\n \"bison\",\n \"broccoli\",\n \"butter\",\n \"celery\",\n \"cilantro\",\n \"citrus\",\n \"cinnamon\",\n \"cocoa\",\n \"cod\",\n \"coriander\",\n \"corn\",\n \"cornstarch\",\n \"couscous\",\n \"cream\",\n \"deer\",\n \"fish\",\n \"flour\",\n \"gambas\",\n \"garlic\",\n \"ginger\",\n \"ham\",\n \"honey\",\n \"hummus\",\n \"jus\",\n \"kale\",\n \"lamb\",\n \"macaroni\",\n \"milk\",\n \"miso\",\n \"musk\",\n \"moose\",\n \"mutton\",\n \"nutmeg\",\n \"oil\",\n \"okra\",\n \"oregano\",\n \"parsley\",\n \"pasta\",\n \"pork\",\n \"quinoa\",\n \"rice\",\n \"rosemary\",\n \"saffron\",\n \"salmon\",\n \"salt\",\n \"shrimp\",\n \"spinach\",\n \"squid\",\n \"sugar\",\n \"thyme\",\n \"tofu\",\n \"trout\",\n \"tuna\",\n \"vanilla\",\n \"veal\",\n \"venison\",\n \"vinegar\",\n \"water\",\n \"yeast\",\n \"yogurt\",\n]);\n\n/** Names whose plural the ordinary English rules get wrong. */\nconst IRREGULAR_PLURAL: Record<string, string> = {\n calf: \"calves\",\n chili: \"chilies\",\n chilli: \"chillies\",\n goose: \"geese\",\n half: \"halves\",\n knife: \"knives\",\n leaf: \"leaves\",\n loaf: \"loaves\",\n mango: \"mangoes\",\n potato: \"potatoes\",\n shelf: \"shelves\",\n tomato: \"tomatoes\",\n};\n\nconst IRREGULAR_SINGULAR: Record<string, string> = Object.fromEntries(\n Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one]),\n);\n\n/**\n * Keep the capitalisation the line used while looking the word up in lower\n * case.\n *\n * A page writes its ingredients in whichever case it likes, and the shape of a\n * word is the page's rather than this server's: a list set in capitals reads as\n * a list somebody typed over when one line of it comes back in lower case.\n * Three shapes carry: all capitals, one leading capital, and everything else,\n * which keeps the replacement as the vocabulary spells it.\n */\nfunction matchCase(source: string, replacement: string): string {\n const hasLetter = LETTER.test(source);\n if (hasLetter && source === source.toUpperCase()) {\n return replacement.toUpperCase();\n }\n const first = source.slice(0, 1);\n if (\n first !== \"\" &&\n first === first.toUpperCase() &&\n source.slice(1) === source.slice(1).toLowerCase()\n ) {\n return (replacement[0] ?? \"\").toUpperCase() + replacement.slice(1);\n }\n return replacement;\n}\n\nfunction toEnglishPlural(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_PLURAL[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (SIBILANT_ENDING.test(word)) {\n return `${word}es`;\n }\n if (CONSONANT_THEN_Y.test(word)) {\n return `${word.slice(0, -1)}ies`;\n }\n if (F_ENDING.test(word)) {\n return `${word.replace(F_OR_FE_ENDING, \"\")}ves`;\n }\n return `${word}s`;\n}\n\nfunction toEnglishSingular(word: string): string {\n const key = word.toLowerCase();\n if (INVARIABLE_ITEM.has(key)) {\n return word;\n }\n const irregular = IRREGULAR_SINGULAR[key];\n if (irregular) {\n return matchCase(word, irregular);\n }\n if (IES_ENDING.test(word) && word.length > 4) {\n return `${word.slice(0, -3)}y`;\n }\n // A -ves plural belongs to a noun ending in -f or -fe, and those are named\n // one by one in `IRREGULAR_PLURAL`: turning every -ves back into -f makes a\n // \"clof\" out of \"cloves\" and an \"olif\" out of \"olives\".\n if (SIBILANT_PLURAL.test(word)) {\n return word.slice(0, -2);\n }\n // \"glass\", \"molasses\": the -s belongs to the singular. Beyond the doubled -s\n // the ending settles nothing, \"couscous\" and \"kiwis\" both closing on -us, so\n // the names that carry their -s are named in `INVARIABLE_ITEM`.\n if (DOUBLE_S_ENDING.test(word)) {\n return word;\n }\n if (TRAILING_S_ANY_CASE.test(word)) {\n return word.slice(0, -1);\n }\n return word;\n}\n\n/**\n * Make an English item agree with its number, in both directions.\n *\n * English marks the plural above one, so \"5 egg yolks\" divided reads \"1 egg\n * yolk\" and \"1 loaf\" tripled reads \"3 loaves\". Only the head noun is touched,\n * which is the last word before any comma: everything after a comma is\n * preparation, and \"cloves garlic, minced\" must not become \"minceds\".\n *\n * \"of\" moves the head to the left of it. What is being counted in \"bottles of\n * orange blossom water\" is the bottles, and the number says nothing about the\n * water.\n */\nfunction agreeInEnglish(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const comma = item.indexOf(\",\");\n const head = comma < 0 ? item : item.slice(0, comma);\n const tail = comma < 0 ? \"\" : item.slice(comma);\n\n const preposition = OF_JOINER.exec(head);\n if (preposition) {\n const counted = agreeInEnglish(head.slice(0, preposition.index), amount);\n return `${counted}${head.slice(preposition.index)}${tail}`;\n }\n\n const words = head.trimEnd().split(\" \");\n const last = words.at(-1) ?? \"\";\n if (!LETTERS_ONLY.test(last) || last.length <= 2) {\n return item;\n }\n\n const wantsPlural = amount > 1;\n const plural = toEnglishPlural(last);\n const singular = toEnglishSingular(last);\n const isPlural = last.toLowerCase() !== singular.toLowerCase();\n\n if (wantsPlural && !isPlural) {\n words[words.length - 1] = plural;\n } else if (!wantsPlural && isPlural) {\n words[words.length - 1] = singular;\n } else {\n return item;\n }\n\n return `${words.join(\" \")}${tail}`;\n}\n\n/**\n * French nouns carrying a final -s, -x or -z in the singular.\n *\n * The word is the same whatever the number, so the ending a plural would give\n * back belongs to the singular and must stay.\n */\n/**\n * French nouns in -ou whose plural takes an -x.\n *\n * The ending settles nothing on its own: \"clou\" and \"trou\" take the ordinary\n * -s, and only these few take the -x. A cabbage is the one a recipe counts, and\n * the others are here because the list is the rule.\n */\nconst FRENCH_OU_PLURAL_IN_X = new Set([\n \"bijou\",\n \"caillou\",\n \"chou\",\n \"genou\",\n \"hibou\",\n \"joujou\",\n \"pou\",\n]);\n\nconst INVARIABLE_FRENCH_NOUN = new Set([\n \"ananas\",\n \"anis\",\n \"brebis\",\n \"cassis\",\n \"colis\",\n \"coulis\",\n \"couscous\",\n \"gambas\",\n \"houmous\",\n \"jus\",\n \"mais\",\n \"pastis\",\n \"pois\",\n \"radis\",\n \"ris\",\n \"souris\",\n \"tamis\",\n \"tapas\",\n]);\n\n/**\n * Adjectives a French recipe puts after the noun, and which take a plain -s.\n *\n * A French adjective agrees with the noun it qualifies, so \"1 piment entier\"\n * counted four times reads \"4 piments entiers\". Only this list is declined: an\n * unknown trailing word can be a brand (\"Golden\"), a proper noun (\"Cayenne\") or\n * a phrase whose head sits elsewhere, and a word left as the recipe wrote it\n * reads as faithful where an invented ending reads as wrong.\n */\nconst AGREEABLE_ADJECTIVES = new Set([\n \"entier\",\n \"entiere\",\n \"etoile\",\n \"etoilee\",\n \"moyen\",\n \"moyenne\",\n \"petit\",\n \"petite\",\n \"grand\",\n \"grande\",\n \"gros\",\n \"grosse\",\n \"mur\",\n \"mure\",\n \"vert\",\n \"verte\",\n \"rouge\",\n \"jaune\",\n \"noir\",\n \"noire\",\n \"blanc\",\n \"blanche\",\n \"rond\",\n \"ronde\",\n \"hache\",\n \"hachee\",\n \"coupe\",\n \"coupee\",\n \"rape\",\n \"rapee\",\n \"pele\",\n \"pelee\",\n \"epluche\",\n \"epluchee\",\n \"denoyaute\",\n \"denoyautee\",\n \"emince\",\n \"emincee\",\n]);\n\n/** Lowercase and strip accents, so \"entière\" and \"entiere\" hit the same entry. */\nfunction foldWord(word: string): string {\n return word.toLowerCase().normalize(\"NFD\").replace(/[̀-ͯ]/g, \"\");\n}\n\n/** The trailing adjective agreed with the count, or null when it is left alone. */\nfunction agreeTrailingAdjective(word: string, wantsPlural: boolean): string | null {\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\");\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return null;\n }\n if (wantsPlural === isPlural) {\n return null;\n }\n\n return wantsPlural ? `${word}s` : singular;\n}\n\n/**\n * The head of a French noun phrase, put in the number its count asks for.\n *\n * The marks are not uniform: \"morceau\" and \"chou\" take -x where an ordinary\n * noun takes -s, \"bocal\" takes -aux, a word ending in -s, -x or -z takes no\n * mark at all, and \"ananas\" or \"couscous\" carry their -s in the singular. A\n * head already in the number wanted is left as written.\n */\nfunction agreeFrenchHead(\n head: string,\n number: { wantsPlural: boolean; isPlural: boolean; ouPlural: boolean },\n): string {\n const { wantsPlural, isPlural, ouPlural } = number;\n\n if (wantsPlural && !isPlural) {\n if (SIBILANT_LETTER.test(head)) {\n return head;\n }\n if (EAU_ENDING.test(head)) {\n return `${head}x`;\n }\n if (FRENCH_OU_PLURAL_IN_X.has(foldWord(head))) {\n return `${head}x`;\n }\n if (AL_ENDING.test(head)) {\n return `${head.slice(0, -2)}aux`;\n }\n return `${head}s`;\n }\n\n if (!wantsPlural && isPlural) {\n if (EAUX_ENDING.test(head)) {\n return head.slice(0, -1);\n }\n if (AUX_ENDING.test(head)) {\n return `${head.slice(0, -3)}al`;\n }\n if (ouPlural) {\n return head.slice(0, -1);\n }\n if (INVARIABLE_FRENCH_NOUN.has(foldWord(head))) {\n return head;\n }\n return head.slice(0, -1);\n }\n\n return head;\n}\n\n/**\n * Make a French item agree with its number, in both directions.\n *\n * French takes the plural from two onwards, so \"2 oeufs\" halved reads \"1 oeuf\"\n * and \"1 brioche\" tripled reads \"3 brioches\". Only the head word is touched,\n * and only its trailing \"s\": nouns already ending in -s, -x or -z are\n * invariable in the plural (\"ananas\", \"choux\"), and forcing one would be worse\n * than leaving the word as the recipe wrote it.\n *\n * Going back down needs `INVARIABLE_FRENCH_NOUN`, because the ending settles\n * nothing on its own: \"jus\" and \"clous\" both end in -us, and the first is a\n * singular where the second is a plural of \"clou\".\n */\nfunction agreeInFrench(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 3) {\n return item;\n }\n\n const wantsPlural = amount >= 2;\n // A final -x marks the plural of the few nouns in -ou that take one, and it\n // belongs to the singular of everything else (\"prix\", \"houx\").\n const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord(head.slice(0, -1)));\n const isPlural = PLURAL_ENDING.test(head) || ouPlural;\n\n words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });\n\n const last = words.length - 1;\n const trailing = last > 0 ? words[last] : undefined;\n if (trailing !== undefined) {\n const adjective = agreeTrailingAdjective(trailing, wantsPlural);\n if (adjective) {\n words[last] = adjective;\n }\n }\n\n return words.join(\" \");\n}\n\n/**\n * Agree the adjective a line put in front of its measure, as in the \"grosse\" of\n * \"1 grosse pincée\".\n *\n * The word qualifies the measure, so in French it takes the number the measure\n * takes: \"2 grosses pincées\". English adjectives do not decline, and a French\n * word outside the declinable list stays as the recipe wrote it, for the same\n * reason it does after the noun.\n */\nfunction agreeLeadingAdjective(word: string, amount: number, language: Language): string {\n if (language === \"en\") {\n return word;\n }\n if (language === \"es\") {\n return agreeSpanishAdjective(word, amount);\n }\n\n const wantsPlural = amount >= 2;\n const folded = foldWord(word);\n const isPlural = folded.endsWith(\"s\") && !AGREEABLE_ADJECTIVES.has(folded);\n const singular = isPlural ? word.slice(0, -1) : word;\n\n if (!AGREEABLE_ADJECTIVES.has(foldWord(singular))) {\n return word;\n }\n if (wantsPlural === isPlural) {\n return word;\n }\n return wantsPlural ? `${singular}s` : singular;\n}\n\nfunction agreeWithAmount(item: string, amount: number, language: Language): string {\n if (language === \"fr\") {\n return agreeInFrench(item, amount);\n }\n return language === \"es\" ? agreeInSpanish(item, amount) : agreeInEnglish(item, amount);\n}\n\n/**\n * Agree a Spanish item with the number in front of it.\n *\n * Spanish marks the plural on the noun and on every adjective that follows it,\n * so \"1 diente de ajo picado\" becomes \"2 dientes de ajo picado\": the head word\n * counts, and what stands after the partitive names the food rather than being\n * counted. The head is therefore the only word the number reaches, and the\n * adjective sitting immediately after it agrees with it.\n */\nfunction agreeInSpanish(item: string, amount: number): string {\n if (!item) {\n return item;\n }\n\n const words = item.split(\" \");\n const head = words[0] ?? \"\";\n if (head.length <= 2 || !WORD_LETTERS.test(head)) {\n return item;\n }\n\n const wantsPlural = amount > 1;\n const singular = spanishSingular(head);\n const isPlural = head.toLowerCase() !== singular.toLowerCase();\n if (wantsPlural === isPlural) {\n return item;\n }\n\n words[0] = wantsPlural ? spanishPlural(head) : singular;\n\n // Every adjective standing after the head takes the same number, and a\n // partitive ends the run: what follows one names the food, \"2 dientes de ajo\n // picado\", where the head's own adjectives describe the head, \"2 nueces\n // moscadas molidas\".\n for (let index = 1; index < words.length; index += 1) {\n const word = words[index] ?? \"\";\n if (!WORD_LETTERS.test(word) || SPANISH_PARTITIVE.test(word)) {\n break;\n }\n const adjectiveSingular = spanishSingular(word);\n const adjectiveIsPlural = word.toLowerCase() !== adjectiveSingular.toLowerCase();\n if (wantsPlural !== adjectiveIsPlural) {\n words[index] = wantsPlural ? spanishPlural(word) : adjectiveSingular;\n }\n }\n\n return words.join(\" \");\n}\n\n/** The words that mark what follows as the food rather than as a description. */\nconst SPANISH_PARTITIVE = /^(?:de|del|con|sin|para|al|y|en|o)$/i;\n\n/**\n * Agree a Spanish adjective standing between the amount and the measure, as in\n * \"1 cucharada colmada\" taken to \"2 cucharadas colmadas\".\n *\n * Only the number changes. The gender belongs to the measure the adjective\n * qualifies, and it is already written the way the page wrote it.\n */\nfunction agreeSpanishAdjective(word: string, amount: number): string {\n if (!WORD_LETTERS.test(word)) {\n return word;\n }\n const wantsPlural = amount > 1;\n const singular = spanishSingular(word);\n const isPlural = word.toLowerCase() !== singular.toLowerCase();\n if (wantsPlural === isPlural) {\n return word;\n }\n return wantsPlural ? spanishPlural(word) : singular;\n}\n\n/**\n * Agree the container a line counts, which is the word the item opens with.\n *\n * \"2 (14 oz) cans tomatoes\" counts tins and names what is in them, so the\n * number belongs to the tin. English otherwise agrees the last word before a\n * comma, which would put the plural mark on the tomatoes and leave the tin\n * reading as one.\n */\nfunction agreeCountedContainer(item: string, amount: number, language: Language): string {\n if (language !== \"en\") {\n return agreeWithAmount(item, amount, language);\n }\n const space = item.indexOf(\" \");\n if (space < 0) {\n return agreeInEnglish(item, amount);\n }\n return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;\n}\n\n/**\n * \"de\" becomes \"d'\" before a vowel sound.\n *\n * The h is the hard case: it is silent in \"huile\" and sounded in \"haricot\", and\n * only a word list separates them. Elision is therefore limited to vowels plus\n * the handful of h-words a recipe actually uses, because \"de haricots\" merely\n * reads as careless while \"d'haricots\" reads as wrong.\n */\nconst MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\\b/i;\n\n/**\n * Put the item back after the measure, the way the language joins them.\n *\n * French needs the partitive: \"6 cuillères à soupe **de** beurre\". English puts\n * the two side by side: \"6 tablespoons butter\".\n */\nfunction joinItem(item: string, language: Language, partitive: boolean): string {\n if (!item) {\n return \"\";\n }\n // A page writes the partitive where it chose to: \"2 tazas de arroz\" and \"2\n // tazas arroz\" are both Spanish, and putting one back where the page wrote\n // none rewrites the line rather than rescaling it.\n if (language === \"en\" || !partitive) {\n return ` ${item}`;\n }\n // Spanish writes the partitive in full whatever follows it, so there is no\n // elision to decide: \"6 cucharadas de aceite\".\n if (language === \"es\") {\n return ` de ${item}`;\n }\n const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);\n return elides ? ` d'${item}` : ` de ${item}`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Scaling one line */\n/* -------------------------------------------------------------------------- */\n\n/** How a line writes the choice between two quantities, in each language. */\nconst BRANCH_SEPARATORS: Record<Language, RegExp> = {\n en: /\\s+or\\s+/gi,\n fr: /\\s+ou\\s+/gi,\n es: /\\s+o\\s+/gi,\n};\n\ninterface Branch {\n head: string;\n /** The separator as published, so the rewrite reads the way the line did. */\n separator: string;\n tail: string;\n}\n\n/**\n * Split a line that offers one ingredient twice, at the word that offers the\n * choice.\n *\n * The search starts where the item name starts, so the \"or\" of a published\n * range such as \"2 or 3 cloves garlic\" is left to the range parser. A branch\n * counts only when it carries a quantity of its own; \"butter or margarine\"\n * names one amount and stays one line.\n */\nfunction splitBranch(text: string, parsed: ParsedIngredient): Branch | null {\n const itemStart = parsed.item ? text.indexOf(parsed.item) : text.length;\n if (itemStart < 0) {\n return null;\n }\n\n const separator = BRANCH_SEPARATORS[parsed.language];\n separator.lastIndex = 0;\n for (let match = separator.exec(text); match; match = separator.exec(text)) {\n if (match.index < itemStart) {\n continue;\n }\n const tail = text.slice(match.index + match[0].length);\n if (parseIngredient(tail, parsed.language).amount === null) {\n continue;\n }\n return { head: text.slice(0, match.index), separator: match[0], tail };\n }\n return null;\n}\n\n/**\n * Scale one ingredient line.\n *\n * Countable items are rounded to something a kitchen can measure, ranges are\n * scaled at both ends, equivalents and alternatives are scaled with the amount\n * they stand beside, and an approximate measure such as a pinch has its count\n * scaled with a note saying how loosely one of them is defined.\n */\nexport function scaleIngredient(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n // Read before the quantity, because a tool line can carry one: \"2 moldes de\n // 20 cm\" counts moulds, and how many a recipe needs at once is not something\n // the number of eaters changes.\n if (isEquipmentLine(line)) {\n return equipmentIngredient(line, options.language);\n }\n // A factor of one changes nothing, and rewriting the line anyway would round\n // \"178 ml\" to \"180 ml\" and report a difference the caller never asked for.\n if (factor === 1) {\n return passthroughIngredient(line, options.language);\n }\n\n const text = line.trim();\n const parsed = parseIngredient(text, options.language ?? \"auto\");\n const branch = splitBranch(text, parsed);\n if (branch) {\n return scaleBranchedLine(line, branch, { ...options, language: parsed.language });\n }\n\n return scaleSingleLine(line, options);\n}\n\n/**\n * Scale a line that offers a choice, one branch at a time.\n *\n * A cook follows one branch and ignores the other, so both have to carry the\n * same share of the recipe: a doubled line whose second branch still reads as\n * published hands whoever takes it half the ingredient. The two branches name\n * different things, and how far one stands for the other is the page's claim\n * rather than arithmetic, so such a line is never reported as exact.\n */\nfunction scaleBranchedLine(line: string, branch: Branch, options: ScaleOptions): ScaledIngredient {\n const head = scaleSingleLine(branch.head, options);\n if (head.scaling === \"unscaled\") {\n return { ...head, text: line.trim(), original: line };\n }\n\n const tail = scaleAlternative(branch.tail, options);\n const result: ScaledIngredient = {\n ...head,\n original: line,\n text: `${head.text}${branch.separator}${tail.text}`,\n scaling: \"rounded\",\n };\n\n const branchNote = tail.rewritten\n ? \"This line offers a choice between two quantities, and each was scaled on its own. \" +\n \"How far one stands for the other is the page's own claim.\"\n : \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\";\n result.note = head.note ? `${head.note} ${branchNote}` : branchNote;\n\n return result;\n}\n\n/**\n * Scale the branch a line offers as an alternative, when it can be stated in\n * the measure the line offered it in.\n *\n * Under one of that measure the branch would have to be restated in another\n * one, which changes the shape of the choice the cook is being handed, so it\n * keeps its published wording and the line says that it did.\n */\nfunction scaleAlternative(\n tail: string,\n options: ScaleOptions,\n): { text: string; rewritten: boolean } {\n const parsed = parseIngredient(tail, options.language ?? \"auto\");\n const published = tail.trim();\n if (parsed.amount === null) {\n return { text: published, rewritten: false };\n }\n\n const largest = (parsed.amountMax ?? parsed.amount) * options.factor;\n if (largest < 1) {\n return { text: published, rewritten: false };\n }\n\n return { text: scaleIngredient(tail, options).text, rewritten: true };\n}\n\n/**\n * The equivalents beside a measure, written the way the line wrote them.\n *\n * A trailing restatement is printed after the item rather than beside the\n * amount, so it carries no label here.\n */\nfunction alternateLabel(\n count: number,\n style: string | null,\n texts: readonly string[],\n bracketed: string,\n): string {\n if (count === 0) {\n return \"\";\n }\n if (style === \"slash\") {\n return ` / ${texts.join(\" / \")}`;\n }\n if (style === \"trailing\") {\n return \"\";\n }\n return bracketed;\n}\n\n/**\n * What follows the amount: the partitive French puts between a measure and what\n * it measures, or the counted item itself, which stands straight after its\n * number in every language and agrees with it.\n */\nfunction itemLabelFor(\n named: UnitInfo | null,\n item: string,\n counted: string,\n language: Language,\n partitive: boolean,\n): string {\n if (named) {\n return joinItem(item, language, partitive);\n }\n if (counted) {\n return ` ${counted}`;\n }\n return \"\";\n}\n\n/** What the answer says about a line beyond the figure it came to. */\ninterface LineOutcome {\n parsed: ParsedIngredient;\n bounds: readonly ScaledBound[];\n unit: UnitInfo | null;\n low: ScaledBound;\n clamped: ScaledBound | null;\n movedPrimary: boolean;\n movedAlternate: boolean;\n restated: boolean;\n collapsed: boolean;\n language: Language;\n carriesMore: boolean;\n}\n\n/**\n * Everything the answer owes a caller about one scaled line, in one string.\n *\n * The reasons stack: a line can be clamped, carry a second quantity, collapse\n * its range and state an approximation, and each has to be said without\n * cancelling the others.\n */\nfunction noteForScaledLine(outcome: LineOutcome): string | undefined {\n const {\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n } = outcome;\n let note: string | undefined;\n\n /**\n * The exact product, written for a note.\n *\n * Decimals rather than kitchen fractions: this number exists to be compared\n * against the one on the line, and a fraction snapped from 0.32 to \"1/3\"\n * reads as the exact product while being a different number.\n */\n const asPublished = (value: number, source: UnitInfo | null) =>\n `${formatAmount(value, language, { fractions: false })}${\n source ? ` ${formatUnit(source, value, language)}` : \"\"\n }`;\n\n const sentences: string[] = [];\n\n if (clamped) {\n sentences.push(\n `Clamped up to ${formatAmount(clamped.amount, language)} from ` +\n `${formatAmount(Math.round(clamped.raw * 1000) / 1000, language)}, the smallest amount ` +\n \"worth measuring. This line no longer holds its share of the recipe.\",\n );\n } else if (movedPrimary) {\n // Every bound that moved is named, with the direction it moved in. On a\n // range the two ends can move opposite ways, and reporting one of them as\n // though it spoke for both states the wrong direction for half the\n // quantity.\n const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));\n sentences.push(\n moved\n .map(\n (bound) =>\n `Rounded ${bound.amount > bound.exact ? \"up\" : \"down\"} from ` +\n `${asPublished(bound.raw, parsed.unit)}.`,\n )\n .join(\" \"),\n );\n } else if (movedAlternate) {\n // The amount itself came out exact, and only the equivalent beside it had\n // to move. Saying \"rounded from 300 g\" when 300 g is exact would send a\n // cook looking for an error that is not there.\n sentences.push(\n `The amount is exact; the equivalent ${\n restated ? \"beside it\" : \"in brackets\"\n } was rounded to stay readable.`,\n );\n } else if (restated) {\n sentences.push(\n \"This line states one quantity twice, and both readings were scaled. \" +\n \"They agree as closely as the page wrote them, and no closer.\",\n );\n }\n\n // A line can offer a substitute with its own amount, as in \"1 Tbsp vanilla\n // sugar OR 1 tsp vanilla extract\". Only the amount the line opens with is\n // scaled, and a substitute left at its published size contradicts it. This is\n // said whatever else happened to the line: a line that was also rounded is the\n // one where a stale second quantity is hardest to spot.\n if (carriesMore) {\n sentences.push(\n \"This line carries a further quantity after the first one, and only the first was scaled. \" +\n \"Read the rest as published.\",\n );\n }\n\n if (parsed.capacity) {\n sentences.push(\n `${parsed.capacity} is what one container holds, so it stays as published and the count is ` +\n \"what the factor moved. Scaling this line is a matter of opening that many containers of \" +\n \"the size named.\",\n );\n }\n\n if (collapsed) {\n sentences.push(\"The page gave a range, and at this size both ends come to the same amount.\");\n }\n\n // Below what any scale shows, the arithmetic is right and the kitchen cannot\n // follow it. Saying so is the difference between an answer and a number.\n if (unit?.kind === \"measured\" && low.amount > 0 && low.amount < 0.05) {\n sentences.push(\n \"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye.\",\n );\n }\n\n // The page put the amount forward as loose, and multiplying it keeps it that\n // way: the answer is as approximate as the figure it came from.\n if (parsed.approximation) {\n sentences.push(\n \"The page gave this amount as an approximation, and the scaled figure is no firmer.\",\n );\n }\n\n if (sentences.length > 0) {\n note = sentences.join(\" \");\n }\n\n if (parsed.unit && parsed.unit.kind === \"approximate\") {\n note = withApproximateNote(parsed.unit, note);\n }\n\n // A line that wrote its amount as a word says which word it was, so a caller\n // can see the figure came from the grammar rather than from a digit.\n if (parsed.articleWord) {\n // `amount` carries the product once a word such as \"dozen\" has multiplied\n // it, and quoting that back would credit the article with a figure it never\n // gave.\n const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);\n const read = `\"${parsed.articleWord}\" read as ${formatAmount(stood, language)}.`;\n note = note ? `${read} ${note}` : read;\n }\n\n return note;\n}\n\n/**\n * The line as it reads once scaled: the amount, its measure, what the container\n * holds, the equivalents beside it, and the item they belong to.\n *\n * The size word the page put in front of its measure goes back in front of it,\n * and the equivalents go back the way the line offered them, since a slash and\n * a bracket are two different ways of restating one quantity.\n */\nfunction renderScaledLine(\n parsed: ParsedIngredient,\n alternates: ReadonlyArray<{ text: string }>,\n unit: UnitInfo | null,\n shown: number,\n language: Language,\n): {\n named: UnitInfo | null;\n unitLabel: string;\n capacityLabel: string;\n altLabel: string;\n trailingLabel: string;\n itemLabel: string;\n} {\n const named = unit && !countsBarePieces(unit) ? unit : null;\n // The size word goes back where the page had it, in front of the measure or\n // behind it: the page asked for a grosse pincée and for a cucharada colmada,\n // and neither a pincée nor a cucharada is the same ask.\n const adjective =\n named && parsed.measureAdjective\n ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}`\n : \"\";\n const measure = named ? ` ${formatUnit(named, shown, language)}` : \"\";\n const unitLabel = parsed.measureAdjectiveFollows\n ? `${measure}${adjective}`\n : `${adjective}${measure}`;\n const alternateTexts = alternates.map((entry) => entry.text);\n const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : \"\";\n const bracketed = alternates.length === 0 ? \"\" : ` (${introduced}${alternateTexts.join(\" / \")})`;\n // Equivalents go back where the line offered them: inside brackets beside the\n // amount, after a slash, or in the bracket the line closes on.\n const altLabel = alternateLabel(\n alternates.length,\n parsed.alternateStyle,\n alternateTexts,\n bracketed,\n );\n const trailingLabel = parsed.alternateStyle === \"trailing\" ? bracketed : \"\";\n // What a container holds is the page's figure and not the factor's, so it\n // goes back between the count and the container, exactly as published.\n const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : \"\";\n // A measure needs the partitive that French puts between it and what it\n // measures. A counted item stands straight after its number in both\n // languages, and agrees with it: \"1 egg yolk\", \"3 brioches\".\n const counted = parsed.capacity\n ? agreeCountedContainer(parsed.item, shown, language)\n : agreeWithAmount(parsed.item, shown, language);\n const itemLabel = itemLabelFor(named, parsed.item, counted, language, parsed.itemPartitive);\n\n return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };\n}\n\n/** Why a line showing a figure came back as the page published it. */\nconst HELD_BACK_NOTE: Record<HeldBack, string> = {\n sizeQualifier:\n \"The figures here give the size of one item rather than how many, so the line is \" +\n \"left as published.\",\n itemSize:\n \"The measure standing behind the item gives the size of one of them rather than how many, \" +\n \"so the line is left as published. Serving more people is a matter of taking a bigger one, \" +\n \"and serving fewer a smaller one.\",\n containerSize:\n \"The measure here gives what one container holds rather than how many of them the recipe \" +\n \"uses, and the line states no count, so it is left as published. Scaling it is a matter of \" +\n \"opening that many containers of the size named.\",\n perPerson:\n \"This line already states an amount for one person, and the factor is what changes \" +\n \"how many people the recipe serves, so the line is left as published.\",\n duration:\n \"This line states a length of time rather than an amount of an ingredient, so it is left as \" +\n \"published. A rest, a proof or a bake takes as long for a large batch as for a small one, \" +\n \"and the factor has nothing to say about it.\",\n ambiguousDecimal:\n \"The comma in this number marks thousands in one convention and the decimal point in \" +\n \"another, and the line gives no sign which was meant, so it is left as published.\",\n};\n\nfunction scaleSingleLine(line: string, options: ScaleOptions): ScaledIngredient {\n const { factor } = options;\n const parsed = parseIngredient(line, options.language ?? \"auto\");\n const language = parsed.language;\n\n if (parsed.amount === null || parsed.heldBack) {\n return {\n text: parsed.original,\n original: parsed.original,\n scaling: \"unscaled\",\n amount: null,\n amountMax: null,\n unit: null,\n language,\n isEquipment: false,\n note: parsed.heldBack\n ? HELD_BACK_NOTE[parsed.heldBack]\n : \"No quantity given; adjust to taste.\",\n };\n }\n\n const divisibility = divisibilityOf(parsed.unit, parsed.item);\n const primary = scaleMeasure(parsed.amount, parsed.amountMax, parsed.unit, factor, divisibility);\n const alternates = parsed.alternates.map((measure) => renderMeasure(measure, factor, language));\n\n const primaryBounds = primary.bounds;\n const alternateBounds = alternates.flatMap((entry) => entry.bounds);\n const movedPrimary = primaryBounds.some((bound) => !boundLandedExactly(bound));\n const movedAlternate = alternateBounds.some((bound) => !boundLandedExactly(bound));\n const clamped = [...primaryBounds, ...alternateBounds].find((bound) => bound.clamped) ?? null;\n // Two figures beside each other agree only as closely as the page wrote\n // them, and multiplying both keeps that gap rather than closing it.\n const restated = parsed.alternateStyle === \"slash\";\n const carriesMore = hasEmbeddedMeasure(parsed.item, language);\n // A further quantity beside a mass or a volume restates or qualifies it, and\n // it did not move, so the line as a whole is not the exact product of what\n // the page published. Beside a counted container the same figure gives the\n // size of one of them, which is a thing the factor must leave alone: \"2\n // boîtes de 400 g\" is exactly twice \"1 boîte de 400 g\".\n const leftover = carriesMore && parsed.unit?.kind === \"measured\";\n\n const low = primaryBounds[0];\n const high = primaryBounds[1] ?? null;\n const unit = primary.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n // A range whose two ends land on the same amount stopped being a range. \"1 to\n // 1 clove\" is not something a cook reads, so the line states the one amount\n // both ends came to.\n const collapsed = high !== null && high.amount === low.amount;\n const amountText = renderRange(\n asText(low.amount),\n high === null || collapsed ? null : asText(high.amount),\n parsed.rangeSeparator,\n );\n // \"ea\" announces that the figure counts pieces, and names no measure of them,\n // so the line reads as the count of the thing itself and the marker has\n // nothing to say in it.\n const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(\n parsed,\n alternates,\n unit,\n shown,\n language,\n );\n const result: ScaledIngredient = {\n text: `${parsed.decoration ? `${parsed.decoration} ` : \"\"}${parsed.approximation ?? \"\"}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),\n original: parsed.original,\n scaling: movedPrimary || movedAlternate || restated || leftover ? \"rounded\" : \"scaled\",\n amount: low.amount,\n amountMax: collapsed ? null : (high?.amount ?? null),\n unit: named?.canonical ?? null,\n language,\n isEquipment: false,\n };\n\n const note = noteForScaledLine({\n parsed,\n bounds: primaryBounds,\n unit,\n low,\n clamped,\n movedPrimary,\n movedAlternate,\n restated,\n collapsed,\n language,\n carriesMore,\n });\n if (note !== undefined) {\n result.note = note;\n }\n return result;\n}\n\n/**\n * Say that a measure is held to no better than the hand that produces it, and\n * what a kitchen usually takes one to be.\n *\n * The equivalence belongs in the note. A recipe that asks for four pinches of\n * baking soda has said nothing about teaspoons, and answering in teaspoons\n * would hand back a figure with a precision the page never claimed. The\n * quantity stays in the measure the line used, and the count is what carries\n * the scaling.\n */\nfunction withApproximateNote(unit: UnitInfo, existing: string | undefined): string {\n const equivalence = approximateEquivalent(unit);\n const sentence =\n `A ${unit.canonical} is an approximate measure${equivalence ? `, ${equivalence}` : \"\"}. ` +\n \"The count was scaled and the size of one is the cook's.\";\n return existing ? `${existing} ${sentence}` : sentence;\n}\n\n/**\n * Scale an equivalent the line states beside the amount, and render it the way\n * the line wrote it.\n */\nfunction renderMeasure(\n measure: Measure,\n factor: number,\n language: Language,\n): { text: string; bounds: ScaledBound[] } {\n const scaled = scaleMeasure(\n measure.amount,\n measure.amountMax,\n measure.unit,\n factor,\n divisibilityOf(measure.unit, \"\"),\n );\n const low = scaled.bounds[0];\n const high = scaled.bounds[1] ?? null;\n const unit = scaled.unit;\n const shown = high?.amount ?? low.amount;\n const asText = (value: number) =>\n formatAmount(value, language, { fractions: unit?.kind !== \"measured\" });\n\n return {\n text: `${renderRange(\n asText(low.amount),\n high === null ? null : asText(high.amount),\n measure.rangeSeparator,\n )}${unit ? ` ${formatUnit(unit, shown, language)}` : \"\"}`,\n bounds: scaled.bounds,\n };\n}\n\n/** Keep a range in the shape the recipe wrote it: \"3–4\", \"2 to 3\" or \"2 à 3\". */\nfunction renderRange(low: string, high: string | null, separator: string | null): string {\n if (high === null || separator === null) {\n return low;\n }\n return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;\n}\n\nexport function scaleIngredients(lines: string[], options: ScaleOptions): ScaledIngredient[] {\n return lines.map((line) => scaleIngredient(line, options));\n}\n\n/**\n * A line returned as published, with whatever quantity could be read off it.\n *\n * A line that carries a readable amount is `scaled`, because leaving it alone\n * is what multiplying by one does. A line with no amount at all is `unscaled`\n * and says why.\n */\nexport function passthroughIngredient(\n line: string,\n language: LanguageChoice = \"auto\",\n): ScaledIngredient {\n if (isEquipmentLine(line)) {\n return equipmentIngredient(line, language);\n }\n const parsed = parseIngredient(line, language);\n\n const held = parsed.amount === null || parsed.heldBack !== null;\n const result: ScaledIngredient = {\n text: parsed.original,\n original: parsed.original,\n scaling: held ? \"unscaled\" : \"scaled\",\n amount: held ? null : parsed.amount,\n amountMax: held ? null : parsed.amountMax,\n unit: held ? null : (parsed.unit?.canonical ?? null),\n language: parsed.language,\n isEquipment: false,\n };\n if (parsed.heldBack) {\n result.note = HELD_BACK_NOTE[parsed.heldBack];\n } else if (parsed.amount === null) {\n result.note = \"No quantity given; adjust to taste.\";\n } else if (parsed.unit?.kind === \"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","LEADING_ARTICLE","LEADING_QUANTITY","fraction","written","AL_ENDING","AUX_ENDING","EAUX_ENDING","EAU_ENDING","SIBILANT_LETTER","TRAILING_S_ANY_CASE","bounds"]}
|