@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
@@ -392,7 +392,7 @@ var RecipeValidationError = class extends RecipeBookError {
392
392
  * @param subject - What was validated, shown as given at the start of the
393
393
  * message, e.g. `'"Buttermilk pancakes"'`. Recipe Book's own errors quote
394
394
  * the recipe's name or the input text like a JSON string, cut with "…"
395
- * when longer than 60 characters.
395
+ * when longer than 60 UTF-16 code units.
396
396
  */
397
397
  constructor(code, issues, subject = code === "invalid_recipe" ? "Recipe" : code === "invalid_quantity" ? "Quantity" : "Ingredient") {
398
398
  super(code, summarize(subject, issues));
@@ -507,6 +507,16 @@ function checkQuantity(fn, quantity) {
507
507
  `${fn}: amount must be a number or a { min, max } range, received ${describeValue(amount)}.`
508
508
  );
509
509
  }
510
+ function checkResult(fn, amount) {
511
+ for (const value of amountEnds(amount)) {
512
+ if (!isPositive(value)) {
513
+ throw invalidArgument(
514
+ `${fn}: the result must be a finite number greater than 0, but it is ${describeValue(value)}.`
515
+ );
516
+ }
517
+ }
518
+ return amount;
519
+ }
510
520
  function checkFactor(fn, name, value) {
511
521
  if (!isPositive(value)) {
512
522
  throw invalidArgument(
@@ -522,45 +532,9 @@ function invalidArgument(message) {
522
532
  }
523
533
 
524
534
  // 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
- };
535
+ var VULGAR = "[\xBC-\xBE\u2150-\u215E]";
545
536
  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
- }
537
+ var amountPattern;
564
538
  function parseQuantity(text) {
565
539
  const issues = [];
566
540
  if (typeof text !== "string") {
@@ -577,7 +551,8 @@ function parseQuantity(text) {
577
551
  }
578
552
  function readQuantity(issues, value, path) {
579
553
  if (typeof value === "string") {
580
- return readQuantityText(issues, value, path);
554
+ const quantity = readQuantityText(issues, value, path);
555
+ return quantity && readTextAmount(issues, quantity.amount, path) ? quantity : void 0;
581
556
  }
582
557
  if (typeof value === "number") {
583
558
  const amount = readPositive(issues, value, path);
@@ -585,7 +560,35 @@ function readQuantity(issues, value, path) {
585
560
  }
586
561
  const record = asRecord(value);
587
562
  if (record) {
588
- return readQuantityObject(issues, record, path);
563
+ checkFields(issues, record, { amount: true, unit: true }, path);
564
+ const amount = readAmount(issues, record.amount, [...path, "amount"]);
565
+ const { unit } = record;
566
+ if (unit === void 0) {
567
+ return amount === void 0 ? void 0 : { amount };
568
+ }
569
+ if (typeof unit !== "string") {
570
+ return error(
571
+ issues,
572
+ "invalid_type",
573
+ [...path, "unit"],
574
+ `Expected a unit such as "g" or "cup", received ${describeValue(unit)}.`
575
+ );
576
+ }
577
+ const id = findUnit(unit);
578
+ if (!id) {
579
+ const problem = unknownUnit(
580
+ unit,
581
+ amount === void 0 ? void 0 : typeof amount === "number" ? String(amount) : `{ amount: { min: ${amount.min}, max: ${amount.max} } }`
582
+ );
583
+ return error(
584
+ issues,
585
+ problem.code,
586
+ [...path, "unit"],
587
+ problem.message,
588
+ problem.suggestion
589
+ );
590
+ }
591
+ return amount === void 0 ? void 0 : { amount, unit: id };
589
592
  }
590
593
  return error(
591
594
  issues,
@@ -640,12 +643,18 @@ function matchAmount(text, allowZero) {
640
643
  if (article) {
641
644
  return { amount: 1, amountText: "1", rest: text.slice(article[0].length) };
642
645
  }
643
- const match = getPatterns().amount.exec(text);
646
+ if (!amountPattern) {
647
+ const number = String.raw`(?:\d+\s+)?\d+\s*[/⁄]\s*\d+|(?:\d+\s*)?${VULGAR}|\d+(?:\.\d+)?|\.\d+`;
648
+ amountPattern = new RegExp(
649
+ String.raw`^(${number})(?:(?:\s*[-–—]\s*|\s+to\s+)(${number}))?`,
650
+ "i"
651
+ );
652
+ }
653
+ const match = amountPattern.exec(text);
644
654
  if (!match) {
645
655
  return void 0;
646
656
  }
647
- const [matched, lowText = "", highTextDash, highTextTo] = match;
648
- const highText = highTextDash ?? highTextTo;
657
+ const [matched, lowText = "", highText] = match;
649
658
  const low = toNumber(lowText);
650
659
  const high = highText === void 0 ? low : toNumber(highText);
651
660
  if (low === Infinity || high === Infinity) {
@@ -691,50 +700,25 @@ function unreadable(text) {
691
700
  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
701
  };
693
702
  }
703
+ var fractions = /* @__PURE__ */ new Map();
694
704
  function toNumber(text) {
695
- const ascii = getPatterns().asciiFraction.exec(text);
705
+ const ascii = /^(?:(\d+)\s+)?(\d+)\s*[/⁄]\s*(\d+)$/.exec(text);
696
706
  if (ascii) {
697
707
  const [, whole = "0", numerator, denominator] = ascii;
698
708
  return Number(whole) + Number(numerator) / (Number(denominator) || NaN);
699
709
  }
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;
710
+ const last = text.slice(-1);
711
+ if (last > "9") {
712
+ let fraction = fractions.get(last);
713
+ if (fraction === void 0) {
714
+ const [numerator, denominator] = last.normalize("NFKD").split("\u2044");
715
+ fraction = Number(numerator) / Number(denominator);
716
+ fractions.set(last, fraction);
717
+ }
718
+ return Number(text.slice(0, -1)) + fraction;
704
719
  }
705
720
  return Number(text);
706
721
  }
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
722
  function readAmount(issues, value, path) {
739
723
  if (value === void 0) {
740
724
  return error(
@@ -773,16 +757,19 @@ function readAmount(issues, value, path) {
773
757
  );
774
758
  }
775
759
  function readPositive(issues, value, path) {
776
- if (isPositive(value)) {
760
+ if (isPositive(value) && value >= 1e-300 && value <= 1e290) {
777
761
  return value;
778
762
  }
779
763
  return error(
780
764
  issues,
781
765
  typeof value === "number" ? "invalid_amount" : "invalid_type",
782
766
  path,
783
- `Expected a positive number, received ${describeValue(value)}.`
767
+ isPositive(value) ? `Expected a number from 1e-300 to 1e290, received ${describeValue(value)}.` : `Expected a positive number, received ${describeValue(value)}.`
784
768
  );
785
769
  }
770
+ function readTextAmount(issues, amount, path) {
771
+ return (typeof amount === "number" ? [amount] : [amount.min, amount.max]).every((end) => readPositive(issues, end, path) !== void 0);
772
+ }
786
773
 
787
774
  // src/recipe/duration.ts
788
775
  var ISO_DURATION = /^P(?!$)(?:(\d+(?:[.,]\d+)?)W)?(?:(\d+(?:[.,]\d+)?)D)?(?:T(?=\d)(?:(\d+(?:[.,]\d+)?)H)?(?:(\d+(?:[.,]\d+)?)M)?(?:(\d+(?:[.,]\d+)?)S)?)?$/i;
@@ -1008,9 +995,14 @@ var NUTRIENT_EXAMPLES = {
1008
995
  mg: 'milligrams, e.g. 300, "300 mg", or "0.3 g"'
1009
996
  };
1010
997
  function parseRecipe(input, options) {
998
+ return parseRecipeWith(
999
+ input,
1000
+ readLineOption("parseRecipe", options) ?? "pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })"
1001
+ );
1002
+ }
1003
+ function parseRecipeWith(input, lines) {
1011
1004
  const issues = [];
1012
- const readLine2 = readLineOption("parseRecipe", options);
1013
- const recipe = readRecipe(issues, input, readLine2);
1005
+ const recipe = readRecipe(issues, input, lines);
1014
1006
  const label2 = recipe && quote(recipe.name);
1015
1007
  return toResult("invalid_recipe", recipe, issues, label2);
1016
1008
  }
@@ -1024,7 +1016,7 @@ function readLineOption(fn, options) {
1024
1016
  }
1025
1017
  return reader;
1026
1018
  }
1027
- function readRecipe(issues, value, readLine2) {
1019
+ function readRecipe(issues, value, lines) {
1028
1020
  const input = asRecord(value);
1029
1021
  if (!input) {
1030
1022
  return error(
@@ -1054,7 +1046,7 @@ function readRecipe(issues, value, readLine2) {
1054
1046
  input.ingredients,
1055
1047
  ["ingredients"],
1056
1048
  "ingredients",
1057
- (issues2, value2, path) => readIngredient(issues2, value2, path, readLine2)
1049
+ (issues2, value2, path) => readIngredient(issues2, value2, path, lines)
1058
1050
  );
1059
1051
  const steps = readList(
1060
1052
  issues,
@@ -1104,15 +1096,11 @@ function readRecipe(issues, value, readLine2) {
1104
1096
  });
1105
1097
  }
1106
1098
  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
1099
  const sections = [];
1112
1100
  let loose;
1113
1101
  let count2 = 0;
1114
1102
  const read = (entry, entryPath) => readItem(issues, entry, entryPath, ++count2);
1115
- entries.forEach((entry, i) => {
1103
+ const visit = (entry, i) => {
1116
1104
  const entryPath = [...path, i];
1117
1105
  const section = asSection(entry);
1118
1106
  if (section) {
@@ -1135,7 +1123,10 @@ function readList(issues, pending, value, path, noun, readItem) {
1135
1123
  if (item) {
1136
1124
  loose.items.push(item);
1137
1125
  }
1138
- });
1126
+ };
1127
+ if (value !== void 0 && !readArray(issues, value, path, noun, visit)) {
1128
+ return [];
1129
+ }
1139
1130
  if (sections.length === 0) {
1140
1131
  warning(issues, "empty_list", path, `Recipe has no ${noun}.`);
1141
1132
  return [];
@@ -1164,10 +1155,13 @@ function readSection(issues, record, path, noun, read) {
1164
1155
  const title = readText(issues, record.title, [...path, "title"]);
1165
1156
  const items = [];
1166
1157
  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) => {
1158
+ let visited = 0;
1159
+ const entries = readArray(
1160
+ issues,
1161
+ record.items,
1162
+ itemsPath,
1163
+ noun,
1164
+ (entry, j) => {
1171
1165
  visited++;
1172
1166
  const itemPath = [...itemsPath, j];
1173
1167
  if (asSection(entry)) {
@@ -1183,10 +1177,10 @@ function readSection(issues, record, path, noun, read) {
1183
1177
  if (item) {
1184
1178
  items.push(item);
1185
1179
  }
1186
- });
1187
- if (visited === 0) {
1188
- warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
1189
1180
  }
1181
+ );
1182
+ if (entries && visited === 0) {
1183
+ warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
1190
1184
  }
1191
1185
  checkFields(issues, record, SECTION_FIELDS, path);
1192
1186
  return {
@@ -1199,29 +1193,44 @@ function readSection(issues, record, path, noun, read) {
1199
1193
  };
1200
1194
  }
1201
1195
  function readItems(issues, pending, value, path, noun, readItem) {
1202
- const entries = value === void 0 ? [] : readArray(issues, value, path, noun);
1203
- if (!entries) {
1196
+ let n = 0;
1197
+ const drafts = value === void 0 ? [] : readArray(
1198
+ issues,
1199
+ value,
1200
+ path,
1201
+ noun,
1202
+ (entry, i) => readItem(issues, entry, [...path, i], ++n)
1203
+ );
1204
+ if (!drafts) {
1204
1205
  return [];
1205
1206
  }
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
1207
  const ids = assignIds(drafts, pending);
1212
1208
  return drafts.map((draft, i) => draft.build(ids[i]));
1213
1209
  }
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
- );
1210
+ function readArray(issues, value, path, noun, read) {
1211
+ if (!Array.isArray(value)) {
1212
+ return error(
1213
+ issues,
1214
+ "invalid_type",
1215
+ path,
1216
+ `Expected a list of ${noun}, received ${describeValue(value)}.`
1217
+ );
1218
+ }
1219
+ const kept = [];
1220
+ for (let i = 0, n = value.length; i < n; i++) {
1221
+ if (i in value) {
1222
+ const entry = value[i];
1223
+ const result = hasOwn(value, i) && read(entry, i);
1224
+ if (result) {
1225
+ kept.push(result);
1226
+ }
1227
+ }
1228
+ }
1229
+ return kept;
1221
1230
  }
1222
- function readIngredient(issues, value, path, readLine2) {
1223
- if (typeof value === "string" && readLine2) {
1224
- const result = readLine2(value);
1231
+ function readIngredient(issues, value, path, lines) {
1232
+ if (typeof value === "string" && typeof lines === "function") {
1233
+ const result = lines(value);
1225
1234
  for (const issue2 of result.success ? result.warnings : result.error.issues) {
1226
1235
  issues.push({ ...issue2, path: [...path, ...issue2.path] });
1227
1236
  }
@@ -1233,7 +1242,7 @@ function readIngredient(issues, value, path, readLine2) {
1233
1242
  issues,
1234
1243
  "invalid_type",
1235
1244
  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 })." : ""}`
1245
+ `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
1246
  );
1238
1247
  }
1239
1248
  const position = issues.length;
@@ -1274,22 +1283,13 @@ function readIngredient(issues, value, path, readLine2) {
1274
1283
  };
1275
1284
  }
1276
1285
  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
- });
1286
+ return value !== void 0 && readArray(
1287
+ issues,
1288
+ value,
1289
+ path,
1290
+ 'quantities, e.g. ["190 g"]',
1291
+ (entry, i) => readQuantity(issues, entry, [...path, i])
1292
+ ) || [];
1293
1293
  }
1294
1294
  function readStep(issues, value, path, n) {
1295
1295
  const isText = typeof value === "string";
@@ -1524,10 +1524,11 @@ function readRangeText(issues, text, path, expected) {
1524
1524
  return error(issues, match.code, path, match.message);
1525
1525
  }
1526
1526
  const { amount, rest } = match;
1527
- return {
1528
- range: typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max },
1529
- rest
1530
- };
1527
+ const range = typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max };
1528
+ if (readPositive(issues, range.amount, path) === void 0 || range.max !== void 0 && readPositive(issues, range.max, path) === void 0) {
1529
+ return void 0;
1530
+ }
1531
+ return { range, rest };
1531
1532
  }
1532
1533
  function readRange(issues, record, path, missingAmount) {
1533
1534
  const value = record.amount;
@@ -1750,26 +1751,26 @@ var ABOUT = /(about|approx(?:imately|\.)?) /iy;
1750
1751
  var PHRASE = /((?:an? )?(?:handful|few|couple|little|splash|drizzle|knob|squeeze|sprinkle|dollop))(?: of)? /iy;
1751
1752
  var HYPHENATED_MIXED = /^(\d+)-(?=(\d+)[/⁄](\d+)(?![\d/⁄]))/;
1752
1753
  var AMOUNT_CHARS = /(?:an? )?(?:[\d .,/⁄¼-¾⅐-⅞–—-]|to )*/iy;
1753
- var UNIT_OF = /([a-z]+) of /iy;
1754
1754
  var TIMES = / ?[x×] ?(?=\d)/iy;
1755
- var UNREAD = / ?(?:(?:or|to|and|plus|half)\b|[-–—/+&]|\d+(?:\.\d+)?(?= |$))/iy;
1755
+ var UNREAD = / ?(?:(?:or|to|and|plus|half)(?![\p{L}\p{M}\d_])|[-–—/+&]|\d+(?:\.\d+)?(?= |$))/iuy;
1756
1756
  var UNREAD_AFTER_UNIT = / (?:or|to|[-–—&])(?= )|[-–—+&](?=\d)/iy;
1757
1757
  var PAREN = / ?\(([^()]*)\)/y;
1758
1758
  var PARENS = / ?\(([^()]*)\)/g;
1759
1759
  var MODIFIER = / ?(heaping|heaped|level|scant|rounded|generous|good|small|medium|large|big)(?= )/iy;
1760
- var JOIN = /(?: (plus|and|\+)(?![a-z]))? ?/iy;
1760
+ var JOIN = /(?: (plus|and|\+)(?![\p{L}\p{M}]))? ?/iuy;
1761
1761
  var SEPARATOR = / ?[;/] ?/y;
1762
1762
  var OF = / ?of(?: (?:an?|the))?(?= |$)/iy;
1763
+ 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
1764
  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
1765
  var OPTIONAL_NOTE = /(?:^|,) ?optional(?= ?(?:,|$))/gi;
1765
1766
  var OPTIONAL_PAREN = /^optional\b[,;:]? ?/i;
1766
1767
  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
1768
  var BONELESS = /(?:^|[^a-z])(?:bone|skin)less$/i;
1768
- var patterns2;
1769
- function getPatterns2() {
1770
- return patterns2 ?? (patterns2 = compilePatterns2());
1769
+ var patterns;
1770
+ function getPatterns() {
1771
+ return patterns ?? (patterns = compilePatterns());
1771
1772
  }
1772
- function compilePatterns2() {
1773
+ function compilePatterns() {
1773
1774
  const word = String.raw`([a-z]+(?:-[a-z]+)?)\.?`;
1774
1775
  const end = String.raw`(?=[ ,;()/]|[-–—+&]\d|$)`;
1775
1776
  return {
@@ -1817,7 +1818,7 @@ function parseIngredientLine(text) {
1817
1818
  return toResult("invalid_ingredient", ingredient, issues);
1818
1819
  }
1819
1820
  function readLine(line) {
1820
- const p = getPatterns2();
1821
+ const p = getPatterns();
1821
1822
  const notes = [];
1822
1823
  const equivalents = [];
1823
1824
  let optional = false;
@@ -1877,11 +1878,22 @@ function readLine(line) {
1877
1878
  }
1878
1879
  if (found) {
1879
1880
  [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;
1881
+ } else {
1882
+ const modifier = exec(MODIFIER, line, i);
1883
+ const unit = readUnit(line, modifier ? MODIFIER.lastIndex : i);
1884
+ if (unit && exec(OF, line, unit[1])) {
1885
+ if (modifier) {
1886
+ addNote(modifier[1]);
1887
+ }
1888
+ quantity = { amount: 1, unit: unit[0] };
1889
+ i = unit[1];
1890
+ } else if (match = exec(NAMED, line, i)) {
1891
+ const measure = readMeasure(line, NAMED.lastIndex, false);
1892
+ if (measure && (line[measure[1]] ?? ",") === ",") {
1893
+ quantity = measure[0];
1894
+ line = match[1] + line.slice(measure[1]);
1895
+ i = 0;
1896
+ }
1885
1897
  }
1886
1898
  }
1887
1899
  }
@@ -2012,10 +2024,10 @@ function isProblem(value) {
2012
2024
  return typeof value === "object" && value !== null && "code" in value;
2013
2025
  }
2014
2026
  function readUnit(text, start) {
2015
- const { twoWordUnit, unit } = getPatterns2();
2027
+ const { twoWordUnit, unit } = getPatterns();
2016
2028
  for (const pattern of [twoWordUnit, unit]) {
2017
2029
  const match = exec(pattern, text, start);
2018
- const id = match && findUnit(match[2] ? `${match[1]} ${match[2]}` : match[1]);
2030
+ const id = match && findUnit(match.slice(1).join(" "));
2019
2031
  if (id) {
2020
2032
  return [id, pattern.lastIndex];
2021
2033
  }
@@ -2113,7 +2125,13 @@ function countForms(name, many) {
2113
2125
  k = CONTAINER.test(words[0]) ? 0 : words.length - 1;
2114
2126
  }
2115
2127
  const word = words[k];
2116
- const other = many ? singular(word) : pluralize(word);
2128
+ const other = many ? word.replace(
2129
+ /(?:(lea|hal|loa)ves|([rtdv])ies|(o|ch|sh|x|ss|chill?i)es|([^su])s)$/i,
2130
+ (_, f, y, es, s) => f ? `${f}f` : y ? `${y}y` : es ?? s
2131
+ ) : 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
+ );
2117
2135
  if (other === word) {
2118
2136
  return [name, void 0];
2119
2137
  }
@@ -2121,24 +2139,14 @@ function countForms(name, many) {
2121
2139
  const swapped = words.join(" ");
2122
2140
  return many ? [swapped, name] : [name, swapped];
2123
2141
  }
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
2142
 
2137
2143
  // src/quantity/scale.ts
2138
2144
  function scaleQuantity(quantity, factor) {
2139
- checkQuantity("scaleQuantity", quantity);
2145
+ const amount = checkQuantity("scaleQuantity", quantity);
2140
2146
  checkFactor("scaleQuantity", "factor", factor);
2141
- return multiply(quantity, factor);
2147
+ const result = multiply({ amount, unit: quantity.unit }, factor);
2148
+ checkResult("scaleQuantity", result.amount);
2149
+ return result;
2142
2150
  }
2143
2151
  function multiply(quantity, factor) {
2144
2152
  const amount = mapAmount(quantity.amount, (value) => value * factor);
@@ -2235,7 +2243,7 @@ function resolveRecipe(recipe, options) {
2235
2243
  } = options ?? {};
2236
2244
  const servingsIsBase = isPositive(recipe.servings?.amount);
2237
2245
  const base = [recipe.servings?.amount, recipe.yield?.amount].find(isPositive) ?? 1;
2238
- const ratio = typeof servings === "number" ? servings / base : Number.NaN;
2246
+ const ratio = isServingsFor(servings, base) ? Math.min(Math.max(servings / base, 1e-6), 1e6) : Number.NaN;
2239
2247
  const exactServings = isScale(requestedScale) || !isScale(ratio) || isUnity(ratio) ? void 0 : servings;
2240
2248
  const scale = isScale(requestedScale) ? requestedScale : isScale(ratio) ? ratio : 1;
2241
2249
  const system = [requestedSystem, recipe.system].find(isSystem) ?? "metric";
@@ -2320,6 +2328,11 @@ function withEnds(quantity) {
2320
2328
  const { amount } = quantity;
2321
2329
  return typeof amount === "number" || amount.min < amount.max ? quantity : { ...quantity, amount: amount.min };
2322
2330
  }
2331
+ function isServingsFor(servings, base) {
2332
+ const [mantissa, exponent] = base.toExponential().split("e");
2333
+ const shift = (places) => Number(`${mantissa}e${Number(exponent) + places}`);
2334
+ return isPositive(servings) && servings >= shift(-6) && servings <= shift(6);
2335
+ }
2323
2336
  function isScale(value) {
2324
2337
  return typeof value === "number" && value >= 1e-6 && value <= 1e6;
2325
2338
  }
@@ -2439,7 +2452,7 @@ function integerText(value) {
2439
2452
  function invalidLocale(fn, locale) {
2440
2453
  return new RecipeBookError(
2441
2454
  "invalid_argument",
2442
- `${fn}: locale must be a BCP 47 language tag such as "en-US", received ${describeValue(locale)}.`
2455
+ `${fn}: locale must be a language tag that Intl accepts, such as "en-US", received ${describeValue(locale)}.`
2443
2456
  );
2444
2457
  }
2445
2458
 
@@ -2541,7 +2554,7 @@ function joinParts(parts) {
2541
2554
  return parts.map((part) => part.value).join("");
2542
2555
  }
2543
2556
  function showAmount(value, settings, steps = QUARTERS) {
2544
- const { rounding, fractions } = settings;
2557
+ const { rounding, fractions: fractions2 } = settings;
2545
2558
  if (rounding === "none") {
2546
2559
  return decimal(value, 3, settings);
2547
2560
  }
@@ -2553,7 +2566,7 @@ function showAmount(value, settings, steps = QUARTERS) {
2553
2566
  return decimal(value, 1, settings);
2554
2567
  }
2555
2568
  const [whole, part, glyph] = snap(value, steps);
2556
- if (!fractions) {
2569
+ if (!fractions2) {
2557
2570
  return decimal(whole + part, 2, settings);
2558
2571
  }
2559
2572
  const wholeText = settings.number(whole, 0);
@@ -2698,6 +2711,7 @@ exports.RecipeBookError = RecipeBookError;
2698
2711
  exports.RecipeValidationError = RecipeValidationError;
2699
2712
  exports.asRecord = asRecord;
2700
2713
  exports.checkQuantity = checkQuantity;
2714
+ exports.checkResult = checkResult;
2701
2715
  exports.checkSystem = checkSystem;
2702
2716
  exports.describeValue = describeValue;
2703
2717
  exports.disallowedScheme = disallowedScheme;
@@ -2723,6 +2737,7 @@ exports.parseIngredientLine = parseIngredientLine;
2723
2737
  exports.parseIsoDuration = parseIsoDuration;
2724
2738
  exports.parseQuantity = parseQuantity;
2725
2739
  exports.parseRecipe = parseRecipe;
2740
+ exports.parseRecipeWith = parseRecipeWith;
2726
2741
  exports.pluralForm = pluralForm;
2727
2742
  exports.quote = quote;
2728
2743
  exports.readLineOption = readLineOption;
@@ -2735,5 +2750,5 @@ exports.toResult = toResult;
2735
2750
  exports.units = units;
2736
2751
  exports.unknownUnitError = unknownUnitError;
2737
2752
  exports.warning = warning;
2738
- //# sourceMappingURL=chunk-VWVIBCTG.cjs.map
2739
- //# sourceMappingURL=chunk-VWVIBCTG.cjs.map
2753
+ //# sourceMappingURL=chunk-D5CV3UF7.cjs.map
2754
+ //# sourceMappingURL=chunk-D5CV3UF7.cjs.map