@recipe-book/core 0.1.0 → 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VWVIBCTG.cjs → chunk-ISRDJR2O.cjs} +227 -179
  4. package/dist/chunk-ISRDJR2O.cjs.map +1 -0
  5. package/dist/errors.js +29 -1
  6. package/dist/errors.js.map +1 -1
  7. package/dist/format/duration.js.map +1 -1
  8. package/dist/format/ingredient.js.map +1 -1
  9. package/dist/format/number.js +1 -1
  10. package/dist/format/number.js.map +1 -1
  11. package/dist/format/options.js.map +1 -1
  12. package/dist/format/quantity.js.map +1 -1
  13. package/dist/format/servings.js.map +1 -1
  14. package/dist/format/temperature.js.map +1 -1
  15. package/dist/index.cjs +87 -74
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +71 -28
  18. package/dist/index.d.ts +71 -28
  19. package/dist/index.js +6 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/ingredient/parse.js +27 -22
  22. package/dist/ingredient/parse.js.map +1 -1
  23. package/dist/issues.js.map +1 -1
  24. package/dist/quantity/amount.js +11 -0
  25. package/dist/quantity/amount.js.map +1 -1
  26. package/dist/quantity/parse.js +58 -81
  27. package/dist/quantity/parse.js.map +1 -1
  28. package/dist/quantity/scale.js +5 -2
  29. package/dist/quantity/scale.js.map +1 -1
  30. package/dist/recipe/ids.js.map +1 -1
  31. package/dist/recipe/parse.js +80 -60
  32. package/dist/recipe/parse.js.map +1 -1
  33. package/dist/recipe/schema.js +8 -6
  34. package/dist/recipe/schema.js.map +1 -1
  35. package/dist/resolve.js +6 -1
  36. package/dist/resolve.js.map +1 -1
  37. package/dist/schema-org/find.js +46 -13
  38. package/dist/schema-org/find.js.map +1 -1
  39. package/dist/schema-org/from-json-ld.js +134 -71
  40. package/dist/schema-org/from-json-ld.js.map +1 -1
  41. package/dist/schema-org/text.js.map +1 -1
  42. package/dist/schema-org/to-json-ld.js +12 -15
  43. package/dist/schema-org/to-json-ld.js.map +1 -1
  44. package/dist/schema-org.cjs +239 -147
  45. package/dist/schema-org.cjs.map +1 -1
  46. package/dist/schema-org.d.cts +35 -21
  47. package/dist/schema-org.d.ts +35 -21
  48. package/dist/steps/segment.js.map +1 -1
  49. package/dist/steps/temperature.js +13 -13
  50. package/dist/steps/temperature.js.map +1 -1
  51. package/dist/{types-DSMf45H5.d.cts → types-CKIoWsz8.d.cts} +25 -7
  52. package/dist/{types-DSMf45H5.d.ts → types-CKIoWsz8.d.ts} +25 -7
  53. package/dist/units/convert.js +16 -8
  54. package/dist/units/convert.js.map +1 -1
  55. package/dist/units/temperature.js +7 -1
  56. package/dist/units/temperature.js.map +1 -1
  57. package/dist/validation.js +1 -1
  58. package/dist/validation.js.map +1 -1
  59. package/package.json +1 -1
  60. package/dist/chunk-VWVIBCTG.cjs.map +0 -1
@@ -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 const issues: RecipeIssue[] = []\n const readLine = readLineOption('parseRecipe', options)\n const recipe = readRecipe(issues, input, readLine)\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\ntype 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 readLine: LineReader | undefined,\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, readLine),\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 entries =\n value === undefined ? [] : readArray(issues, value, path, noun)\n\n if (!entries) {\n return []\n }\n\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 entries.forEach((entry: unknown, i) => {\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 // forEach 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 const entries = readArray(issues, record.items, itemsPath, noun)\n\n if (entries) {\n let visited = 0\n entries.forEach((entry: unknown, 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 if (visited === 0) {\n warning(issues, 'empty_list', itemsPath, `Section has no ${noun}.`)\n }\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 const entries =\n value === undefined ? [] : readArray(issues, value, path, noun)\n\n if (!entries) {\n return []\n }\n\n let n = 0\n const drafts = entries.flatMap((entry: unknown, i) => {\n const draft = readItem(issues, entry, [...path, i], ++n)\n return draft ? [draft] : []\n })\n const ids = assignIds(drafts, pending)\n\n return drafts.map((draft, i) => draft.build(ids[i]!))\n}\n\nfunction readArray(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n noun: string,\n): unknown[] | undefined {\n return Array.isArray(value)\n ? value\n : error(\n issues,\n 'invalid_type',\n path,\n `Expected a list of ${noun}, received ${describeValue(value)}.`,\n )\n}\n\nfunction readIngredient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n readLine?: LineReader,\n): Draft<Ingredient> | undefined {\n if (typeof value === 'string' && readLine) {\n const result = readLine(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 ${readLine ? '\"2 cups flour\" or ' : ''}{ name: \"flour\", quantity: \"2 cups\" }, received ${describeValue(value)}.${typeof value === 'string' ? ' To read ingredient lines, pass parseIngredientLine: parseRecipe(input, { parseIngredientLine }).' : ''}`,\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 if (value === undefined) {\n return []\n }\n\n if (!Array.isArray(value)) {\n error(\n issues,\n 'invalid_type',\n path,\n `Expected a list of quantities, e.g. [\"190 g\"], received ${describeValue(value)}.`,\n )\n return []\n }\n\n return value.flatMap((entry: unknown, i) => {\n const quantity = readQuantity(issues, entry, [...path, i])\n return quantity === undefined ? [] : [quantity]\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 return {\n range:\n typeof amount === 'number'\n ? { amount }\n : { amount: amount.min, max: amount.max },\n rest,\n }\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"]}
@@ -1,18 +1,20 @@
1
1
  // src/recipe/schema.ts
2
- import { parseRecipe, readLineOption } from "./parse.js";
2
+ import {
3
+ parseRecipeWith,
4
+ readLineOption
5
+ } from "./parse.js";
3
6
  var recipeSchema = /* @__PURE__ */ standardSchema();
4
7
  function createRecipeSchema(options) {
5
- return standardSchema({
6
- parseIngredientLine: readLineOption("createRecipeSchema", options)
7
- });
8
+ return standardSchema(readLineOption("createRecipeSchema", options));
8
9
  }
9
- function standardSchema(options) {
10
+ function standardSchema(readLine) {
11
+ const lines = readLine ?? "use createRecipeSchema({ parseIngredientLine })";
10
12
  return {
11
13
  "~standard": {
12
14
  version: 1,
13
15
  vendor: "recipe-book",
14
16
  validate: (value) => {
15
- const result = parseRecipe(value, options);
17
+ const result = parseRecipeWith(value, lines);
16
18
  if (result.success) {
17
19
  return { value: result.data };
18
20
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/recipe/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '../standard-schema'\nimport type { RecipeInput } from './input'\nimport { parseRecipe, type ParseRecipeOptions, readLineOption } from './parse'\nimport type { Recipe } from './types'\n\n/**\n * The type of {@link recipeSchema}: a Standard Schema v1 whose `validate`\n * returns its result directly, never a promise. `RecipeSchema<string>`, from\n * {@link createRecipeSchema}, also takes ingredient lines.\n */\nexport interface RecipeSchema<\n Line extends string = never,\n> extends StandardSchemaV1<RecipeInput<Line>, Recipe> {\n readonly '~standard': {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n options?: StandardSchemaV1.Options,\n ) => StandardSchemaV1.Result<Recipe>\n readonly types?:\n StandardSchemaV1.Types<RecipeInput<Line>, Recipe> | undefined\n }\n}\n\n/**\n * `parseRecipe` as a [Standard Schema](https://standardschema.dev) v1, for\n * libraries that accept one: tRPC, TanStack Form, react-hook-form, Hono, and\n * others. Validation is synchronous, so `validate` returns its result\n * directly. Only errors are reported as issues; warnings don't fail\n * validation. It reads ingredient objects only; use\n * {@link createRecipeSchema} for ingredient lines.\n *\n * @example\n * import { recipeSchema } from '@recipe-book/core'\n * import { initTRPC } from '@trpc/server'\n *\n * const t = initTRPC.create()\n * export const appRouter = t.router({\n * saveRecipe: t.procedure.input(recipeSchema).mutation(({ input }) => input.id), // input: Recipe\n * })\n */\nexport const recipeSchema: RecipeSchema = /* @__PURE__ */ standardSchema()\n\n/**\n * Makes a schema like {@link recipeSchema} that parses with `options`. Pass\n * `{ parseIngredientLine }` to accept ingredients written as text.\n *\n * @throws {RecipeBookError} `invalid_argument` when\n * `options.parseIngredientLine` is present but isn't a function.\n *\n * @example\n * import { createRecipeSchema, parseIngredientLine } from '@recipe-book/core'\n * import { initTRPC } from '@trpc/server'\n *\n * const t = initTRPC.create()\n * const recipeInput = createRecipeSchema({ parseIngredientLine })\n * export const appRouter = t.router({\n * saveRecipe: t.procedure.input(recipeInput).mutation(({ input }) => input.id), // input: Recipe\n * })\n */\nexport function createRecipeSchema(\n options: ParseRecipeOptions,\n): RecipeSchema<string> {\n return standardSchema({\n parseIngredientLine: readLineOption('createRecipeSchema', options),\n })\n}\n\nfunction standardSchema(options?: ParseRecipeOptions): RecipeSchema {\n return {\n '~standard': {\n version: 1,\n vendor: 'recipe-book',\n validate: (value) => {\n const result = parseRecipe(value, options)\n\n if (result.success) {\n return { value: result.data }\n }\n\n return {\n issues: result.error.issues\n .filter((issue) => issue.severity === 'error')\n .map(({ message, path }) => ({ message, path })),\n }\n },\n },\n }\n}\n"],"mappings":";AAEA,SAAS,aAAsC,sBAAsB;AAwC9D,IAAM,eAA6C,+BAAe;AAmBlE,SAAS,mBACd,SACsB;AACtB,SAAO,eAAe;AAAA,IACpB,qBAAqB,eAAe,sBAAsB,OAAO;AAAA,EACnE,CAAC;AACH;AAEA,SAAS,eAAe,SAA4C;AAClE,SAAO;AAAA,IACL,aAAa;AAAA,MACX,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,UAAU,CAAC,UAAU;AACnB,cAAM,SAAS,YAAY,OAAO,OAAO;AAEzC,YAAI,OAAO,SAAS;AAClB,iBAAO,EAAE,OAAO,OAAO,KAAK;AAAA,QAC9B;AAEA,eAAO;AAAA,UACL,QAAQ,OAAO,MAAM,OAClB,OAAO,CAAC,UAAU,MAAM,aAAa,OAAO,EAC5C,IAAI,CAAC,EAAE,SAAS,KAAK,OAAO,EAAE,SAAS,KAAK,EAAE;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/recipe/schema.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '../standard-schema'\nimport type { RecipeInput } from './input'\nimport {\n type LineReader,\n parseRecipeWith,\n type ParseRecipeOptions,\n readLineOption,\n} from './parse'\nimport type { Recipe } from './types'\n\n/**\n * The type of {@link recipeSchema}: a Standard Schema v1 whose `validate`\n * returns its result directly, never a promise. `RecipeSchema<string>`, from\n * {@link createRecipeSchema}, also takes ingredient lines.\n */\nexport interface RecipeSchema<\n Line extends string = never,\n> extends StandardSchemaV1<RecipeInput<Line>, Recipe> {\n readonly '~standard': {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n options?: StandardSchemaV1.Options,\n ) => StandardSchemaV1.Result<Recipe>\n readonly types?:\n StandardSchemaV1.Types<RecipeInput<Line>, Recipe> | undefined\n }\n}\n\n/**\n * `parseRecipe` as a [Standard Schema](https://standardschema.dev) v1, for\n * libraries that accept one: tRPC, TanStack Form, react-hook-form, Hono, and\n * others. Validation is synchronous, so `validate` returns its result\n * directly. Only errors are reported as issues; warnings don't fail\n * validation. It reads ingredient objects only; use\n * {@link createRecipeSchema} for ingredient lines.\n *\n * @example\n * import { recipeSchema } from '@recipe-book/core'\n * import { initTRPC } from '@trpc/server'\n *\n * const t = initTRPC.create()\n * export const appRouter = t.router({\n * saveRecipe: t.procedure.input(recipeSchema).mutation(({ input }) => input.id), // input: Recipe\n * })\n */\nexport const recipeSchema: RecipeSchema = /* @__PURE__ */ standardSchema()\n\n/**\n * Makes a schema like {@link recipeSchema} that parses with `options`. Pass\n * `{ parseIngredientLine }` to accept ingredients written as text.\n *\n * @throws {RecipeBookError} `invalid_argument` when\n * `options.parseIngredientLine` is present but isn't a function.\n *\n * @example\n * import { createRecipeSchema, parseIngredientLine } from '@recipe-book/core'\n * import { initTRPC } from '@trpc/server'\n *\n * const t = initTRPC.create()\n * const recipeInput = createRecipeSchema({ parseIngredientLine })\n * export const appRouter = t.router({\n * saveRecipe: t.procedure.input(recipeInput).mutation(({ input }) => input.id), // input: Recipe\n * })\n */\nexport function createRecipeSchema(\n options: ParseRecipeOptions,\n): RecipeSchema<string> {\n return standardSchema(readLineOption('createRecipeSchema', options))\n}\n\nfunction standardSchema(readLine?: LineReader): RecipeSchema {\n const lines = readLine ?? 'use createRecipeSchema({ parseIngredientLine })'\n\n return {\n '~standard': {\n version: 1,\n vendor: 'recipe-book',\n validate: (value) => {\n const result = parseRecipeWith(value, lines)\n\n if (result.success) {\n return { value: result.data }\n }\n\n return {\n issues: result.error.issues\n .filter((issue) => issue.severity === 'error')\n .map(({ message, path }) => ({ message, path })),\n }\n },\n },\n }\n}\n"],"mappings":";AAEA;AAAA,EAEE;AAAA,EAEA;AAAA,OACK;AAwCA,IAAM,eAA6C,+BAAe;AAmBlE,SAAS,mBACd,SACsB;AACtB,SAAO,eAAe,eAAe,sBAAsB,OAAO,CAAC;AACrE;AAEA,SAAS,eAAe,UAAqC;AAC3D,QAAM,QAAQ,YAAY;AAE1B,SAAO;AAAA,IACL,aAAa;AAAA,MACX,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,UAAU,CAAC,UAAU;AACnB,cAAM,SAAS,gBAAgB,OAAO,KAAK;AAE3C,YAAI,OAAO,SAAS;AAClB,iBAAO,EAAE,OAAO,OAAO,KAAK;AAAA,QAC9B;AAEA,eAAO;AAAA,UACL,QAAQ,OAAO,MAAM,OAClB,OAAO,CAAC,UAAU,MAAM,aAAa,OAAO,EAC5C,IAAI,CAAC,EAAE,SAAS,KAAK,OAAO,EAAE,SAAS,KAAK,EAAE;AAAA,QACnD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/resolve.js CHANGED
@@ -12,7 +12,7 @@ function resolveRecipe(recipe, options) {
12
12
  } = options ?? {};
13
13
  const servingsIsBase = isPositive(recipe.servings?.amount);
14
14
  const base = [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1;
15
- const ratio = typeof servings === "number" ? servings / base : Number.NaN;
15
+ const ratio = isServingsFor(servings, base) ? Math.min(Math.max(servings / base, 1e-6), 1e6) : Number.NaN;
16
16
  const exactServings = isScale(requestedScale) || !isScale(ratio) || isUnity(ratio) ? void 0 : servings;
17
17
  const scale = isScale(requestedScale) ? requestedScale : isScale(ratio) ? ratio : 1;
18
18
  const system = [requestedSystem, recipe.system].find(isSystem) ?? "metric";
@@ -97,6 +97,11 @@ function withEnds(quantity) {
97
97
  const { amount } = quantity;
98
98
  return typeof amount === "number" || amount.min < amount.max ? quantity : { ...quantity, amount: amount.min };
99
99
  }
100
+ function isServingsFor(servings, base) {
101
+ const [mantissa, exponent] = base.toExponential().split("e");
102
+ const shift = (places) => Number(`${mantissa}e${Number(exponent) + places}`);
103
+ return isPositive(servings) && servings >= shift(-6) && servings <= shift(6);
104
+ }
100
105
  function isScale(value) {
101
106
  return typeof value === "number" && value >= 1e-6 && value <= 1e6;
102
107
  }
@@ -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. */\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 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 that\n * would scale the recipe by less than 1e-6 or more than 1e6, or isn't a\n * number, 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 const ratio = typeof servings === 'number' ? servings / base : 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\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":";AAAA,SAAS,aAAa,kBAAkB;AACxC,SAAS,gBAAgB;AAGzB,SAAS,kBAA+B;AACxC,SAAS,aAAa,UAAU,UAAU,iBAAiB;AAkHpD,SAAS,cACd,QACA,SACgB;AAChB,QAAM;AAAA,IACJ;AAAA,IACA,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAAA;AAAA,EACF,IAA0B,WAAW,CAAC;AACtC,QAAM,iBAAiB,WAAW,OAAO,UAAU,MAAM;AACzD,QAAM,OACJ,CAAC,OAAO,UAAU,QAAQ,OAAO,OAAO,MAAM,EAAE,KAAK,UAAU,KAAK;AACtE,QAAM,QAAQ,OAAO,aAAa,WAAW,WAAW,OAAO,OAAO;AACtE,QAAM,gBACJ,QAAQ,cAAc,KAAK,CAAC,QAAQ,KAAK,KAAK,QAAQ,KAAK,IACvD,SACA;AACN,QAAM,QAAQ,QAAQ,cAAc,IAChC,iBACA,QAAQ,KAAK,IACX,QACA;AACN,QAAM,SAAS,CAAC,iBAAiB,OAAO,MAAM,EAAE,KAAK,QAAQ,KAAK;AAClE,QAAM,kBAAkBA,eAAc;AACtC,MAAI,SAAS;AAEb,WAAS,OACP,OACA,QAC2B;AAC3B,QAAI,kBAAkB,UAAa,QAAQ;AACzC,aAAO,UAAU,OAAO,aAAa;AAAA,IACvC;AACA,WAAO,QAAQ,KAAK,IAChB,QACA,UAAU,OAAO,MAAM,SAAS,QAAQ,MAAM,OAAO,KAAK,KAAK;AAAA,EACrE;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAI,OAAO,YAAY;AAAA,MACrB,UAAU,OAAO,OAAO,UAAU,cAAc;AAAA,IAClD;AAAA,IACA,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,OAAO,OAAO,CAAC,cAAc,EAAE;AAAA,IACnE;AAAA,IACA,YAAY,OAAO,YAAY;AAAA,MAAK,CAAC,YACnC,QAAQ,MAAM;AAAA,QACZ,CAAC,SAAS,WAAW,KAAK,UAAU,IAAI,GAAG,QAAQ,WAAW;AAAA,MAChE;AAAA,IACF;AAAA,IACA,aAAa,OAAO,YAAY,IAAI,CAAC,aAAa;AAAA,MAChD,IAAI,QAAQ;AAAA,MACZ,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,MAAM;AAAA,MAC1D,OAAO,QAAQ,MAAM;AAAA,QAAI,CAAC,eACxB,YAAY,YAAY,OAAO,QAAQ,eAAe;AAAA,MACxD;AAAA,IACF,EAAE;AAAA,IACF,OAAO,OAAO,MAAM,IAAI,CAAC,aAAa;AAAA,MACpC,IAAI,QAAQ;AAAA,MACZ,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,MAAM;AAAA,MAC1D,OAAO,QAAQ,MAAM,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,QAAQ,EAAE,OAAO,EAAE;AAAA,IACpE,EAAE;AAAA,EACJ;AACF;AAeO,SAAS,kBACd,YACA,SACoB;AACpB,cAAY,qBAAqB,SAAS,SAAS,KAAK;AACxD,cAAY,qBAAqB,QAAQ,MAAM;AAE/C,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ,cAAc;AAAA,EACxB;AACF;AAEA,SAAS,YACP,YACA,OACA,QACA,iBACoB;AACpB,QAAM,EAAE,IAAI,SAAS,IAAI;AAEzB,MAAI,aAAa,QAAW;AAC1B,WAAO,EAAE,IAAI,YAAY,QAAQ,WAAW;AAAA,EAC9C;AAEA,QAAM,aAAa,CAAC,UAAU,GAAG,WAAW,WAAW;AACvD,QAAM,SAAS,WAAW;AAAA,IAAU,CAAC,cACnC,SAAS,WAAW,MAAM;AAAA,EAC5B;AACA,QAAM,SAAS,WAAW,MAAM,KAAK;AACrC,QAAM,SAAS,QAAQ,KAAK,IAAI,SAAS,SAAS,QAAQ,KAAK;AAE/D,MAAI,SAAS,GAAG;AACd,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU,SAAS,SAAS,QAAQ,MAAM,CAAC;AAAA,MAC3C,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU;AAAA,MACR,mBAAmB,CAAC,QAAQ,KAAK,IAAI,UAAU,QAAQ,MAAM,IAAI;AAAA,IACnE;AAAA,IACA,QAAQ,WAAW,IAAI,aAAa;AAAA,EACtC;AACF;AAIA,SAAS,UACP,OACA,QACA,MAAM,GACqB;AAC3B,QAAM,EAAE,KAAK,aAAa,GAAG,KAAK,IAAI;AACtC,SAAO,EAAE,GAAG,MAAM,QAAQ,GAAI,MAAM,UAAU,EAAE,IAAI,EAAG;AACzD;AAEA,SAAS,SAAS,UAA8B;AAC9C,QAAM,EAAE,OAAO,IAAI;AACnB,SAAO,OAAO,WAAW,YAAY,OAAO,MAAM,OAAO,MACrD,WACA,EAAE,GAAG,UAAU,QAAQ,OAAO,IAAI;AACxC;AAEA,SAAS,QAAQ,OAAiC;AAChD,SAAO,OAAO,UAAU,YAAY,SAAS,QAAQ,SAAS;AAChE;AAEA,SAAS,QAAQ,OAAwB;AACvC,SAAO,KAAK,IAAI,QAAQ,CAAC,KAAK;AAChC;AAEO,SAAS,SAAS,UAAoB,QAAyB;AACpE,QAAM,OAAO,WAAW,SAAS,IAAI;AACrC,SAAO,SAAS,UAAa,KAAK,QAAQ,SAAS,MAAM;AAC3D;","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"]}
@@ -1,31 +1,38 @@
1
1
  // src/schema-org/find.ts
2
2
  import { describeValue, RecipeBookError } from "../errors.js";
3
- import { isRecord } from "../issues.js";
3
+ import { hasOwn, isRecord } from "../issues.js";
4
4
  import { asRecord } from "../validation.js";
5
5
  var MAX_DEPTH = 32;
6
6
  function locate(documents) {
7
+ const direct = scan(documents, false);
8
+ return direct.recipes.length > 0 ? direct : scan(documents, true);
9
+ }
10
+ function scan(documents, lists) {
7
11
  const recipes = [];
8
12
  const ids = /* @__PURE__ */ new Map();
9
13
  const recipeIds = /* @__PURE__ */ new Map();
10
- const visit = (value, path, mainEntity = false) => {
14
+ const visit = (value, path, reach = "any") => {
11
15
  if (path.length > MAX_DEPTH) {
12
16
  return;
13
17
  }
14
18
  if (Array.isArray(value)) {
15
- value.forEach(
16
- (entry, i) => visit(entry, [...path, i], mainEntity)
17
- );
19
+ for (const [entry, entryPath] of entriesOf(value, path)) {
20
+ visit(entry, entryPath, reach);
21
+ }
18
22
  return;
19
23
  }
20
24
  const fields = asRecord(value);
21
25
  if (!fields) {
22
26
  return;
23
27
  }
24
- const type = fields["@type"];
25
- const isRecipe = [type].flat().some(
26
- (name) => typeof name === "string" && /(?:^|[/:#])Recipe$/.test(name)
27
- );
28
- if (mainEntity && !isRecipe) {
28
+ const isRecipe = hasType(fields, "Recipe");
29
+ const isList = lists && hasType(fields, "ItemList");
30
+ if (reach !== "any" && !isRecipe) {
31
+ if (isList && reach === "mainEntity") {
32
+ visit(fields.itemListElement, [...path, "itemListElement"], "entry");
33
+ } else if (lists && reach === "entry" && hasType(fields, "ListItem")) {
34
+ visit(fields.item, [...path, "item"], "item");
35
+ }
29
36
  return;
30
37
  }
31
38
  const located = { node: value, path };
@@ -46,7 +53,10 @@ function locate(documents) {
46
53
  }
47
54
  } else {
48
55
  visit(fields["@graph"], [...path, "@graph"]);
49
- visit(fields.mainEntity, [...path, "mainEntity"], true);
56
+ visit(fields.mainEntity, [...path, "mainEntity"], "mainEntity");
57
+ if (isList) {
58
+ visit(fields.itemListElement, [...path, "itemListElement"], "entry");
59
+ }
50
60
  }
51
61
  };
52
62
  for (const document of documents) {
@@ -54,8 +64,30 @@ function locate(documents) {
54
64
  }
55
65
  return { recipes, ids };
56
66
  }
67
+ var TYPES = {
68
+ Recipe: /(?:^|[/:#])Recipe$/,
69
+ ItemList: /(?:^|[/:#])ItemList$/,
70
+ ListItem: /(?:^|[/:#])ListItem$/
71
+ };
72
+ function hasType(node, name) {
73
+ return entriesOf(node["@type"], []).some(
74
+ ([type]) => typeof type === "string" && TYPES[name].test(type)
75
+ );
76
+ }
57
77
  function hasContent(node) {
58
- return Object.keys(node).some((key) => key[0] !== "@");
78
+ return Object.keys(node).some((key) => !key.startsWith("@"));
79
+ }
80
+ function entriesOf(value, path) {
81
+ if (!Array.isArray(value)) {
82
+ return [[value, path]];
83
+ }
84
+ const entries = [];
85
+ for (let i = 0; i < value.length; i++) {
86
+ if (hasOwn(value, i)) {
87
+ entries.push([value[i], [...path, i]]);
88
+ }
89
+ }
90
+ return entries;
59
91
  }
60
92
  function findRecipeJsonLd(html) {
61
93
  if (typeof html !== "string") {
@@ -84,7 +116,7 @@ function findRecipeJsonLd(html) {
84
116
  }
85
117
  function withImages(node, ids) {
86
118
  const follow = (value) => {
87
- const id = isRecord(value) && Object.keys(value).length === 1 && value["@id"];
119
+ const id = isRecord(value) && Object.keys(value).join() === "@id" && value["@id"];
88
120
  const target = typeof id === "string" && ids.get(id)?.node;
89
121
  return target && target !== node ? target : value;
90
122
  };
@@ -119,6 +151,7 @@ function parseJson(text) {
119
151
  }
120
152
  export {
121
153
  MAX_DEPTH,
154
+ entriesOf,
122
155
  findRecipeJsonLd,
123
156
  locate
124
157
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema-org/find.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport { isRecord, type Path } from '../issues'\nimport { asRecord } from '../validation'\n\n/** A JSON-LD node, as passed in, and its path in the input. */\nexport type Located = { node: Record<string, unknown>; path: Path }\n\n/**\n * The longest path read in JSON-LD. Real markup nests about ten levels, and\n * the bound keeps a hostile page from overflowing the stack.\n */\nexport const MAX_DEPTH = 32\n\n/**\n * Finds the Recipe nodes in parsed JSON-LD documents, in order, through arrays,\n * `@graph`, and one level of `mainEntity`. It indexes by `@id` each Recipe, and\n * each node at the top level of the JSON-LD, or reached from there through\n * arrays and `@graph`, so references can be followed. An `@id` names its first\n * node, and a Recipe with the `@id` of an earlier Recipe is left out, unless\n * the earlier one holds nothing but keywords such as `@type`: then it takes\n * that one's place in `recipes`, and in `ids` when that one was the first node\n * with its `@id`. An empty `@id` is no `@id`.\n */\nexport function locate(documents: readonly unknown[]): {\n recipes: Located[]\n ids: Map<string, Located>\n} {\n const recipes: Located[] = []\n const ids = new Map<string, Located>()\n // A stub Recipe, or `true` once the `@id`'s Recipe holds content, so a\n // duplicate never rescans the first one's keys.\n const recipeIds = new Map<unknown, Located | true>()\n\n const visit = (value: unknown, path: Path, mainEntity = false): void => {\n if (path.length > MAX_DEPTH) {\n return\n }\n\n if (Array.isArray(value)) {\n value.forEach((entry: unknown, i) =>\n visit(entry, [...path, i], mainEntity),\n )\n return\n }\n\n const fields = asRecord(value)\n if (!fields) {\n return\n }\n\n const type = fields['@type']\n const isRecipe = [type]\n .flat()\n .some(\n (name) => typeof name === 'string' && /(?:^|[/:#])Recipe$/.test(name),\n )\n\n if (mainEntity && !isRecipe) {\n return\n }\n\n const located = { node: value as Record<string, unknown>, path }\n const id = fields['@id'] || undefined\n if (typeof id === 'string' && !ids.has(id)) {\n ids.set(id, located)\n }\n\n if (isRecipe) {\n const earlier = recipeIds.get(id)\n if (!earlier) {\n recipes.push(located)\n if (typeof id === 'string') {\n recipeIds.set(id, hasContent(fields) || located)\n }\n } else if (earlier !== true && hasContent(fields)) {\n // `recipes` holds this same object, and so does `ids` when the stub was\n // the first node with its `@id`, so a reference to the recipe still\n // finds the recipe itself.\n Object.assign(earlier, located)\n recipeIds.set(id, true)\n }\n } else {\n visit(fields['@graph'], [...path, '@graph'])\n visit(fields.mainEntity, [...path, 'mainEntity'], true)\n }\n }\n\n for (const document of documents) {\n visit(document, [])\n }\n return { recipes, ids }\n}\n\nfunction hasContent(node: Record<string, unknown>): boolean {\n return Object.keys(node).some((key) => key[0] !== '@')\n}\n\n/**\n * Finds the schema.org Recipe nodes in a page's\n * `<script type=\"application/ld+json\">` tags, without a DOM. Pass one to\n * `fromJsonLd`.\n *\n * Reads arrays and `@graph`, as Yoast and WP Recipe Maker write them, and a\n * Recipe in a node's `mainEntity`, and matches a node whose `@type` is or\n * includes `\"Recipe\"`. A tag whose JSON doesn't parse is retried with the raw\n * line breaks and tabs inside its strings escaped, as real pages have them,\n * and skipped if it still doesn't. A tag inside an HTML comment is skipped,\n * and a `<!--` in an attribute value or in a `style` or `textarea` element is\n * text, as in a browser. A quote starts an attribute value only right after\n * `=`, so a stray one, as in `content=\"a 9\" pan\"`, doesn't hide later tags. A\n * Recipe node with the `@id` of an earlier one is returned once, where the\n * first appears. When the first holds only keywords, such as a `mainEntity`\n * of `{ \"@type\": \"Recipe\", \"@id\": \"#recipe\" }`, the later node is returned in\n * its place. An empty `@id` is no `@id`, so it merges nothing.\n *\n * In each Recipe node returned, an image that is an `@id` reference to a node\n * at the top level of the JSON-LD, or reached from there through arrays and\n * `@graph`, holds that node instead of the reference, an object that holds only\n * that `@id`. That applies to the recipe's `image` and each step's `image` in\n * `recipeInstructions`, and lets `fromJsonLd` read them. A reference to a node\n * in any other field, such as an `Organization`'s `logo` or an `ImageObject` in\n * a `WebPage`'s `mainEntity`, isn't followed. Other references, such as\n * `author` and `mainEntityOfPage`, and references inside the node an image\n * holds, stay as the page wrote them.\n *\n * @param html - The page's HTML, such as `await response.text()`.\n * @returns Every Recipe node, in document order, or `[]` when there are none.\n * @throws {RecipeBookError} `invalid_argument` when `html` isn't a string.\n *\n * @example\n * const [node] = findRecipeJsonLd(await response.text())\n * const result = fromJsonLd(node)\n */\nexport function findRecipeJsonLd(html: string): Record<string, unknown>[] {\n if (typeof html !== 'string') {\n throw new RecipeBookError(\n 'invalid_argument',\n `findRecipeJsonLd: html must be a string, such as await response.text(), received ${describeValue(html)}.`,\n )\n }\n\n const documents: unknown[] = []\n\n // At each `<`, in this order: a script, style, or textarea element with its\n // content; a comment, where `<!-->` and `<!--->` close at once; or any other\n // tag with its quoted attribute values. What one of these holds, such as a\n // `<!--` in an attribute value, starts nothing, as in a browser. A quote\n // starts a value only right after `=`, so a stray one, as in\n // `content=\"a 9\" pan\"`, is text. The element's attributes are read once, in\n // a lookahead, then matched by backreference, so a tag with no `>` fails\n // without retrying other splits of them, which would be exponential.\n for (const [, element, attributes = '', body = ''] of html.matchAll(\n /<(script|style|textarea)(?=[\\s/>])(?=((?:=\\s*(?:\"[^\"]*\"|'[^']*')|[^<>])*))\\2>([\\s\\S]*?)(?:<\\/\\1\\s*>|$)|<!--(?:-?>|[\\s\\S]*?(?:--!?>|$))|<\\/?[a-z][^\\s/>]*(?:=\\s*(?:\"[^\"]*\"|'[^']*')|[^<>])*/gi,\n )) {\n if (\n element?.toLowerCase() === 'script' &&\n /[\\s/]type\\s*=\\s*[\"']?application\\/ld\\+json\\b/i.test(attributes)\n ) {\n const text = body\n .replace(/^\\s*(?:\\/\\/\\s*)?(?:<!--|<!\\[CDATA\\[)/, '')\n .replace(/(?:\\/\\/\\s*)?(?:-->|\\]\\]>)\\s*$/, '')\n const json =\n parseJson(text) ??\n parseJson(\n text.replace(/\"(?:[^\"\\\\]|\\\\[\\s\\S]?)*(?:\"|$)/g, (string) =>\n string.replace(/[\\n\\r\\t]/g, (c) => JSON.stringify(c).slice(1, -1)),\n ),\n )\n\n documents.push(json)\n }\n }\n\n const { recipes, ids } = locate(documents)\n return recipes.map(({ node }) => withImages(node, ids))\n}\n\n/**\n * A copy of a Recipe node with each image that is a one-hop reference to\n * another node replaced by that node: the recipe's `image`, and each step's\n * `image` in `recipeInstructions`, through sections, `ItemList`s, and\n * `ListItem`s. The fields `fromJsonLd` doesn't read keep their references.\n */\nfunction withImages(\n node: Record<string, unknown>,\n ids: Map<string, Located>,\n): Record<string, unknown> {\n const follow = (value: unknown): unknown => {\n const id =\n isRecord(value) && Object.keys(value).length === 1 && value['@id']\n const target = typeof id === 'string' && ids.get(id)?.node\n return target && target !== node ? target : value\n }\n const images = (value: unknown): unknown =>\n Array.isArray(value) ? value.map(follow) : follow(value)\n const copy = (value: unknown, depth: number, keys: string[]): unknown => {\n if (depth > MAX_DEPTH) {\n return value\n }\n if (Array.isArray(value)) {\n return value.map((entry: unknown) => copy(entry, depth + 1, keys))\n }\n\n const fields = asRecord(value)\n if (!fields) {\n return value\n }\n\n const result = { ...fields }\n for (const key of keys) {\n if (fields[key] !== undefined) {\n result[key] =\n key === 'image'\n ? images(fields[key])\n : copy(fields[key], depth + 1, ['itemListElement', 'item', 'image'])\n }\n }\n return result\n }\n\n return copy(node, 0, ['image', 'recipeInstructions']) as typeof node\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/schema-org/find.ts"],"sourcesContent":["import { describeValue, RecipeBookError } from '../errors'\nimport { hasOwn, isRecord, type Path } from '../issues'\nimport { asRecord } from '../validation'\n\n/** A JSON-LD node, as passed in, and its path in the input. */\nexport type Located = { node: Record<string, unknown>; path: Path }\n\n/**\n * The longest path read in JSON-LD. Real markup nests about ten levels, and\n * the bound keeps a hostile page from overflowing the stack.\n */\nexport const MAX_DEPTH = 32\n\n/**\n * Finds the Recipe nodes in parsed JSON-LD documents, in order, through arrays,\n * `@graph`, and one level of `mainEntity`, or, when that finds none, also\n * through each `ItemList`'s `itemListElement` and each `ListItem`'s `item`, to\n * `MAX_DEPTH`. It indexes by `@id` each Recipe, and each node at the top level\n * of the JSON-LD, or reached from there through arrays and `@graph`, so\n * references can be followed. An `@id` names its first node, and a Recipe\n * with the `@id` of an earlier Recipe is left out, unless the earlier one\n * holds nothing but keywords such as `@type`: then it takes that one's place\n * in `recipes`, and in `ids` when that one was the first node with its `@id`.\n * An empty `@id` is no `@id`.\n */\nexport function locate(documents: readonly unknown[]): {\n recipes: Located[]\n ids: Map<string, Located>\n} {\n const direct = scan(documents, false)\n return direct.recipes.length > 0 ? direct : scan(documents, true)\n}\n\n/** How `scan` reached a value, which decides what it reads there. */\ntype Reach = 'any' | 'mainEntity' | 'entry' | 'item'\n\nfunction scan(\n documents: readonly unknown[],\n lists: boolean,\n): { recipes: Located[]; ids: Map<string, Located> } {\n const recipes: Located[] = []\n const ids = new Map<string, Located>()\n // A stub Recipe, or `true` once the `@id`'s Recipe holds content, so a\n // duplicate never rescans the first one's keys.\n const recipeIds = new Map<unknown, Located | true>()\n\n const visit = (value: unknown, path: Path, reach: Reach = 'any'): void => {\n if (path.length > MAX_DEPTH) {\n return\n }\n\n if (Array.isArray(value)) {\n for (const [entry, entryPath] of entriesOf(value, path)) {\n visit(entry, entryPath, reach)\n }\n return\n }\n\n const fields = asRecord(value)\n if (!fields) {\n return\n }\n\n const isRecipe = hasType(fields, 'Recipe')\n const isList = lists && hasType(fields, 'ItemList')\n\n if (reach !== 'any' && !isRecipe) {\n if (isList && reach === 'mainEntity') {\n visit(fields.itemListElement, [...path, 'itemListElement'], 'entry')\n } else if (lists && reach === 'entry' && hasType(fields, 'ListItem')) {\n visit(fields.item, [...path, 'item'], 'item')\n }\n return\n }\n\n const located = { node: value as Record<string, unknown>, path }\n const id = fields['@id'] || undefined\n if (typeof id === 'string' && !ids.has(id)) {\n ids.set(id, located)\n }\n\n if (isRecipe) {\n const earlier = recipeIds.get(id)\n if (!earlier) {\n recipes.push(located)\n if (typeof id === 'string') {\n recipeIds.set(id, hasContent(fields) || located)\n }\n } else if (earlier !== true && hasContent(fields)) {\n // `recipes` holds this same object, and so does `ids` when the stub was\n // the first node with its `@id`, so a reference to the recipe still\n // finds the recipe itself.\n Object.assign(earlier, located)\n recipeIds.set(id, true)\n }\n } else {\n visit(fields['@graph'], [...path, '@graph'])\n visit(fields.mainEntity, [...path, 'mainEntity'], 'mainEntity')\n if (isList) {\n visit(fields.itemListElement, [...path, 'itemListElement'], 'entry')\n }\n }\n }\n\n for (const document of documents) {\n visit(document, [])\n }\n return { recipes, ids }\n}\n\nconst TYPES = {\n Recipe: /(?:^|[/:#])Recipe$/,\n ItemList: /(?:^|[/:#])ItemList$/,\n ListItem: /(?:^|[/:#])ListItem$/,\n}\n\nfunction hasType(\n node: Record<string, unknown>,\n name: keyof typeof TYPES,\n): boolean {\n return entriesOf(node['@type'], []).some(\n ([type]) => typeof type === 'string' && TYPES[name].test(type),\n )\n}\n\nfunction hasContent(node: Record<string, unknown>): boolean {\n return Object.keys(node).some((key) => !key.startsWith('@'))\n}\n\n/**\n * An array's elements with their paths, or a value that isn't an array with\n * its own path. A hole in an array, which a program can leave but\n * `JSON.parse` can't, is left out. It reads by index, because a program can\n * also give an array another prototype, or a `constructor` that isn't one,\n * and then array methods such as `flatMap` throw.\n */\nexport function entriesOf(value: unknown, path: Path): [unknown, Path][] {\n if (!Array.isArray(value)) {\n return [[value, path]]\n }\n const entries: [unknown, Path][] = []\n for (let i = 0; i < value.length; i++) {\n if (hasOwn(value, i)) {\n entries.push([value[i], [...path, i]])\n }\n }\n return entries\n}\n\n/**\n * Finds the schema.org Recipe nodes in a page's\n * `<script type=\"application/ld+json\">` tags, without a DOM. Pass one to\n * `fromJsonLd`.\n *\n * Reads arrays and `@graph`, as Yoast and WP Recipe Maker write them, and the\n * `mainEntity` of a node there, up to 32 levels deep, and matches a node whose\n * `@type` is or includes `\"Recipe\"`. When that finds none, it also reads each\n * `ItemList` in those places, as a roundup page lists its recipes, taking each\n * Recipe in its `itemListElement`, or in a `ListItem`'s `item` there. A\n * `ListItem` with only a `url` is skipped: fetch that page and call\n * `findRecipeJsonLd` on it. A `mainEntity`, or an entry of a `mainEntity`\n * array, that is neither a Recipe nor an `ItemList` is skipped with everything\n * in it, and a Recipe anywhere else, such as inside another Recipe or in a list\n * inside a `ListItem` or another list, isn't found.\n *\n * A tag whose JSON doesn't parse is retried with the raw line breaks and tabs\n * inside its strings escaped, as real pages have them, and skipped if it still\n * doesn't. A tag inside an HTML comment is skipped, and a `<!--` in an\n * attribute value or in a `style` or `textarea` element is text, as in a\n * browser. A quote starts an attribute value only right after `=`, so a stray\n * one, as in `content=\"a 9\" pan\"`, doesn't hide later tags. A Recipe node with\n * the `@id` of an earlier one is returned once, where the first appears. When\n * the first holds only keywords, such as a `mainEntity` of\n * `{ \"@type\": \"Recipe\", \"@id\": \"#recipe\" }`, the later node is returned in its\n * place. An empty `@id` is no `@id`, so it merges nothing.\n *\n * In each Recipe node returned, an image that is an `@id` reference to a node\n * at the top level of the JSON-LD, or reached from there through arrays and\n * `@graph`, holds that node instead of the reference, an object that holds only\n * that `@id`. That applies to the recipe's `image` and each step's `image` in\n * `recipeInstructions`, and lets `fromJsonLd` read them. A reference to a node\n * in any other field, such as an `Organization`'s `logo` or an `ImageObject` in\n * a `WebPage`'s `mainEntity`, isn't followed. Other references, such as\n * `author` and `mainEntityOfPage`, and references inside the node an image\n * holds, stay as the page wrote them.\n *\n * @param html - The page's HTML, such as `await response.text()`.\n * @returns The Recipe nodes it finds, in document order, or `[]` when there\n * are none.\n * @throws {RecipeBookError} `invalid_argument` when `html` isn't a string.\n *\n * @example\n * const [node] = findRecipeJsonLd(await response.text())\n * const result = fromJsonLd(node)\n */\nexport function findRecipeJsonLd(html: string): Record<string, unknown>[] {\n if (typeof html !== 'string') {\n throw new RecipeBookError(\n 'invalid_argument',\n `findRecipeJsonLd: html must be a string, such as await response.text(), received ${describeValue(html)}.`,\n )\n }\n\n const documents: unknown[] = []\n\n // At each `<`, in this order: a script, style, or textarea element with its\n // content; a comment, where `<!-->` and `<!--->` close at once; or any other\n // tag with its quoted attribute values. What one of these holds, such as a\n // `<!--` in an attribute value, starts nothing, as in a browser. A quote\n // starts a value only right after `=`, so a stray one, as in\n // `content=\"a 9\" pan\"`, is text. The element's attributes are read once, in\n // a lookahead, then matched by backreference, so a tag with no `>` fails\n // without retrying other splits of them, which would be exponential.\n for (const [, element, attributes = '', body = ''] of html.matchAll(\n /<(script|style|textarea)(?=[\\s/>])(?=((?:=\\s*(?:\"[^\"]*\"|'[^']*')|[^<>])*))\\2>([\\s\\S]*?)(?:<\\/\\1\\s*>|$)|<!--(?:-?>|[\\s\\S]*?(?:--!?>|$))|<\\/?[a-z][^\\s/>]*(?:=\\s*(?:\"[^\"]*\"|'[^']*')|[^<>])*/gi,\n )) {\n if (\n element?.toLowerCase() === 'script' &&\n /[\\s/]type\\s*=\\s*[\"']?application\\/ld\\+json\\b/i.test(attributes)\n ) {\n const text = body\n .replace(/^\\s*(?:\\/\\/\\s*)?(?:<!--|<!\\[CDATA\\[)/, '')\n .replace(/(?:\\/\\/\\s*)?(?:-->|\\]\\]>)\\s*$/, '')\n const json =\n parseJson(text) ??\n parseJson(\n text.replace(/\"(?:[^\"\\\\]|\\\\[\\s\\S]?)*(?:\"|$)/g, (string) =>\n string.replace(/[\\n\\r\\t]/g, (c) => JSON.stringify(c).slice(1, -1)),\n ),\n )\n\n documents.push(json)\n }\n }\n\n const { recipes, ids } = locate(documents)\n return recipes.map(({ node }) => withImages(node, ids))\n}\n\n/**\n * A copy of a Recipe node with each image that is a one-hop reference to\n * another node replaced by that node: the recipe's `image`, and each step's\n * `image` in `recipeInstructions`, through sections, `ItemList`s, and\n * `ListItem`s. The fields `fromJsonLd` doesn't read keep their references.\n */\nfunction withImages(\n node: Record<string, unknown>,\n ids: Map<string, Located>,\n): Record<string, unknown> {\n const follow = (value: unknown): unknown => {\n const id =\n isRecord(value) && Object.keys(value).join() === '@id' && value['@id']\n const target = typeof id === 'string' && ids.get(id)?.node\n return target && target !== node ? target : value\n }\n const images = (value: unknown): unknown =>\n Array.isArray(value) ? value.map(follow) : follow(value)\n const copy = (value: unknown, depth: number, keys: string[]): unknown => {\n if (depth > MAX_DEPTH) {\n return value\n }\n if (Array.isArray(value)) {\n return value.map((entry: unknown) => copy(entry, depth + 1, keys))\n }\n\n const fields = asRecord(value)\n if (!fields) {\n return value\n }\n\n const result = { ...fields }\n for (const key of keys) {\n if (fields[key] !== undefined) {\n result[key] =\n key === 'image'\n ? images(fields[key])\n : copy(fields[key], depth + 1, ['itemListElement', 'item', 'image'])\n }\n }\n return result\n }\n\n return copy(node, 0, ['image', 'recipeInstructions']) as typeof node\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n"],"mappings":";AAAA,SAAS,eAAe,uBAAuB;AAC/C,SAAS,QAAQ,gBAA2B;AAC5C,SAAS,gBAAgB;AASlB,IAAM,YAAY;AAclB,SAAS,OAAO,WAGrB;AACA,QAAM,SAAS,KAAK,WAAW,KAAK;AACpC,SAAO,OAAO,QAAQ,SAAS,IAAI,SAAS,KAAK,WAAW,IAAI;AAClE;AAKA,SAAS,KACP,WACA,OACmD;AACnD,QAAM,UAAqB,CAAC;AAC5B,QAAM,MAAM,oBAAI,IAAqB;AAGrC,QAAM,YAAY,oBAAI,IAA6B;AAEnD,QAAM,QAAQ,CAAC,OAAgB,MAAY,QAAe,UAAgB;AACxE,QAAI,KAAK,SAAS,WAAW;AAC3B;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAW,CAAC,OAAO,SAAS,KAAK,UAAU,OAAO,IAAI,GAAG;AACvD,cAAM,OAAO,WAAW,KAAK;AAAA,MAC/B;AACA;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,KAAK;AAC7B,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AAEA,UAAM,WAAW,QAAQ,QAAQ,QAAQ;AACzC,UAAM,SAAS,SAAS,QAAQ,QAAQ,UAAU;AAElD,QAAI,UAAU,SAAS,CAAC,UAAU;AAChC,UAAI,UAAU,UAAU,cAAc;AACpC,cAAM,OAAO,iBAAiB,CAAC,GAAG,MAAM,iBAAiB,GAAG,OAAO;AAAA,MACrE,WAAW,SAAS,UAAU,WAAW,QAAQ,QAAQ,UAAU,GAAG;AACpE,cAAM,OAAO,MAAM,CAAC,GAAG,MAAM,MAAM,GAAG,MAAM;AAAA,MAC9C;AACA;AAAA,IACF;AAEA,UAAM,UAAU,EAAE,MAAM,OAAkC,KAAK;AAC/D,UAAM,KAAK,OAAO,KAAK,KAAK;AAC5B,QAAI,OAAO,OAAO,YAAY,CAAC,IAAI,IAAI,EAAE,GAAG;AAC1C,UAAI,IAAI,IAAI,OAAO;AAAA,IACrB;AAEA,QAAI,UAAU;AACZ,YAAM,UAAU,UAAU,IAAI,EAAE;AAChC,UAAI,CAAC,SAAS;AACZ,gBAAQ,KAAK,OAAO;AACpB,YAAI,OAAO,OAAO,UAAU;AAC1B,oBAAU,IAAI,IAAI,WAAW,MAAM,KAAK,OAAO;AAAA,QACjD;AAAA,MACF,WAAW,YAAY,QAAQ,WAAW,MAAM,GAAG;AAIjD,eAAO,OAAO,SAAS,OAAO;AAC9B,kBAAU,IAAI,IAAI,IAAI;AAAA,MACxB;AAAA,IACF,OAAO;AACL,YAAM,OAAO,QAAQ,GAAG,CAAC,GAAG,MAAM,QAAQ,CAAC;AAC3C,YAAM,OAAO,YAAY,CAAC,GAAG,MAAM,YAAY,GAAG,YAAY;AAC9D,UAAI,QAAQ;AACV,cAAM,OAAO,iBAAiB,CAAC,GAAG,MAAM,iBAAiB,GAAG,OAAO;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AAEA,aAAW,YAAY,WAAW;AAChC,UAAM,UAAU,CAAC,CAAC;AAAA,EACpB;AACA,SAAO,EAAE,SAAS,IAAI;AACxB;AAEA,IAAM,QAAQ;AAAA,EACZ,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,UAAU;AACZ;AAEA,SAAS,QACP,MACA,MACS;AACT,SAAO,UAAU,KAAK,OAAO,GAAG,CAAC,CAAC,EAAE;AAAA,IAClC,CAAC,CAAC,IAAI,MAAM,OAAO,SAAS,YAAY,MAAM,IAAI,EAAE,KAAK,IAAI;AAAA,EAC/D;AACF;AAEA,SAAS,WAAW,MAAwC;AAC1D,SAAO,OAAO,KAAK,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,WAAW,GAAG,CAAC;AAC7D;AASO,SAAS,UAAU,OAAgB,MAA+B;AACvE,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,WAAO,CAAC,CAAC,OAAO,IAAI,CAAC;AAAA,EACvB;AACA,QAAM,UAA6B,CAAC;AACpC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,OAAO,OAAO,CAAC,GAAG;AACpB,cAAQ,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAgDO,SAAS,iBAAiB,MAAyC;AACxE,MAAI,OAAO,SAAS,UAAU;AAC5B,UAAM,IAAI;AAAA,MACR;AAAA,MACA,oFAAoF,cAAc,IAAI,CAAC;AAAA,IACzG;AAAA,EACF;AAEA,QAAM,YAAuB,CAAC;AAU9B,aAAW,CAAC,EAAE,SAAS,aAAa,IAAI,OAAO,EAAE,KAAK,KAAK;AAAA,IACzD;AAAA,EACF,GAAG;AACD,QACE,SAAS,YAAY,MAAM,YAC3B,gDAAgD,KAAK,UAAU,GAC/D;AACA,YAAM,OAAO,KACV,QAAQ,wCAAwC,EAAE,EAClD,QAAQ,iCAAiC,EAAE;AAC9C,YAAM,OACJ,UAAU,IAAI,KACd;AAAA,QACE,KAAK;AAAA,UAAQ;AAAA,UAAkC,CAAC,WAC9C,OAAO,QAAQ,aAAa,CAAC,MAAM,KAAK,UAAU,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,QACnE;AAAA,MACF;AAEF,gBAAU,KAAK,IAAI;AAAA,IACrB;AAAA,EACF;AAEA,QAAM,EAAE,SAAS,IAAI,IAAI,OAAO,SAAS;AACzC,SAAO,QAAQ,IAAI,CAAC,EAAE,KAAK,MAAM,WAAW,MAAM,GAAG,CAAC;AACxD;AAQA,SAAS,WACP,MACA,KACyB;AACzB,QAAM,SAAS,CAAC,UAA4B;AAC1C,UAAM,KACJ,SAAS,KAAK,KAAK,OAAO,KAAK,KAAK,EAAE,KAAK,MAAM,SAAS,MAAM,KAAK;AACvE,UAAM,SAAS,OAAO,OAAO,YAAY,IAAI,IAAI,EAAE,GAAG;AACtD,WAAO,UAAU,WAAW,OAAO,SAAS;AAAA,EAC9C;AACA,QAAM,SAAS,CAAC,UACd,MAAM,QAAQ,KAAK,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO,KAAK;AACzD,QAAM,OAAO,CAAC,OAAgB,OAAe,SAA4B;AACvE,QAAI,QAAQ,WAAW;AACrB,aAAO;AAAA,IACT;AACA,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,aAAO,MAAM,IAAI,CAAC,UAAmB,KAAK,OAAO,QAAQ,GAAG,IAAI,CAAC;AAAA,IACnE;AAEA,UAAM,SAAS,SAAS,KAAK;AAC7B,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,EAAE,GAAG,OAAO;AAC3B,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,GAAG,MAAM,QAAW;AAC7B,eAAO,GAAG,IACR,QAAQ,UACJ,OAAO,OAAO,GAAG,CAAC,IAClB,KAAK,OAAO,GAAG,GAAG,QAAQ,GAAG,CAAC,mBAAmB,QAAQ,OAAO,CAAC;AAAA,MACzE;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO,KAAK,MAAM,GAAG,CAAC,SAAS,oBAAoB,CAAC;AACtD;AAEA,SAAS,UAAU,MAAuB;AACxC,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}