@recipe-book/core 0.1.0 → 0.1.1

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 (50) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VWVIBCTG.cjs → chunk-D5CV3UF7.cjs} +188 -173
  4. package/dist/chunk-D5CV3UF7.cjs.map +1 -0
  5. package/dist/errors.js.map +1 -1
  6. package/dist/format/number.js +1 -1
  7. package/dist/format/number.js.map +1 -1
  8. package/dist/format/options.js.map +1 -1
  9. package/dist/index.cjs +83 -74
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +35 -19
  12. package/dist/index.d.ts +35 -19
  13. package/dist/ingredient/parse.js +27 -22
  14. package/dist/ingredient/parse.js.map +1 -1
  15. package/dist/issues.js.map +1 -1
  16. package/dist/quantity/amount.js +11 -0
  17. package/dist/quantity/amount.js.map +1 -1
  18. package/dist/quantity/parse.js +58 -81
  19. package/dist/quantity/parse.js.map +1 -1
  20. package/dist/quantity/scale.js +5 -2
  21. package/dist/quantity/scale.js.map +1 -1
  22. package/dist/recipe/parse.js +70 -55
  23. package/dist/recipe/parse.js.map +1 -1
  24. package/dist/recipe/schema.js +8 -6
  25. package/dist/recipe/schema.js.map +1 -1
  26. package/dist/resolve.js +6 -1
  27. package/dist/resolve.js.map +1 -1
  28. package/dist/schema-org/find.js +46 -13
  29. package/dist/schema-org/find.js.map +1 -1
  30. package/dist/schema-org/from-json-ld.js +127 -57
  31. package/dist/schema-org/from-json-ld.js.map +1 -1
  32. package/dist/schema-org/text.js.map +1 -1
  33. package/dist/schema-org/to-json-ld.js +12 -15
  34. package/dist/schema-org/to-json-ld.js.map +1 -1
  35. package/dist/schema-org.cjs +231 -132
  36. package/dist/schema-org.cjs.map +1 -1
  37. package/dist/schema-org.d.cts +32 -19
  38. package/dist/schema-org.d.ts +32 -19
  39. package/dist/steps/temperature.js +13 -13
  40. package/dist/steps/temperature.js.map +1 -1
  41. package/dist/{types-DSMf45H5.d.cts → types-BknM8KOd.d.cts} +4 -3
  42. package/dist/{types-DSMf45H5.d.ts → types-BknM8KOd.d.ts} +4 -3
  43. package/dist/units/convert.js +16 -8
  44. package/dist/units/convert.js.map +1 -1
  45. package/dist/units/temperature.js +7 -1
  46. package/dist/units/temperature.js.map +1 -1
  47. package/dist/validation.js +1 -1
  48. package/dist/validation.js.map +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-VWVIBCTG.cjs.map +0 -1
@@ -99,9 +99,14 @@ var NUTRIENT_EXAMPLES = {
99
99
  mg: 'milligrams, e.g. 300, "300 mg", or "0.3 g"'
100
100
  };
101
101
  function parseRecipe(input, options) {
102
+ return parseRecipeWith(
103
+ input,
104
+ readLineOption("parseRecipe", options) ?? "pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })"
105
+ );
106
+ }
107
+ function parseRecipeWith(input, lines) {
102
108
  const issues = [];
103
- const readLine = readLineOption("parseRecipe", options);
104
- const recipe = readRecipe(issues, input, readLine);
109
+ const recipe = readRecipe(issues, input, lines);
105
110
  const label = recipe && quote(recipe.name);
106
111
  return toResult("invalid_recipe", recipe, issues, label);
107
112
  }
@@ -115,7 +120,7 @@ function readLineOption(fn, options) {
115
120
  }
116
121
  return reader;
117
122
  }
118
- function readRecipe(issues, value, readLine) {
123
+ function readRecipe(issues, value, lines) {
119
124
  const input = asRecord(value);
120
125
  if (!input) {
121
126
  return error(
@@ -145,7 +150,7 @@ function readRecipe(issues, value, readLine) {
145
150
  input.ingredients,
146
151
  ["ingredients"],
147
152
  "ingredients",
148
- (issues2, value2, path) => readIngredient(issues2, value2, path, readLine)
153
+ (issues2, value2, path) => readIngredient(issues2, value2, path, lines)
149
154
  );
150
155
  const steps = readList(
151
156
  issues,
@@ -195,15 +200,11 @@ function readRecipe(issues, value, readLine) {
195
200
  });
196
201
  }
197
202
  function readList(issues, pending, value, path, noun, readItem) {
198
- const entries = value === void 0 ? [] : readArray(issues, value, path, noun);
199
- if (!entries) {
200
- return [];
201
- }
202
203
  const sections = [];
203
204
  let loose;
204
205
  let count = 0;
205
206
  const read = (entry, entryPath) => readItem(issues, entry, entryPath, ++count);
206
- entries.forEach((entry, i) => {
207
+ const visit = (entry, i) => {
207
208
  const entryPath = [...path, i];
208
209
  const section = asSection(entry);
209
210
  if (section) {
@@ -226,7 +227,10 @@ function readList(issues, pending, value, path, noun, readItem) {
226
227
  if (item) {
227
228
  loose.items.push(item);
228
229
  }
229
- });
230
+ };
231
+ if (value !== void 0 && !readArray(issues, value, path, noun, visit)) {
232
+ return [];
233
+ }
230
234
  if (sections.length === 0) {
231
235
  warning(issues, "empty_list", path, `Recipe has no ${noun}.`);
232
236
  return [];
@@ -255,10 +259,13 @@ function readSection(issues, record, path, noun, read) {
255
259
  const title = readText(issues, record.title, [...path, "title"]);
256
260
  const items = [];
257
261
  const itemsPath = [...path, "items"];
258
- const entries = readArray(issues, record.items, itemsPath, noun);
259
- if (entries) {
260
- let visited = 0;
261
- entries.forEach((entry, j) => {
262
+ let visited = 0;
263
+ const entries = readArray(
264
+ issues,
265
+ record.items,
266
+ itemsPath,
267
+ noun,
268
+ (entry, j) => {
262
269
  visited++;
263
270
  const itemPath = [...itemsPath, j];
264
271
  if (asSection(entry)) {
@@ -274,10 +281,10 @@ function readSection(issues, record, path, noun, read) {
274
281
  if (item) {
275
282
  items.push(item);
276
283
  }
277
- });
278
- if (visited === 0) {
279
- warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
280
284
  }
285
+ );
286
+ if (entries && visited === 0) {
287
+ warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
281
288
  }
282
289
  checkFields(issues, record, SECTION_FIELDS, path);
283
290
  return {
@@ -290,29 +297,44 @@ function readSection(issues, record, path, noun, read) {
290
297
  };
291
298
  }
292
299
  function readItems(issues, pending, value, path, noun, readItem) {
293
- const entries = value === void 0 ? [] : readArray(issues, value, path, noun);
294
- if (!entries) {
300
+ let n = 0;
301
+ const drafts = value === void 0 ? [] : readArray(
302
+ issues,
303
+ value,
304
+ path,
305
+ noun,
306
+ (entry, i) => readItem(issues, entry, [...path, i], ++n)
307
+ );
308
+ if (!drafts) {
295
309
  return [];
296
310
  }
297
- let n = 0;
298
- const drafts = entries.flatMap((entry, i) => {
299
- const draft = readItem(issues, entry, [...path, i], ++n);
300
- return draft ? [draft] : [];
301
- });
302
311
  const ids = assignIds(drafts, pending);
303
312
  return drafts.map((draft, i) => draft.build(ids[i]));
304
313
  }
305
- function readArray(issues, value, path, noun) {
306
- return Array.isArray(value) ? value : error(
307
- issues,
308
- "invalid_type",
309
- path,
310
- `Expected a list of ${noun}, received ${describeValue(value)}.`
311
- );
314
+ function readArray(issues, value, path, noun, read) {
315
+ if (!Array.isArray(value)) {
316
+ return error(
317
+ issues,
318
+ "invalid_type",
319
+ path,
320
+ `Expected a list of ${noun}, received ${describeValue(value)}.`
321
+ );
322
+ }
323
+ const kept = [];
324
+ for (let i = 0, n = value.length; i < n; i++) {
325
+ if (i in value) {
326
+ const entry = value[i];
327
+ const result = hasOwn(value, i) && read(entry, i);
328
+ if (result) {
329
+ kept.push(result);
330
+ }
331
+ }
332
+ }
333
+ return kept;
312
334
  }
313
- function readIngredient(issues, value, path, readLine) {
314
- if (typeof value === "string" && readLine) {
315
- const result = readLine(value);
335
+ function readIngredient(issues, value, path, lines) {
336
+ if (typeof value === "string" && typeof lines === "function") {
337
+ const result = lines(value);
316
338
  for (const issue of result.success ? result.warnings : result.error.issues) {
317
339
  issues.push({ ...issue, path: [...path, ...issue.path] });
318
340
  }
@@ -324,7 +346,7 @@ function readIngredient(issues, value, path, readLine) {
324
346
  issues,
325
347
  "invalid_type",
326
348
  path,
327
- `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 })." : ""}`
349
+ `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}.` : ""}`
328
350
  );
329
351
  }
330
352
  const position = issues.length;
@@ -365,22 +387,13 @@ function readIngredient(issues, value, path, readLine) {
365
387
  };
366
388
  }
367
389
  function readEquivalents(issues, value, path) {
368
- if (value === void 0) {
369
- return [];
370
- }
371
- if (!Array.isArray(value)) {
372
- error(
373
- issues,
374
- "invalid_type",
375
- path,
376
- `Expected a list of quantities, e.g. ["190 g"], received ${describeValue(value)}.`
377
- );
378
- return [];
379
- }
380
- return value.flatMap((entry, i) => {
381
- const quantity = readQuantity(issues, entry, [...path, i]);
382
- return quantity === void 0 ? [] : [quantity];
383
- });
390
+ return value !== void 0 && readArray(
391
+ issues,
392
+ value,
393
+ path,
394
+ 'quantities, e.g. ["190 g"]',
395
+ (entry, i) => readQuantity(issues, entry, [...path, i])
396
+ ) || [];
384
397
  }
385
398
  function readStep(issues, value, path, n) {
386
399
  const isText = typeof value === "string";
@@ -615,10 +628,11 @@ function readRangeText(issues, text, path, expected) {
615
628
  return error(issues, match.code, path, match.message);
616
629
  }
617
630
  const { amount, rest } = match;
618
- return {
619
- range: typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max },
620
- rest
621
- };
631
+ const range = typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max };
632
+ if (readPositive(issues, range.amount, path) === void 0 || range.max !== void 0 && readPositive(issues, range.max, path) === void 0) {
633
+ return void 0;
634
+ }
635
+ return { range, rest };
622
636
  }
623
637
  function readRange(issues, record, path, missingAmount) {
624
638
  const value = record.amount;
@@ -834,6 +848,7 @@ function readBoolean(issues, value, path) {
834
848
  }
835
849
  export {
836
850
  parseRecipe,
851
+ parseRecipeWith,
837
852
  readLineOption
838
853
  };
839
854
  //# sourceMappingURL=parse.js.map
@@ -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 const sectionIds = assignIds(sections, pending)\n const itemIds = assignIds(\n sections.flatMap((section) => section.items),\n pending,\n )\n\n let next = 0\n return sections.map((section, i) =>\n defined({\n id: sectionIds[i]!,\n title: section.title,\n items: section.items.map((item) => item.build(itemIds[next++]!)),\n }),\n )\n}\n\nfunction asSection(entry: unknown): Record<string, unknown> | undefined {\n const record = asRecord(entry)\n return record && 'items' in record ? record : undefined\n}\n\nfunction readSection<T>(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n noun: 'ingredients' | 'steps',\n read: (value: unknown, path: Path) => Draft<T> | undefined,\n): SectionDraft<T> {\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const items: Draft<T>[] = []\n const itemsPath = [...path, 'items']\n let visited = 0\n const entries = readArray(\n issues,\n record.items,\n itemsPath,\n noun,\n (entry, j) => {\n visited++\n const itemPath = [...itemsPath, j]\n\n if (asSection(entry)) {\n error(\n issues,\n 'invalid_type',\n itemPath,\n `Sections can't be nested. Make this a top-level section in ${noun}.`,\n )\n return\n }\n\n const item = read(entry, itemPath)\n if (item) {\n items.push(item)\n }\n },\n )\n\n if (entries && visited === 0) {\n warning(issues, 'empty_list', itemsPath, `Section has no ${noun}.`)\n }\n\n checkFields(issues, record, SECTION_FIELDS, path)\n\n return {\n authored,\n derived: (title && slugify(title)) || noun,\n path,\n position,\n title,\n items,\n }\n}\n\n/** Reads a flat list, such as notes or equipment. Its ids have their own namespace. */\nfunction readItems<T>(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n noun: string,\n readItem: ItemReader<T>,\n): T[] {\n let n = 0\n const drafts =\n value === undefined\n ? []\n : readArray(issues, value, path, noun, (entry, i) =>\n readItem(issues, entry, [...path, i], ++n),\n )\n\n if (!drafts) {\n return []\n }\n\n const ids = assignIds(drafts, pending)\n\n return drafts.map((draft, i) => draft.build(ids[i]!))\n}\n\n/**\n * Calls `read` with each entry the list has as its own, in order, and returns\n * the objects it returns. Like `forEach`, it reads `length` once and checks\n * each index with `in` before reading it. It keeps an entry only if the list\n * still has it as its own after the read. It calls none of the list's methods,\n * so a list with another prototype, its own `forEach`, or its own\n * `constructor` reads like any other.\n */\nfunction readArray<T>(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n noun: string,\n read: (entry: unknown, i: number) => T | undefined,\n): T[] | undefined {\n if (!Array.isArray(value)) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a list of ${noun}, received ${describeValue(value)}.`,\n )\n }\n\n const kept: T[] = []\n for (let i = 0, n = value.length; i < n; i++) {\n if (i in value) {\n const entry: unknown = value[i]\n const result = hasOwn(value, i) && read(entry, i)\n if (result) {\n kept.push(result)\n }\n }\n }\n return kept\n}\n\nfunction readIngredient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n lines?: Lines,\n): Draft<Ingredient> | undefined {\n if (typeof value === 'string' && typeof lines === 'function') {\n const result = lines(value)\n\n for (const issue of result.success\n ? result.warnings\n : result.error.issues) {\n issues.push({ ...issue, path: [...path, ...issue.path] })\n }\n\n return result.success\n ? readIngredient(issues, result.data, path)\n : undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected an ingredient such as ${typeof lines === 'function' ? '\"2 cups flour\" or ' : ''}{ name: \"flour\", quantity: \"2 cups\" }, received ${describeValue(value)}.${typeof value === 'string' && typeof lines === 'string' ? ` To read ingredient lines, ${lines}.` : ''}`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n [...path, 'name'],\n 'Add a name, e.g. name: \"flour\".',\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readQuantity(issues, record.quantity, [...path, 'quantity'])\n const equivalents = readEquivalents(issues, record.equivalents, [\n ...path,\n 'equivalents',\n ])\n const note = readText(issues, record.note, [...path, 'note'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const optional = readBoolean(issues, record.optional, [...path, 'optional'])\n\n if (equivalents.length > 0 && record.quantity === undefined) {\n error(\n issues,\n 'missing_field',\n [...path, 'quantity'],\n 'Add a quantity. Equivalents are alternatives to it, so they need one.',\n )\n }\n\n checkFields(issues, record, INGREDIENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'ingredient',\n path,\n position,\n build: (id) =>\n defined({ id, name, plural, quantity, equivalents, note, url, optional }),\n }\n}\n\nfunction readEquivalents(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Quantity[] {\n return (\n (value !== undefined &&\n readArray(issues, value, path, 'quantities, e.g. [\"190 g\"]', (entry, i) =>\n readQuantity(issues, entry, [...path, i]),\n )) ||\n []\n )\n}\n\nfunction readStep(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Step> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the step text.',\n )\n const name = readText(issues, record.name, [...path, 'name'])\n const image = readUrl(issues, record.image, [...path, 'image'])\n checkFields(issues, record, STEP_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (name && slugify(name)) || `step-${n}`,\n path,\n position,\n build: (id) => defined({ id, text, name, image }),\n }\n}\n\nfunction readNote(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n n: number,\n): Draft<Note> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { text: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { text }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const title = readText(issues, record.title, [...path, 'title'])\n const text = readText(\n issues,\n record.text,\n isText ? path : [...path, 'text'],\n 'Add the note text.',\n )\n checkFields(issues, record, NOTE_FIELDS, path)\n\n if (text === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: (title && slugify(title)) || `note-${n}`,\n path,\n position,\n build: (id) => defined({ id, title, text }),\n }\n}\n\nfunction readEquipment(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<Equipment> | undefined {\n const isText = typeof value === 'string'\n const record = asRecord(isText ? { name: value } : value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected a string or { name }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const name = readText(\n issues,\n record.name,\n isText ? path : [...path, 'name'],\n `Add a name, e.g. ${isText ? '' : 'name: '}\"whisk\".`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n const quantity =\n record.quantity === undefined\n ? undefined\n : readCount(issues, record.quantity, [...path, 'quantity'])\n const url = readUrl(issues, record.url, [...path, 'url'])\n const note = readText(issues, record.note, [...path, 'note'])\n checkFields(issues, record, EQUIPMENT_FIELDS, path)\n\n if (name === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(name) || 'equipment',\n path,\n position,\n build: (id) => defined({ id, name, plural, quantity, url, note }),\n }\n}\n\n/** `T` with each field that may be `undefined` made optional instead. */\ntype Defined<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n} & {\n [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<\n T[K],\n undefined\n >\n}\n\n/** Copies the own fields of `fields` that aren't `undefined`, keeping their order. A polluted `Object.prototype` adds none. */\nfunction defined<T extends object>(fields: T): Defined<T> {\n const copy: Record<string, unknown> = {}\n\n for (const key in fields) {\n if (hasOwn(fields, key) && fields[key] !== undefined) {\n copy[key] = fields[key]\n }\n }\n\n return copy as Defined<T>\n}\n\nfunction readCount(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) {\n return value\n }\n\n return error(\n issues,\n typeof value === 'number' ? 'invalid_amount' : 'invalid_type',\n path,\n `Expected a whole number greater than 0, received ${describeValue(value)}.`,\n )\n}\n\nfunction readServings(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Servings | undefined {\n if (value === undefined) {\n return undefined\n }\n\n if (typeof value === 'number') {\n const amount = readPositive(issues, value, path)\n return amount === undefined ? undefined : { amount }\n }\n\n const expected = `Expected a number of servings, e.g. 4 or \"4–6\", received ${describeValue(value)}.`\n\n if (typeof value === 'string') {\n const match = readRangeText(issues, value, path, expected)\n\n if (match?.rest) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n return match?.range\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(issues, 'invalid_type', path, expected)\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 4, max: 6 }.',\n )\n checkFields(issues, record, SERVINGS_FIELDS, path)\n return range\n}\n\nfunction readYield(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Yield | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const isText = typeof value === 'string'\n const expected = `Expected a batch such as \"24 cookies\" or ${isText ? '\"3–4 loaves\"' : '{ amount, unit }'}, received ${describeValue(value)}.`\n\n if (isText) {\n const match = readRangeText(issues, value, path, expected)\n\n if (!match) {\n return undefined\n }\n\n const { range, rest } = match\n\n if (!rest) {\n return error(\n issues,\n 'missing_field',\n path,\n `Add what the recipe makes, e.g. \"24 cookies\".${servingsHint(range)}`,\n )\n }\n\n // A second number, a word or sign that joins numbers, or a dash would\n // otherwise become part of the unit and not scale: \"3 or 4 loaves\",\n // \"2 and 1/2 dozen\", \"4 & 5 loaves\", \"1 000 cookies\", \"6 ⅓-cup bowls\".\n if (\n !/\\p{L}/u.test(rest) ||\n /^(?:[-–—&]|(?:[\\d.,/\\p{No}]+|or|to|x|and)(?:\\s|$))/iu.test(rest)\n ) {\n const suggestion = suggestJoinedYield(value, range, rest)\n return error(\n issues,\n 'invalid_quantity',\n path,\n suggestion ? `${expected} Did you mean \"${suggestion}\"?` : expected,\n suggestion,\n )\n }\n\n return { ...range, unit: rest }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `${expected}${isPositive(value) ? servingsHint({ amount: value }) : ''}`,\n )\n }\n\n const range = readRange(\n issues,\n record,\n path,\n 'Add an amount, e.g. { amount: 24, unit: \"cookie\" }.',\n )\n const unit = readText(\n issues,\n record.unit,\n [...path, 'unit'],\n `Add a unit, e.g. { amount: 24, unit: \"cookie\" }.${servingsHint(range)}`,\n )\n const plural = readText(issues, record.plural, [...path, 'plural'])\n checkFields(issues, record, YIELD_FIELDS, path)\n\n if (range === undefined || unit === undefined) {\n return undefined\n }\n\n return defined({ ...range, unit, plural })\n}\n\n/**\n * Rewrites yield text whose amount is followed by \"and\" or \"&\" and a second\n * number: a fraction makes a mixed number (\"2 and 1/2 dozen\" to \"2 1/2 dozen\"),\n * and a whole number makes a range (\"4 & 5 loaves\" to \"4-5 loaves\"). Returns\n * `undefined` unless the rewritten text parses as that amount.\n */\nfunction suggestJoinedYield(\n text: string,\n { amount }: Servings,\n rest: string,\n): string | undefined {\n const joiner = /^(?:and|&)\\s*/i.exec(rest)\n const next = joiner && matchAmount(rest.slice(joiner[0].length))\n\n if (!next || 'code' in next || typeof next.amount !== 'number') {\n return undefined\n }\n\n const isFraction = next.amount < 1\n\n if (!isFraction && !Number.isInteger(next.amount)) {\n return undefined\n }\n\n const head = text.replace(/\\s+/g, ' ').trim().slice(0, -rest.length)\n const suggestion = `${head.trim()}${isFraction ? ' ' : '-'}${next.amountText} ${next.rest}`\n const parsed = readYield([], suggestion, [])\n\n return parsed &&\n (isFraction\n ? parsed.amount === amount + next.amount && parsed.max === undefined\n : parsed.amount === amount && parsed.max === next.amount)\n ? suggestion\n : undefined\n}\n\n/** Reads text that starts with an amount or range, such as \"4–6\" or \"24 cookies\". */\nfunction readRangeText(\n issues: RecipeIssue[],\n text: string,\n path: Path,\n expected: string,\n): { range: Servings; rest: string } | undefined {\n const match = matchAmount(text.replace(/\\s+/g, ' ').trim())\n\n if (!match) {\n return error(issues, 'invalid_quantity', path, expected)\n }\n\n if ('code' in match) {\n return error(issues, match.code, path, match.message)\n }\n\n const { amount, rest } = match\n const range: Servings =\n typeof amount === 'number'\n ? { amount }\n : { amount: amount.min, max: amount.max }\n\n if (\n readPositive(issues, range.amount, path) === undefined ||\n (range.max !== undefined &&\n readPositive(issues, range.max, path) === undefined)\n ) {\n return undefined\n }\n\n return { range, rest }\n}\n\n/** Reads `{ amount, max }`, where `max` is optional and a `max` equal to `amount` is dropped. */\nfunction readRange(\n issues: RecipeIssue[],\n record: Record<string, unknown>,\n path: Path,\n missingAmount: string,\n): Servings | undefined {\n const value = record.amount\n const amount =\n value === undefined\n ? error(issues, 'missing_field', [...path, 'amount'], missingAmount)\n : isRecord(value) && hasOwn(value, 'min') && hasOwn(value, 'max')\n ? error(\n issues,\n 'invalid_type',\n [...path, 'amount'],\n 'Expected a positive number, received an object. Write a range as { amount: 4, max: 6 }.',\n )\n : readPositive(issues, value, [...path, 'amount'])\n const max =\n record.max === undefined\n ? undefined\n : readPositive(issues, record.max, [...path, 'max'])\n\n if (amount === undefined) {\n return undefined\n }\n\n if (max !== undefined && max < amount) {\n return error(\n issues,\n 'invalid_range',\n [...path, 'max'],\n `Expected a maximum greater than the amount (${amount}), received ${max}.`,\n )\n }\n\n return max === undefined || max === amount ? { amount } : { amount, max }\n}\n\nfunction servingsHint(servings: Servings | undefined): string {\n if (!servings) {\n return ''\n }\n\n const { amount, max } = servings\n return ` For a number of servings, use servings: ${max === undefined ? amount : quote(`${amount}–${max}`)}.`\n}\n\nfunction readSystem(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): System | undefined {\n if (value === undefined || value === 'metric' || value === 'us') {\n return value\n }\n\n const suggestion = value === 'imperial' ? 'us' : undefined\n\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected \"metric\" or \"us\", received ${describeValue(value)}.${suggestion ? ` Did you mean \"${suggestion}\"?` : ''}`,\n suggestion,\n )\n}\n\nfunction readTimes(\n issues: RecipeIssue[],\n pending: PendingIssue[],\n value: unknown,\n path: Path,\n): Times {\n if (value === undefined) {\n return { custom: [] }\n }\n\n const record = asRecord(value)\n\n if (!record) {\n error(\n issues,\n 'invalid_type',\n path,\n `Expected { prep, cook, total, custom }, received ${describeValue(value)}.`,\n )\n return { custom: [] }\n }\n\n const prep = readDuration(issues, record.prep, [...path, 'prep'])\n const cook = readDuration(issues, record.cook, [...path, 'cook'])\n const total = readDuration(issues, record.total, [...path, 'total'])\n const custom = readItems(\n issues,\n pending,\n record.custom,\n [...path, 'custom'],\n 'custom times',\n readCustomTime,\n )\n checkFields(issues, record, TIMES_FIELDS, path)\n return defined({ prep, cook, total, custom })\n}\n\nfunction readCustomTime(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Draft<CustomTime> | undefined {\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected { label, minutes }, e.g. { label: \"Chill\", minutes: 60 }, received ${describeValue(value)}.`,\n )\n }\n\n const position = issues.length\n const authored = readText(issues, record.id, [...path, 'id'])\n const label = readText(\n issues,\n record.label,\n [...path, 'label'],\n 'Add a label, e.g. label: \"Chill\".',\n )\n const minutes =\n record.minutes === undefined\n ? error(\n issues,\n 'missing_field',\n [...path, 'minutes'],\n 'Add the time, e.g. minutes: 60 or minutes: \"PT1H\".',\n )\n : readDuration(issues, record.minutes, [...path, 'minutes'])\n checkFields(issues, record, CUSTOM_TIME_FIELDS, path)\n\n if (label === undefined || minutes === undefined) {\n return undefined\n }\n\n return {\n authored,\n derived: slugify(label) || 'time',\n path,\n position,\n build: (id) => ({ id, label, minutes }),\n }\n}\n\nfunction readDuration(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): number | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const minutes =\n typeof value === 'number'\n ? nonNegative(value)\n : typeof value === 'string'\n ? parseIsoDuration(value)\n : undefined\n\n if (minutes === undefined) {\n return error(\n issues,\n 'invalid_duration',\n path,\n `Expected minutes or an ISO 8601 duration like \"PT30M\", received ${describeValue(value)}.`,\n )\n }\n\n return minutes\n}\n\nfunction readNutrition(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): Nutrition | undefined {\n if (value === undefined) {\n return undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected nutrition for one serving, e.g. { calories: 250, fat: \"12 g\" }, received ${describeValue(value)}.`,\n )\n }\n\n const nutrition: Nutrition = {}\n const servingSize = readText(issues, record.servingSize, [\n ...path,\n 'servingSize',\n ])\n\n if (servingSize !== undefined) {\n nutrition.servingSize = servingSize\n }\n\n for (const key of Object.keys(NUTRIENT_UNITS) as NutrientKey[]) {\n const amount = readNutrient(\n issues,\n record[key],\n [...path, key],\n NUTRIENT_UNITS[key],\n )\n\n if (amount !== undefined) {\n nutrition[key] = amount\n }\n }\n\n // Spread here: a module-level spread can't be tree-shaken, so it would add NUTRIENT_UNITS to nearly every entry point.\n checkFields(issues, record, { servingSize: true, ...NUTRIENT_UNITS }, path)\n return Object.keys(nutrition).length > 0 ? nutrition : undefined\n}\n\n/** Reads an amount of `unit`. Empty text counts as absent. */\nfunction readNutrient(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n unit: NutrientUnit,\n): number | undefined {\n const text = typeof value === 'string' ? value.trim() : undefined\n\n if (value === undefined || text === '') {\n return undefined\n }\n\n const amount =\n typeof value === 'number'\n ? nonNegative(value)\n : text === undefined\n ? undefined\n : parseNutrient(text, unit)\n\n if (typeof amount === 'number') {\n return amount\n }\n\n return error(\n issues,\n typeof value === 'number'\n ? 'invalid_amount'\n : text === undefined\n ? 'invalid_type'\n : 'invalid_quantity',\n path,\n amount?.message ??\n `Expected ${NUTRIENT_EXAMPLES[unit]}, received ${describeValue(value)}.`,\n )\n}\n\n/** `value` if it's finite and not negative, with `-0` read as `0`. */\nfunction nonNegative(value: number): number | undefined {\n return Number.isFinite(value) && value >= 0 ? Math.abs(value) : undefined\n}\n\n/**\n * Reads optional text, trimmed. Empty text counts as absent. With\n * `requiredMessage`, absent text is a `missing_field` error.\n */\nfunction readText(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n requiredMessage?: string,\n): string | undefined {\n if (value !== undefined && typeof value !== 'string') {\n return error(\n issues,\n 'invalid_type',\n path,\n `Expected text, received ${describeValue(value)}.`,\n )\n }\n\n const text = value?.trim()\n\n if (!text) {\n return requiredMessage === undefined\n ? undefined\n : error(issues, 'missing_field', path, requiredMessage)\n }\n\n return text\n}\n\n/**\n * Reads an optional link or image URL, which must be http, https, or relative.\n * Other schemes, such as `javascript:`, are errors, so a URL is safe to render\n * as an `href` or `src`.\n */\nfunction readUrl(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): string | undefined {\n const url = readText(issues, value, path)\n const scheme = url === undefined ? undefined : disallowedScheme(url)\n\n return scheme === undefined\n ? url\n : error(\n issues,\n 'invalid_url',\n path,\n `Expected an http or https URL, or a relative one, received ${quote(`${scheme}…`)}.`,\n )\n}\n\nfunction readBoolean(\n issues: RecipeIssue[],\n value: unknown,\n path: Path,\n): boolean {\n if (value === undefined || typeof value === 'boolean') {\n return value ?? false\n }\n\n error(\n issues,\n 'invalid_type',\n path,\n `Expected true or false, received ${describeValue(value)}.`,\n )\n return false\n}\n"],"mappings":"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","names":["issues","value","range"]}
@@ -1,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":"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","names":["rebalance"]}
1
+ {"version":3,"sources":["../src/resolve.ts"],"sourcesContent":["import { checkFactor, isPositive } from './quantity/amount'\nimport { multiply } from './quantity/scale'\nimport type { Quantity } from './quantity/types'\nimport type { Ingredient, Recipe, Servings, Step, Yield } from './recipe/types'\nimport { lookupUnit, type System } from './units/registry'\nimport { checkSystem, inSystem, isSystem, rebalance } from './units/system'\n\n/**\n * Where a resolved quantity comes from: the ingredient's `quantity`, one of its\n * `equivalents`, or a conversion into the display system. May grow in minor releases.\n */\nexport type QuantityOrigin = 'authored' | 'equivalent' | 'converted'\n\n/** An ingredient, scaled and expressed in the display system. */\nexport type ResolvedIngredient = {\n id: string\n /** The ingredient as authored, unscaled, not a copy; treat it as read-only. */\n ingredient: Ingredient\n /**\n * Absent for unquantified ingredients, such as \"salt, to taste\". May be the\n * authored object; treat it as read-only.\n */\n quantity?: Quantity | undefined\n /** `'authored'` for unquantified ingredients. */\n origin: QuantityOrigin\n}\n\n/** What {@link resolveRecipe} returns: the recipe at a scale, in one system, unrounded. */\nexport type ResolvedRecipe = {\n /** The recipe passed in, not a copy; treat it as read-only. */\n recipe: Recipe\n /**\n * The `scale` option, else `servings` divided by the scaling base. Always\n * between 1e-6 and 1e6, and 1 when both options fall back.\n */\n scale: number\n /**\n * The recipe's servings at this scale. At 1×, the authored servings. When\n * the `servings` option sets a scale other than 1, exactly that number with\n * no `max`; with `scale`, both ends of a range scale.\n */\n servings?: Servings | undefined\n /**\n * The recipe's yield at this scale. At 1×, the authored yield. When the\n * yield is the scaling base and the `servings` option sets a scale other\n * than 1, exactly that number with no `max`; otherwise both ends of a range\n * scale.\n */\n yield?: Yield | undefined\n system: System\n /**\n * True when an ingredient's quantity has a unit that belongs to only one\n * system. Step text isn't read; `segmentStepText` finds its temperatures.\n */\n canConvert: boolean\n ingredients: Array<{\n id: string\n title?: string | undefined\n items: ResolvedIngredient[]\n }>\n /** Steps numbered from 1, continuing across sections. */\n steps: Array<{\n id: string\n title?: string | undefined\n items: Array<Step & { number: number }>\n }>\n}\n\n/** Options for {@link resolveRecipe}. */\nexport type ResolveRecipeOptions = {\n /**\n * Scales by `servings / base`, where the base is the recipe's servings\n * amount, else its yield amount, else 1. Defaults to the base. A value\n * outside a millionth to a million times the base, or that isn't a number,\n * also uses the default.\n */\n servings?: number | undefined\n /**\n * A direct multiplier, for 1×, 2×, and 3× controls. Wins over `servings`.\n * A value outside 1e-6 to 1e6, or that isn't a number, is ignored.\n */\n scale?: number | undefined\n /** Defaults to `recipe.system`, then `'metric'`. An unknown system also uses the default. */\n system?: System | undefined\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/** Options for {@link resolveIngredient}. */\nexport type ResolveIngredientOptions = {\n scale: number\n system: System\n /** Moves scaled amounts to a better unit on their own ladder (6 tsp to 2 tbsp). Default `true`. */\n rebalance?: boolean | undefined\n}\n\n/**\n * Scales a recipe to a number of servings, or by a multiplier, and expresses\n * each ingredient in a measurement system. Pure, and never throws for an\n * unchanged recipe from `parseRecipe`: invalid options fall back to their\n * defaults, and an error thrown by a getter or proxy in the options passes\n * through. Nothing is rounded; format for display.\n *\n * Each ingredient uses the first of `[quantity, ...equivalents]` that is native to\n * the system (a bare count is native to both), scaled. When none is, `quantity`\n * is scaled, then converted with `toSystem`. Units never change dimension.\n * At any scale but 1, the chosen quantity may move along its unit's ladder, so\n * 3 tsp at 2× is 2 tbsp; pass `rebalance: false` to keep the chosen units.\n *\n * @example\n * // Serves 4–6; flour is 1 ½ cups with a 190 g equivalent.\n * const resolved = resolveRecipe(pancakes, { servings: 8, system: 'metric' })\n * resolved.scale // 2\n * resolved.servings // { amount: 8 }\n * const flour = resolved.ingredients[0]?.items[0]\n * flour?.quantity // { amount: 380, unit: 'g' }\n * flour?.origin // 'equivalent'\n *\n * resolveRecipe(pancakes, { scale: 2 }).servings // { amount: 8, max: 12 }\n */\nexport function resolveRecipe(\n recipe: Recipe,\n options?: ResolveRecipeOptions,\n): ResolvedRecipe {\n const {\n servings,\n scale: requestedScale,\n system: requestedSystem,\n rebalance,\n }: ResolveRecipeOptions = options ?? {}\n const servingsIsBase = isPositive(recipe.servings?.amount)\n const base =\n [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1\n // servings / base can land just outside the window at a bound, as\n // 700000 / 0.7 is 1000000.0000000001.\n const ratio = isServingsFor(servings, base)\n ? Math.min(Math.max(servings / base, 1e-6), 1e6)\n : Number.NaN\n const exactServings =\n isScale(requestedScale) || !isScale(ratio) || isUnity(ratio)\n ? undefined\n : servings\n const scale = isScale(requestedScale)\n ? requestedScale\n : isScale(ratio)\n ? ratio\n : 1\n const system = [requestedSystem, recipe.system].find(isSystem) ?? 'metric'\n const shouldRebalance = rebalance !== false\n let number = 0\n\n function resize<T extends Servings>(\n range: T,\n isBase: boolean,\n ): Omit<T, 'max'> & Servings {\n if (exactServings !== undefined && isBase) {\n return withRange(range, exactServings)\n }\n return isUnity(scale)\n ? range\n : withRange(range, range.amount * scale, (range.max ?? 0) * scale)\n }\n\n return {\n recipe,\n scale,\n ...(recipe.servings && {\n servings: resize(recipe.servings, servingsIsBase),\n }),\n ...(recipe.yield && { yield: resize(recipe.yield, !servingsIsBase) }),\n system,\n canConvert: recipe.ingredients.some((section) =>\n section.items.some(\n (item) => lookupUnit(item.quantity?.unit)?.systems.length === 1,\n ),\n ),\n ingredients: recipe.ingredients.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((ingredient) =>\n resolveLine(ingredient, scale, system, shouldRebalance),\n ),\n })),\n steps: recipe.steps.map((section) => ({\n id: section.id,\n ...(section.title !== undefined && { title: section.title }),\n items: section.items.map((step) => ({ ...step, number: ++number })),\n })),\n }\n}\n\n/**\n * Resolves one ingredient the way {@link resolveRecipe} does, at a raw\n * scale factor. Unlike `resolveRecipe`, it doesn't limit the scale to\n * 1e-6 to 1e6, so a huge scale can overflow an amount to `Infinity`, which\n * the format functions reject.\n *\n * @throws {RecipeBookError} `invalid_argument` when `scale` is not a finite\n * number greater than 0, or `system` is not `'metric'` or `'us'`.\n *\n * @example\n * // salt is 3 tsp.\n * resolveIngredient(salt, { scale: 2, system: 'us' }).quantity // { amount: 2, unit: 'tbsp' }\n */\nexport function resolveIngredient(\n ingredient: Ingredient,\n options: ResolveIngredientOptions,\n): ResolvedIngredient {\n checkFactor('resolveIngredient', 'scale', options?.scale)\n checkSystem('resolveIngredient', options.system)\n\n return resolveLine(\n ingredient,\n options.scale,\n options.system,\n options.rebalance !== false,\n )\n}\n\nfunction resolveLine(\n ingredient: Ingredient,\n scale: number,\n system: System,\n shouldRebalance: boolean,\n): ResolvedIngredient {\n const { id, quantity } = ingredient\n\n if (quantity === undefined) {\n return { id, ingredient, origin: 'authored' }\n }\n\n const candidates = [quantity, ...ingredient.equivalents]\n const native = candidates.findIndex((candidate) =>\n isNative(candidate, system),\n )\n const chosen = candidates[native] ?? quantity\n const scaled = isUnity(scale) ? chosen : multiply(chosen, scale)\n\n if (native < 0) {\n return {\n id,\n ingredient,\n quantity: withEnds(inSystem(scaled, system)),\n origin: 'converted',\n }\n }\n\n return {\n id,\n ingredient,\n quantity: withEnds(\n shouldRebalance && !isUnity(scale) ? rebalance(scaled, system) : scaled,\n ),\n origin: native === 0 ? 'authored' : 'equivalent',\n }\n}\n\n// Ends a few ulps apart can round to equal when scaled or converted, so a range\n// is kept only while its ends differ.\nfunction withRange<T extends Servings>(\n range: T,\n amount: number,\n max = 0,\n): Omit<T, 'max'> & Servings {\n const { max: authoredMax, ...rest } = range\n return { ...rest, amount, ...(max > amount && { max }) }\n}\n\nfunction withEnds(quantity: Quantity): Quantity {\n const { amount } = quantity\n return typeof amount === 'number' || amount.min < amount.max\n ? quantity\n : { ...quantity, amount: amount.min }\n}\n\n// A millionth to a million times the base, found by moving the base's decimal\n// point six places, so a bound written out, such as 700000 for a base of 0.7,\n// is in. @recipe-book/react's isServingsFor applies the same rule.\nfunction isServingsFor(servings: unknown, base: number): servings is number {\n const [mantissa, exponent] = base.toExponential().split('e')\n const shift = (places: number) =>\n Number(`${mantissa}e${Number(exponent) + places}`)\n return isPositive(servings) && servings >= shift(-6) && servings <= shift(6)\n}\n\nfunction isScale(value: unknown): value is number {\n return typeof value === 'number' && value >= 1e-6 && value <= 1e6\n}\n\nfunction isUnity(scale: number): boolean {\n return Math.abs(scale - 1) <= 1e-9\n}\n\nexport function isNative(quantity: Quantity, system: System): boolean {\n const unit = lookupUnit(quantity.unit)\n return unit === undefined || unit.systems.includes(system)\n}\n"],"mappings":"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","names":["rebalance"]}