@recipe-book/core 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/{chunk-D5CV3UF7.cjs → chunk-ISRDJR2O.cjs} +42 -9
- package/dist/chunk-ISRDJR2O.cjs.map +1 -0
- package/dist/errors.js +29 -1
- package/dist/errors.js.map +1 -1
- package/dist/format/duration.js.map +1 -1
- package/dist/format/ingredient.js.map +1 -1
- package/dist/format/quantity.js.map +1 -1
- package/dist/format/servings.js.map +1 -1
- package/dist/format/temperature.js.map +1 -1
- package/dist/index.cjs +62 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -11
- package/dist/index.d.ts +38 -11
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/quantity/parse.js.map +1 -1
- package/dist/quantity/scale.js.map +1 -1
- package/dist/recipe/ids.js.map +1 -1
- package/dist/recipe/parse.js +11 -6
- package/dist/recipe/parse.js.map +1 -1
- package/dist/resolve.js.map +1 -1
- package/dist/schema-org/from-json-ld.js +16 -23
- package/dist/schema-org/from-json-ld.js.map +1 -1
- package/dist/schema-org/to-json-ld.js.map +1 -1
- package/dist/schema-org.cjs +75 -82
- package/dist/schema-org.cjs.map +1 -1
- package/dist/schema-org.d.cts +3 -2
- package/dist/schema-org.d.ts +3 -2
- package/dist/steps/segment.js.map +1 -1
- package/dist/steps/temperature.js +1 -1
- package/dist/steps/temperature.js.map +1 -1
- package/dist/{types-BknM8KOd.d.cts → types-CKIoWsz8.d.cts} +21 -4
- package/dist/{types-BknM8KOd.d.ts → types-CKIoWsz8.d.ts} +21 -4
- package/dist/units/convert.js.map +1 -1
- package/dist/units/temperature.js.map +1 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-D5CV3UF7.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/quantity/parse.ts"],"sourcesContent":["import {\n describeValue,\n MAX_ECHOED,\n type ParseResult,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport type { Path } from '../issues'\nimport { findUnit, suggestUnit } from '../units/lookup'\nimport { units } from '../units/registry'\nimport { asRecord, checkFields, error, toResult } from '../validation'\nimport { isPositive } from './amount'\nimport type { Amount, Quantity } from './types'\n\n// ¼, ½, ¾ and ⅐ to ⅞, which decompose to their fractions, such as '½' to\n// '1⁄2'. ⅟ has no denominator, and ↉ is zero.\nconst VULGAR = '[¼-¾⅐-⅞]'\n\nconst ARTICLE = /^an?\\s+(?=\\D)/i\n\nlet amountPattern: RegExp | undefined\n\nexport type Problem = {\n code: RecipeIssueCode\n message: string\n suggestion?: string\n}\n\n/**\n * Parses quantity text, such as `'1 ½ cups'`, `'2-3 tbsp'`, `'a pinch'`, or\n * `'3'` for a bare count. Never throws.\n *\n * Accepts whole numbers, decimals, fractions (`1/2`, `½`, also with the\n * fraction slash `⁄`), mixed numbers (`1 1/2`, `1½`) and ranges (`2-3`, `2–3`,\n * `2 to 3`), each followed by an optional unit alias, and `a` or `an` for 1\n * before a unit alias, as in `'a pinch'`.\n *\n * @example\n * parseQuantity('1 ½ cups') // { success: true, data: { amount: 1.5, unit: 'cup' }, warnings: [] }\n *\n * const result = parseQuantity('2 cupz')\n * if (!result.success) {\n * result.error.message // '\"2 cupz\" has 1 problem:\\n ✖ (root) Unknown unit \"cupz\". Did you mean \"cups\"?\\nSee https://recipe-book.siraj.page/errors/invalid_quantity'\n * }\n */\nexport function parseQuantity(text: string): ParseResult<Quantity> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected quantity text such as \"1 ½ cups\", received ${describeValue(text)}.`,\n )\n return toResult<Quantity>('invalid_quantity', undefined, issues)\n }\n\n const quantity = readQuantityText(issues, text, [])\n return toResult('invalid_quantity', quantity, issues, quote(text))\n}\n\n/** Reads a quantity in any input form: text, a number, or `{ amount, unit }`. */\nexport function readQuantity(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity | undefined {\n if (typeof value === 'string') {\n const quantity = readQuantityText(issues, value, path)\n return quantity && readTextAmount(issues, quantity.amount, path)\n ? quantity\n : undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const record = asRecord(value)\n if (record) {\n checkFields(issues, record, { amount: true, unit: true }, path)\n\n const amount = readAmount(issues, record.amount, [...path, 'amount'])\n const { unit } = record\n\n if (unit === undefined) {\n return amount === undefined ? undefined : { amount }\n }\n\n if (typeof unit !== 'string') {\n return error(\n issues,\n 'invalid_type',\n [...path, 'unit'],\n `Expected a unit such as \"g\" or \"cup\", received ${describeValue(unit)}.`,\n )\n }\n\n const id = findUnit(unit)\n\n if (!id) {\n const problem = unknownUnit(\n unit,\n amount === undefined\n ? undefined\n : typeof amount === 'number'\n ? String(amount)\n : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`,\n )\n return error(\n issues,\n problem.code,\n [...path, 'unit'],\n problem.message,\n problem.suggestion,\n )\n }\n\n return amount === undefined ? undefined : { amount, unit: id }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a quantity such as \"2 cups\", 2, or { amount: 2, unit: \"cup\" }, received ${describeValue(value)}.`,\n )\n}\n\nfunction readQuantityText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n): Quantity | undefined {\n const result = parseText(text.replace(/\\s+/g, ' ').trim())\n\n if ('code' in result) {\n return error(issues, result.code, path, result.message, result.suggestion)\n }\n\n return result\n}\n\nfunction parseText(text: string): Quantity | Problem {\n const match = matchAmount(text)\n\n if (!match) {\n const unit = findUnit(text)\n return unit\n ? {\n code: 'invalid_quantity',\n message: `${quote(text)} has no amount. Write a full quantity, e.g. \"1 ${units[unit].labels.short.one}\".`,\n }\n : unreadable(text)\n }\n\n if ('code' in match) {\n return match\n }\n\n const { amount, amountText, rest: unitText } = match\n\n if (unitText === '') {\n return { amount }\n }\n\n if (/\\d/.test(unitText) || !/\\p{L}/u.test(unitText)) {\n return unreadable(text)\n }\n\n const unit = findUnit(unitText)\n if (unit) {\n return { amount, unit }\n }\n\n return unknownUnit(\n unitText,\n amountText.length <= MAX_ECHOED && /^\\d+(?:\\.\\d+)?$/.test(amountText)\n ? amountText\n : quote(amountText),\n )\n}\n\n/**\n * Reads the amount that starts `text`, which is trimmed with single spaces: a\n * number, fraction, mixed number, range, or \"a\" or \"an\" for 1. A comma between\n * digits anywhere in the text, as in \"1,5\", is a problem. Otherwise returns\n * `undefined` when the text doesn't start with an amount, and a problem when\n * the amount is too large to read, isn't greater than 0 (0 is allowed with\n * `allowZero`), or is a range whose low end is above its high end. When it\n * reads one, it returns the amount, its text as written, and the text after it.\n */\nexport function matchAmount(\n text: string,\n allowZero?: boolean,\n): { amount: Amount; amountText: string; rest: string } | Problem | undefined {\n // \"1,5\" is a decimal in much of Europe and \"1,500\" a thousands separator in\n // the US, so neither is guessed.\n if (/\\d,\\d/.test(text)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. \"1.5\", and leave out thousands separators.`,\n }\n }\n\n const article = ARTICLE.exec(text)\n if (article) {\n return { amount: 1, amountText: '1', rest: text.slice(article[0].length) }\n }\n\n if (!amountPattern) {\n // Alternatives are tried in order, so a fraction with an optional whole\n // number, as in \"1 1/2\", \"1/2\", \"1½\" or \"½\", comes before decimals and\n // integers.\n const number = String.raw`(?:\\d+\\s+)?\\d+\\s*[/⁄]\\s*\\d+|(?:\\d+\\s*)?${VULGAR}|\\d+(?:\\.\\d+)?|\\.\\d+`\n amountPattern = new RegExp(\n String.raw`^(${number})(?:(?:\\s*[-–—]\\s*|\\s+to\\s+)(${number}))?`,\n 'i',\n )\n }\n\n const match = amountPattern.exec(text)\n if (!match) {\n return undefined\n }\n\n const [matched, lowText = '', highText] = match\n const low = toNumber(lowText)\n const high = highText === undefined ? low : toNumber(highText)\n\n if (low === Infinity || high === Infinity) {\n return {\n code: 'invalid_amount',\n message: `${quote(text)} has an amount too large to read. Check it for extra digits.`,\n }\n }\n\n if (\n !(isPositive(low) || (allowZero && low === 0)) ||\n !(isPositive(high) || (allowZero && high === 0))\n ) {\n return {\n code: 'invalid_amount',\n message: `Expected a positive amount, received ${quote(text)}.`,\n }\n }\n\n if (low > high) {\n const mixedHint =\n /^\\d+$/.test(lowText) && high < 1\n ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.`\n : ''\n return {\n code: 'invalid_range',\n message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText!)}).${mixedHint}`,\n }\n }\n\n return {\n amount: low === high ? low : { min: low, max: high },\n amountText: matched,\n rest: text.slice(matched.length).trim(),\n }\n}\n\n/**\n * The `unknown_unit` problem. `quantity` is the line's quantity without the\n * unit, as it would be written in `RecipeInput`, for the \"move the word into\n * the name\" fix.\n */\nfunction unknownUnit(unitText: string, quantity: string | undefined): Problem {\n const suggestion = suggestUnit(unitText)\n const move =\n quantity === undefined\n ? 'move the word into the name'\n : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`\n\n return {\n code: 'unknown_unit',\n message: suggestion\n ? `Unknown unit ${quote(unitText)}. Did you mean \"${suggestion}\"?`\n : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,\n ...(suggestion && { suggestion }),\n }\n}\n\nfunction truncateNumber(text: string): string {\n return text.length > 20 ? `${text.slice(0, 19)}…` : text\n}\n\nfunction unreadable(text: string): Problem {\n return {\n code: 'invalid_quantity',\n message:\n text === ''\n ? 'Expected a quantity such as \"1 ½ cups\", received an empty string.'\n : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. \"1 ½ cups\".`,\n }\n}\n\n// Decomposing a glyph is slow, so each glyph's value is read once.\nconst fractions = new Map<string, number>()\n\nfunction toNumber(text: string): number {\n // The `[/⁄]` must match the one in `amountPattern`.\n const ascii = /^(?:(\\d+)\\s+)?(\\d+)\\s*[/⁄]\\s*(\\d+)$/.exec(text)\n if (ascii) {\n const [, whole = '0', numerator, denominator] = ascii\n // A zero denominator reads as NaN, not Infinity, so \"1/0\" isn't too large.\n return Number(whole) + Number(numerator) / (Number(denominator) || NaN)\n }\n\n // Amounts end in a digit or a glyph.\n const last = text.slice(-1)\n if (last > '9') {\n let fraction = fractions.get(last)\n if (fraction === undefined) {\n const [numerator, denominator] = last.normalize('NFKD').split('⁄')\n fraction = Number(numerator) / Number(denominator)\n fractions.set(last, fraction)\n }\n // Before a glyph is a whole number and any spaces, or nothing, which\n // Number reads as 0.\n return Number(text.slice(0, -1)) + fraction\n }\n\n return Number(text)\n}\n\nfunction readAmount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Amount | undefined {\n if (value === undefined) {\n return error(\n issues,\n 'missing_field',\n path,\n 'Add an amount, e.g. { amount: 2, unit: \"cup\" }.',\n )\n }\n\n if (typeof value === 'number') {\n return readPositive(issues, value, path)\n }\n\n const range = asRecord(value)\n if (range && 'min' in range && 'max' in range) {\n checkFields(issues, range, { min: true, max: true }, path)\n\n const min = readPositive(issues, range.min, [...path, 'min'])\n const max = readPositive(issues, range.max, [...path, 'max'])\n\n if (min === undefined || max === undefined) {\n return undefined\n }\n\n if (min > max) {\n return error(\n issues,\n 'invalid_range',\n path,\n `Range minimum (${min}) must be less than maximum (${max}).`,\n )\n }\n\n return min === max ? min : { min, max }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a number or { min, max }, received ${describeValue(value)}.`,\n )\n}\n\n/**\n * Reads a number from 1e-300 to 1e290. Scaling by 1e-6 to 1e6 and converting\n * between any two units in the registry keeps an amount in that range finite\n * and greater than 0.\n */\nexport function readPositive(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (isPositive(value) && value >= 1e-300 && value <= 1e290) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n isPositive(value)\n ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.`\n : `Expected a positive number, received ${describeValue(value)}.`,\n )\n}\n\n/** Checks each end of an amount read from text with `readPositive`. */\nfunction readTextAmount(\n issues: RecipeIssue[],\n amount: Amount,\n path: Path,\n): boolean {\n return (\n typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n ).every((end) => readPositive(issues, end, path) !== undefined)\n}\n"],"mappings":"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","names":["unit"]}
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/parse.ts"],"sourcesContent":["import {\n describeValue,\n MAX_ECHOED,\n type ParseResult,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport type { Path } from '../issues'\nimport { findUnit, suggestUnit } from '../units/lookup'\nimport { units } from '../units/registry'\nimport { asRecord, checkFields, error, toResult } from '../validation'\nimport { isPositive } from './amount'\nimport type { Amount, Quantity } from './types'\n\n// ¼, ½, ¾ and ⅐ to ⅞, which decompose to their fractions, such as '½' to\n// '1⁄2'. ⅟ has no denominator, and ↉ is zero.\nconst VULGAR = '[¼-¾⅐-⅞]'\n\nconst ARTICLE = /^an?\\s+(?=\\D)/i\n\nlet amountPattern: RegExp | undefined\n\nexport type Problem = {\n code: RecipeIssueCode\n message: string\n suggestion?: string\n}\n\n/**\n * Parses quantity text, such as `'1 ½ cups'`, `'2-3 tbsp'`, `'a pinch'`, or\n * `'3'` for a bare count. Never throws.\n *\n * Accepts whole numbers, decimals, fractions (`1/2`, `½`, also with the\n * fraction slash `⁄`), mixed numbers (`1 1/2`, `1½`) and ranges (`2-3`, `2–3`,\n * `2 to 3`), each followed by an optional unit alias, and `a` or `an` for 1\n * before a unit alias, as in `'a pinch'`.\n *\n * @example\n * parseQuantity('1 ½ cups') // { success: true, data: { amount: 1.5, unit: 'cup' }, warnings: [] }\n *\n * const result = parseQuantity('2 cupz')\n * if (!result.success) {\n * result.error.message // '\"2 cupz\" has 1 problem:\\n ✖ (root) Unknown unit \"cupz\". Did you mean \"cups\"?\\nSee https://recipe-book.siraj.page/errors/invalid_quantity/'\n * }\n */\nexport function parseQuantity(text: string): ParseResult<Quantity> {\n const issues: RecipeIssue[] = []\n\n if (typeof text !== 'string') {\n error(\n issues,\n 'invalid_type',\n [],\n `Expected quantity text such as \"1 ½ cups\", received ${describeValue(text)}.`,\n )\n return toResult<Quantity>('invalid_quantity', undefined, issues)\n }\n\n const quantity = readQuantityText(issues, text, [])\n return toResult('invalid_quantity', quantity, issues, quote(text))\n}\n\n/** Reads a quantity in any input form: text, a number, or `{ amount, unit }`. */\nexport function readQuantity(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity | undefined {\n if (typeof value === 'string') {\n const quantity = readQuantityText(issues, value, path)\n return quantity && readTextAmount(issues, quantity.amount, path)\n ? quantity\n : undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const record = asRecord(value)\n if (record) {\n checkFields(issues, record, { amount: true, unit: true }, path)\n\n const amount = readAmount(issues, record.amount, [...path, 'amount'])\n const { unit } = record\n\n if (unit === undefined) {\n return amount === undefined ? undefined : { amount }\n }\n\n if (typeof unit !== 'string') {\n return error(\n issues,\n 'invalid_type',\n [...path, 'unit'],\n `Expected a unit such as \"g\" or \"cup\", received ${describeValue(unit)}.`,\n )\n }\n\n const id = findUnit(unit)\n\n if (!id) {\n const problem = unknownUnit(\n unit,\n amount === undefined\n ? undefined\n : typeof amount === 'number'\n ? String(amount)\n : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`,\n )\n return error(\n issues,\n problem.code,\n [...path, 'unit'],\n problem.message,\n problem.suggestion,\n )\n }\n\n return amount === undefined ? undefined : { amount, unit: id }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a quantity such as \"2 cups\", 2, or { amount: 2, unit: \"cup\" }, received ${describeValue(value)}.`,\n )\n}\n\nfunction readQuantityText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n): Quantity | undefined {\n const result = parseText(text.replace(/\\s+/g, ' ').trim())\n\n if ('code' in result) {\n return error(issues, result.code, path, result.message, result.suggestion)\n }\n\n return result\n}\n\nfunction parseText(text: string): Quantity | Problem {\n const match = matchAmount(text)\n\n if (!match) {\n const unit = findUnit(text)\n return unit\n ? {\n code: 'invalid_quantity',\n message: `${quote(text)} has no amount. Write a full quantity, e.g. \"1 ${units[unit].labels.short.one}\".`,\n }\n : unreadable(text)\n }\n\n if ('code' in match) {\n return match\n }\n\n const { amount, amountText, rest: unitText } = match\n\n if (unitText === '') {\n return { amount }\n }\n\n if (/\\d/.test(unitText) || !/\\p{L}/u.test(unitText)) {\n return unreadable(text)\n }\n\n const unit = findUnit(unitText)\n if (unit) {\n return { amount, unit }\n }\n\n return unknownUnit(\n unitText,\n amountText.length <= MAX_ECHOED && /^\\d+(?:\\.\\d+)?$/.test(amountText)\n ? amountText\n : quote(amountText),\n )\n}\n\n/**\n * Reads the amount that starts `text`, which is trimmed with single spaces: a\n * number, fraction, mixed number, range, or \"a\" or \"an\" for 1. A comma between\n * digits anywhere in the text, as in \"1,5\", is a problem. Otherwise returns\n * `undefined` when the text doesn't start with an amount, and a problem when\n * the amount is too large to read, isn't greater than 0 (0 is allowed with\n * `allowZero`), or is a range whose low end is above its high end. When it\n * reads one, it returns the amount, its text as written, and the text after it.\n */\nexport function matchAmount(\n text: string,\n allowZero?: boolean,\n): { amount: Amount; amountText: string; rest: string } | Problem | undefined {\n // \"1,5\" is a decimal in much of Europe and \"1,500\" a thousands separator in\n // the US, so neither is guessed.\n if (/\\d,\\d/.test(text)) {\n return {\n code: 'invalid_quantity',\n message: `Couldn't read ${quote(text)}. Write decimals with a period, e.g. \"1.5\", and leave out thousands separators.`,\n }\n }\n\n const article = ARTICLE.exec(text)\n if (article) {\n return { amount: 1, amountText: '1', rest: text.slice(article[0].length) }\n }\n\n if (!amountPattern) {\n // Alternatives are tried in order, so a fraction with an optional whole\n // number, as in \"1 1/2\", \"1/2\", \"1½\" or \"½\", comes before decimals and\n // integers.\n const number = String.raw`(?:\\d+\\s+)?\\d+\\s*[/⁄]\\s*\\d+|(?:\\d+\\s*)?${VULGAR}|\\d+(?:\\.\\d+)?|\\.\\d+`\n amountPattern = new RegExp(\n String.raw`^(${number})(?:(?:\\s*[-–—]\\s*|\\s+to\\s+)(${number}))?`,\n 'i',\n )\n }\n\n const match = amountPattern.exec(text)\n if (!match) {\n return undefined\n }\n\n const [matched, lowText = '', highText] = match\n const low = toNumber(lowText)\n const high = highText === undefined ? low : toNumber(highText)\n\n if (low === Infinity || high === Infinity) {\n return {\n code: 'invalid_amount',\n message: `${quote(text)} has an amount too large to read. Check it for extra digits.`,\n }\n }\n\n if (\n !(isPositive(low) || (allowZero && low === 0)) ||\n !(isPositive(high) || (allowZero && high === 0))\n ) {\n return {\n code: 'invalid_amount',\n message: `Expected a positive amount, received ${quote(text)}.`,\n }\n }\n\n if (low > high) {\n const mixedHint =\n /^\\d+$/.test(lowText) && high < 1\n ? ` To write a mixed number, use ${quote(`${lowText} ${highText}`)}.`\n : ''\n return {\n code: 'invalid_range',\n message: `Range minimum (${truncateNumber(lowText)}) must be less than maximum (${truncateNumber(highText!)}).${mixedHint}`,\n }\n }\n\n return {\n amount: low === high ? low : { min: low, max: high },\n amountText: matched,\n rest: text.slice(matched.length).trim(),\n }\n}\n\n/**\n * The `unknown_unit` problem. `quantity` is the line's quantity without the\n * unit, as it would be written in `RecipeInput`, for the \"move the word into\n * the name\" fix.\n */\nfunction unknownUnit(unitText: string, quantity: string | undefined): Problem {\n const suggestion = suggestUnit(unitText)\n const move =\n quantity === undefined\n ? 'move the word into the name'\n : `move the word into the name: { name: ${quote(unitText)}, quantity: ${quantity} }`\n\n return {\n code: 'unknown_unit',\n message: suggestion\n ? `Unknown unit ${quote(unitText)}. Did you mean \"${suggestion}\"?`\n : `Unknown unit ${quote(unitText)}. Use a known unit, or ${move}.`,\n ...(suggestion && { suggestion }),\n }\n}\n\nfunction truncateNumber(text: string): string {\n return text.length > 20 ? `${text.slice(0, 19)}…` : text\n}\n\nfunction unreadable(text: string): Problem {\n return {\n code: 'invalid_quantity',\n message:\n text === ''\n ? 'Expected a quantity such as \"1 ½ cups\", received an empty string.'\n : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. \"1 ½ cups\".`,\n }\n}\n\n// Decomposing a glyph is slow, so each glyph's value is read once.\nconst fractions = new Map<string, number>()\n\nfunction toNumber(text: string): number {\n // The `[/⁄]` must match the one in `amountPattern`.\n const ascii = /^(?:(\\d+)\\s+)?(\\d+)\\s*[/⁄]\\s*(\\d+)$/.exec(text)\n if (ascii) {\n const [, whole = '0', numerator, denominator] = ascii\n // A zero denominator reads as NaN, not Infinity, so \"1/0\" isn't too large.\n return Number(whole) + Number(numerator) / (Number(denominator) || NaN)\n }\n\n // Amounts end in a digit or a glyph.\n const last = text.slice(-1)\n if (last > '9') {\n let fraction = fractions.get(last)\n if (fraction === undefined) {\n const [numerator, denominator] = last.normalize('NFKD').split('⁄')\n fraction = Number(numerator) / Number(denominator)\n fractions.set(last, fraction)\n }\n // Before a glyph is a whole number and any spaces, or nothing, which\n // Number reads as 0.\n return Number(text.slice(0, -1)) + fraction\n }\n\n return Number(text)\n}\n\nfunction readAmount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Amount | undefined {\n if (value === undefined) {\n return error(\n issues,\n 'missing_field',\n path,\n 'Add an amount, e.g. { amount: 2, unit: \"cup\" }.',\n )\n }\n\n if (typeof value === 'number') {\n return readPositive(issues, value, path)\n }\n\n const range = asRecord(value)\n if (range && 'min' in range && 'max' in range) {\n checkFields(issues, range, { min: true, max: true }, path)\n\n const min = readPositive(issues, range.min, [...path, 'min'])\n const max = readPositive(issues, range.max, [...path, 'max'])\n\n if (min === undefined || max === undefined) {\n return undefined\n }\n\n if (min > max) {\n return error(\n issues,\n 'invalid_range',\n path,\n `Range minimum (${min}) must be less than maximum (${max}).`,\n )\n }\n\n return min === max ? min : { min, max }\n }\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a number or { min, max }, received ${describeValue(value)}.`,\n )\n}\n\n/**\n * Reads a number from 1e-300 to 1e290. Scaling by 1e-6 to 1e6 and converting\n * between any two units in the registry keeps an amount in that range finite\n * and greater than 0.\n */\nexport function readPositive(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (isPositive(value) && value >= 1e-300 && value <= 1e290) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n isPositive(value)\n ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.`\n : `Expected a positive number, received ${describeValue(value)}.`,\n )\n}\n\n/** Checks each end of an amount read from text with `readPositive`. */\nfunction readTextAmount(\n issues: RecipeIssue[],\n amount: Amount,\n path: Path,\n): boolean {\n return (\n typeof amount === 'number' ? [amount] : [amount.min, amount.max]\n ).every((end) => readPositive(issues, end, path) !== undefined)\n}\n"],"mappings":"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","names":["unit"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/quantity/scale.ts"],"sourcesContent":["import type { UnitId } from '../units/registry'\nimport {\n checkFactor,\n checkQuantity,\n checkResult,\n mapAmount,\n type SameAmount,\n} from './amount'\nimport type { Amount, Quantity } from './types'\n\n/**\n * Multiplies a quantity by `factor`. Both ends of a range scale, the unit is\n * kept, and nothing is rounded.\n *\n * @throws {RecipeBookError} `invalid_argument` when `factor` is not a finite\n * number greater than 0, `quantity` isn't a quantity object, the amount is\n * not a finite number greater than 0 or a range of them, a range's `min`\n * isn't less than its `max`, or the quantity has `max` set, as a `Servings`\n * or `Yield` has. `resolveRecipe` scales those. The unit isn't checked.\n * Also thrown when an end times `factor` overflows to `Infinity` or\n * underflows to 0: past about 1.8e308, or below about 5e-324.\n *\n * @example\n * scaleQuantity({ amount: 1.5, unit: 'cup' }, 2) // { amount: 3, unit: 'cup' }\n * scaleQuantity({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 2) // { amount: { min: 4, max: 6 }, unit: 'tbsp' }\n */\nexport function scaleQuantity<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = checkQuantity('scaleQuantity', quantity) as A\n checkFactor('scaleQuantity', 'factor', factor)\n\n const result = multiply({ amount, unit: quantity.unit }, factor)\n checkResult('scaleQuantity', result.amount)\n return result\n}\n\n/** {@link scaleQuantity} without argument checks. */\nexport function multiply<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = mapAmount(quantity.amount, (value) => value * factor)\n return quantity.unit === undefined\n ? { amount }\n : { amount, unit: quantity.unit }\n}\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;
|
|
1
|
+
{"version":3,"sources":["../../src/quantity/scale.ts"],"sourcesContent":["import type { UnitId } from '../units/registry'\nimport {\n checkFactor,\n checkQuantity,\n checkResult,\n mapAmount,\n type SameAmount,\n} from './amount'\nimport type { Amount, Quantity } from './types'\n\n/**\n * Multiplies a quantity by `factor`. Both ends of a range scale, the unit is\n * kept, and nothing is rounded.\n *\n * An error thrown by a getter or proxy in the input passes through.\n *\n * @throws {RecipeBookError} `invalid_argument` when `factor` is not a finite\n * number greater than 0, `quantity` isn't a quantity object, the amount is\n * not a finite number greater than 0 or a range of them, a range's `min`\n * isn't less than its `max`, or the quantity has `max` set, as a `Servings`\n * or `Yield` has. `resolveRecipe` scales those. The unit isn't checked.\n * Also thrown when an end times `factor` overflows to `Infinity` or\n * underflows to 0: past about 1.8e308, or below about 5e-324.\n *\n * @example\n * scaleQuantity({ amount: 1.5, unit: 'cup' }, 2) // { amount: 3, unit: 'cup' }\n * scaleQuantity({ amount: { min: 2, max: 3 }, unit: 'tbsp' }, 2) // { amount: { min: 4, max: 6 }, unit: 'tbsp' }\n */\nexport function scaleQuantity<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = checkQuantity('scaleQuantity', quantity) as A\n checkFactor('scaleQuantity', 'factor', factor)\n\n const result = multiply({ amount, unit: quantity.unit }, factor)\n checkResult('scaleQuantity', result.amount)\n return result\n}\n\n/** {@link scaleQuantity} without argument checks. */\nexport function multiply<A extends Amount>(\n quantity: Quantity & { amount: A },\n factor: number,\n): { amount: SameAmount<A>; unit?: UnitId | undefined } {\n const amount = mapAmount(quantity.amount, (value) => value * factor)\n return quantity.unit === undefined\n ? { amount }\n : { amount, unit: quantity.unit }\n}\n"],"mappings":";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAqBA,SAAS,cACd,UACA,QACsD;AACtD,QAAM,SAAS,cAAc,iBAAiB,QAAQ;AACtD,cAAY,iBAAiB,UAAU,MAAM;AAE7C,QAAM,SAAS,SAAS,EAAE,QAAQ,MAAM,SAAS,KAAK,GAAG,MAAM;AAC/D,cAAY,iBAAiB,OAAO,MAAM;AAC1C,SAAO;AACT;AAGO,SAAS,SACd,UACA,QACsD;AACtD,QAAM,SAAS,UAAU,SAAS,QAAQ,CAAC,UAAU,QAAQ,MAAM;AACnE,SAAO,SAAS,SAAS,SACrB,EAAE,OAAO,IACT,EAAE,QAAQ,MAAM,SAAS,KAAK;AACpC;","names":[]}
|
package/dist/recipe/ids.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/recipe/ids.ts"],"sourcesContent":["import { quote, type RecipeIssue } from '../errors'\nimport { issue, type Path } from '../issues'\n\n/**\n * An id to assign. `position` is where the item's issues start in the issue\n * list, so a `duplicate_id` warning can be placed in input order.\n */\nexport type IdRequest = {\n authored: string | undefined\n derived: string\n path: Path\n position: number\n}\n\nexport type PendingIssue = { position: number; issue: RecipeIssue }\n\n/**\n * Lowercase words joined by hyphens,
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/ids.ts"],"sourcesContent":["import { quote, type RecipeIssue } from '../errors'\nimport { issue, type Path } from '../issues'\n\n/**\n * An id to assign. `position` is where the item's issues start in the issue\n * list, so a `duplicate_id` warning can be placed in input order.\n */\nexport type IdRequest = {\n authored: string | undefined\n derived: string\n path: Path\n position: number\n}\n\nexport type PendingIssue = { position: number; issue: RecipeIssue }\n\n/**\n * Lowercase words joined by hyphens, with compatibility characters spelled out\n * (`½` → `1-2`, `fig` → `fig`) and accents dropped from Latin letters:\n * \"Crème fraîche\" → \"creme-fraiche\", \"わさび\" → \"わさび\".\n */\nexport function slugify(text: string): string {\n return (\n text\n .normalize('NFKD')\n // Marks in other scripts are part of the letter: dropping them turns わさび into わさひ.\n .replace(/(\\p{Script=Latin})\\p{M}+/gu, '$1')\n // Lowercase before NFC: some Greek capitals with marks, such as Ϊ́, have no precomposed form but their lowercase does.\n .toLowerCase()\n .normalize('NFC')\n // A mark with no letter before it, such as the variation selector in ❤️, would be invisible.\n .replace(/(^|[^\\p{L}\\p{M}])\\p{M}+/gu, '$1')\n .replace(/[^\\p{L}\\p{M}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n )\n}\n\n/**\n * Gives each request a unique id. Authored ids are claimed first, so a derived\n * id never takes one; a repeated authored id is renamed with a warning. Taken\n * ids get the next free suffix: `egg`, `egg-2`, `egg-3`.\n */\nexport function assignIds(\n requests: readonly IdRequest[],\n pending: PendingIssue[],\n): string[] {\n const used = new Set<string>()\n const nextSuffix = new Map<string, number>()\n const ids = requests.map(({ authored }) => {\n if (authored === undefined || used.has(authored)) {\n return undefined\n }\n used.add(authored)\n return authored\n })\n\n return requests.map((request, i) => {\n const claimed = ids[i]\n if (claimed !== undefined) {\n return claimed\n }\n\n const base = request.authored ?? request.derived\n let id = base\n if (used.has(id)) {\n // Resuming from the last suffix keeps many repeats of one id linear.\n let n = nextSuffix.get(base) ?? 2\n while (used.has((id = `${base}-${n}`))) {\n n++\n }\n nextSuffix.set(base, n + 1)\n }\n used.add(id)\n\n if (request.authored !== undefined) {\n pending.push({\n position: request.position,\n issue: issue(\n 'warning',\n 'duplicate_id',\n [...request.path, 'id'],\n `${quote(request.authored)} is already used; renamed to ${quote(id)}.`,\n ),\n })\n }\n\n return id\n })\n}\n\n/** Inserts pending issues at their positions, keeping input order. */\nexport function insertPending(\n issues: RecipeIssue[],\n pending: readonly PendingIssue[],\n): void {\n if (pending.length === 0) {\n return\n }\n\n const sorted = [...pending].sort((a, b) => a.position - b.position)\n const original = issues.splice(0)\n let next = 0\n for (const { position, issue } of sorted) {\n while (next < position) {\n issues.push(original[next++]!)\n }\n issues.push(issue)\n }\n while (next < original.length) {\n issues.push(original[next++]!)\n }\n}\n"],"mappings":";AAAA,SAAS,aAA+B;AACxC,SAAS,aAAwB;AAoB1B,SAAS,QAAQ,MAAsB;AAC5C,SACE,KACG,UAAU,MAAM,EAEhB,QAAQ,8BAA8B,IAAI,EAE1C,YAAY,EACZ,UAAU,KAAK,EAEf,QAAQ,6BAA6B,IAAI,EACzC,QAAQ,yBAAyB,GAAG,EACpC,QAAQ,YAAY,EAAE;AAE7B;AAOO,SAAS,UACd,UACA,SACU;AACV,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,aAAa,oBAAI,IAAoB;AAC3C,QAAM,MAAM,SAAS,IAAI,CAAC,EAAE,SAAS,MAAM;AACzC,QAAI,aAAa,UAAa,KAAK,IAAI,QAAQ,GAAG;AAChD,aAAO;AAAA,IACT;AACA,SAAK,IAAI,QAAQ;AACjB,WAAO;AAAA,EACT,CAAC;AAED,SAAO,SAAS,IAAI,CAAC,SAAS,MAAM;AAClC,UAAM,UAAU,IAAI,CAAC;AACrB,QAAI,YAAY,QAAW;AACzB,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,QAAQ,YAAY,QAAQ;AACzC,QAAI,KAAK;AACT,QAAI,KAAK,IAAI,EAAE,GAAG;AAEhB,UAAI,IAAI,WAAW,IAAI,IAAI,KAAK;AAChC,aAAO,KAAK,IAAK,KAAK,GAAG,IAAI,IAAI,CAAC,EAAG,GAAG;AACtC;AAAA,MACF;AACA,iBAAW,IAAI,MAAM,IAAI,CAAC;AAAA,IAC5B;AACA,SAAK,IAAI,EAAE;AAEX,QAAI,QAAQ,aAAa,QAAW;AAClC,cAAQ,KAAK;AAAA,QACX,UAAU,QAAQ;AAAA,QAClB,OAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,CAAC,GAAG,QAAQ,MAAM,IAAI;AAAA,UACtB,GAAG,MAAM,QAAQ,QAAQ,CAAC,gCAAgC,MAAM,EAAE,CAAC;AAAA,QACrE;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,cACd,QACA,SACM;AACN,MAAI,QAAQ,WAAW,GAAG;AACxB;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAClE,QAAM,WAAW,OAAO,OAAO,CAAC;AAChC,MAAI,OAAO;AACX,aAAW,EAAE,UAAU,OAAAA,OAAM,KAAK,QAAQ;AACxC,WAAO,OAAO,UAAU;AACtB,aAAO,KAAK,SAAS,MAAM,CAAE;AAAA,IAC/B;AACA,WAAO,KAAKA,MAAK;AAAA,EACnB;AACA,SAAO,OAAO,SAAS,QAAQ;AAC7B,WAAO,KAAK,SAAS,MAAM,CAAE;AAAA,EAC/B;AACF;","names":["issue"]}
|
package/dist/recipe/parse.js
CHANGED
|
@@ -203,10 +203,10 @@ function readList(issues, pending, value, path, noun, readItem) {
|
|
|
203
203
|
const sections = [];
|
|
204
204
|
let loose;
|
|
205
205
|
let count = 0;
|
|
206
|
-
const read = (
|
|
207
|
-
const visit = (
|
|
206
|
+
const read = (entry2, entryPath) => readItem(issues, entry2, entryPath, ++count);
|
|
207
|
+
const visit = (entry2, i) => {
|
|
208
208
|
const entryPath = [...path, i];
|
|
209
|
-
const section = asSection(
|
|
209
|
+
const section = asSection(entry2);
|
|
210
210
|
if (section) {
|
|
211
211
|
loose = void 0;
|
|
212
212
|
sections.push(readSection(issues, section, entryPath, noun, read));
|
|
@@ -223,7 +223,7 @@ function readList(issues, pending, value, path, noun, readItem) {
|
|
|
223
223
|
};
|
|
224
224
|
sections.push(loose);
|
|
225
225
|
}
|
|
226
|
-
const item = read(
|
|
226
|
+
const item = read(entry2, entryPath);
|
|
227
227
|
if (item) {
|
|
228
228
|
loose.items.push(item);
|
|
229
229
|
}
|
|
@@ -235,11 +235,16 @@ function readList(issues, pending, value, path, noun, readItem) {
|
|
|
235
235
|
warning(issues, "empty_list", path, `Recipe has no ${noun}.`);
|
|
236
236
|
return [];
|
|
237
237
|
}
|
|
238
|
-
const
|
|
238
|
+
const renamed = [];
|
|
239
|
+
const sectionIds = assignIds(sections, renamed);
|
|
239
240
|
const itemIds = assignIds(
|
|
240
241
|
sections.flatMap((section) => section.items),
|
|
241
|
-
|
|
242
|
+
renamed
|
|
242
243
|
);
|
|
244
|
+
const entry = ({ issue }) => issue.path[path.length];
|
|
245
|
+
for (const issue of renamed.sort((a, b) => entry(a) - entry(b))) {
|
|
246
|
+
pending.push(issue);
|
|
247
|
+
}
|
|
243
248
|
let next = 0;
|
|
244
249
|
return sections.map(
|
|
245
250
|
(section, i) => defined({
|
package/dist/recipe/parse.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/recipe/parse.ts"],"sourcesContent":["import {\n describeValue,\n type ParseResult,\n quote,\n RecipeBookError,\n type RecipeIssue,\n} from '../errors'\nimport type { IngredientLine } from '../ingredient/parse'\nimport { hasOwn, isRecord, type Path, warning } from '../issues'\nimport { isPositive } from '../quantity/amount'\nimport { matchAmount, readPositive, readQuantity } from '../quantity/parse'\nimport type { Quantity } from '../quantity/types'\nimport type { System } from '../units/registry'\nimport {\n asRecord,\n checkFields,\n error,\n type Fields,\n toResult,\n} from '../validation'\nimport { parseIsoDuration } from './duration'\nimport {\n assignIds,\n type IdRequest,\n insertPending,\n type PendingIssue,\n slugify,\n} from './ids'\nimport { inferSystem } from './infer'\nimport type {\n CustomTimeInput,\n EquipmentInput,\n IngredientInput,\n IngredientSectionInput,\n NoteInput,\n RecipeInput,\n ServingsInput,\n StepInput,\n StepSectionInput,\n TimesInput,\n YieldInput,\n} from './input'\nimport { NUTRIENT_UNITS, type NutrientKey } from './nutrient-units'\nimport { type NutrientUnit, parseNutrient } from './nutrition'\nimport type {\n CustomTime,\n Equipment,\n Ingredient,\n Note,\n Nutrition,\n Recipe,\n Servings,\n Step,\n Times,\n Yield,\n} from './types'\nimport { disallowedScheme } from './url'\n\nconst RECIPE_FIELDS: Fields<RecipeInput> = {\n id: true,\n name: true,\n description: true,\n image: true,\n servings: true,\n yield: true,\n system: true,\n times: true,\n ingredients: true,\n steps: true,\n notes: true,\n equipment: true,\n nutrition: true,\n}\nconst INGREDIENT_FIELDS: Fields<IngredientInput> = {\n id: true,\n name: true,\n plural: true,\n quantity: true,\n equivalents: true,\n note: true,\n url: true,\n optional: true,\n}\nconst SECTION_FIELDS: Fields<IngredientSectionInput & StepSectionInput> = {\n id: true,\n title: true,\n items: true,\n}\nconst STEP_FIELDS: Fields<StepInput> = {\n id: true,\n text: true,\n name: true,\n image: true,\n}\nconst SERVINGS_FIELDS: Fields<Exclude<ServingsInput, number | string>> = {\n amount: true,\n max: true,\n}\nconst YIELD_FIELDS: Fields<Exclude<YieldInput, string>> = {\n amount: true,\n max: true,\n unit: true,\n plural: true,\n}\nconst TIMES_FIELDS: Fields<TimesInput> = {\n prep: true,\n cook: true,\n total: true,\n custom: true,\n}\nconst CUSTOM_TIME_FIELDS: Fields<CustomTimeInput> = {\n id: true,\n label: true,\n minutes: true,\n}\nconst NOTE_FIELDS: Fields<NoteInput> = {\n id: true,\n title: true,\n text: true,\n}\nconst EQUIPMENT_FIELDS: Fields<EquipmentInput> = {\n id: true,\n name: true,\n plural: true,\n quantity: true,\n url: true,\n note: true,\n}\nconst NUTRIENT_EXAMPLES: Readonly<Record<NutrientUnit, string>> = {\n kcal: 'kcal, e.g. 250, \"250 kcal\", or \"1046 kJ\"',\n g: 'grams, e.g. 12 or \"12 g\"',\n mg: 'milligrams, e.g. 300, \"300 mg\", or \"0.3 g\"',\n}\n\n/**\n * Validates and normalizes recipe data: resolves unit aliases, parses quantity\n * text and ISO 8601 durations, groups items into sections, derives ids, and\n * infers the source system. Accepts `unknown`, so JSON needs no cast, and reads\n * only an object's own fields. Never throws for bad data; an error thrown by a\n * getter or proxy in the input passes through.\n *\n * Unknown fields, duplicate ids, missing or empty `ingredients` or `steps`, and\n * empty sections are warnings and don't fail the parse. Each issue has the path\n * of the value it is about.\n *\n * @example\n * import { parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe({\n * name: 'Pancakes',\n * ingredients: [{ name: 'flour', quantity: '1 ½ cups' }],\n * steps: ['Whisk the batter.'],\n * })\n * result.success && result.data.ingredients[0]?.items[0]?.quantity // { amount: 1.5, unit: 'cup' }\n *\n * @example\n * import { parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe({\n * name: 'Pancakes',\n * ingredients: [{ name: 'flour', quantity: '1 ½ cupz' }],\n * steps: ['Whisk the batter.'],\n * })\n * if (!result.success) {\n * console.error(result.error.message)\n * // \"Pancakes\" has 1 problem:\n * // ✖ ingredients[0].quantity Unknown unit \"cupz\". Did you mean \"cups\"?\n * // See https://recipe-book.siraj.page/errors/invalid_recipe\n * }\n */\nexport function parseRecipe(input: unknown): ParseResult<Recipe>\n/**\n * Parses recipe data as `parseRecipe(input)` does, and reads ingredients\n * written as text with `options.parseIngredientLine`. Without it, a text\n * ingredient is an `invalid_type` error.\n *\n * @throws {RecipeBookError} `invalid_argument` when\n * `options.parseIngredientLine` is present but isn't a function.\n *\n * @example\n * import { parseIngredientLine, parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe(\n * { name: 'Toast', ingredients: ['2 slices bread', '1 tbsp butter, softened'] },\n * { parseIngredientLine },\n * )\n * result.success && result.data.ingredients[0]?.items[1] // { id: 'butter', name: 'butter', quantity: { amount: 1, unit: 'tbsp' }, equivalents: [], note: 'softened', optional: false }\n */\nexport function parseRecipe(\n input: unknown,\n options: ParseRecipeOptions | undefined,\n): ParseResult<Recipe>\nexport function parseRecipe(\n input: unknown,\n options?: unknown,\n): ParseResult<Recipe> {\n return parseRecipeWith(\n input,\n readLineOption('parseRecipe', options) ??\n 'pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })',\n )\n}\n\n/**\n * How text ingredients are treated: the function that reads them, or the\n * advice that ends a text ingredient's `invalid_type` message, naming the call\n * that would read it.\n */\ntype Lines = LineReader | string\n\n/** Parses `input` as `parseRecipe` does, treating text ingredients by `lines`. */\nexport function parseRecipeWith(\n input: unknown,\n lines: Lines,\n): ParseResult<Recipe> {\n const issues: RecipeIssue[] = []\n const recipe = readRecipe(issues, input, lines)\n const label = recipe && quote(recipe.name)\n\n return toResult('invalid_recipe', recipe, issues, label)\n}\n\n/** Options for `parseRecipe`. */\nexport type ParseRecipeOptions = {\n /**\n * Reads ingredients written as text, such as `'2 cups flour'`. Pass\n * `parseIngredientLine`. Without it, a text ingredient is an `invalid_type` error.\n * A function of your own must return what `parseIngredientLine` returns.\n */\n parseIngredientLine?:\n ((text: string) => ParseResult<IngredientLine>) | undefined\n}\n\nexport type LineReader = NonNullable<ParseRecipeOptions['parseIngredientLine']>\n\n/**\n * Reads `options.parseIngredientLine`, ignoring `options` that aren't an\n * object, as when `parseRecipe` is passed to `Array.prototype.map`.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn`, when\n * `parseIngredientLine` is present but isn't a function.\n */\nexport function readLineOption(\n fn: string,\n options: unknown,\n): LineReader | undefined {\n const reader = asRecord(options)?.parseIngredientLine\n\n if (reader !== undefined && typeof reader !== 'function') {\n throw new RecipeBookError(\n 'invalid_argument',\n `${fn}: parseIngredientLine must be a function such as parseIngredientLine, received ${describeValue(reader)}.`,\n )\n }\n\n return reader as LineReader | undefined\n}\n\nfunction readRecipe(\n issues: RecipeIssue[],\n value: unknown,\n lines: Lines,\n): Recipe | undefined {\n const input = asRecord(value)\n\n if (!input) {\n return error(\n issues,\n 'invalid_type',\n [],\n `Expected a recipe object such as { name, ingredients, steps }, received ${describeValue(value)}.`,\n )\n }\n\n const pending: PendingIssue[] = []\n const id = readText(issues, input.id, ['id'])\n const name = readText(\n issues,\n input.name,\n ['name'],\n 'Add a name for the recipe.',\n )\n const description = readText(issues, input.description, ['description'])\n const image = readUrl(issues, input.image, ['image'])\n const servings = readServings(issues, input.servings, ['servings'])\n const recipeYield = readYield(issues, input.yield, ['yield'])\n const authoredSystem = readSystem(issues, input.system, ['system'])\n const times = readTimes(issues, pending, input.times, ['times'])\n const ingredients = readList(\n issues,\n pending,\n input.ingredients,\n ['ingredients'],\n 'ingredients',\n (issues, value, path) => readIngredient(issues, value, path, lines),\n )\n const steps = readList(\n issues,\n pending,\n input.steps,\n ['steps'],\n 'steps',\n readStep,\n )\n const notes = readItems(\n issues,\n pending,\n input.notes,\n ['notes'],\n 'notes',\n readNote,\n )\n const equipment = readItems(\n issues,\n pending,\n input.equipment,\n ['equipment'],\n 'equipment',\n readEquipment,\n )\n const nutrition = readNutrition(issues, input.nutrition, ['nutrition'])\n checkFields(issues, input, RECIPE_FIELDS, [])\n insertPending(issues, pending)\n\n if (name === undefined) {\n return undefined\n }\n\n const system = authoredSystem ?? inferSystem(ingredients)\n\n return defined({\n id: id ?? (slugify(name) || 'recipe'),\n name,\n description,\n image,\n servings,\n yield: recipeYield,\n system,\n times,\n ingredients,\n steps,\n notes,\n equipment,\n nutrition,\n })\n}\n\n/** A parsed item whose id is assigned once every id in its list is known. */\ntype Draft<T> = IdRequest & { build: (id: string) => T }\n\n/** Reads one list item. `n` is its 1-based position in the list, across sections. */\ntype ItemReader<T> = (\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n) => Draft<T> | undefined\n\ntype SectionDraft<T> = IdRequest & {\n title: string | undefined\n items: Draft<T>[]\n}\n\n/**\n * Reads a list of sections and loose items. Each run of loose items becomes an\n * untitled section, so the result always has one shape. `noun` names the list\n * in messages and is the id of untitled sections.\n */\nfunction readList<T>(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n noun: 'ingredients' | 'steps',\n readItem: ItemReader<T>,\n): { id: string; title?: string; items: T[] }[] {\n const sections: SectionDraft<T>[] = []\n let loose: SectionDraft<T> | undefined\n let count = 0\n const read = (entry: unknown, entryPath: Path) =>\n readItem(issues, entry, entryPath, ++count)\n\n const visit = (entry: unknown, i: number) => {\n const entryPath = [...path, i]\n\n const section = asSection(entry)\n\n if (section) {\n loose = undefined\n sections.push(readSection(issues, section, entryPath, noun, read))\n return\n }\n\n if (!loose) {\n loose = {\n authored: undefined,\n derived: noun,\n path: entryPath,\n position: issues.length,\n title: undefined,\n items: [],\n }\n sections.push(loose)\n }\n\n const item = read(entry, entryPath)\n if (item) {\n loose.items.push(item)\n }\n }\n\n if (value !== undefined && !readArray(issues, value, path, noun, visit)) {\n return []\n }\n\n // readArray skips holes, so a sparse array counts only the entries it has.\n if (sections.length === 0) {\n warning(issues, 'empty_list', path, `Recipe has no ${noun}.`)\n return []\n }\n\n const sectionIds = assignIds(sections, pending)\n const itemIds = assignIds(\n sections.flatMap((section) => section.items),\n pending,\n )\n\n let next = 0\n return sections.map((section, i) =>\n defined({\n id: sectionIds[i]!,\n title: section.title,\n items: section.items.map((item) => item.build(itemIds[next++]!)),\n }),\n )\n}\n\nfunction asSection(entry: unknown): Record<string, unknown> | undefined {\n const record = asRecord(entry)\n return record && 'items' in record ? record : undefined\n}\n\nfunction readSection<T>(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n noun: 'ingredients' | 'steps',\n read: (value: unknown, path: Path) => Draft<T> | undefined,\n): SectionDraft<T> {\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const items: Draft<T>[] = []\n const itemsPath = [...path, 'items']\n let visited = 0\n const entries = readArray(\n issues,\n record.items,\n itemsPath,\n noun,\n (entry, j) => {\n visited++\n const itemPath = [...itemsPath, j]\n\n if (asSection(entry)) {\n error(\n issues,\n 'invalid_type',\n itemPath,\n `Sections can't be nested. Make this a top-level section in ${noun}.`,\n )\n return\n }\n\n const item = read(entry, itemPath)\n if (item) {\n items.push(item)\n }\n },\n )\n\n if (entries && visited === 0) {\n warning(issues, 'empty_list', itemsPath, `Section has no ${noun}.`)\n }\n\n checkFields(issues, record, SECTION_FIELDS, path)\n\n return {\n authored,\n derived: (title && slugify(title)) || noun,\n path,\n position,\n title,\n items,\n }\n}\n\n/** Reads a flat list, such as notes or equipment. Its ids have their own namespace. */\nfunction readItems<T>(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n noun: string,\n readItem: ItemReader<T>,\n): T[] {\n let n = 0\n const drafts =\n value === undefined\n ? []\n : readArray(issues, value, path, noun, (entry, i) =>\n readItem(issues, entry, [...path, i], ++n),\n )\n\n if (!drafts) {\n return []\n }\n\n const ids = assignIds(drafts, pending)\n\n return drafts.map((draft, i) => draft.build(ids[i]!))\n}\n\n/**\n * Calls `read` with each entry the list has as its own, in order, and returns\n * the objects it returns. Like `forEach`, it reads `length` once and checks\n * each index with `in` before reading it. It keeps an entry only if the list\n * still has it as its own after the read. It calls none of the list's methods,\n * so a list with another prototype, its own `forEach`, or its own\n * `constructor` reads like any other.\n */\nfunction readArray<T>(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n noun: string,\n read: (entry: unknown, i: number) => T | undefined,\n): T[] | undefined {\n if (!Array.isArray(value)) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a list of ${noun}, received ${describeValue(value)}.`,\n )\n }\n\n const kept: T[] = []\n for (let i = 0, n = value.length; i < n; i++) {\n if (i in value) {\n const entry: unknown = value[i]\n const result = hasOwn(value, i) && read(entry, i)\n if (result) {\n kept.push(result)\n }\n }\n }\n return kept\n}\n\nfunction readIngredient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n lines?: Lines,\n): Draft<Ingredient> | undefined {\n if (typeof value === 'string' && typeof lines === 'function') {\n const result = lines(value)\n\n for (const issue of result.success\n ? result.warnings\n : result.error.issues) {\n issues.push({ ...issue, path: [...path, ...issue.path] })\n }\n\n return result.success\n ? readIngredient(issues, result.data, path)\n : undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected an ingredient such as ${typeof lines === 'function' ? '\"2 cups flour\" or ' : ''}{ name: \"flour\", quantity: \"2 cups\" }, received ${describeValue(value)}.${typeof value === 'string' && typeof lines === 'string' ? ` To read ingredient lines, ${lines}.` : ''}`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n [...path, 'name'],\n 'Add a name, e.g. name: \"flour\".',\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readQuantity(issues, record.quantity, [...path, 'quantity'])\n const equivalents = readEquivalents(issues, record.equivalents, [\n ...path,\n 'equivalents',\n ])\n const note = readText(issues, record.note, [...path, 'note'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const optional = readBoolean(issues, record.optional, [...path, 'optional'])\n\n if (equivalents.length > 0 && record.quantity === undefined) {\n error(\n issues,\n 'missing_field',\n [...path, 'quantity'],\n 'Add a quantity. Equivalents are alternatives to it, so they need one.',\n )\n }\n\n checkFields(issues, record, INGREDIENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'ingredient',\n path,\n position,\n build: (id) =>\n defined({ id, name, plural, quantity, equivalents, note, url, optional }),\n }\n}\n\nfunction readEquivalents(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity[] {\n return (\n (value !== undefined &&\n readArray(issues, value, path, 'quantities, e.g. [\"190 g\"]', (entry, i) =>\n readQuantity(issues, entry, [...path, i]),\n )) ||\n []\n )\n}\n\nfunction readStep(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Step> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the step text.',\n )\n const name = readText(issues, record.name, [...path, 'name'])\n const image = readUrl(issues, record.image, [...path, 'image'])\n checkFields(issues, record, STEP_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (name && slugify(name)) || `step-${n}`,\n path,\n position,\n build: (id) => defined({ id, text, name, image }),\n }\n}\n\nfunction readNote(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Note> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the note text.',\n )\n checkFields(issues, record, NOTE_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (title && slugify(title)) || `note-${n}`,\n path,\n position,\n build: (id) => defined({ id, title, text }),\n }\n}\n\nfunction readEquipment(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<Equipment> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { name: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { name }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n isText ? path : [...path, 'name'],\n `Add a name, e.g. ${isText ? '' : 'name: '}\"whisk\".`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readCount(issues, record.quantity, [...path, 'quantity'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const note = readText(issues, record.note, [...path, 'note'])\n checkFields(issues, record, EQUIPMENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'equipment',\n path,\n position,\n build: (id) => defined({ id, name, plural, quantity, url, note }),\n }\n}\n\n/** `T` with each field that may be `undefined` made optional instead. */\ntype Defined<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n} & {\n [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<\n T[K],\n undefined\n >\n}\n\n/** Copies the own fields of `fields` that aren't `undefined`, keeping their order. A polluted `Object.prototype` adds none. */\nfunction defined<T extends object>(fields: T): Defined<T> {\n const copy: Record<string, unknown> = {}\n\n for (const key in fields) {\n if (hasOwn(fields, key) && fields[key] !== undefined) {\n copy[key] = fields[key]\n }\n }\n\n return copy as Defined<T>\n}\n\nfunction readCount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n `Expected a whole number greater than 0, received ${describeValue(value)}.`,\n )\n}\n\nfunction readServings(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Servings | undefined {\n if (value === undefined) {\n return undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const expected = `Expected a number of servings, e.g. 4 or \"4–6\", received ${describeValue(value)}.`\n\n if (typeof value === 'string') {\n const match = readRangeText(issues, value, path, expected)\n\n if (match?.rest) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n return match?.range\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(issues, 'invalid_type', path, expected)\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 4, max: 6 }.',\n )\n checkFields(issues, record, SERVINGS_FIELDS, path)\n return range\n}\n\nfunction readYield(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Yield | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const isText = typeof value === 'string'\n const expected = `Expected a batch such as \"24 cookies\" or ${isText ? '\"3–4 loaves\"' : '{ amount, unit }'}, received ${describeValue(value)}.`\n\n if (isText) {\n const match = readRangeText(issues, value, path, expected)\n\n if (!match) {\n return undefined\n }\n\n const { range, rest } = match\n\n if (!rest) {\n return error(\n issues,\n 'missing_field',\n path,\n `Add what the recipe makes, e.g. \"24 cookies\".${servingsHint(range)}`,\n )\n }\n\n // A second number, a word or sign that joins numbers, or a dash would\n // otherwise become part of the unit and not scale: \"3 or 4 loaves\",\n // \"2 and 1/2 dozen\", \"4 & 5 loaves\", \"1 000 cookies\", \"6 ⅓-cup bowls\".\n if (\n !/\\p{L}/u.test(rest) ||\n /^(?:[-–—&]|(?:[\\d.,/\\p{No}]+|or|to|x|and)(?:\\s|$))/iu.test(rest)\n ) {\n const suggestion = suggestJoinedYield(value, range, rest)\n return error(\n issues,\n 'invalid_quantity',\n path,\n suggestion ? `${expected} Did you mean \"${suggestion}\"?` : expected,\n suggestion,\n )\n }\n\n return { ...range, unit: rest }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `${expected}${isPositive(value) ? servingsHint({ amount: value }) : ''}`,\n )\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 24, unit: \"cookie\" }.',\n )\n const unit = readText(\n issues,\n record.unit,\n [...path, 'unit'],\n `Add a unit, e.g. { amount: 24, unit: \"cookie\" }.${servingsHint(range)}`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n checkFields(issues, record, YIELD_FIELDS, path)\n\n if (range === undefined || unit === undefined) {\n return undefined\n }\n\n return defined({ ...range, unit, plural })\n}\n\n/**\n * Rewrites yield text whose amount is followed by \"and\" or \"&\" and a second\n * number: a fraction makes a mixed number (\"2 and 1/2 dozen\" to \"2 1/2 dozen\"),\n * and a whole number makes a range (\"4 & 5 loaves\" to \"4-5 loaves\"). Returns\n * `undefined` unless the rewritten text parses as that amount.\n */\nfunction suggestJoinedYield(\n text: string,\n { amount }: Servings,\n rest: string,\n): string | undefined {\n const joiner = /^(?:and|&)\\s*/i.exec(rest)\n const next = joiner && matchAmount(rest.slice(joiner[0].length))\n\n if (!next || 'code' in next || typeof next.amount !== 'number') {\n return undefined\n }\n\n const isFraction = next.amount < 1\n\n if (!isFraction && !Number.isInteger(next.amount)) {\n return undefined\n }\n\n const head = text.replace(/\\s+/g, ' ').trim().slice(0, -rest.length)\n const suggestion = `${head.trim()}${isFraction ? ' ' : '-'}${next.amountText} ${next.rest}`\n const parsed = readYield([], suggestion, [])\n\n return parsed &&\n (isFraction\n ? parsed.amount === amount + next.amount && parsed.max === undefined\n : parsed.amount === amount && parsed.max === next.amount)\n ? suggestion\n : undefined\n}\n\n/** Reads text that starts with an amount or range, such as \"4–6\" or \"24 cookies\". */\nfunction readRangeText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n expected: string,\n): { range: Servings; rest: string } | undefined {\n const match = matchAmount(text.replace(/\\s+/g, ' ').trim())\n\n if (!match) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n if ('code' in match) {\n return error(issues, match.code, path, match.message)\n }\n\n const { amount, rest } = match\n const range: Servings =\n typeof amount === 'number'\n ? { amount }\n : { amount: amount.min, max: amount.max }\n\n if (\n readPositive(issues, range.amount, path) === undefined ||\n (range.max !== undefined &&\n readPositive(issues, range.max, path) === undefined)\n ) {\n return undefined\n }\n\n return { range, rest }\n}\n\n/** Reads `{ amount, max }`, where `max` is optional and a `max` equal to `amount` is dropped. */\nfunction readRange(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n missingAmount: string,\n): Servings | undefined {\n const value = record.amount\n const amount =\n value === undefined\n ? error(issues, 'missing_field', [...path, 'amount'], missingAmount)\n : isRecord(value) && hasOwn(value, 'min') && hasOwn(value, 'max')\n ? error(\n issues,\n 'invalid_type',\n [...path, 'amount'],\n 'Expected a positive number, received an object. Write a range as { amount: 4, max: 6 }.',\n )\n : readPositive(issues, value, [...path, 'amount'])\n const max =\n record.max === undefined\n ? undefined\n : readPositive(issues, record.max, [...path, 'max'])\n\n if (amount === undefined) {\n return undefined\n }\n\n if (max !== undefined && max < amount) {\n return error(\n issues,\n 'invalid_range',\n [...path, 'max'],\n `Expected a maximum greater than the amount (${amount}), received ${max}.`,\n )\n }\n\n return max === undefined || max === amount ? { amount } : { amount, max }\n}\n\nfunction servingsHint(servings: Servings | undefined): string {\n if (!servings) {\n return ''\n }\n\n const { amount, max } = servings\n return ` For a number of servings, use servings: ${max === undefined ? amount : quote(`${amount}–${max}`)}.`\n}\n\nfunction readSystem(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): System | undefined {\n if (value === undefined || value === 'metric' || value === 'us') {\n return value\n }\n\n const suggestion = value === 'imperial' ? 'us' : undefined\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected \"metric\" or \"us\", received ${describeValue(value)}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n suggestion,\n )\n}\n\nfunction readTimes(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n): Times {\n if (value === undefined) {\n return { custom: [] }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n error(\n issues,\n 'invalid_type',\n path,\n `Expected { prep, cook, total, custom }, received ${describeValue(value)}.`,\n )\n return { custom: [] }\n }\n\n const prep = readDuration(issues, record.prep, [...path, 'prep'])\n const cook = readDuration(issues, record.cook, [...path, 'cook'])\n const total = readDuration(issues, record.total, [...path, 'total'])\n const custom = readItems(\n issues,\n pending,\n record.custom,\n [...path, 'custom'],\n 'custom times',\n readCustomTime,\n )\n checkFields(issues, record, TIMES_FIELDS, path)\n return defined({ prep, cook, total, custom })\n}\n\nfunction readCustomTime(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<CustomTime> | undefined {\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected { label, minutes }, e.g. { label: \"Chill\", minutes: 60 }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const label = readText(\n issues,\n record.label,\n [...path, 'label'],\n 'Add a label, e.g. label: \"Chill\".',\n )\n const minutes =\n record.minutes === undefined\n ? error(\n issues,\n 'missing_field',\n [...path, 'minutes'],\n 'Add the time, e.g. minutes: 60 or minutes: \"PT1H\".',\n )\n : readDuration(issues, record.minutes, [...path, 'minutes'])\n checkFields(issues, record, CUSTOM_TIME_FIELDS, path)\n\n if (label === undefined || minutes === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(label) || 'time',\n path,\n position,\n build: (id) => ({ id, label, minutes }),\n }\n}\n\nfunction readDuration(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const minutes =\n typeof value === 'number'\n ? nonNegative(value)\n : typeof value === 'string'\n ? parseIsoDuration(value)\n : undefined\n\n if (minutes === undefined) {\n return error(\n issues,\n 'invalid_duration',\n path,\n `Expected minutes or an ISO 8601 duration like \"PT30M\", received ${describeValue(value)}.`,\n )\n }\n\n return minutes\n}\n\nfunction readNutrition(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Nutrition | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected nutrition for one serving, e.g. { calories: 250, fat: \"12 g\" }, received ${describeValue(value)}.`,\n )\n }\n\n const nutrition: Nutrition = {}\n const servingSize = readText(issues, record.servingSize, [\n ...path,\n 'servingSize',\n ])\n\n if (servingSize !== undefined) {\n nutrition.servingSize = servingSize\n }\n\n for (const key of Object.keys(NUTRIENT_UNITS) as NutrientKey[]) {\n const amount = readNutrient(\n issues,\n record[key],\n [...path, key],\n NUTRIENT_UNITS[key],\n )\n\n if (amount !== undefined) {\n nutrition[key] = amount\n }\n }\n\n // Spread here: a module-level spread can't be tree-shaken, so it would add NUTRIENT_UNITS to nearly every entry point.\n checkFields(issues, record, { servingSize: true, ...NUTRIENT_UNITS }, path)\n return Object.keys(nutrition).length > 0 ? nutrition : undefined\n}\n\n/** Reads an amount of `unit`. Empty text counts as absent. */\nfunction readNutrient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n unit: NutrientUnit,\n): number | undefined {\n const text = typeof value === 'string' ? value.trim() : undefined\n\n if (value === undefined || text === '') {\n return undefined\n }\n\n const amount =\n typeof value === 'number'\n ? nonNegative(value)\n : text === undefined\n ? undefined\n : parseNutrient(text, unit)\n\n if (typeof amount === 'number') {\n return amount\n }\n\n return error(\n issues,\n typeof value === 'number'\n ? 'invalid_amount'\n : text === undefined\n ? 'invalid_type'\n : 'invalid_quantity',\n path,\n amount?.message ??\n `Expected ${NUTRIENT_EXAMPLES[unit]}, received ${describeValue(value)}.`,\n )\n}\n\n/** `value` if it's finite and not negative, with `-0` read as `0`. */\nfunction nonNegative(value: number): number | undefined {\n return Number.isFinite(value) && value >= 0 ? Math.abs(value) : undefined\n}\n\n/**\n * Reads optional text, trimmed. Empty text counts as absent. With\n * `requiredMessage`, absent text is a `missing_field` error.\n */\nfunction readText(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n requiredMessage?: string,\n): string | undefined {\n if (value !== undefined && typeof value !== 'string') {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected text, received ${describeValue(value)}.`,\n )\n }\n\n const text = value?.trim()\n\n if (!text) {\n return requiredMessage === undefined\n ? undefined\n : error(issues, 'missing_field', path, requiredMessage)\n }\n\n return text\n}\n\n/**\n * Reads an optional link or image URL, which must be http, https, or relative.\n * Other schemes, such as `javascript:`, are errors, so a URL is safe to render\n * as an `href` or `src`.\n */\nfunction readUrl(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): string | undefined {\n const url = readText(issues, value, path)\n const scheme = url === undefined ? undefined : disallowedScheme(url)\n\n return scheme === undefined\n ? url\n : error(\n issues,\n 'invalid_url',\n path,\n `Expected an http or https URL, or a relative one, received ${quote(`${scheme}…`)}.`,\n )\n}\n\nfunction readBoolean(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): boolean {\n if (value === undefined || typeof value === 'boolean') {\n return value ?? false\n }\n\n error(\n issues,\n 'invalid_type',\n path,\n `Expected true or false, received ${describeValue(value)}.`,\n )\n return false\n}\n"],"mappings":"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","names":["issues","value","range"]}
|
|
1
|
+
{"version":3,"sources":["../../src/recipe/parse.ts"],"sourcesContent":["import {\n describeValue,\n type ParseResult,\n quote,\n RecipeBookError,\n type RecipeIssue,\n} from '../errors'\nimport type { IngredientLine } from '../ingredient/parse'\nimport { hasOwn, isRecord, type Path, warning } from '../issues'\nimport { isPositive } from '../quantity/amount'\nimport { matchAmount, readPositive, readQuantity } from '../quantity/parse'\nimport type { Quantity } from '../quantity/types'\nimport type { System } from '../units/registry'\nimport {\n asRecord,\n checkFields,\n error,\n type Fields,\n toResult,\n} from '../validation'\nimport { parseIsoDuration } from './duration'\nimport {\n assignIds,\n type IdRequest,\n insertPending,\n type PendingIssue,\n slugify,\n} from './ids'\nimport { inferSystem } from './infer'\nimport type {\n CustomTimeInput,\n EquipmentInput,\n IngredientInput,\n IngredientSectionInput,\n NoteInput,\n RecipeInput,\n ServingsInput,\n StepInput,\n StepSectionInput,\n TimesInput,\n YieldInput,\n} from './input'\nimport { NUTRIENT_UNITS, type NutrientKey } from './nutrient-units'\nimport { type NutrientUnit, parseNutrient } from './nutrition'\nimport type {\n CustomTime,\n Equipment,\n Ingredient,\n Note,\n Nutrition,\n Recipe,\n Servings,\n Step,\n Times,\n Yield,\n} from './types'\nimport { disallowedScheme } from './url'\n\nconst RECIPE_FIELDS: Fields<RecipeInput> = {\n id: true,\n name: true,\n description: true,\n image: true,\n servings: true,\n yield: true,\n system: true,\n times: true,\n ingredients: true,\n steps: true,\n notes: true,\n equipment: true,\n nutrition: true,\n}\nconst INGREDIENT_FIELDS: Fields<IngredientInput> = {\n id: true,\n name: true,\n plural: true,\n quantity: true,\n equivalents: true,\n note: true,\n url: true,\n optional: true,\n}\nconst SECTION_FIELDS: Fields<IngredientSectionInput & StepSectionInput> = {\n id: true,\n title: true,\n items: true,\n}\nconst STEP_FIELDS: Fields<StepInput> = {\n id: true,\n text: true,\n name: true,\n image: true,\n}\nconst SERVINGS_FIELDS: Fields<Exclude<ServingsInput, number | string>> = {\n amount: true,\n max: true,\n}\nconst YIELD_FIELDS: Fields<Exclude<YieldInput, string>> = {\n amount: true,\n max: true,\n unit: true,\n plural: true,\n}\nconst TIMES_FIELDS: Fields<TimesInput> = {\n prep: true,\n cook: true,\n total: true,\n custom: true,\n}\nconst CUSTOM_TIME_FIELDS: Fields<CustomTimeInput> = {\n id: true,\n label: true,\n minutes: true,\n}\nconst NOTE_FIELDS: Fields<NoteInput> = {\n id: true,\n title: true,\n text: true,\n}\nconst EQUIPMENT_FIELDS: Fields<EquipmentInput> = {\n id: true,\n name: true,\n plural: true,\n quantity: true,\n url: true,\n note: true,\n}\nconst NUTRIENT_EXAMPLES: Readonly<Record<NutrientUnit, string>> = {\n kcal: 'kcal, e.g. 250, \"250 kcal\", or \"1046 kJ\"',\n g: 'grams, e.g. 12 or \"12 g\"',\n mg: 'milligrams, e.g. 300, \"300 mg\", or \"0.3 g\"',\n}\n\n/**\n * Validates and normalizes recipe data: resolves unit aliases, parses quantity\n * text and ISO 8601 durations, groups items into sections, derives ids, and\n * infers the source system. Accepts `unknown`, so JSON needs no cast, and reads\n * only an object's own fields. Never throws for bad data; an error thrown by a\n * getter or proxy in the input passes through.\n *\n * Unknown fields, duplicate ids, missing or empty `ingredients` or `steps`, and\n * empty sections are warnings and don't fail the parse. Each issue has the path\n * of the value it is about.\n *\n * @example\n * import { parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe({\n * name: 'Pancakes',\n * ingredients: [{ name: 'flour', quantity: '1 ½ cups' }],\n * steps: ['Whisk the batter.'],\n * })\n * result.success && result.data.ingredients[0]?.items[0]?.quantity // { amount: 1.5, unit: 'cup' }\n *\n * @example\n * import { parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe({\n * name: 'Pancakes',\n * ingredients: [{ name: 'flour', quantity: '1 ½ cupz' }],\n * steps: ['Whisk the batter.'],\n * })\n * if (!result.success) {\n * console.error(result.error.message)\n * // \"Pancakes\" has 1 problem:\n * // ✖ ingredients[0].quantity Unknown unit \"cupz\". Did you mean \"cups\"?\n * // See https://recipe-book.siraj.page/errors/invalid_recipe/\n * }\n */\nexport function parseRecipe(input: unknown): ParseResult<Recipe>\n/**\n * Parses recipe data as `parseRecipe(input)` does, and reads ingredients\n * written as text with `options.parseIngredientLine`. Without it, a text\n * ingredient is an `invalid_type` error.\n *\n * @throws {RecipeBookError} `invalid_argument` when\n * `options.parseIngredientLine` is present but isn't a function.\n *\n * @example\n * import { parseIngredientLine, parseRecipe } from '@recipe-book/core'\n *\n * const result = parseRecipe(\n * { name: 'Toast', ingredients: ['2 slices bread', '1 tbsp butter, softened'] },\n * { parseIngredientLine },\n * )\n * result.success && result.data.ingredients[0]?.items[1] // { id: 'butter', name: 'butter', quantity: { amount: 1, unit: 'tbsp' }, equivalents: [], note: 'softened', optional: false }\n */\nexport function parseRecipe(\n input: unknown,\n options: ParseRecipeOptions | undefined,\n): ParseResult<Recipe>\nexport function parseRecipe(\n input: unknown,\n options?: unknown,\n): ParseResult<Recipe> {\n return parseRecipeWith(\n input,\n readLineOption('parseRecipe', options) ??\n 'pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })',\n )\n}\n\n/**\n * How text ingredients are treated: the function that reads them, or the\n * advice that ends a text ingredient's `invalid_type` message, naming the call\n * that would read it.\n */\ntype Lines = LineReader | string\n\n/** Parses `input` as `parseRecipe` does, treating text ingredients by `lines`. */\nexport function parseRecipeWith(\n input: unknown,\n lines: Lines,\n): ParseResult<Recipe> {\n const issues: RecipeIssue[] = []\n const recipe = readRecipe(issues, input, lines)\n const label = recipe && quote(recipe.name)\n\n return toResult('invalid_recipe', recipe, issues, label)\n}\n\n/** Options for `parseRecipe`. */\nexport type ParseRecipeOptions = {\n /**\n * Reads ingredients written as text, such as `'2 cups flour'`. Pass\n * `parseIngredientLine`. Without it, a text ingredient is an `invalid_type` error.\n * A function of your own must return what `parseIngredientLine` returns.\n */\n parseIngredientLine?:\n ((text: string) => ParseResult<IngredientLine>) | undefined\n}\n\nexport type LineReader = NonNullable<ParseRecipeOptions['parseIngredientLine']>\n\n/**\n * Reads `options.parseIngredientLine`, ignoring `options` that aren't an\n * object, as when `parseRecipe` is passed to `Array.prototype.map`.\n *\n * @throws {RecipeBookError} `invalid_argument`, naming `fn`, when\n * `parseIngredientLine` is present but isn't a function.\n */\nexport function readLineOption(\n fn: string,\n options: unknown,\n): LineReader | undefined {\n const reader = asRecord(options)?.parseIngredientLine\n\n if (reader !== undefined && typeof reader !== 'function') {\n throw new RecipeBookError(\n 'invalid_argument',\n `${fn}: parseIngredientLine must be a function such as parseIngredientLine, received ${describeValue(reader)}.`,\n )\n }\n\n return reader as LineReader | undefined\n}\n\nfunction readRecipe(\n issues: RecipeIssue[],\n value: unknown,\n lines: Lines,\n): Recipe | undefined {\n const input = asRecord(value)\n\n if (!input) {\n return error(\n issues,\n 'invalid_type',\n [],\n `Expected a recipe object such as { name, ingredients, steps }, received ${describeValue(value)}.`,\n )\n }\n\n const pending: PendingIssue[] = []\n const id = readText(issues, input.id, ['id'])\n const name = readText(\n issues,\n input.name,\n ['name'],\n 'Add a name for the recipe.',\n )\n const description = readText(issues, input.description, ['description'])\n const image = readUrl(issues, input.image, ['image'])\n const servings = readServings(issues, input.servings, ['servings'])\n const recipeYield = readYield(issues, input.yield, ['yield'])\n const authoredSystem = readSystem(issues, input.system, ['system'])\n const times = readTimes(issues, pending, input.times, ['times'])\n const ingredients = readList(\n issues,\n pending,\n input.ingredients,\n ['ingredients'],\n 'ingredients',\n (issues, value, path) => readIngredient(issues, value, path, lines),\n )\n const steps = readList(\n issues,\n pending,\n input.steps,\n ['steps'],\n 'steps',\n readStep,\n )\n const notes = readItems(\n issues,\n pending,\n input.notes,\n ['notes'],\n 'notes',\n readNote,\n )\n const equipment = readItems(\n issues,\n pending,\n input.equipment,\n ['equipment'],\n 'equipment',\n readEquipment,\n )\n const nutrition = readNutrition(issues, input.nutrition, ['nutrition'])\n checkFields(issues, input, RECIPE_FIELDS, [])\n insertPending(issues, pending)\n\n if (name === undefined) {\n return undefined\n }\n\n const system = authoredSystem ?? inferSystem(ingredients)\n\n return defined({\n id: id ?? (slugify(name) || 'recipe'),\n name,\n description,\n image,\n servings,\n yield: recipeYield,\n system,\n times,\n ingredients,\n steps,\n notes,\n equipment,\n nutrition,\n })\n}\n\n/** A parsed item whose id is assigned once every id in its list is known. */\ntype Draft<T> = IdRequest & { build: (id: string) => T }\n\n/** Reads one list item. `n` is its 1-based position in the list, across sections. */\ntype ItemReader<T> = (\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n) => Draft<T> | undefined\n\ntype SectionDraft<T> = IdRequest & {\n title: string | undefined\n items: Draft<T>[]\n}\n\n/**\n * Reads a list of sections and loose items. Each run of loose items becomes an\n * untitled section, so the result always has one shape. `noun` names the list\n * in messages and is the id of untitled sections.\n */\nfunction readList<T>(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n noun: 'ingredients' | 'steps',\n readItem: ItemReader<T>,\n): { id: string; title?: string; items: T[] }[] {\n const sections: SectionDraft<T>[] = []\n let loose: SectionDraft<T> | undefined\n let count = 0\n const read = (entry: unknown, entryPath: Path) =>\n readItem(issues, entry, entryPath, ++count)\n\n const visit = (entry: unknown, i: number) => {\n const entryPath = [...path, i]\n\n const section = asSection(entry)\n\n if (section) {\n loose = undefined\n sections.push(readSection(issues, section, entryPath, noun, read))\n return\n }\n\n if (!loose) {\n loose = {\n authored: undefined,\n derived: noun,\n path: entryPath,\n position: issues.length,\n title: undefined,\n items: [],\n }\n sections.push(loose)\n }\n\n const item = read(entry, entryPath)\n if (item) {\n loose.items.push(item)\n }\n }\n\n if (value !== undefined && !readArray(issues, value, path, noun, visit)) {\n return []\n }\n\n // readArray skips holes, so a sparse array counts only the entries it has.\n if (sections.length === 0) {\n warning(issues, 'empty_list', path, `Recipe has no ${noun}.`)\n return []\n }\n\n // A section can share a position with the items before it, and\n // insertPending keeps ties in the order they are pushed. Each call pushes in\n // input order and the sections' warnings are pushed first, so a stable sort\n // by entry merges the two with a section before its own items.\n const renamed: PendingIssue[] = []\n const sectionIds = assignIds(sections, renamed)\n const itemIds = assignIds(\n sections.flatMap((section) => section.items),\n renamed,\n )\n const entry = ({ issue }: PendingIssue) => issue.path[path.length] as number\n // A spread passes each warning as an argument, which overflows the stack\n // for a long list.\n for (const issue of renamed.sort((a, b) => entry(a) - entry(b))) {\n pending.push(issue)\n }\n\n let next = 0\n return sections.map((section, i) =>\n defined({\n id: sectionIds[i]!,\n title: section.title,\n items: section.items.map((item) => item.build(itemIds[next++]!)),\n }),\n )\n}\n\nfunction asSection(entry: unknown): Record<string, unknown> | undefined {\n const record = asRecord(entry)\n return record && 'items' in record ? record : undefined\n}\n\nfunction readSection<T>(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n noun: 'ingredients' | 'steps',\n read: (value: unknown, path: Path) => Draft<T> | undefined,\n): SectionDraft<T> {\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const items: Draft<T>[] = []\n const itemsPath = [...path, 'items']\n let visited = 0\n const entries = readArray(\n issues,\n record.items,\n itemsPath,\n noun,\n (entry, j) => {\n visited++\n const itemPath = [...itemsPath, j]\n\n if (asSection(entry)) {\n error(\n issues,\n 'invalid_type',\n itemPath,\n `Sections can't be nested. Make this a top-level section in ${noun}.`,\n )\n return\n }\n\n const item = read(entry, itemPath)\n if (item) {\n items.push(item)\n }\n },\n )\n\n if (entries && visited === 0) {\n warning(issues, 'empty_list', itemsPath, `Section has no ${noun}.`)\n }\n\n checkFields(issues, record, SECTION_FIELDS, path)\n\n return {\n authored,\n derived: (title && slugify(title)) || noun,\n path,\n position,\n title,\n items,\n }\n}\n\n/** Reads a flat list, such as notes or equipment. Its ids have their own namespace. */\nfunction readItems<T>(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n noun: string,\n readItem: ItemReader<T>,\n): T[] {\n let n = 0\n const drafts =\n value === undefined\n ? []\n : readArray(issues, value, path, noun, (entry, i) =>\n readItem(issues, entry, [...path, i], ++n),\n )\n\n if (!drafts) {\n return []\n }\n\n const ids = assignIds(drafts, pending)\n\n return drafts.map((draft, i) => draft.build(ids[i]!))\n}\n\n/**\n * Calls `read` with each entry the list has as its own, in order, and returns\n * the objects it returns. Like `forEach`, it reads `length` once and checks\n * each index with `in` before reading it. It keeps an entry only if the list\n * still has it as its own after the read. It calls none of the list's methods,\n * so a list with another prototype, its own `forEach`, or its own\n * `constructor` reads like any other.\n */\nfunction readArray<T>(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n noun: string,\n read: (entry: unknown, i: number) => T | undefined,\n): T[] | undefined {\n if (!Array.isArray(value)) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a list of ${noun}, received ${describeValue(value)}.`,\n )\n }\n\n const kept: T[] = []\n for (let i = 0, n = value.length; i < n; i++) {\n if (i in value) {\n const entry: unknown = value[i]\n const result = hasOwn(value, i) && read(entry, i)\n if (result) {\n kept.push(result)\n }\n }\n }\n return kept\n}\n\nfunction readIngredient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n lines?: Lines,\n): Draft<Ingredient> | undefined {\n if (typeof value === 'string' && typeof lines === 'function') {\n const result = lines(value)\n\n for (const issue of result.success\n ? result.warnings\n : result.error.issues) {\n issues.push({ ...issue, path: [...path, ...issue.path] })\n }\n\n return result.success\n ? readIngredient(issues, result.data, path)\n : undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected an ingredient such as ${typeof lines === 'function' ? '\"2 cups flour\" or ' : ''}{ name: \"flour\", quantity: \"2 cups\" }, received ${describeValue(value)}.${typeof value === 'string' && typeof lines === 'string' ? ` To read ingredient lines, ${lines}.` : ''}`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n [...path, 'name'],\n 'Add a name, e.g. name: \"flour\".',\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readQuantity(issues, record.quantity, [...path, 'quantity'])\n const equivalents = readEquivalents(issues, record.equivalents, [\n ...path,\n 'equivalents',\n ])\n const note = readText(issues, record.note, [...path, 'note'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const optional = readBoolean(issues, record.optional, [...path, 'optional'])\n\n if (equivalents.length > 0 && record.quantity === undefined) {\n error(\n issues,\n 'missing_field',\n [...path, 'quantity'],\n 'Add a quantity. Equivalents are alternatives to it, so they need one.',\n )\n }\n\n checkFields(issues, record, INGREDIENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'ingredient',\n path,\n position,\n build: (id) =>\n defined({ id, name, plural, quantity, equivalents, note, url, optional }),\n }\n}\n\nfunction readEquivalents(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity[] {\n return (\n (value !== undefined &&\n readArray(issues, value, path, 'quantities, e.g. [\"190 g\"]', (entry, i) =>\n readQuantity(issues, entry, [...path, i]),\n )) ||\n []\n )\n}\n\nfunction readStep(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Step> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the step text.',\n )\n const name = readText(issues, record.name, [...path, 'name'])\n const image = readUrl(issues, record.image, [...path, 'image'])\n checkFields(issues, record, STEP_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (name && slugify(name)) || `step-${n}`,\n path,\n position,\n build: (id) => defined({ id, text, name, image }),\n }\n}\n\nfunction readNote(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Note> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the note text.',\n )\n checkFields(issues, record, NOTE_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (title && slugify(title)) || `note-${n}`,\n path,\n position,\n build: (id) => defined({ id, title, text }),\n }\n}\n\nfunction readEquipment(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<Equipment> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { name: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { name }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n isText ? path : [...path, 'name'],\n `Add a name, e.g. ${isText ? '' : 'name: '}\"whisk\".`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readCount(issues, record.quantity, [...path, 'quantity'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const note = readText(issues, record.note, [...path, 'note'])\n checkFields(issues, record, EQUIPMENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'equipment',\n path,\n position,\n build: (id) => defined({ id, name, plural, quantity, url, note }),\n }\n}\n\n/** `T` with each field that may be `undefined` made optional instead. */\ntype Defined<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n} & {\n [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<\n T[K],\n undefined\n >\n}\n\n/** Copies the own fields of `fields` that aren't `undefined`, keeping their order. A polluted `Object.prototype` adds none. */\nfunction defined<T extends object>(fields: T): Defined<T> {\n const copy: Record<string, unknown> = {}\n\n for (const key in fields) {\n if (hasOwn(fields, key) && fields[key] !== undefined) {\n copy[key] = fields[key]\n }\n }\n\n return copy as Defined<T>\n}\n\nfunction readCount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n `Expected a whole number greater than 0, received ${describeValue(value)}.`,\n )\n}\n\nfunction readServings(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Servings | undefined {\n if (value === undefined) {\n return undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const expected = `Expected a number of servings, e.g. 4 or \"4–6\", received ${describeValue(value)}.`\n\n if (typeof value === 'string') {\n const match = readRangeText(issues, value, path, expected)\n\n if (match?.rest) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n return match?.range\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(issues, 'invalid_type', path, expected)\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 4, max: 6 }.',\n )\n checkFields(issues, record, SERVINGS_FIELDS, path)\n return range\n}\n\nfunction readYield(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Yield | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const isText = typeof value === 'string'\n const expected = `Expected a batch such as \"24 cookies\" or ${isText ? '\"3–4 loaves\"' : '{ amount, unit }'}, received ${describeValue(value)}.`\n\n if (isText) {\n const match = readRangeText(issues, value, path, expected)\n\n if (!match) {\n return undefined\n }\n\n const { range, rest } = match\n\n if (!rest) {\n return error(\n issues,\n 'missing_field',\n path,\n `Add what the recipe makes, e.g. \"24 cookies\".${servingsHint(range)}`,\n )\n }\n\n // A second number, a word or sign that joins numbers, or a dash would\n // otherwise become part of the unit and not scale: \"3 or 4 loaves\",\n // \"2 and 1/2 dozen\", \"4 & 5 loaves\", \"1 000 cookies\", \"6 ⅓-cup bowls\".\n if (\n !/\\p{L}/u.test(rest) ||\n /^(?:[-–—&]|(?:[\\d.,/\\p{No}]+|or|to|x|and)(?:\\s|$))/iu.test(rest)\n ) {\n const suggestion = suggestJoinedYield(value, range, rest)\n return error(\n issues,\n 'invalid_quantity',\n path,\n suggestion ? `${expected} Did you mean \"${suggestion}\"?` : expected,\n suggestion,\n )\n }\n\n return { ...range, unit: rest }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `${expected}${isPositive(value) ? servingsHint({ amount: value }) : ''}`,\n )\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 24, unit: \"cookie\" }.',\n )\n const unit = readText(\n issues,\n record.unit,\n [...path, 'unit'],\n `Add a unit, e.g. { amount: 24, unit: \"cookie\" }.${servingsHint(range)}`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n checkFields(issues, record, YIELD_FIELDS, path)\n\n if (range === undefined || unit === undefined) {\n return undefined\n }\n\n return defined({ ...range, unit, plural })\n}\n\n/**\n * Rewrites yield text whose amount is followed by \"and\" or \"&\" and a second\n * number: a fraction makes a mixed number (\"2 and 1/2 dozen\" to \"2 1/2 dozen\"),\n * and a whole number makes a range (\"4 & 5 loaves\" to \"4-5 loaves\"). Returns\n * `undefined` unless the rewritten text parses as that amount.\n */\nfunction suggestJoinedYield(\n text: string,\n { amount }: Servings,\n rest: string,\n): string | undefined {\n const joiner = /^(?:and|&)\\s*/i.exec(rest)\n const next = joiner && matchAmount(rest.slice(joiner[0].length))\n\n if (!next || 'code' in next || typeof next.amount !== 'number') {\n return undefined\n }\n\n const isFraction = next.amount < 1\n\n if (!isFraction && !Number.isInteger(next.amount)) {\n return undefined\n }\n\n const head = text.replace(/\\s+/g, ' ').trim().slice(0, -rest.length)\n const suggestion = `${head.trim()}${isFraction ? ' ' : '-'}${next.amountText} ${next.rest}`\n const parsed = readYield([], suggestion, [])\n\n return parsed &&\n (isFraction\n ? parsed.amount === amount + next.amount && parsed.max === undefined\n : parsed.amount === amount && parsed.max === next.amount)\n ? suggestion\n : undefined\n}\n\n/** Reads text that starts with an amount or range, such as \"4–6\" or \"24 cookies\". */\nfunction readRangeText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n expected: string,\n): { range: Servings; rest: string } | undefined {\n const match = matchAmount(text.replace(/\\s+/g, ' ').trim())\n\n if (!match) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n if ('code' in match) {\n return error(issues, match.code, path, match.message)\n }\n\n const { amount, rest } = match\n const range: Servings =\n typeof amount === 'number'\n ? { amount }\n : { amount: amount.min, max: amount.max }\n\n if (\n readPositive(issues, range.amount, path) === undefined ||\n (range.max !== undefined &&\n readPositive(issues, range.max, path) === undefined)\n ) {\n return undefined\n }\n\n return { range, rest }\n}\n\n/** Reads `{ amount, max }`, where `max` is optional and a `max` equal to `amount` is dropped. */\nfunction readRange(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n missingAmount: string,\n): Servings | undefined {\n const value = record.amount\n const amount =\n value === undefined\n ? error(issues, 'missing_field', [...path, 'amount'], missingAmount)\n : isRecord(value) && hasOwn(value, 'min') && hasOwn(value, 'max')\n ? error(\n issues,\n 'invalid_type',\n [...path, 'amount'],\n 'Expected a positive number, received an object. Write a range as { amount: 4, max: 6 }.',\n )\n : readPositive(issues, value, [...path, 'amount'])\n const max =\n record.max === undefined\n ? undefined\n : readPositive(issues, record.max, [...path, 'max'])\n\n if (amount === undefined) {\n return undefined\n }\n\n if (max !== undefined && max < amount) {\n return error(\n issues,\n 'invalid_range',\n [...path, 'max'],\n `Expected a maximum greater than the amount (${amount}), received ${max}.`,\n )\n }\n\n return max === undefined || max === amount ? { amount } : { amount, max }\n}\n\nfunction servingsHint(servings: Servings | undefined): string {\n if (!servings) {\n return ''\n }\n\n const { amount, max } = servings\n return ` For a number of servings, use servings: ${max === undefined ? amount : quote(`${amount}–${max}`)}.`\n}\n\nfunction readSystem(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): System | undefined {\n if (value === undefined || value === 'metric' || value === 'us') {\n return value\n }\n\n const suggestion = value === 'imperial' ? 'us' : undefined\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected \"metric\" or \"us\", received ${describeValue(value)}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n suggestion,\n )\n}\n\nfunction readTimes(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n): Times {\n if (value === undefined) {\n return { custom: [] }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n error(\n issues,\n 'invalid_type',\n path,\n `Expected { prep, cook, total, custom }, received ${describeValue(value)}.`,\n )\n return { custom: [] }\n }\n\n const prep = readDuration(issues, record.prep, [...path, 'prep'])\n const cook = readDuration(issues, record.cook, [...path, 'cook'])\n const total = readDuration(issues, record.total, [...path, 'total'])\n const custom = readItems(\n issues,\n pending,\n record.custom,\n [...path, 'custom'],\n 'custom times',\n readCustomTime,\n )\n checkFields(issues, record, TIMES_FIELDS, path)\n return defined({ prep, cook, total, custom })\n}\n\nfunction readCustomTime(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<CustomTime> | undefined {\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected { label, minutes }, e.g. { label: \"Chill\", minutes: 60 }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const label = readText(\n issues,\n record.label,\n [...path, 'label'],\n 'Add a label, e.g. label: \"Chill\".',\n )\n const minutes =\n record.minutes === undefined\n ? error(\n issues,\n 'missing_field',\n [...path, 'minutes'],\n 'Add the time, e.g. minutes: 60 or minutes: \"PT1H\".',\n )\n : readDuration(issues, record.minutes, [...path, 'minutes'])\n checkFields(issues, record, CUSTOM_TIME_FIELDS, path)\n\n if (label === undefined || minutes === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(label) || 'time',\n path,\n position,\n build: (id) => ({ id, label, minutes }),\n }\n}\n\nfunction readDuration(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const minutes =\n typeof value === 'number'\n ? nonNegative(value)\n : typeof value === 'string'\n ? parseIsoDuration(value)\n : undefined\n\n if (minutes === undefined) {\n return error(\n issues,\n 'invalid_duration',\n path,\n `Expected minutes or an ISO 8601 duration like \"PT30M\", received ${describeValue(value)}.`,\n )\n }\n\n return minutes\n}\n\nfunction readNutrition(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Nutrition | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected nutrition for one serving, e.g. { calories: 250, fat: \"12 g\" }, received ${describeValue(value)}.`,\n )\n }\n\n const nutrition: Nutrition = {}\n const servingSize = readText(issues, record.servingSize, [\n ...path,\n 'servingSize',\n ])\n\n if (servingSize !== undefined) {\n nutrition.servingSize = servingSize\n }\n\n for (const key of Object.keys(NUTRIENT_UNITS) as NutrientKey[]) {\n const amount = readNutrient(\n issues,\n record[key],\n [...path, key],\n NUTRIENT_UNITS[key],\n )\n\n if (amount !== undefined) {\n nutrition[key] = amount\n }\n }\n\n // Spread here: a module-level spread can't be tree-shaken, so it would add NUTRIENT_UNITS to nearly every entry point.\n checkFields(issues, record, { servingSize: true, ...NUTRIENT_UNITS }, path)\n return Object.keys(nutrition).length > 0 ? nutrition : undefined\n}\n\n/** Reads an amount of `unit`. Empty text counts as absent. */\nfunction readNutrient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n unit: NutrientUnit,\n): number | undefined {\n const text = typeof value === 'string' ? value.trim() : undefined\n\n if (value === undefined || text === '') {\n return undefined\n }\n\n const amount =\n typeof value === 'number'\n ? nonNegative(value)\n : text === undefined\n ? undefined\n : parseNutrient(text, unit)\n\n if (typeof amount === 'number') {\n return amount\n }\n\n return error(\n issues,\n typeof value === 'number'\n ? 'invalid_amount'\n : text === undefined\n ? 'invalid_type'\n : 'invalid_quantity',\n path,\n amount?.message ??\n `Expected ${NUTRIENT_EXAMPLES[unit]}, received ${describeValue(value)}.`,\n )\n}\n\n/** `value` if it's finite and not negative, with `-0` read as `0`. */\nfunction nonNegative(value: number): number | undefined {\n return Number.isFinite(value) && value >= 0 ? Math.abs(value) : undefined\n}\n\n/**\n * Reads optional text, trimmed. Empty text counts as absent. With\n * `requiredMessage`, absent text is a `missing_field` error.\n */\nfunction readText(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n requiredMessage?: string,\n): string | undefined {\n if (value !== undefined && typeof value !== 'string') {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected text, received ${describeValue(value)}.`,\n )\n }\n\n const text = value?.trim()\n\n if (!text) {\n return requiredMessage === undefined\n ? undefined\n : error(issues, 'missing_field', path, requiredMessage)\n }\n\n return text\n}\n\n/**\n * Reads an optional link or image URL, which must be http, https, or relative.\n * Other schemes, such as `javascript:`, are errors, so a URL is safe to render\n * as an `href` or `src`.\n */\nfunction readUrl(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): string | undefined {\n const url = readText(issues, value, path)\n const scheme = url === undefined ? undefined : disallowedScheme(url)\n\n return scheme === undefined\n ? url\n : error(\n issues,\n 'invalid_url',\n path,\n `Expected an http or https URL, or a relative one, received ${quote(`${scheme}…`)}.`,\n )\n}\n\nfunction readBoolean(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): boolean {\n if (value === undefined || typeof value === 'boolean') {\n return value ?? false\n }\n\n error(\n issues,\n 'invalid_type',\n path,\n `Expected true or false, received ${describeValue(value)}.`,\n )\n return false\n}\n"],"mappings":"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","names":["issues","value","entry","range"]}
|
package/dist/resolve.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["import { checkFactor, isPositive } from './quantity/amount'\nimport { multiply } from './quantity/scale'\nimport type { Quantity } from './quantity/types'\nimport type { Ingredient, Recipe, Servings, Step, Yield } from './recipe/types'\nimport { lookupUnit, type System } from './units/registry'\nimport { checkSystem, inSystem, isSystem, rebalance } from './units/system'\n\n/**\n * Where a resolved quantity comes from: the ingredient's `quantity`, one of its\n * `equivalents`, or a conversion into the display system. May grow in minor releases.\n */\nexport type QuantityOrigin = 'authored' | 'equivalent' | 'converted'\n\n/** An ingredient, scaled and expressed in the display system. */\nexport type ResolvedIngredient = {\n id: string\n /** The ingredient as authored, unscaled, not a copy; treat it as read-only. */\n ingredient: Ingredient\n /**\n * Absent for unquantified ingredients, such as \"salt, to taste\". May be the\n * authored object; treat it as read-only.\n */\n quantity?: Quantity | undefined\n /** `'authored'` for unquantified ingredients. */\n origin: QuantityOrigin\n}\n\n/** What {@link resolveRecipe} returns: the recipe at a scale, in one system, unrounded. */\nexport type ResolvedRecipe = {\n /** The recipe passed in, not a copy; treat it as read-only. */\n recipe: Recipe\n /**\n * The `scale` option, else `servings` divided by the scaling base. Always\n * between 1e-6 and 1e6, and 1 when both options fall back.\n */\n scale: number\n /**\n * The recipe's servings at this scale. At 1×, the authored servings. When\n * the `servings` option sets a scale other than 1, exactly that number with\n * no `max`; with `scale`, both ends of a range scale.\n */\n servings?: Servings | undefined\n /**\n * The recipe's yield at this scale. At 1×, the authored yield. When the\n * yield is the scaling base and the `servings` option sets a scale other\n * than 1, exactly that number with no `max`; otherwise both ends of a range\n * scale.\n */\n yield?: Yield | undefined\n system: System\n /**\n * True when an ingredient's quantity has a unit that belongs to only one\n * system. Step text isn't read; `segmentStepText` finds its temperatures.\n */\n canConvert: boolean\n ingredients: Array<{\n id: string\n title?: string | undefined\n items: ResolvedIngredient[]\n }>\n /** Steps numbered from 1, continuing across sections. */\n steps: Array<{\n id: string\n title?: string | undefined\n items: Array<Step & { number: number }>\n }>\n}\n\n/** Options for {@link resolveRecipe}. */\nexport type ResolveRecipeOptions = {\n /**\n * Scales by `servings / base`, where the base is the recipe's servings\n * amount, else its yield amount, else 1. Defaults to the base. A value\n * outside a millionth to a million times the base, or that isn't a number,\n * also uses the default.\n */\n servings?: number | undefined\n /**\n * A direct multiplier, for 1×, 2×, and 3× controls. Wins over `servings`.\n * A value outside 1e-6 to 1e6, or that isn't a number, is ignored.\n */\n scale?: number | undefined\n /** Defaults to `recipe.system`, then `'metric'`. An unknown system also uses the default. */\n system?: System | undefined\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/** Options for {@link resolveIngredient}. */\nexport type ResolveIngredientOptions = {\n scale: number\n system: System\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/**\n * Scales a recipe to a number of servings, or by a multiplier, and expresses\n * each ingredient in a measurement system. Pure, and never throws for an\n * unchanged recipe from `parseRecipe`: invalid options fall back to their\n * defaults, and an error thrown by a getter or proxy in the options passes\n * through. Nothing is rounded; format for display.\n *\n * Each ingredient uses the first of `[quantity, ...equivalents]` that is native to\n * the system (a bare count is native to both), scaled. When none is, `quantity`\n * is scaled, then converted with `toSystem`. Units never change dimension.\n * At any scale but 1, the chosen quantity may move along its unit's ladder, so\n * 3 tsp at 2× is 2 tbsp; pass `rebalance: false` to keep the chosen units.\n *\n * @example\n * // Serves 4–6; flour is 1 ½ cups with a 190 g equivalent.\n * const resolved = resolveRecipe(pancakes, { servings: 8, system: 'metric' })\n * resolved.scale // 2\n * resolved.servings // { amount: 8 }\n * const flour = resolved.ingredients[0]?.items[0]\n * flour?.quantity // { amount: 380, unit: 'g' }\n * flour?.origin // 'equivalent'\n *\n * resolveRecipe(pancakes, { scale: 2 }).servings // { amount: 8, max: 12 }\n */\nexport function resolveRecipe(\n recipe: Recipe,\n options?: ResolveRecipeOptions,\n): ResolvedRecipe {\n const {\n servings,\n scale: requestedScale,\n system: requestedSystem,\n rebalance,\n }: ResolveRecipeOptions = options ?? {}\n const servingsIsBase = isPositive(recipe.servings?.amount)\n const base =\n [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1\n // servings / base can land just outside the window at a bound, as\n // 700000 / 0.7 is 1000000.0000000001.\n const ratio = isServingsFor(servings, base)\n ? Math.min(Math.max(servings / base, 1e-6), 1e6)\n : Number.NaN\n const exactServings =\n isScale(requestedScale) || !isScale(ratio) || isUnity(ratio)\n ? undefined\n : servings\n const scale = isScale(requestedScale)\n ? requestedScale\n : isScale(ratio)\n ? ratio\n : 1\n const system = [requestedSystem, recipe.system].find(isSystem) ?? 'metric'\n const shouldRebalance = rebalance !== false\n let number = 0\n\n function resize<T extends Servings>(\n range: T,\n isBase: boolean,\n ): Omit<T, 'max'> & Servings {\n if (exactServings !== undefined && isBase) {\n return withRange(range, exactServings)\n }\n return isUnity(scale)\n ? range\n : withRange(range, range.amount * scale, (range.max ?? 0) * scale)\n }\n\n return {\n recipe,\n scale,\n ...(recipe.servings && {\n servings: resize(recipe.servings, servingsIsBase),\n }),\n ...(recipe.yield && { yield: resize(recipe.yield, !servingsIsBase) }),\n system,\n canConvert: recipe.ingredients.some((section) =>\n section.items.some(\n (item) => lookupUnit(item.quantity?.unit)?.systems.length === 1,\n ),\n ),\n ingredients: recipe.ingredients.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((ingredient) =>\n resolveLine(ingredient, scale, system, shouldRebalance),\n ),\n })),\n steps: recipe.steps.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((step) => ({ ...step, number: ++number })),\n })),\n }\n}\n\n/**\n * Resolves one ingredient the way {@link resolveRecipe} does, at a raw\n * scale factor. Unlike `resolveRecipe`, it doesn't limit the scale to\n * 1e-6 to 1e6, so a huge scale can overflow an amount to `Infinity`, which\n * the format functions reject.\n *\n * @throws {RecipeBookError} `invalid_argument` when `scale` is not a finite\n * number greater than 0, or `system` is not `'metric'` or `'us'`.\n *\n * @example\n * // salt is 3 tsp.\n * resolveIngredient(salt, { scale: 2, system: 'us' }).quantity // { amount: 2, unit: 'tbsp' }\n */\nexport function resolveIngredient(\n ingredient: Ingredient,\n options: ResolveIngredientOptions,\n): ResolvedIngredient {\n checkFactor('resolveIngredient', 'scale', options?.scale)\n checkSystem('resolveIngredient', options.system)\n\n return resolveLine(\n ingredient,\n options.scale,\n options.system,\n options.rebalance !== false,\n )\n}\n\nfunction resolveLine(\n ingredient: Ingredient,\n scale: number,\n system: System,\n shouldRebalance: boolean,\n): ResolvedIngredient {\n const { id, quantity } = ingredient\n\n if (quantity === undefined) {\n return { id, ingredient, origin: 'authored' }\n }\n\n const candidates = [quantity, ...ingredient.equivalents]\n const native = candidates.findIndex((candidate) =>\n isNative(candidate, system),\n )\n const chosen = candidates[native] ?? quantity\n const scaled = isUnity(scale) ? chosen : multiply(chosen, scale)\n\n if (native < 0) {\n return {\n id,\n ingredient,\n quantity: withEnds(inSystem(scaled, system)),\n origin: 'converted',\n }\n }\n\n return {\n id,\n ingredient,\n quantity: withEnds(\n shouldRebalance && !isUnity(scale) ? rebalance(scaled, system) : scaled,\n ),\n origin: native === 0 ? 'authored' : 'equivalent',\n }\n}\n\n// Ends a few ulps apart can round to equal when scaled or converted, so a range\n// is kept only while its ends differ.\nfunction withRange<T extends Servings>(\n range: T,\n amount: number,\n max = 0,\n): Omit<T, 'max'> & Servings {\n const { max: authoredMax, ...rest } = range\n return { ...rest, amount, ...(max > amount && { max }) }\n}\n\nfunction withEnds(quantity: Quantity): Quantity {\n const { amount } = quantity\n return typeof amount === 'number' || amount.min < amount.max\n ? quantity\n : { ...quantity, amount: amount.min }\n}\n\n// A millionth to a million times the base, found by moving the base's decimal\n// point six places, so a bound written out, such as 700000 for a base of 0.7,\n// is in. @recipe-book/react's isServingsFor applies the same rule.\nfunction isServingsFor(servings: unknown, base: number): servings is number {\n const [mantissa, exponent] = base.toExponential().split('e')\n const shift = (places: number) =>\n Number(`${mantissa}e${Number(exponent) + places}`)\n return isPositive(servings) && servings >= shift(-6) && servings <= shift(6)\n}\n\nfunction isScale(value: unknown): value is number {\n return typeof value === 'number' && value >= 1e-6 && value <= 1e6\n}\n\nfunction isUnity(scale: number): boolean {\n return Math.abs(scale - 1) <= 1e-9\n}\n\nexport function isNative(quantity: Quantity, system: System): boolean {\n const unit = lookupUnit(quantity.unit)\n return unit === undefined || unit.systems.includes(system)\n}\n"],"mappings":"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","names":["rebalance"]}
|
|
1
|
+
{"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["import { checkFactor, isPositive } from './quantity/amount'\nimport { multiply } from './quantity/scale'\nimport type { Quantity } from './quantity/types'\nimport type { Ingredient, Recipe, Servings, Step, Yield } from './recipe/types'\nimport { lookupUnit, type System } from './units/registry'\nimport { checkSystem, inSystem, isSystem, rebalance } from './units/system'\n\n/**\n * Where a resolved quantity comes from: the ingredient's `quantity`, one of its\n * `equivalents`, or a conversion into the display system. May grow in minor releases.\n */\nexport type QuantityOrigin = 'authored' | 'equivalent' | 'converted'\n\n/** An ingredient, scaled and expressed in the display system. */\nexport type ResolvedIngredient = {\n id: string\n /** The ingredient as authored, unscaled, not a copy; treat it as read-only. */\n ingredient: Ingredient\n /**\n * Absent for unquantified ingredients, such as \"salt, to taste\". May be the\n * authored object; treat it as read-only.\n */\n quantity?: Quantity | undefined\n /** `'authored'` for unquantified ingredients. */\n origin: QuantityOrigin\n}\n\n/** What {@link resolveRecipe} returns: the recipe at a scale, in one system, unrounded. */\nexport type ResolvedRecipe = {\n /** The recipe passed in, not a copy; treat it as read-only. */\n recipe: Recipe\n /**\n * The `scale` option, else `servings` divided by the scaling base. Always\n * between 1e-6 and 1e6, and 1 when both options fall back.\n */\n scale: number\n /**\n * The recipe's servings at this scale. At a scale within 1e-9 of 1, the\n * authored servings. When the `servings` option sets any other scale,\n * exactly that number with no `max`; with `scale`, both ends of a range\n * scale.\n */\n servings?: Servings | undefined\n /**\n * The recipe's yield at this scale. At a scale within 1e-9 of 1, the\n * authored yield. When the yield is the scaling base and the `servings`\n * option sets any other scale, exactly that number with no `max`; otherwise\n * both ends of a range scale.\n */\n yield?: Yield | undefined\n system: System\n /**\n * True when an ingredient's quantity has a unit that belongs to only one\n * system. Step text isn't read; `segmentStepText` finds its temperatures.\n */\n canConvert: boolean\n ingredients: Array<{\n id: string\n title?: string | undefined\n items: ResolvedIngredient[]\n }>\n /** Steps numbered from 1, continuing across sections. */\n steps: Array<{\n id: string\n title?: string | undefined\n items: Array<Step & { number: number }>\n }>\n}\n\n/** Options for {@link resolveRecipe}. */\nexport type ResolveRecipeOptions = {\n /**\n * Scales by `servings / base`, where the base is the recipe's servings\n * amount, else its yield amount, else 1. Defaults to the base. A value\n * outside a millionth to a million times the base, or that isn't a number,\n * also uses the default.\n */\n servings?: number | undefined\n /**\n * A direct multiplier, for 1×, 2×, and 3× controls. Wins over `servings`.\n * A value outside 1e-6 to 1e6, or that isn't a number, is ignored.\n */\n scale?: number | undefined\n /** Defaults to `recipe.system`, then `'metric'`. An unknown system also uses the default. */\n system?: System | undefined\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/** Options for {@link resolveIngredient}. */\nexport type ResolveIngredientOptions = {\n scale: number\n system: System\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/**\n * Scales a recipe to a number of servings, or by a multiplier, and expresses\n * each ingredient in a measurement system. Pure, and never throws for an\n * unchanged recipe from `parseRecipe`: invalid options fall back to their\n * defaults, and an error thrown by a getter or proxy in the options passes\n * through. Nothing is rounded; format for display.\n *\n * Each ingredient uses the first of `[quantity, ...equivalents]` that is native to\n * the system (a bare count is native to both), scaled. When none is, `quantity`\n * is scaled, then converted with `toSystem`. Units never change dimension.\n * At any scale but 1, the chosen quantity may move along its unit's ladder, so\n * 3 tsp at 2× is 2 tbsp; pass `rebalance: false` to keep the chosen units.\n *\n * @example\n * // Serves 4–6; flour is 1 ½ cups with a 190 g equivalent.\n * const resolved = resolveRecipe(pancakes, { servings: 8, system: 'metric' })\n * resolved.scale // 2\n * resolved.servings // { amount: 8 }\n * const flour = resolved.ingredients[0]?.items[0]\n * flour?.quantity // { amount: 380, unit: 'g' }\n * flour?.origin // 'equivalent'\n *\n * resolveRecipe(pancakes, { scale: 2 }).servings // { amount: 8, max: 12 }\n */\nexport function resolveRecipe(\n recipe: Recipe,\n options?: ResolveRecipeOptions,\n): ResolvedRecipe {\n const {\n servings,\n scale: requestedScale,\n system: requestedSystem,\n rebalance,\n }: ResolveRecipeOptions = options ?? {}\n const servingsIsBase = isPositive(recipe.servings?.amount)\n const base =\n [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1\n // servings / base can land just outside the window at a bound, as\n // 700000 / 0.7 is 1000000.0000000001.\n const ratio = isServingsFor(servings, base)\n ? Math.min(Math.max(servings / base, 1e-6), 1e6)\n : Number.NaN\n const exactServings =\n isScale(requestedScale) || !isScale(ratio) || isUnity(ratio)\n ? undefined\n : servings\n const scale = isScale(requestedScale)\n ? requestedScale\n : isScale(ratio)\n ? ratio\n : 1\n const system = [requestedSystem, recipe.system].find(isSystem) ?? 'metric'\n const shouldRebalance = rebalance !== false\n let number = 0\n\n function resize<T extends Servings>(\n range: T,\n isBase: boolean,\n ): Omit<T, 'max'> & Servings {\n if (exactServings !== undefined && isBase) {\n return withRange(range, exactServings)\n }\n return isUnity(scale)\n ? range\n : withRange(range, range.amount * scale, (range.max ?? 0) * scale)\n }\n\n return {\n recipe,\n scale,\n ...(recipe.servings && {\n servings: resize(recipe.servings, servingsIsBase),\n }),\n ...(recipe.yield && { yield: resize(recipe.yield, !servingsIsBase) }),\n system,\n canConvert: recipe.ingredients.some((section) =>\n section.items.some(\n (item) => lookupUnit(item.quantity?.unit)?.systems.length === 1,\n ),\n ),\n ingredients: recipe.ingredients.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((ingredient) =>\n resolveLine(ingredient, scale, system, shouldRebalance),\n ),\n })),\n steps: recipe.steps.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((step) => ({ ...step, number: ++number })),\n })),\n }\n}\n\n/**\n * Resolves one ingredient the way {@link resolveRecipe} does, at a raw\n * scale factor. Unlike `resolveRecipe`, it doesn't limit the scale to\n * 1e-6 to 1e6, so a huge scale can overflow an amount to `Infinity`, which\n * the format functions reject.\n *\n * An error thrown by a getter or proxy in the input or options passes through.\n *\n * @throws {RecipeBookError} `invalid_argument` when `scale` is not a finite\n * number greater than 0, or `system` is not `'metric'` or `'us'`.\n *\n * @example\n * // salt is 3 tsp.\n * resolveIngredient(salt, { scale: 2, system: 'us' }).quantity // { amount: 2, unit: 'tbsp' }\n */\nexport function resolveIngredient(\n ingredient: Ingredient,\n options: ResolveIngredientOptions,\n): ResolvedIngredient {\n checkFactor('resolveIngredient', 'scale', options?.scale)\n checkSystem('resolveIngredient', options.system)\n\n return resolveLine(\n ingredient,\n options.scale,\n options.system,\n options.rebalance !== false,\n )\n}\n\nfunction resolveLine(\n ingredient: Ingredient,\n scale: number,\n system: System,\n shouldRebalance: boolean,\n): ResolvedIngredient {\n const { id, quantity } = ingredient\n\n if (quantity === undefined) {\n return { id, ingredient, origin: 'authored' }\n }\n\n const candidates = [quantity, ...ingredient.equivalents]\n const native = candidates.findIndex((candidate) =>\n isNative(candidate, system),\n )\n const chosen = candidates[native] ?? quantity\n const scaled = isUnity(scale) ? chosen : multiply(chosen, scale)\n\n if (native < 0) {\n return {\n id,\n ingredient,\n quantity: withEnds(inSystem(scaled, system)),\n origin: 'converted',\n }\n }\n\n return {\n id,\n ingredient,\n quantity: withEnds(\n shouldRebalance && !isUnity(scale) ? rebalance(scaled, system) : scaled,\n ),\n origin: native === 0 ? 'authored' : 'equivalent',\n }\n}\n\n// Ends a few ulps apart can round to equal when scaled or converted, so a range\n// is kept only while its ends differ.\nfunction withRange<T extends Servings>(\n range: T,\n amount: number,\n max = 0,\n): Omit<T, 'max'> & Servings {\n const { max: authoredMax, ...rest } = range\n return { ...rest, amount, ...(max > amount && { max }) }\n}\n\nfunction withEnds(quantity: Quantity): Quantity {\n const { amount } = quantity\n return typeof amount === 'number' || amount.min < amount.max\n ? quantity\n : { ...quantity, amount: amount.min }\n}\n\n// A millionth to a million times the base, found by moving the base's decimal\n// point six places, so a bound written out, such as 700000 for a base of 0.7,\n// is in. @recipe-book/react's isServingsFor applies the same rule.\nfunction isServingsFor(servings: unknown, base: number): servings is number {\n const [mantissa, exponent] = base.toExponential().split('e')\n const shift = (places: number) =>\n Number(`${mantissa}e${Number(exponent) + places}`)\n return isPositive(servings) && servings >= shift(-6) && servings <= shift(6)\n}\n\nfunction isScale(value: unknown): value is number {\n return typeof value === 'number' && value >= 1e-6 && value <= 1e6\n}\n\nfunction isUnity(scale: number): boolean {\n return Math.abs(scale - 1) <= 1e-9\n}\n\nexport function isNative(quantity: Quantity, system: System): boolean {\n const unit = lookupUnit(quantity.unit)\n return unit === undefined || unit.systems.includes(system)\n}\n"],"mappings":"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","names":["rebalance"]}
|
|
@@ -28,11 +28,6 @@ var READ_FIELDS = [
|
|
|
28
28
|
"tool",
|
|
29
29
|
"nutrition"
|
|
30
30
|
];
|
|
31
|
-
var TIMES = [
|
|
32
|
-
["prep", "prepTime"],
|
|
33
|
-
["cook", "cookTime"],
|
|
34
|
-
["total", "totalTime"]
|
|
35
|
-
];
|
|
36
31
|
var MAX_URL_LENGTH = 4096;
|
|
37
32
|
var SERVINGS_WORD = /^(?:servings?|portions?|(?:serving|portion)\(s\)|people|persons?)\.?$/i;
|
|
38
33
|
function fromJsonLd(input) {
|
|
@@ -165,7 +160,8 @@ function readRecipe(reader, node, base) {
|
|
|
165
160
|
}
|
|
166
161
|
readYield(reader, model, node.recipeYield, at("recipeYield"));
|
|
167
162
|
const times = {};
|
|
168
|
-
for (const [
|
|
163
|
+
for (const key of ["prep", "cook", "total"]) {
|
|
164
|
+
const field = `${key}Time`;
|
|
169
165
|
const minutes = readTime(reader, node[field], at(field));
|
|
170
166
|
if (minutes !== void 0) {
|
|
171
167
|
times[key] = minutes;
|
|
@@ -274,15 +270,17 @@ function findImage(reader, value, source, referenced = false) {
|
|
|
274
270
|
const id = record["@id"];
|
|
275
271
|
const target = typeof id === "string" && !referenced ? reader.ids.get(id) : void 0;
|
|
276
272
|
let image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
|
|
277
|
-
if (!image && target
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
273
|
+
if (!image && target) {
|
|
274
|
+
if (target.node === reader.recipe) {
|
|
275
|
+
ignore(
|
|
276
|
+
reader,
|
|
277
|
+
"invalid_type",
|
|
278
|
+
source,
|
|
279
|
+
"Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself."
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
image = findImage(reader, target.node, target.path, true);
|
|
283
|
+
}
|
|
286
284
|
}
|
|
287
285
|
reads.push([
|
|
288
286
|
image,
|
|
@@ -477,15 +475,10 @@ function readList(reader, key, value, source, readItem, expected) {
|
|
|
477
475
|
return list;
|
|
478
476
|
}
|
|
479
477
|
function readLine(reader, value, source) {
|
|
480
|
-
if (typeof value === "string") {
|
|
481
|
-
return cleanLines(value).map((line) => ({
|
|
482
|
-
value: line,
|
|
483
|
-
source,
|
|
484
|
-
fields: []
|
|
485
|
-
}));
|
|
486
|
-
}
|
|
487
478
|
const record = asRecord(value);
|
|
488
|
-
const parts = [record?.value, record?.unitText, record?.name ?? record?.text].filter(
|
|
479
|
+
const parts = typeof value === "string" ? value : [record?.value, record?.unitText, record?.name ?? record?.text].filter(
|
|
480
|
+
(part) => typeof part === "string" || typeof part === "number"
|
|
481
|
+
).join(" ");
|
|
489
482
|
return parts ? cleanLines(parts).map((line) => ({ value: line, source, fields: [] })) : void 0;
|
|
490
483
|
}
|
|
491
484
|
function readStep(reader, value, source) {
|