@tmlmt/cooklang-parser 1.0.7 → 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 };
665
+ }
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 };
343
672
  }
344
- return Number(clean_str);
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
  {
@@ -548,16 +887,27 @@ var Recipe = class _Recipe {
548
887
  },
549
888
  reference
550
889
  );
551
- items.push({ type: "ingredient", value: idxInList });
890
+ const newItem = {
891
+ type: "ingredient",
892
+ value: idxInList
893
+ };
894
+ if (reference) {
895
+ newItem.partialQuantity = quantity;
896
+ newItem.partialUnit = units2;
897
+ newItem.partialPreparation = preparation;
898
+ }
899
+ items.push(newItem);
552
900
  } else if (groups.mCookwareName || groups.sCookwareName) {
553
901
  const name = groups.mCookwareName || groups.sCookwareName;
554
902
  const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
903
+ const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
555
904
  const optional = modifier === "?";
556
905
  const hidden = modifier === "-";
557
906
  const reference = modifier === "&";
907
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
558
908
  const idxInList = findAndUpsertCookware(
559
909
  this.cookware,
560
- { name, optional, hidden },
910
+ { name, quantity, optional, hidden },
561
911
  reference
562
912
  );
563
913
  items.push({ type: "cookware", value: idxInList });
@@ -568,7 +918,7 @@ var Recipe = class _Recipe {
568
918
  throw new Error("Timer missing units");
569
919
  }
570
920
  const name = groups.timerName || void 0;
571
- const duration = parseNumber(durationStr);
921
+ const duration = parseQuantityInput(durationStr);
572
922
  const timerObj = {
573
923
  name,
574
924
  duration,
@@ -576,7 +926,7 @@ var Recipe = class _Recipe {
576
926
  };
577
927
  const idxInList = findOrPush(
578
928
  this.timers,
579
- (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,
580
930
  () => timerObj
581
931
  );
582
932
  items.push({ type: "timer", value: idxInList });
@@ -619,8 +969,11 @@ var Recipe = class _Recipe {
619
969
  throw new Error("Error scaling recipe: no initial servings value set");
620
970
  }
621
971
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
622
- if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {
623
- 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
+ );
624
977
  }
625
978
  return ingredient;
626
979
  }).filter((ingredient) => ingredient.quantity !== null);
@@ -645,6 +998,11 @@ var Recipe = class _Recipe {
645
998
  }
646
999
  return newRecipe;
647
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
+ */
648
1006
  getServings() {
649
1007
  if (this.servings) {
650
1008
  return this.servings;
@@ -707,7 +1065,7 @@ var ShoppingList = class {
707
1065
  continue;
708
1066
  }
709
1067
  const existingIngredient = this.ingredients.find(
710
- (i) => i.name === ingredient.name
1068
+ (i2) => i2.name === ingredient.name
711
1069
  );
712
1070
  let addSeparate = false;
713
1071
  try {
@@ -817,6 +1175,7 @@ var ShoppingList = class {
817
1175
  export {
818
1176
  AisleConfig,
819
1177
  Recipe,
1178
+ Section,
820
1179
  ShoppingList
821
1180
  };
822
1181
  //# sourceMappingURL=index.js.map