@tmlmt/cooklang-parser 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -65,23 +65,233 @@ var AisleConfig = class {
65
65
 
66
66
  // src/classes/section.ts
67
67
  var Section = class {
68
+ /**
69
+ * Creates an instance of Section.
70
+ * @param name - The name of the section. Defaults to an empty string.
71
+ */
68
72
  constructor(name = "") {
73
+ /** The name of the section. Can be an empty string for the default (first) section. */
69
74
  __publicField(this, "name");
75
+ /** An array of steps and notes that make up the content of the section. */
70
76
  __publicField(this, "content", []);
71
77
  this.name = name;
72
78
  }
79
+ /**
80
+ * Checks if the section is blank (has no name and no content).
81
+ * Used during recipe parsing
82
+ * @returns `true` if the section is blank, otherwise `false`.
83
+ */
73
84
  isBlank() {
74
85
  return this.name === "" && this.content.length === 0;
75
86
  }
76
87
  };
77
88
 
89
+ // node_modules/.pnpm/human-regex@2.1.5_patch_hash=6d6bd9e233f99785a7c2187fd464edc114b76d47001dbb4eb6b5d72168de7460/node_modules/human-regex/dist/human-regex.esm.js
90
+ var t = /* @__PURE__ */ new Map();
91
+ var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
92
+ var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
93
+ var n = Object.freeze({ zeroOrMore: "*", oneOrMore: "+", optional: "?" });
94
+ var a = class {
95
+ constructor() {
96
+ this.parts = [], this.flags = /* @__PURE__ */ new Set();
97
+ }
98
+ digit() {
99
+ return this.add("\\d");
100
+ }
101
+ special() {
102
+ return this.add("(?=.*[!@#$%^&*])");
103
+ }
104
+ word() {
105
+ return this.add("\\w");
106
+ }
107
+ whitespace() {
108
+ return this.add("\\s");
109
+ }
110
+ nonWhitespace() {
111
+ return this.add("\\S");
112
+ }
113
+ literal(r2) {
114
+ return this.add((function(r3) {
115
+ t.has(r3) || t.set(r3, r3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
116
+ return t.get(r3);
117
+ })(r2));
118
+ }
119
+ or() {
120
+ return this.add("|");
121
+ }
122
+ range(t2) {
123
+ const r2 = e[t2];
124
+ if (!r2) throw new Error(`Unknown range: ${t2}`);
125
+ return this.add(`[${r2}]`);
126
+ }
127
+ notRange(t2) {
128
+ const r2 = e[t2];
129
+ if (!r2) throw new Error(`Unknown range: ${t2}`);
130
+ return this.add(`[^${r2}]`);
131
+ }
132
+ anyOf(t2) {
133
+ return this.add(`[${t2}]`);
134
+ }
135
+ notAnyOf(t2) {
136
+ return this.add(`[^${t2}]`);
137
+ }
138
+ lazy() {
139
+ const t2 = this.parts.pop();
140
+ if (!t2) throw new Error("No quantifier to make lazy");
141
+ return this.add(`${t2}?`);
142
+ }
143
+ letter() {
144
+ return this.add("[a-zA-Z]");
145
+ }
146
+ anyCharacter() {
147
+ return this.add(".");
148
+ }
149
+ newline() {
150
+ return this.add("(?:\\r\\n|\\r|\\n)");
151
+ }
152
+ negativeLookahead(t2) {
153
+ return this.add(`(?!${t2})`);
154
+ }
155
+ positiveLookahead(t2) {
156
+ return this.add(`(?=${t2})`);
157
+ }
158
+ positiveLookbehind(t2) {
159
+ return this.add(`(?<=${t2})`);
160
+ }
161
+ negativeLookbehind(t2) {
162
+ return this.add(`(?<!${t2})`);
163
+ }
164
+ hasSpecialCharacter() {
165
+ return this.add("(?=.*[!@#$%^&*])");
166
+ }
167
+ hasDigit() {
168
+ return this.add("(?=.*\\d)");
169
+ }
170
+ hasLetter() {
171
+ return this.add("(?=.*[a-zA-Z])");
172
+ }
173
+ optional() {
174
+ return this.add(n.optional);
175
+ }
176
+ exactly(t2) {
177
+ return this.add(`{${t2}}`);
178
+ }
179
+ atLeast(t2) {
180
+ return this.add(`{${t2},}`);
181
+ }
182
+ atMost(t2) {
183
+ return this.add(`{0,${t2}}`);
184
+ }
185
+ between(t2, r2) {
186
+ return this.add(`{${t2},${r2}}`);
187
+ }
188
+ oneOrMore() {
189
+ return this.add(n.oneOrMore);
190
+ }
191
+ zeroOrMore() {
192
+ return this.add(n.zeroOrMore);
193
+ }
194
+ startNamedGroup(t2) {
195
+ return this.add(`(?<${t2}>`);
196
+ }
197
+ startGroup() {
198
+ return this.add("(?:");
199
+ }
200
+ startCaptureGroup() {
201
+ return this.add("(");
202
+ }
203
+ wordBoundary() {
204
+ return this.add("\\b");
205
+ }
206
+ nonWordBoundary() {
207
+ return this.add("\\B");
208
+ }
209
+ endGroup() {
210
+ return this.add(")");
211
+ }
212
+ startAnchor() {
213
+ return this.add("^");
214
+ }
215
+ endAnchor() {
216
+ return this.add("$");
217
+ }
218
+ global() {
219
+ return this.flags.add(r.GLOBAL), this;
220
+ }
221
+ nonSensitive() {
222
+ return this.flags.add(r.NON_SENSITIVE), this;
223
+ }
224
+ multiline() {
225
+ return this.flags.add(r.MULTILINE), this;
226
+ }
227
+ dotAll() {
228
+ return this.flags.add(r.DOT_ALL), this;
229
+ }
230
+ sticky() {
231
+ return this.flags.add(r.STICKY), this;
232
+ }
233
+ unicodeChar(t2) {
234
+ this.flags.add(r.UNICODE);
235
+ const e2 = /* @__PURE__ */ new Set(["u", "l", "t", "m", "o"]);
236
+ if (void 0 !== t2 && !e2.has(t2)) throw new Error(`Invalid Unicode letter variant: ${t2}`);
237
+ return this.add(`\\p{L${null != t2 ? t2 : ""}}`);
238
+ }
239
+ unicodeDigit() {
240
+ return this.flags.add(r.UNICODE), this.add("\\p{N}");
241
+ }
242
+ unicodePunctuation() {
243
+ return this.flags.add(r.UNICODE), this.add("\\p{P}");
244
+ }
245
+ unicodeSymbol() {
246
+ return this.flags.add(r.UNICODE), this.add("\\p{S}");
247
+ }
248
+ repeat(t2) {
249
+ if (0 === this.parts.length) throw new Error("No pattern to repeat");
250
+ const r2 = this.parts.pop();
251
+ return this.parts.push(`(${r2}){${t2}}`), this;
252
+ }
253
+ ipv4Octet() {
254
+ return this.add("(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)");
255
+ }
256
+ protocol() {
257
+ return this.add("https?://");
258
+ }
259
+ www() {
260
+ return this.add("(www\\.)?");
261
+ }
262
+ tld() {
263
+ return this.add("(com|org|net)");
264
+ }
265
+ path() {
266
+ return this.add("(/\\w+)*");
267
+ }
268
+ add(t2) {
269
+ return this.parts.push(t2), this;
270
+ }
271
+ toString() {
272
+ return this.parts.join("");
273
+ }
274
+ toRegExp() {
275
+ return new RegExp(this.toString(), [...this.flags].join(""));
276
+ }
277
+ };
278
+ var d = () => new a();
279
+ var i = (() => {
280
+ const t2 = (t3) => {
281
+ const r2 = t3().toRegExp();
282
+ return () => new RegExp(r2.source, r2.flags);
283
+ };
284
+ return { email: t2((() => d().startAnchor().word().oneOrMore().literal("@").word().oneOrMore().startGroup().literal(".").word().oneOrMore().endGroup().zeroOrMore().literal(".").letter().atLeast(2).endAnchor())), url: t2((() => d().startAnchor().protocol().www().word().oneOrMore().literal(".").tld().path().endAnchor())), phoneInternational: t2((() => d().startAnchor().literal("+").digit().between(1, 3).literal("-").digit().between(3, 14).endAnchor())) };
285
+ })();
286
+
78
287
  // src/regex.ts
79
- var metadataRegex = /---\n(.*?)\n---/s;
80
- var multiwordIngredient = /@(?<mIngredientModifier>[@\-&+*!?])?(?<mIngredientName>(?:[^\s@#~\[\]{(.,;:!?]+(?:\s+[^\s@#~\[\]{(.,;:!?]+)+))(?=\s*(?:\{|\}|\(\s*[^)]*\s*\)))(?:\{(?<mIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))?(?:%(?<mIngredientUnits>[^}]+?))?\})?(?:\((?<mIngredientPreparation>[^)]*?)\))?/gu;
81
- var singleWordIngredient = /@(?<sIngredientModifier>[@\-&+*!?])?(?<sIngredientName>[^\s@#~\[\]{(.,;:!?]+)(?:\{(?:(?<sIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))(?:%(?<sIngredientUnits>[^}]+?))?)?\})?(?:\((?<sIngredientPreparation>[^)]*?)\))?/gu;
82
- var multiwordCookware = /#(?<mCookwareModifier>[\-&+*!?])?(?<mCookwareName>(?:[^\s@#~\[\]{(.,;:!?]+(?:\s+[^\s@#~\[\]{(.,;:!?]+)+))(?=\s*(?:\{|\}|\(\s*[^)]*\s*\)))\{(?<mCookwareQuantity>.*?)\}/;
83
- var singleWordCookware = /#(?<sCookwareModifier>[\-&+*!?])?(?<sCookwareName>[^\s@#~\[\]{(.,;:!?]+)(?:\{(?<sCookwareQuantity>.*?)\})?/u;
84
- var timer = /~(?<timerName>.*?)(?:\{(?<timerQuantity>.*?)(?:%(?<timerUnits>.+?))?\})/;
288
+ var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
289
+ var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
290
+ var multiwordIngredient = d().literal("@").startNamedGroup("mIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").startGroup().literal("{").startNamedGroup("mIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("mIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("mIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
291
+ var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("sIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("sIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
292
+ var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").literal("{").startNamedGroup("mCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").toRegExp();
293
+ var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
294
+ var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnits").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
85
295
  var tokensRegex = new RegExp(
86
296
  [
87
297
  multiwordIngredient,
@@ -89,11 +299,14 @@ var tokensRegex = new RegExp(
89
299
  multiwordCookware,
90
300
  singleWordCookware,
91
301
  timer
92
- ].map((r) => r.source).join("|"),
302
+ ].map((r2) => r2.source).join("|"),
93
303
  "gu"
94
304
  );
95
- var commentRegex = /--.*/g;
96
- var blockCommentRegex = /\s*\[\-.*?\-\]\s*/g;
305
+ var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
306
+ var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
307
+ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
308
+ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
309
+ var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
97
310
 
98
311
  // src/units.ts
99
312
  var units = [
@@ -211,46 +424,148 @@ for (const unit of units) {
211
424
  function normalizeUnit(unit) {
212
425
  return unitMap.get(unit.toLowerCase().trim());
213
426
  }
427
+ var CannotAddTextValueError = class extends Error {
428
+ constructor() {
429
+ super("Cannot add a quantity with a text value.");
430
+ this.name = "CannotAddTextValueError";
431
+ }
432
+ };
433
+ var IncompatibleUnitsError = class extends Error {
434
+ constructor(unit1, unit2) {
435
+ super(
436
+ `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
437
+ );
438
+ this.name = "IncompatibleUnitsError";
439
+ }
440
+ };
441
+ function gcd(a2, b) {
442
+ return b === 0 ? a2 : gcd(b, a2 % b);
443
+ }
444
+ function simplifyFraction(num, den) {
445
+ if (den === 0) {
446
+ throw new Error("Denominator cannot be zero.");
447
+ }
448
+ const commonDivisor = gcd(Math.abs(num), Math.abs(den));
449
+ let simplifiedNum = num / commonDivisor;
450
+ let simplifiedDen = den / commonDivisor;
451
+ if (simplifiedDen < 0) {
452
+ simplifiedNum = -simplifiedNum;
453
+ simplifiedDen = -simplifiedDen;
454
+ }
455
+ if (simplifiedDen === 1) {
456
+ return { type: "decimal", value: simplifiedNum };
457
+ } else {
458
+ return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
459
+ }
460
+ }
461
+ function multiplyNumericValue(v, factor) {
462
+ if (v.type === "decimal") {
463
+ return { type: "decimal", value: v.value * factor };
464
+ }
465
+ return simplifyFraction(v.num * factor, v.den);
466
+ }
467
+ function addNumericValues(val1, val2) {
468
+ let num1;
469
+ let den1;
470
+ let num2;
471
+ let den2;
472
+ if (val1.type === "decimal") {
473
+ num1 = val1.value;
474
+ den1 = 1;
475
+ } else {
476
+ num1 = val1.num;
477
+ den1 = val1.den;
478
+ }
479
+ if (val2.type === "decimal") {
480
+ num2 = val2.value;
481
+ den2 = 1;
482
+ } else {
483
+ num2 = val2.num;
484
+ den2 = val2.den;
485
+ }
486
+ if (val1.type === "fraction" && val2.type === "fraction") {
487
+ const commonDen = den1 * den2;
488
+ const sumNum = num1 * den2 + num2 * den1;
489
+ return simplifyFraction(sumNum, commonDen);
490
+ } else {
491
+ return { type: "decimal", value: num1 / den1 + num2 / den2 };
492
+ }
493
+ }
494
+ var toRoundedDecimal = (v) => {
495
+ const value = v.type === "decimal" ? v.value : v.num / v.den;
496
+ return { type: "decimal", value: Math.floor(value * 100) / 100 };
497
+ };
498
+ function multiplyQuantityValue(value, factor) {
499
+ if (value.type === "fixed") {
500
+ return {
501
+ type: "fixed",
502
+ value: toRoundedDecimal(
503
+ multiplyNumericValue(
504
+ value.value,
505
+ factor
506
+ )
507
+ )
508
+ };
509
+ }
510
+ return {
511
+ type: "range",
512
+ min: toRoundedDecimal(
513
+ multiplyNumericValue(value.min, factor)
514
+ ),
515
+ max: toRoundedDecimal(
516
+ multiplyNumericValue(value.max, factor)
517
+ )
518
+ };
519
+ }
520
+ var convertQuantityValue = (value, def, targetDef) => {
521
+ if (def.name === targetDef.name) return value;
522
+ const factor = def.toBase / targetDef.toBase;
523
+ return multiplyQuantityValue(value, factor);
524
+ };
214
525
  function addQuantities(q1, q2) {
526
+ const v1 = q1.value;
527
+ const v2 = q2.value;
528
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
529
+ throw new CannotAddTextValueError();
530
+ }
215
531
  const unit1Def = normalizeUnit(q1.unit);
216
532
  const unit2Def = normalizeUnit(q2.unit);
217
- if (isNaN(Number(q1.value))) {
218
- throw new Error(
219
- `Cannot add quantity to string-quantified value: ${q1.value}`
533
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
534
+ if (val1.type === "fixed" && val2.type === "fixed") {
535
+ const res = addNumericValues(
536
+ val1.value,
537
+ val2.value
538
+ );
539
+ return { value: { type: "fixed", value: res }, unit };
540
+ }
541
+ const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
542
+ const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
543
+ const newMin = addNumericValues(
544
+ r1.min,
545
+ r2.min
220
546
  );
221
- }
222
- if (isNaN(Number(q2.value))) {
223
- throw new Error(
224
- `Cannot add quantity to string-quantified value: ${q2.value}`
547
+ const newMax = addNumericValues(
548
+ r1.max,
549
+ r2.max
225
550
  );
226
- }
551
+ return { value: { type: "range", min: newMin, max: newMax }, unit };
552
+ };
227
553
  if (q1.unit === "" && unit2Def) {
228
- return {
229
- value: Math.round((q1.value + q2.value) * 100) / 100,
230
- unit: q2.unit
231
- };
554
+ return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
232
555
  }
233
556
  if (q2.unit === "" && unit1Def) {
234
- return {
235
- value: Math.round((q1.value + q2.value) * 100) / 100,
236
- unit: q1.unit
237
- };
557
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
238
558
  }
239
559
  if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
240
- return {
241
- value: Math.round((q1.value + q2.value) * 100) / 100,
242
- unit: q1.unit
243
- };
560
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
244
561
  }
245
562
  if (unit1Def && unit2Def) {
246
563
  if (unit1Def.type !== unit2Def.type) {
247
- throw new Error(
248
- `Cannot add quantities of different types: ${unit1Def.type} (${q1.unit}) and ${unit2Def.type} (${q2.unit})`
564
+ throw new IncompatibleUnitsError(
565
+ `${unit1Def.type} (${q1.unit})`,
566
+ `${unit2Def.type} (${q2.unit})`
249
567
  );
250
568
  }
251
- const baseValue1 = q1.value * unit1Def.toBase;
252
- const baseValue2 = q2.value * unit2Def.toBase;
253
- const totalBaseValue = baseValue1 + baseValue2;
254
569
  let targetUnitDef;
255
570
  if (unit1Def.system !== unit2Def.system) {
256
571
  const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
@@ -260,15 +575,15 @@ function addQuantities(q1, q2) {
260
575
  } else {
261
576
  targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
262
577
  }
263
- const finalValue = totalBaseValue / targetUnitDef.toBase;
264
- return {
265
- value: Math.round(finalValue * 100) / 100,
266
- unit: targetUnitDef.name
267
- };
578
+ const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
579
+ const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
580
+ return addQuantityValuesAndSetUnit(
581
+ convertedV1,
582
+ convertedV2,
583
+ targetUnitDef.name
584
+ );
268
585
  }
269
- throw new Error(
270
- `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`
271
- );
586
+ throw new IncompatibleUnitsError(q1.unit, q2.unit);
272
587
  }
273
588
 
274
589
  // src/parser_helpers.ts
@@ -298,7 +613,7 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
298
613
  const { name, quantity, unit } = newIngredient;
299
614
  if (isReference) {
300
615
  const index = ingredients.findIndex(
301
- (i) => i.name.toLowerCase() === name.toLowerCase()
616
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
302
617
  );
303
618
  if (index === -1) {
304
619
  throw new Error(
@@ -308,13 +623,22 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
308
623
  const existingIngredient = ingredients[index];
309
624
  if (quantity !== void 0) {
310
625
  const currentQuantity = {
311
- value: existingIngredient.quantity ?? 0,
626
+ value: existingIngredient.quantity ?? {
627
+ type: "fixed",
628
+ value: { type: "decimal", value: 0 }
629
+ },
312
630
  unit: existingIngredient.unit ?? ""
313
631
  };
314
632
  const newQuantity = { value: quantity, unit: unit ?? "" };
315
- const total = addQuantities(currentQuantity, newQuantity);
316
- existingIngredient.quantity = total.value;
317
- existingIngredient.unit = total.unit || void 0;
633
+ try {
634
+ const total = addQuantities(currentQuantity, newQuantity);
635
+ existingIngredient.quantity = total.value;
636
+ existingIngredient.unit = total.unit || void 0;
637
+ } catch (e2) {
638
+ if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
639
+ return ingredients.push(newIngredient) - 1;
640
+ }
641
+ }
318
642
  }
319
643
  return index;
320
644
  }
@@ -324,7 +648,7 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
324
648
  const { name } = newCookware;
325
649
  if (isReference) {
326
650
  const index = cookware.findIndex(
327
- (i) => i.name.toLowerCase() === name.toLowerCase()
651
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
328
652
  );
329
653
  if (index === -1) {
330
654
  throw new Error(
@@ -335,13 +659,28 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
335
659
  }
336
660
  return cookware.push(newCookware) - 1;
337
661
  }
338
- function parseNumber(input_str) {
339
- const clean_str = String(input_str).replace(",", ".");
340
- if (!clean_str.startsWith("/") && clean_str.includes("/")) {
341
- const [num, den] = clean_str.split("/").map(Number);
342
- return num / den;
662
+ var parseFixedValue = (input_str) => {
663
+ if (!numberLikeRegex.test(input_str)) {
664
+ return { type: "text", value: input_str };
343
665
  }
344
- return Number(clean_str);
666
+ const s = input_str.trim().replace(",", ".");
667
+ if (s.includes("/")) {
668
+ const parts = s.split("/");
669
+ const num = Number(parts[0]);
670
+ const den = Number(parts[1]);
671
+ return { type: "fraction", num, den };
672
+ }
673
+ return { type: "decimal", value: Number(s) };
674
+ };
675
+ function parseQuantityInput(input_str) {
676
+ const clean_str = String(input_str).trim();
677
+ if (rangeRegex.test(clean_str)) {
678
+ const range_parts = clean_str.split("-");
679
+ const min = parseFixedValue(range_parts[0].trim());
680
+ const max = parseFixedValue(range_parts[1].trim());
681
+ return { type: "range", min, max };
682
+ }
683
+ return { type: "fixed", value: parseFixedValue(clean_str) };
345
684
  }
346
685
  function parseSimpleMetaVar(content, varName) {
347
686
  const varMatch = content.match(
@@ -534,7 +873,7 @@ var Recipe = class _Recipe {
534
873
  const hidden = modifier === "-";
535
874
  const reference = modifier === "&";
536
875
  const isRecipe = modifier === "@";
537
- const quantity = quantityRaw ? parseNumber(quantityRaw) : void 0;
876
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
538
877
  const idxInList = findAndUpsertIngredient(
539
878
  this.ingredients,
540
879
  {
@@ -561,12 +900,14 @@ var Recipe = class _Recipe {
561
900
  } else if (groups.mCookwareName || groups.sCookwareName) {
562
901
  const name = groups.mCookwareName || groups.sCookwareName;
563
902
  const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
903
+ const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
564
904
  const optional = modifier === "?";
565
905
  const hidden = modifier === "-";
566
906
  const reference = modifier === "&";
907
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
567
908
  const idxInList = findAndUpsertCookware(
568
909
  this.cookware,
569
- { name, optional, hidden },
910
+ { name, quantity, optional, hidden },
570
911
  reference
571
912
  );
572
913
  items.push({ type: "cookware", value: idxInList });
@@ -577,7 +918,7 @@ var Recipe = class _Recipe {
577
918
  throw new Error("Timer missing units");
578
919
  }
579
920
  const name = groups.timerName || void 0;
580
- const duration = parseNumber(durationStr);
921
+ const duration = parseQuantityInput(durationStr);
581
922
  const timerObj = {
582
923
  name,
583
924
  duration,
@@ -585,7 +926,7 @@ var Recipe = class _Recipe {
585
926
  };
586
927
  const idxInList = findOrPush(
587
928
  this.timers,
588
- (t) => t.name === timerObj.name && t.duration === timerObj.duration && t.unit === timerObj.unit,
929
+ (t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
589
930
  () => timerObj
590
931
  );
591
932
  items.push({ type: "timer", value: idxInList });
@@ -628,8 +969,11 @@ var Recipe = class _Recipe {
628
969
  throw new Error("Error scaling recipe: no initial servings value set");
629
970
  }
630
971
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
631
- if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {
632
- ingredient.quantity *= factor;
972
+ if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
973
+ ingredient.quantity = multiplyQuantityValue(
974
+ ingredient.quantity,
975
+ factor
976
+ );
633
977
  }
634
978
  return ingredient;
635
979
  }).filter((ingredient) => ingredient.quantity !== null);
@@ -654,6 +998,11 @@ var Recipe = class _Recipe {
654
998
  }
655
999
  return newRecipe;
656
1000
  }
1001
+ /**
1002
+ * Gets the number of servings for the recipe.
1003
+ * @private
1004
+ * @returns The number of servings, or undefined if not set.
1005
+ */
657
1006
  getServings() {
658
1007
  if (this.servings) {
659
1008
  return this.servings;
@@ -716,7 +1065,7 @@ var ShoppingList = class {
716
1065
  continue;
717
1066
  }
718
1067
  const existingIngredient = this.ingredients.find(
719
- (i) => i.name === ingredient.name
1068
+ (i2) => i2.name === ingredient.name
720
1069
  );
721
1070
  let addSeparate = false;
722
1071
  try {
@@ -826,6 +1175,7 @@ var ShoppingList = class {
826
1175
  export {
827
1176
  AisleConfig,
828
1177
  Recipe,
1178
+ Section,
829
1179
  ShoppingList
830
1180
  };
831
1181
  //# sourceMappingURL=index.js.map