@recipe-book/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VWVIBCTG.cjs → chunk-ISRDJR2O.cjs} +227 -179
  4. package/dist/chunk-ISRDJR2O.cjs.map +1 -0
  5. package/dist/errors.js +29 -1
  6. package/dist/errors.js.map +1 -1
  7. package/dist/format/duration.js.map +1 -1
  8. package/dist/format/ingredient.js.map +1 -1
  9. package/dist/format/number.js +1 -1
  10. package/dist/format/number.js.map +1 -1
  11. package/dist/format/options.js.map +1 -1
  12. package/dist/format/quantity.js.map +1 -1
  13. package/dist/format/servings.js.map +1 -1
  14. package/dist/format/temperature.js.map +1 -1
  15. package/dist/index.cjs +87 -74
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +71 -28
  18. package/dist/index.d.ts +71 -28
  19. package/dist/index.js +6 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/ingredient/parse.js +27 -22
  22. package/dist/ingredient/parse.js.map +1 -1
  23. package/dist/issues.js.map +1 -1
  24. package/dist/quantity/amount.js +11 -0
  25. package/dist/quantity/amount.js.map +1 -1
  26. package/dist/quantity/parse.js +58 -81
  27. package/dist/quantity/parse.js.map +1 -1
  28. package/dist/quantity/scale.js +5 -2
  29. package/dist/quantity/scale.js.map +1 -1
  30. package/dist/recipe/ids.js.map +1 -1
  31. package/dist/recipe/parse.js +80 -60
  32. package/dist/recipe/parse.js.map +1 -1
  33. package/dist/recipe/schema.js +8 -6
  34. package/dist/recipe/schema.js.map +1 -1
  35. package/dist/resolve.js +6 -1
  36. package/dist/resolve.js.map +1 -1
  37. package/dist/schema-org/find.js +46 -13
  38. package/dist/schema-org/find.js.map +1 -1
  39. package/dist/schema-org/from-json-ld.js +134 -71
  40. package/dist/schema-org/from-json-ld.js.map +1 -1
  41. package/dist/schema-org/text.js.map +1 -1
  42. package/dist/schema-org/to-json-ld.js +12 -15
  43. package/dist/schema-org/to-json-ld.js.map +1 -1
  44. package/dist/schema-org.cjs +239 -147
  45. package/dist/schema-org.cjs.map +1 -1
  46. package/dist/schema-org.d.cts +35 -21
  47. package/dist/schema-org.d.ts +35 -21
  48. package/dist/steps/segment.js.map +1 -1
  49. package/dist/steps/temperature.js +13 -13
  50. package/dist/steps/temperature.js.map +1 -1
  51. package/dist/{types-DSMf45H5.d.cts → types-CKIoWsz8.d.cts} +25 -7
  52. package/dist/{types-DSMf45H5.d.ts → types-CKIoWsz8.d.ts} +25 -7
  53. package/dist/units/convert.js +16 -8
  54. package/dist/units/convert.js.map +1 -1
  55. package/dist/units/temperature.js +7 -1
  56. package/dist/units/temperature.js.map +1 -1
  57. package/dist/validation.js +1 -1
  58. package/dist/validation.js.map +1 -1
  59. package/package.json +1 -1
  60. package/dist/chunk-VWVIBCTG.cjs.map +0 -1
@@ -17,9 +17,16 @@ var ERROR_NAMES = /* @__PURE__ */ new Set([
17
17
  ]);
18
18
  var RecipeBookError = class extends Error {
19
19
  constructor(code, message, options) {
20
- const docsUrl = `${DOCS_ORIGIN}/errors/${code}`;
20
+ const docsUrl = `${DOCS_ORIGIN}/errors/${code}/`;
21
21
  super(`${message}
22
22
  See ${docsUrl}`, options);
23
+ if ("cause" in Object(options)) {
24
+ Object.defineProperty(this, "cause", {
25
+ value: options?.cause,
26
+ writable: true,
27
+ configurable: true
28
+ });
29
+ }
23
30
  this.name = "RecipeBookError";
24
31
  this.code = code;
25
32
  this.docsUrl = docsUrl;
@@ -36,6 +43,26 @@ function isRecipeBookError(error2) {
36
43
  return false;
37
44
  }
38
45
  }
46
+ var VALIDATION_CODES = /* @__PURE__ */ new Set([
47
+ "invalid_recipe",
48
+ "invalid_quantity",
49
+ "invalid_ingredient"
50
+ ]);
51
+ function isRecipeValidationError(error2) {
52
+ if (typeof error2 !== "object" || error2 === null) {
53
+ return false;
54
+ }
55
+ try {
56
+ const {
57
+ name,
58
+ code,
59
+ issues
60
+ } = error2;
61
+ return name === "RecipeValidationError" && VALIDATION_CODES.has(code) && Array.isArray(issues);
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
39
66
  var MAX_ECHOED = 60;
40
67
  function describeValue(value) {
41
68
  if (typeof value === "string") {
@@ -392,7 +419,7 @@ var RecipeValidationError = class extends RecipeBookError {
392
419
  * @param subject - What was validated, shown as given at the start of the
393
420
  * message, e.g. `'"Buttermilk pancakes"'`. Recipe Book's own errors quote
394
421
  * the recipe's name or the input text like a JSON string, cut with "…"
395
- * when longer than 60 characters.
422
+ * when longer than 60 UTF-16 code units.
396
423
  */
397
424
  constructor(code, issues, subject = code === "invalid_recipe" ? "Recipe" : code === "invalid_quantity" ? "Quantity" : "Ingredient") {
398
425
  super(code, summarize(subject, issues));
@@ -507,6 +534,16 @@ function checkQuantity(fn, quantity) {
507
534
  `${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`
508
535
  );
509
536
  }
537
+ function checkResult(fn, amount) {
538
+ for (const value of amountEnds(amount)) {
539
+ if (!isPositive(value)) {
540
+ throw invalidArgument(
541
+ `${fn}: the result must be a finite number greater than 0, but it is ${describeValue(value)}.`
542
+ );
543
+ }
544
+ }
545
+ return amount;
546
+ }
510
547
  function checkFactor(fn, name, value) {
511
548
  if (!isPositive(value)) {
512
549
  throw invalidArgument(
@@ -522,45 +559,9 @@ function invalidArgument(message) {
522
559
  }
523
560
 
524
561
  // src/quantity/parse.ts
525
- var VULGAR_FRACTIONS = {
526
- "\xBC": [1, 4],
527
- "\xBD": [1, 2],
528
- "\xBE": [3, 4],
529
- "\u2150": [1, 7],
530
- "\u2151": [1, 9],
531
- "\u2152": [1, 10],
532
- "\u2153": [1, 3],
533
- "\u2154": [2, 3],
534
- "\u2155": [1, 5],
535
- "\u2156": [2, 5],
536
- "\u2157": [3, 5],
537
- "\u2158": [4, 5],
538
- "\u2159": [1, 6],
539
- "\u215A": [5, 6],
540
- "\u215B": [1, 8],
541
- "\u215C": [3, 8],
542
- "\u215D": [5, 8],
543
- "\u215E": [7, 8]
544
- };
562
+ var VULGAR = "[\xBC-\xBE\u2150-\u215E]";
545
563
  var ARTICLE = /^an?\s+(?=\D)/i;
546
- var patterns;
547
- function getPatterns() {
548
- return patterns ?? (patterns = compilePatterns());
549
- }
550
- function compilePatterns() {
551
- const vulgar = `[${Object.keys(VULGAR_FRACTIONS).join("")}]`;
552
- const slash = "[/\u2044]";
553
- const number = String.raw`(?:\d+\s+)?\d+\s*${slash}\s*\d+|(?:\d+\s*)?${vulgar}|\d+(?:\.\d+)?|\.\d+`;
554
- return {
555
- amount: new RegExp(
556
- String.raw`^(${number})(?:\s*[-–—]\s*(${number})|\s+to\s+(${number}))?`,
557
- "i"
558
- ),
559
- asciiFraction: new RegExp(
560
- String.raw`^(?:(\d+)\s+)?(\d+)\s*${slash}\s*(\d+)$`
561
- )
562
- };
563
- }
564
+ var amountPattern;
564
565
  function parseQuantity(text) {
565
566
  const issues = [];
566
567
  if (typeof text !== "string") {
@@ -577,7 +578,8 @@ function parseQuantity(text) {
577
578
  }
578
579
  function readQuantity(issues, value, path) {
579
580
  if (typeof value === "string") {
580
- return readQuantityText(issues, value, path);
581
+ const quantity = readQuantityText(issues, value, path);
582
+ return quantity && readTextAmount(issues, quantity.amount, path) ? quantity : void 0;
581
583
  }
582
584
  if (typeof value === "number") {
583
585
  const amount = readPositive(issues, value, path);
@@ -585,7 +587,35 @@ function readQuantity(issues, value, path) {
585
587
  }
586
588
  const record = asRecord(value);
587
589
  if (record) {
588
- return readQuantityObject(issues, record, path);
590
+ checkFields(issues, record, { amount: true, unit: true }, path);
591
+ const amount = readAmount(issues, record.amount, [...path, "amount"]);
592
+ const { unit } = record;
593
+ if (unit === void 0) {
594
+ return amount === void 0 ? void 0 : { amount };
595
+ }
596
+ if (typeof unit !== "string") {
597
+ return error(
598
+ issues,
599
+ "invalid_type",
600
+ [...path, "unit"],
601
+ `Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
602
+ );
603
+ }
604
+ const id = findUnit(unit);
605
+ if (!id) {
606
+ const problem = unknownUnit(
607
+ unit,
608
+ amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
609
+ );
610
+ return error(
611
+ issues,
612
+ problem.code,
613
+ [...path, "unit"],
614
+ problem.message,
615
+ problem.suggestion
616
+ );
617
+ }
618
+ return amount === void 0 ? void 0 : { amount, unit: id };
589
619
  }
590
620
  return error(
591
621
  issues,
@@ -640,12 +670,18 @@ function matchAmount(text, allowZero) {
640
670
  if (article) {
641
671
  return { amount: 1, amountText: "1", rest: text.slice(article[0].length) };
642
672
  }
643
- const match = getPatterns().amount.exec(text);
673
+ if (!amountPattern) {
674
+ const number = String.raw`(?:\d+\s+)?\d+\s*[/⁄]\s*\d+|(?:\d+\s*)?${VULGAR}|\d+(?:\.\d+)?|\.\d+`;
675
+ amountPattern = new RegExp(
676
+ String.raw`^(${number})(?:(?:\s*[-–—]\s*|\s+to\s+)(${number}))?`,
677
+ "i"
678
+ );
679
+ }
680
+ const match = amountPattern.exec(text);
644
681
  if (!match) {
645
682
  return void 0;
646
683
  }
647
- const [matched, lowText = "", highTextDash, highTextTo] = match;
648
- const highText = highTextDash ?? highTextTo;
684
+ const [matched, lowText = "", highText] = match;
649
685
  const low = toNumber(lowText);
650
686
  const high = highText === void 0 ? low : toNumber(highText);
651
687
  if (low === Infinity || high === Infinity) {
@@ -691,50 +727,25 @@ function unreadable(text) {
691
727
  message: text === "" ? 'Expected a quantity such as "1 \xBD cups", received an empty string.' : `Couldn't read ${quote(text)}. Use a number or fraction, e.g. "1 \xBD cups".`
692
728
  };
693
729
  }
730
+ var fractions = /* @__PURE__ */ new Map();
694
731
  function toNumber(text) {
695
- const ascii = getPatterns().asciiFraction.exec(text);
732
+ const ascii = /^(?:(\d+)\s+)?(\d+)\s*[/⁄]\s*(\d+)$/.exec(text);
696
733
  if (ascii) {
697
734
  const [, whole = "0", numerator, denominator] = ascii;
698
735
  return Number(whole) + Number(numerator) / (Number(denominator) || NaN);
699
736
  }
700
- const vulgar = VULGAR_FRACTIONS[text.slice(-1)];
701
- if (vulgar) {
702
- const [numerator, denominator] = vulgar;
703
- return Number(text.slice(0, -1)) + numerator / denominator;
737
+ const last = text.slice(-1);
738
+ if (last > "9") {
739
+ let fraction = fractions.get(last);
740
+ if (fraction === void 0) {
741
+ const [numerator, denominator] = last.normalize("NFKD").split("\u2044");
742
+ fraction = Number(numerator) / Number(denominator);
743
+ fractions.set(last, fraction);
744
+ }
745
+ return Number(text.slice(0, -1)) + fraction;
704
746
  }
705
747
  return Number(text);
706
748
  }
707
- function readQuantityObject(issues, record, path) {
708
- checkFields(issues, record, { amount: true, unit: true }, path);
709
- const amount = readAmount(issues, record.amount, [...path, "amount"]);
710
- const { unit } = record;
711
- if (unit === void 0) {
712
- return amount === void 0 ? void 0 : { amount };
713
- }
714
- if (typeof unit !== "string") {
715
- return error(
716
- issues,
717
- "invalid_type",
718
- [...path, "unit"],
719
- `Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
720
- );
721
- }
722
- const id = findUnit(unit);
723
- if (!id) {
724
- const problem = unknownUnit(
725
- unit,
726
- amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
727
- );
728
- return error(
729
- issues,
730
- problem.code,
731
- [...path, "unit"],
732
- problem.message,
733
- problem.suggestion
734
- );
735
- }
736
- return amount === void 0 ? void 0 : { amount, unit: id };
737
- }
738
749
  function readAmount(issues, value, path) {
739
750
  if (value === void 0) {
740
751
  return error(
@@ -773,16 +784,19 @@ function readAmount(issues, value, path) {
773
784
  );
774
785
  }
775
786
  function readPositive(issues, value, path) {
776
- if (isPositive(value)) {
787
+ if (isPositive(value) && value >= 1e-300 && value <= 1e290) {
777
788
  return value;
778
789
  }
779
790
  return error(
780
791
  issues,
781
792
  typeof value === "number" ? "invalid_amount" : "invalid_type",
782
793
  path,
783
- `Expected a positive number, received ${describeValue(value)}.`
794
+ isPositive(value) ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.` : `Expected a positive number, received ${describeValue(value)}.`
784
795
  );
785
796
  }
797
+ function readTextAmount(issues, amount, path) {
798
+ return (typeof amount === "number" ? [amount] : [amount.min, amount.max]).every((end) => readPositive(issues, end, path) !== void 0);
799
+ }
786
800
 
787
801
  // src/recipe/duration.ts
788
802
  var ISO_DURATION = /^P(?!$)(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?=\d)(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/i;
@@ -1008,9 +1022,14 @@ var NUTRIENT_EXAMPLES = {
1008
1022
  mg: 'milligrams, e.g. 300, "300 mg", or "0.3 g"'
1009
1023
  };
1010
1024
  function parseRecipe(input, options) {
1025
+ return parseRecipeWith(
1026
+ input,
1027
+ readLineOption("parseRecipe", options) ?? "pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })"
1028
+ );
1029
+ }
1030
+ function parseRecipeWith(input, lines) {
1011
1031
  const issues = [];
1012
- const readLine2 = readLineOption("parseRecipe", options);
1013
- const recipe = readRecipe(issues, input, readLine2);
1032
+ const recipe = readRecipe(issues, input, lines);
1014
1033
  const label2 = recipe && quote(recipe.name);
1015
1034
  return toResult("invalid_recipe", recipe, issues, label2);
1016
1035
  }
@@ -1024,7 +1043,7 @@ function readLineOption(fn, options) {
1024
1043
  }
1025
1044
  return reader;
1026
1045
  }
1027
- function readRecipe(issues, value, readLine2) {
1046
+ function readRecipe(issues, value, lines) {
1028
1047
  const input = asRecord(value);
1029
1048
  if (!input) {
1030
1049
  return error(
@@ -1054,7 +1073,7 @@ function readRecipe(issues, value, readLine2) {
1054
1073
  input.ingredients,
1055
1074
  ["ingredients"],
1056
1075
  "ingredients",
1057
- (issues2, value2, path) => readIngredient(issues2, value2, path, readLine2)
1076
+ (issues2, value2, path) => readIngredient(issues2, value2, path, lines)
1058
1077
  );
1059
1078
  const steps = readList(
1060
1079
  issues,
@@ -1104,17 +1123,13 @@ function readRecipe(issues, value, readLine2) {
1104
1123
  });
1105
1124
  }
1106
1125
  function readList(issues, pending, value, path, noun, readItem) {
1107
- const entries = value === void 0 ? [] : readArray(issues, value, path, noun);
1108
- if (!entries) {
1109
- return [];
1110
- }
1111
1126
  const sections = [];
1112
1127
  let loose;
1113
1128
  let count2 = 0;
1114
- const read = (entry, entryPath) => readItem(issues, entry, entryPath, ++count2);
1115
- entries.forEach((entry, i) => {
1129
+ const read = (entry2, entryPath) => readItem(issues, entry2, entryPath, ++count2);
1130
+ const visit = (entry2, i) => {
1116
1131
  const entryPath = [...path, i];
1117
- const section = asSection(entry);
1132
+ const section = asSection(entry2);
1118
1133
  if (section) {
1119
1134
  loose = void 0;
1120
1135
  sections.push(readSection(issues, section, entryPath, noun, read));
@@ -1131,20 +1146,28 @@ function readList(issues, pending, value, path, noun, readItem) {
1131
1146
  };
1132
1147
  sections.push(loose);
1133
1148
  }
1134
- const item = read(entry, entryPath);
1149
+ const item = read(entry2, entryPath);
1135
1150
  if (item) {
1136
1151
  loose.items.push(item);
1137
1152
  }
1138
- });
1153
+ };
1154
+ if (value !== void 0 && !readArray(issues, value, path, noun, visit)) {
1155
+ return [];
1156
+ }
1139
1157
  if (sections.length === 0) {
1140
1158
  warning(issues, "empty_list", path, `Recipe has no ${noun}.`);
1141
1159
  return [];
1142
1160
  }
1143
- const sectionIds = assignIds(sections, pending);
1161
+ const renamed = [];
1162
+ const sectionIds = assignIds(sections, renamed);
1144
1163
  const itemIds = assignIds(
1145
1164
  sections.flatMap((section) => section.items),
1146
- pending
1165
+ renamed
1147
1166
  );
1167
+ const entry = ({ issue: issue2 }) => issue2.path[path.length];
1168
+ for (const issue2 of renamed.sort((a, b) => entry(a) - entry(b))) {
1169
+ pending.push(issue2);
1170
+ }
1148
1171
  let next = 0;
1149
1172
  return sections.map(
1150
1173
  (section, i) => defined({
@@ -1164,10 +1187,13 @@ function readSection(issues, record, path, noun, read) {
1164
1187
  const title = readText(issues, record.title, [...path, "title"]);
1165
1188
  const items = [];
1166
1189
  const itemsPath = [...path, "items"];
1167
- const entries = readArray(issues, record.items, itemsPath, noun);
1168
- if (entries) {
1169
- let visited = 0;
1170
- entries.forEach((entry, j) => {
1190
+ let visited = 0;
1191
+ const entries = readArray(
1192
+ issues,
1193
+ record.items,
1194
+ itemsPath,
1195
+ noun,
1196
+ (entry, j) => {
1171
1197
  visited++;
1172
1198
  const itemPath = [...itemsPath, j];
1173
1199
  if (asSection(entry)) {
@@ -1183,10 +1209,10 @@ function readSection(issues, record, path, noun, read) {
1183
1209
  if (item) {
1184
1210
  items.push(item);
1185
1211
  }
1186
- });
1187
- if (visited === 0) {
1188
- warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
1189
1212
  }
1213
+ );
1214
+ if (entries && visited === 0) {
1215
+ warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
1190
1216
  }
1191
1217
  checkFields(issues, record, SECTION_FIELDS, path);
1192
1218
  return {
@@ -1199,29 +1225,44 @@ function readSection(issues, record, path, noun, read) {
1199
1225
  };
1200
1226
  }
1201
1227
  function readItems(issues, pending, value, path, noun, readItem) {
1202
- const entries = value === void 0 ? [] : readArray(issues, value, path, noun);
1203
- if (!entries) {
1228
+ let n = 0;
1229
+ const drafts = value === void 0 ? [] : readArray(
1230
+ issues,
1231
+ value,
1232
+ path,
1233
+ noun,
1234
+ (entry, i) => readItem(issues, entry, [...path, i], ++n)
1235
+ );
1236
+ if (!drafts) {
1204
1237
  return [];
1205
1238
  }
1206
- let n = 0;
1207
- const drafts = entries.flatMap((entry, i) => {
1208
- const draft = readItem(issues, entry, [...path, i], ++n);
1209
- return draft ? [draft] : [];
1210
- });
1211
1239
  const ids = assignIds(drafts, pending);
1212
1240
  return drafts.map((draft, i) => draft.build(ids[i]));
1213
1241
  }
1214
- function readArray(issues, value, path, noun) {
1215
- return Array.isArray(value) ? value : error(
1216
- issues,
1217
- "invalid_type",
1218
- path,
1219
- `Expected a list of ${noun}, received ${describeValue(value)}.`
1220
- );
1242
+ function readArray(issues, value, path, noun, read) {
1243
+ if (!Array.isArray(value)) {
1244
+ return error(
1245
+ issues,
1246
+ "invalid_type",
1247
+ path,
1248
+ `Expected a list of ${noun}, received ${describeValue(value)}.`
1249
+ );
1250
+ }
1251
+ const kept = [];
1252
+ for (let i = 0, n = value.length; i < n; i++) {
1253
+ if (i in value) {
1254
+ const entry = value[i];
1255
+ const result = hasOwn(value, i) && read(entry, i);
1256
+ if (result) {
1257
+ kept.push(result);
1258
+ }
1259
+ }
1260
+ }
1261
+ return kept;
1221
1262
  }
1222
- function readIngredient(issues, value, path, readLine2) {
1223
- if (typeof value === "string" && readLine2) {
1224
- const result = readLine2(value);
1263
+ function readIngredient(issues, value, path, lines) {
1264
+ if (typeof value === "string" && typeof lines === "function") {
1265
+ const result = lines(value);
1225
1266
  for (const issue2 of result.success ? result.warnings : result.error.issues) {
1226
1267
  issues.push({ ...issue2, path: [...path, ...issue2.path] });
1227
1268
  }
@@ -1233,7 +1274,7 @@ function readIngredient(issues, value, path, readLine2) {
1233
1274
  issues,
1234
1275
  "invalid_type",
1235
1276
  path,
1236
- `Expected an ingredient such as ${readLine2 ? '"2 cups flour" or ' : ""}{ name: "flour", quantity: "2 cups" }, received ${describeValue(value)}.${typeof value === "string" ? " To read ingredient lines, pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })." : ""}`
1277
+ `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}.` : ""}`
1237
1278
  );
1238
1279
  }
1239
1280
  const position = issues.length;
@@ -1274,22 +1315,13 @@ function readIngredient(issues, value, path, readLine2) {
1274
1315
  };
1275
1316
  }
1276
1317
  function readEquivalents(issues, value, path) {
1277
- if (value === void 0) {
1278
- return [];
1279
- }
1280
- if (!Array.isArray(value)) {
1281
- error(
1282
- issues,
1283
- "invalid_type",
1284
- path,
1285
- `Expected a list of quantities, e.g. ["190 g"], received ${describeValue(value)}.`
1286
- );
1287
- return [];
1288
- }
1289
- return value.flatMap((entry, i) => {
1290
- const quantity = readQuantity(issues, entry, [...path, i]);
1291
- return quantity === void 0 ? [] : [quantity];
1292
- });
1318
+ return value !== void 0 && readArray(
1319
+ issues,
1320
+ value,
1321
+ path,
1322
+ 'quantities, e.g. ["190 g"]',
1323
+ (entry, i) => readQuantity(issues, entry, [...path, i])
1324
+ ) || [];
1293
1325
  }
1294
1326
  function readStep(issues, value, path, n) {
1295
1327
  const isText = typeof value === "string";
@@ -1524,10 +1556,11 @@ function readRangeText(issues, text, path, expected) {
1524
1556
  return error(issues, match.code, path, match.message);
1525
1557
  }
1526
1558
  const { amount, rest } = match;
1527
- return {
1528
- range: typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max },
1529
- rest
1530
- };
1559
+ const range = typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max };
1560
+ if (readPositive(issues, range.amount, path) === void 0 || range.max !== void 0 && readPositive(issues, range.max, path) === void 0) {
1561
+ return void 0;
1562
+ }
1563
+ return { range, rest };
1531
1564
  }
1532
1565
  function readRange(issues, record, path, missingAmount) {
1533
1566
  const value = record.amount;
@@ -1750,26 +1783,26 @@ var ABOUT = /(about|approx(?:imately|\.)?) /iy;
1750
1783
  var PHRASE = /((?:an? )?(?:handful|few|couple|little|splash|drizzle|knob|squeeze|sprinkle|dollop))(?: of)? /iy;
1751
1784
  var HYPHENATED_MIXED = /^(\d+)-(?=(\d+)[/⁄](\d+)(?![\d/⁄]))/;
1752
1785
  var AMOUNT_CHARS = /(?:an? )?(?:[\d .,/⁄¼-¾⅐-⅞–—-]|to )*/iy;
1753
- var UNIT_OF = /([a-z]+) of /iy;
1754
1786
  var TIMES = / ?[x×] ?(?=\d)/iy;
1755
- var UNREAD = / ?(?:(?:or|to|and|plus|half)\b|[-–—/+&]|\d+(?:\.\d+)?(?= |$))/iy;
1787
+ var UNREAD = / ?(?:(?:or|to|and|plus|half)(?![\p{L}\p{M}\d_])|[-–—/+&]|\d+(?:\.\d+)?(?= |$))/iuy;
1756
1788
  var UNREAD_AFTER_UNIT = / (?:or|to|[-–—&])(?= )|[-–—+&](?=\d)/iy;
1757
1789
  var PAREN = / ?\(([^()]*)\)/y;
1758
1790
  var PARENS = / ?\(([^()]*)\)/g;
1759
1791
  var MODIFIER = / ?(heaping|heaped|level|scant|rounded|generous|good|small|medium|large|big)(?= )/iy;
1760
- var JOIN = /(?: (plus|and|\+)(?![a-z]))? ?/iy;
1792
+ var JOIN = /(?: (plus|and|\+)(?![\p{L}\p{M}]))? ?/iuy;
1761
1793
  var SEPARATOR = / ?[;/] ?/y;
1762
1794
  var OF = / ?of(?: (?:an?|the))?(?= |$)/iy;
1795
+ var NAMED = /(?!(?:for|to|an?|yields?|total)(?![\p{L}\p{M}\d_])|(?:makes|serv(?:es|ings?)(?: size)?|protein|carbs|carbohydrates?|fiber|fibre|sodium|cholesterol|saturated fat|fat) ?:)([^\d:]+):/iuy;
1763
1796
  var PHRASE_NOTE = / (?=to taste|as needed|as required|plus more|plus extra|or more|or to taste|for (?:serving|garnish|dusting|greasing|frying|drizzling|decoration))/i;
1764
1797
  var OPTIONAL_NOTE = /(?:^|,) ?optional(?= ?(?:,|$))/gi;
1765
1798
  var OPTIONAL_PAREN = /^optional\b[,;:]? ?/i;
1766
1799
  var CONTAINER = /^(?:stalk|rib|ear|sheet|loa(?:f|ves)|knob|strip|bag|box|jar|bottle|packet|envelope|tin|block|ball|handful)(?:e?s)?$/i;
1767
1800
  var BONELESS = /(?:^|[^a-z])(?:bone|skin)less$/i;
1768
- var patterns2;
1769
- function getPatterns2() {
1770
- return patterns2 ?? (patterns2 = compilePatterns2());
1801
+ var patterns;
1802
+ function getPatterns() {
1803
+ return patterns ?? (patterns = compilePatterns());
1771
1804
  }
1772
- function compilePatterns2() {
1805
+ function compilePatterns() {
1773
1806
  const word = String.raw`([a-z]+(?:-[a-z]+)?)\.?`;
1774
1807
  const end = String.raw`(?=[ ,;()/]|[-–—+&]\d|$)`;
1775
1808
  return {
@@ -1817,7 +1850,7 @@ function parseIngredientLine(text) {
1817
1850
  return toResult("invalid_ingredient", ingredient, issues);
1818
1851
  }
1819
1852
  function readLine(line) {
1820
- const p = getPatterns2();
1853
+ const p = getPatterns();
1821
1854
  const notes = [];
1822
1855
  const equivalents = [];
1823
1856
  let optional = false;
@@ -1877,11 +1910,22 @@ function readLine(line) {
1877
1910
  }
1878
1911
  if (found) {
1879
1912
  [amount, i] = found;
1880
- } else if (match = exec(UNIT_OF, line, i)) {
1881
- const unit = findUnit(match[1]);
1882
- if (unit) {
1883
- quantity = { amount: 1, unit };
1884
- i = UNIT_OF.lastIndex;
1913
+ } else {
1914
+ const modifier = exec(MODIFIER, line, i);
1915
+ const unit = readUnit(line, modifier ? MODIFIER.lastIndex : i);
1916
+ if (unit && exec(OF, line, unit[1])) {
1917
+ if (modifier) {
1918
+ addNote(modifier[1]);
1919
+ }
1920
+ quantity = { amount: 1, unit: unit[0] };
1921
+ i = unit[1];
1922
+ } else if (match = exec(NAMED, line, i)) {
1923
+ const measure = readMeasure(line, NAMED.lastIndex, false);
1924
+ if (measure && (line[measure[1]] ?? ",") === ",") {
1925
+ quantity = measure[0];
1926
+ line = match[1] + line.slice(measure[1]);
1927
+ i = 0;
1928
+ }
1885
1929
  }
1886
1930
  }
1887
1931
  }
@@ -2012,10 +2056,10 @@ function isProblem(value) {
2012
2056
  return typeof value === "object" && value !== null && "code" in value;
2013
2057
  }
2014
2058
  function readUnit(text, start) {
2015
- const { twoWordUnit, unit } = getPatterns2();
2059
+ const { twoWordUnit, unit } = getPatterns();
2016
2060
  for (const pattern of [twoWordUnit, unit]) {
2017
2061
  const match = exec(pattern, text, start);
2018
- const id = match && findUnit(match[2] ? `${match[1]} ${match[2]}` : match[1]);
2062
+ const id = match && findUnit(match.slice(1).join(" "));
2019
2063
  if (id) {
2020
2064
  return [id, pattern.lastIndex];
2021
2065
  }
@@ -2113,7 +2157,13 @@ function countForms(name, many) {
2113
2157
  k = CONTAINER.test(words[0]) ? 0 : words.length - 1;
2114
2158
  }
2115
2159
  const word = words[k];
2116
- const other = many ? singular(word) : pluralize(word);
2160
+ const other = many ? word.replace(
2161
+ /(?:(lea|hal|loa)ves|([rtdv])ies|(o|ch|sh|x|ss|chill?i)es|([^su])s)$/i,
2162
+ (_, f, y, es, s) => f ? `${f}f` : y ? `${y}y` : es ?? s
2163
+ ) : word.replace(
2164
+ /(?:(lea|hal|loa)f|([^aeiou])y|(ch|sh|x|z|tomato|potato|mango|chill?i)|[a-rt-z])$/i,
2165
+ (match, f, y, es) => f ? `${f}ves` : y ? `${y}ies` : es ? `${es}es` : `${match}s`
2166
+ );
2117
2167
  if (other === word) {
2118
2168
  return [name, void 0];
2119
2169
  }
@@ -2121,24 +2171,14 @@ function countForms(name, many) {
2121
2171
  const swapped = words.join(" ");
2122
2172
  return many ? [swapped, name] : [name, swapped];
2123
2173
  }
2124
- function singular(word) {
2125
- return word.replace(
2126
- /(?:(lea|hal|loa)ves|([rtdv])ies|(o|ch|sh|x|ss|chill?i)es|([^su])s)$/i,
2127
- (_, f, y, es, s) => f ? `${f}f` : y ? `${y}y` : es ?? s
2128
- );
2129
- }
2130
- function pluralize(word) {
2131
- return word.replace(
2132
- /(?:(lea|hal|loa)f|([^aeiou])y|(ch|sh|x|z|tomato|potato|mango|chill?i)|[a-rt-z])$/i,
2133
- (match, f, y, es) => f ? `${f}ves` : y ? `${y}ies` : es ? `${es}es` : `${match}s`
2134
- );
2135
- }
2136
2174
 
2137
2175
  // src/quantity/scale.ts
2138
2176
  function scaleQuantity(quantity, factor) {
2139
- checkQuantity("scaleQuantity", quantity);
2177
+ const amount = checkQuantity("scaleQuantity", quantity);
2140
2178
  checkFactor("scaleQuantity", "factor", factor);
2141
- return multiply(quantity, factor);
2179
+ const result = multiply({ amount, unit: quantity.unit }, factor);
2180
+ checkResult("scaleQuantity", result.amount);
2181
+ return result;
2142
2182
  }
2143
2183
  function multiply(quantity, factor) {
2144
2184
  const amount = mapAmount(quantity.amount, (value) => value * factor);
@@ -2235,7 +2275,7 @@ function resolveRecipe(recipe, options) {
2235
2275
  } = options ?? {};
2236
2276
  const servingsIsBase = isPositive(recipe.servings?.amount);
2237
2277
  const base = [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1;
2238
- const ratio = typeof servings === "number" ? servings / base : Number.NaN;
2278
+ const ratio = isServingsFor(servings, base) ? Math.min(Math.max(servings / base, 1e-6), 1e6) : Number.NaN;
2239
2279
  const exactServings = isScale(requestedScale) || !isScale(ratio) || isUnity(ratio) ? void 0 : servings;
2240
2280
  const scale = isScale(requestedScale) ? requestedScale : isScale(ratio) ? ratio : 1;
2241
2281
  const system = [requestedSystem, recipe.system].find(isSystem) ?? "metric";
@@ -2320,6 +2360,11 @@ function withEnds(quantity) {
2320
2360
  const { amount } = quantity;
2321
2361
  return typeof amount === "number" || amount.min < amount.max ? quantity : { ...quantity, amount: amount.min };
2322
2362
  }
2363
+ function isServingsFor(servings, base) {
2364
+ const [mantissa, exponent] = base.toExponential().split("e");
2365
+ const shift = (places) => Number(`${mantissa}e${Number(exponent) + places}`);
2366
+ return isPositive(servings) && servings >= shift(-6) && servings <= shift(6);
2367
+ }
2323
2368
  function isScale(value) {
2324
2369
  return typeof value === "number" && value >= 1e-6 && value <= 1e6;
2325
2370
  }
@@ -2439,7 +2484,7 @@ function integerText(value) {
2439
2484
  function invalidLocale(fn, locale) {
2440
2485
  return new RecipeBookError(
2441
2486
  "invalid_argument",
2442
- `${fn}: locale must be a BCP 47 language tag such as "en-US", received ${describeValue(locale)}.`
2487
+ `${fn}: locale must be a language tag that Intl accepts, such as "en-US", received ${describeValue(locale)}.`
2443
2488
  );
2444
2489
  }
2445
2490
 
@@ -2541,7 +2586,7 @@ function joinParts(parts) {
2541
2586
  return parts.map((part) => part.value).join("");
2542
2587
  }
2543
2588
  function showAmount(value, settings, steps = QUARTERS) {
2544
- const { rounding, fractions } = settings;
2589
+ const { rounding, fractions: fractions2 } = settings;
2545
2590
  if (rounding === "none") {
2546
2591
  return decimal(value, 3, settings);
2547
2592
  }
@@ -2553,7 +2598,7 @@ function showAmount(value, settings, steps = QUARTERS) {
2553
2598
  return decimal(value, 1, settings);
2554
2599
  }
2555
2600
  const [whole, part, glyph] = snap(value, steps);
2556
- if (!fractions) {
2601
+ if (!fractions2) {
2557
2602
  return decimal(whole + part, 2, settings);
2558
2603
  }
2559
2604
  const wholeText = settings.number(whole, 0);
@@ -2698,6 +2743,7 @@ exports.RecipeBookError = RecipeBookError;
2698
2743
  exports.RecipeValidationError = RecipeValidationError;
2699
2744
  exports.asRecord = asRecord;
2700
2745
  exports.checkQuantity = checkQuantity;
2746
+ exports.checkResult = checkResult;
2701
2747
  exports.checkSystem = checkSystem;
2702
2748
  exports.describeValue = describeValue;
2703
2749
  exports.disallowedScheme = disallowedScheme;
@@ -2712,6 +2758,7 @@ exports.hasOwn = hasOwn;
2712
2758
  exports.inSystem = inSystem;
2713
2759
  exports.isNative = isNative;
2714
2760
  exports.isRecipeBookError = isRecipeBookError;
2761
+ exports.isRecipeValidationError = isRecipeValidationError;
2715
2762
  exports.isRecord = isRecord;
2716
2763
  exports.issue = issue;
2717
2764
  exports.lookupUnit = lookupUnit;
@@ -2723,6 +2770,7 @@ exports.parseIngredientLine = parseIngredientLine;
2723
2770
  exports.parseIsoDuration = parseIsoDuration;
2724
2771
  exports.parseQuantity = parseQuantity;
2725
2772
  exports.parseRecipe = parseRecipe;
2773
+ exports.parseRecipeWith = parseRecipeWith;
2726
2774
  exports.pluralForm = pluralForm;
2727
2775
  exports.quote = quote;
2728
2776
  exports.readLineOption = readLineOption;
@@ -2735,5 +2783,5 @@ exports.toResult = toResult;
2735
2783
  exports.units = units;
2736
2784
  exports.unknownUnitError = unknownUnitError;
2737
2785
  exports.warning = warning;
2738
- //# sourceMappingURL=chunk-VWVIBCTG.cjs.map
2739
- //# sourceMappingURL=chunk-VWVIBCTG.cjs.map
2786
+ //# sourceMappingURL=chunk-ISRDJR2O.cjs.map
2787
+ //# sourceMappingURL=chunk-ISRDJR2O.cjs.map