@tmlmt/cooklang-parser 1.0.8 → 1.2.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.
package/dist/index.js CHANGED
@@ -65,23 +65,234 @@ 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 ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
293
+ 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();
294
+ 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();
295
+ 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
296
  var tokensRegex = new RegExp(
86
297
  [
87
298
  multiwordIngredient,
@@ -89,11 +300,14 @@ var tokensRegex = new RegExp(
89
300
  multiwordCookware,
90
301
  singleWordCookware,
91
302
  timer
92
- ].map((r) => r.source).join("|"),
303
+ ].map((r2) => r2.source).join("|"),
93
304
  "gu"
94
305
  );
95
- var commentRegex = /--.*/g;
96
- var blockCommentRegex = /\s*\[\-.*?\-\]\s*/g;
306
+ var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
307
+ var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
308
+ 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();
309
+ 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();
310
+ var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
97
311
 
98
312
  // src/units.ts
99
313
  var units = [
@@ -211,46 +425,144 @@ for (const unit of units) {
211
425
  function normalizeUnit(unit) {
212
426
  return unitMap.get(unit.toLowerCase().trim());
213
427
  }
428
+ var CannotAddTextValueError = class extends Error {
429
+ constructor() {
430
+ super("Cannot add a quantity with a text value.");
431
+ this.name = "CannotAddTextValueError";
432
+ }
433
+ };
434
+ var IncompatibleUnitsError = class extends Error {
435
+ constructor(unit1, unit2) {
436
+ super(
437
+ `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
438
+ );
439
+ this.name = "IncompatibleUnitsError";
440
+ }
441
+ };
442
+ function gcd(a2, b) {
443
+ return b === 0 ? a2 : gcd(b, a2 % b);
444
+ }
445
+ function simplifyFraction(num, den) {
446
+ if (den === 0) {
447
+ throw new Error("Denominator cannot be zero.");
448
+ }
449
+ const commonDivisor = gcd(Math.abs(num), Math.abs(den));
450
+ let simplifiedNum = num / commonDivisor;
451
+ let simplifiedDen = den / commonDivisor;
452
+ if (simplifiedDen < 0) {
453
+ simplifiedNum = -simplifiedNum;
454
+ simplifiedDen = -simplifiedDen;
455
+ }
456
+ if (simplifiedDen === 1) {
457
+ return { type: "decimal", value: simplifiedNum };
458
+ } else {
459
+ return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
460
+ }
461
+ }
462
+ function multiplyNumericValue(v, factor) {
463
+ if (v.type === "decimal") {
464
+ return { type: "decimal", value: v.value * factor };
465
+ }
466
+ return simplifyFraction(v.num * factor, v.den);
467
+ }
468
+ function addNumericValues(val1, val2) {
469
+ let num1;
470
+ let den1;
471
+ let num2;
472
+ let den2;
473
+ if (val1.type === "decimal") {
474
+ num1 = val1.value;
475
+ den1 = 1;
476
+ } else {
477
+ num1 = val1.num;
478
+ den1 = val1.den;
479
+ }
480
+ if (val2.type === "decimal") {
481
+ num2 = val2.value;
482
+ den2 = 1;
483
+ } else {
484
+ num2 = val2.num;
485
+ den2 = val2.den;
486
+ }
487
+ if (val1.type === "fraction" && val2.type === "fraction") {
488
+ const commonDen = den1 * den2;
489
+ const sumNum = num1 * den2 + num2 * den1;
490
+ return simplifyFraction(sumNum, commonDen);
491
+ } else {
492
+ return { type: "decimal", value: num1 / den1 + num2 / den2 };
493
+ }
494
+ }
495
+ var toRoundedDecimal = (v) => {
496
+ const value = v.type === "decimal" ? v.value : v.num / v.den;
497
+ return { type: "decimal", value: Math.floor(value * 100) / 100 };
498
+ };
499
+ function multiplyQuantityValue(value, factor) {
500
+ if (value.type === "fixed") {
501
+ return {
502
+ type: "fixed",
503
+ value: toRoundedDecimal(
504
+ multiplyNumericValue(
505
+ value.value,
506
+ factor
507
+ )
508
+ )
509
+ };
510
+ }
511
+ return {
512
+ type: "range",
513
+ min: toRoundedDecimal(multiplyNumericValue(value.min, factor)),
514
+ max: toRoundedDecimal(multiplyNumericValue(value.max, factor))
515
+ };
516
+ }
517
+ var convertQuantityValue = (value, def, targetDef) => {
518
+ if (def.name === targetDef.name) return value;
519
+ const factor = def.toBase / targetDef.toBase;
520
+ return multiplyQuantityValue(value, factor);
521
+ };
214
522
  function addQuantities(q1, q2) {
523
+ const v1 = q1.value;
524
+ const v2 = q2.value;
525
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
526
+ throw new CannotAddTextValueError();
527
+ }
215
528
  const unit1Def = normalizeUnit(q1.unit);
216
529
  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}`
530
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
531
+ if (val1.type === "fixed" && val2.type === "fixed") {
532
+ const res = addNumericValues(
533
+ val1.value,
534
+ val2.value
535
+ );
536
+ return { value: { type: "fixed", value: res }, unit };
537
+ }
538
+ const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
539
+ const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
540
+ const newMin = addNumericValues(
541
+ r1.min,
542
+ r2.min
220
543
  );
221
- }
222
- if (isNaN(Number(q2.value))) {
223
- throw new Error(
224
- `Cannot add quantity to string-quantified value: ${q2.value}`
544
+ const newMax = addNumericValues(
545
+ r1.max,
546
+ r2.max
225
547
  );
226
- }
548
+ return { value: { type: "range", min: newMin, max: newMax }, unit };
549
+ };
227
550
  if (q1.unit === "" && unit2Def) {
228
- return {
229
- value: Math.round((q1.value + q2.value) * 100) / 100,
230
- unit: q2.unit
231
- };
551
+ return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
232
552
  }
233
553
  if (q2.unit === "" && unit1Def) {
234
- return {
235
- value: Math.round((q1.value + q2.value) * 100) / 100,
236
- unit: q1.unit
237
- };
554
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
238
555
  }
239
556
  if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
240
- return {
241
- value: Math.round((q1.value + q2.value) * 100) / 100,
242
- unit: q1.unit
243
- };
557
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
244
558
  }
245
559
  if (unit1Def && unit2Def) {
246
560
  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})`
561
+ throw new IncompatibleUnitsError(
562
+ `${unit1Def.type} (${q1.unit})`,
563
+ `${unit2Def.type} (${q2.unit})`
249
564
  );
250
565
  }
251
- const baseValue1 = q1.value * unit1Def.toBase;
252
- const baseValue2 = q2.value * unit2Def.toBase;
253
- const totalBaseValue = baseValue1 + baseValue2;
254
566
  let targetUnitDef;
255
567
  if (unit1Def.system !== unit2Def.system) {
256
568
  const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
@@ -260,15 +572,15 @@ function addQuantities(q1, q2) {
260
572
  } else {
261
573
  targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
262
574
  }
263
- const finalValue = totalBaseValue / targetUnitDef.toBase;
264
- return {
265
- value: Math.round(finalValue * 100) / 100,
266
- unit: targetUnitDef.name
267
- };
575
+ const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
576
+ const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
577
+ return addQuantityValuesAndSetUnit(
578
+ convertedV1,
579
+ convertedV2,
580
+ targetUnitDef.name
581
+ );
268
582
  }
269
- throw new Error(
270
- `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`
271
- );
583
+ throw new IncompatibleUnitsError(q1.unit, q2.unit);
272
584
  }
273
585
 
274
586
  // src/parser_helpers.ts
@@ -281,14 +593,14 @@ function findOrPush(list, finder, creator) {
281
593
  }
282
594
  function flushPendingNote(section, note) {
283
595
  if (note.length > 0) {
284
- section.content.push({ note });
596
+ section.content.push({ type: "note", note });
285
597
  return "";
286
598
  }
287
599
  return note;
288
600
  }
289
601
  function flushPendingItems(section, items) {
290
602
  if (items.length > 0) {
291
- section.content.push({ items: [...items] });
603
+ section.content.push({ type: "step", items: [...items] });
292
604
  items.length = 0;
293
605
  return true;
294
606
  }
@@ -298,7 +610,7 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
298
610
  const { name, quantity, unit } = newIngredient;
299
611
  if (isReference) {
300
612
  const index = ingredients.findIndex(
301
- (i) => i.name.toLowerCase() === name.toLowerCase()
613
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
302
614
  );
303
615
  if (index === -1) {
304
616
  throw new Error(
@@ -308,13 +620,22 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
308
620
  const existingIngredient = ingredients[index];
309
621
  if (quantity !== void 0) {
310
622
  const currentQuantity = {
311
- value: existingIngredient.quantity ?? 0,
623
+ value: existingIngredient.quantity ?? {
624
+ type: "fixed",
625
+ value: { type: "decimal", value: 0 }
626
+ },
312
627
  unit: existingIngredient.unit ?? ""
313
628
  };
314
629
  const newQuantity = { value: quantity, unit: unit ?? "" };
315
- const total = addQuantities(currentQuantity, newQuantity);
316
- existingIngredient.quantity = total.value;
317
- existingIngredient.unit = total.unit || void 0;
630
+ try {
631
+ const total = addQuantities(currentQuantity, newQuantity);
632
+ existingIngredient.quantity = total.value;
633
+ existingIngredient.unit = total.unit || void 0;
634
+ } catch (e2) {
635
+ if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
636
+ return ingredients.push(newIngredient) - 1;
637
+ }
638
+ }
318
639
  }
319
640
  return index;
320
641
  }
@@ -324,7 +645,7 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
324
645
  const { name } = newCookware;
325
646
  if (isReference) {
326
647
  const index = cookware.findIndex(
327
- (i) => i.name.toLowerCase() === name.toLowerCase()
648
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
328
649
  );
329
650
  if (index === -1) {
330
651
  throw new Error(
@@ -335,13 +656,28 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
335
656
  }
336
657
  return cookware.push(newCookware) - 1;
337
658
  }
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;
659
+ var parseFixedValue = (input_str) => {
660
+ if (!numberLikeRegex.test(input_str)) {
661
+ return { type: "text", value: input_str };
662
+ }
663
+ const s = input_str.trim().replace(",", ".");
664
+ if (s.includes("/")) {
665
+ const parts = s.split("/");
666
+ const num = Number(parts[0]);
667
+ const den = Number(parts[1]);
668
+ return { type: "fraction", num, den };
669
+ }
670
+ return { type: "decimal", value: Number(s) };
671
+ };
672
+ function parseQuantityInput(input_str) {
673
+ const clean_str = String(input_str).trim();
674
+ if (rangeRegex.test(clean_str)) {
675
+ const range_parts = clean_str.split("-");
676
+ const min = parseFixedValue(range_parts[0].trim());
677
+ const max = parseFixedValue(range_parts[1].trim());
678
+ return { type: "range", min, max };
343
679
  }
344
- return Number(clean_str);
680
+ return { type: "fixed", value: parseFixedValue(clean_str) };
345
681
  }
346
682
  function parseSimpleMetaVar(content, varName) {
347
683
  const varMatch = content.match(
@@ -534,11 +870,20 @@ var Recipe = class _Recipe {
534
870
  const hidden = modifier === "-";
535
871
  const reference = modifier === "&";
536
872
  const isRecipe = modifier === "@";
537
- const quantity = quantityRaw ? parseNumber(quantityRaw) : void 0;
873
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
874
+ const aliasMatch = name.match(ingredientAliasRegex);
875
+ let listName, displayName;
876
+ if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
877
+ listName = aliasMatch.groups.ingredientListName.trim();
878
+ displayName = aliasMatch.groups.ingredientDisplayName.trim();
879
+ } else {
880
+ listName = name;
881
+ displayName = name;
882
+ }
538
883
  const idxInList = findAndUpsertIngredient(
539
884
  this.ingredients,
540
885
  {
541
- name,
886
+ name: listName,
542
887
  quantity,
543
888
  unit: units2,
544
889
  optional,
@@ -550,26 +895,30 @@ var Recipe = class _Recipe {
550
895
  );
551
896
  const newItem = {
552
897
  type: "ingredient",
553
- value: idxInList
898
+ value: idxInList,
899
+ itemQuantity: quantity,
900
+ itemUnit: units2,
901
+ displayName
554
902
  };
555
- if (reference) {
556
- newItem.partialQuantity = quantity;
557
- newItem.partialUnit = units2;
558
- newItem.partialPreparation = preparation;
559
- }
560
903
  items.push(newItem);
561
904
  } else if (groups.mCookwareName || groups.sCookwareName) {
562
905
  const name = groups.mCookwareName || groups.sCookwareName;
563
906
  const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
907
+ const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
564
908
  const optional = modifier === "?";
565
909
  const hidden = modifier === "-";
566
910
  const reference = modifier === "&";
911
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
567
912
  const idxInList = findAndUpsertCookware(
568
913
  this.cookware,
569
- { name, optional, hidden },
914
+ { name, quantity, optional, hidden },
570
915
  reference
571
916
  );
572
- items.push({ type: "cookware", value: idxInList });
917
+ items.push({
918
+ type: "cookware",
919
+ value: idxInList,
920
+ itemQuantity: quantity
921
+ });
573
922
  } else if (groups.timerQuantity !== void 0) {
574
923
  const durationStr = groups.timerQuantity.trim();
575
924
  const unit = (groups.timerUnits || "").trim();
@@ -577,7 +926,7 @@ var Recipe = class _Recipe {
577
926
  throw new Error("Timer missing units");
578
927
  }
579
928
  const name = groups.timerName || void 0;
580
- const duration = parseNumber(durationStr);
929
+ const duration = parseQuantityInput(durationStr);
581
930
  const timerObj = {
582
931
  name,
583
932
  duration,
@@ -585,7 +934,7 @@ var Recipe = class _Recipe {
585
934
  };
586
935
  const idxInList = findOrPush(
587
936
  this.timers,
588
- (t) => t.name === timerObj.name && t.duration === timerObj.duration && t.unit === timerObj.unit,
937
+ (t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
589
938
  () => timerObj
590
939
  );
591
940
  items.push({ type: "timer", value: idxInList });
@@ -628,8 +977,11 @@ var Recipe = class _Recipe {
628
977
  throw new Error("Error scaling recipe: no initial servings value set");
629
978
  }
630
979
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
631
- if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {
632
- ingredient.quantity *= factor;
980
+ if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
981
+ ingredient.quantity = multiplyQuantityValue(
982
+ ingredient.quantity,
983
+ factor
984
+ );
633
985
  }
634
986
  return ingredient;
635
987
  }).filter((ingredient) => ingredient.quantity !== null);
@@ -654,6 +1006,11 @@ var Recipe = class _Recipe {
654
1006
  }
655
1007
  return newRecipe;
656
1008
  }
1009
+ /**
1010
+ * Gets the number of servings for the recipe.
1011
+ * @private
1012
+ * @returns The number of servings, or undefined if not set.
1013
+ */
657
1014
  getServings() {
658
1015
  if (this.servings) {
659
1016
  return this.servings;
@@ -667,9 +1024,13 @@ var Recipe = class _Recipe {
667
1024
  clone() {
668
1025
  const newRecipe = new _Recipe();
669
1026
  newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
670
- newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));
1027
+ newRecipe.ingredients = JSON.parse(
1028
+ JSON.stringify(this.ingredients)
1029
+ );
671
1030
  newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
672
- newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));
1031
+ newRecipe.cookware = JSON.parse(
1032
+ JSON.stringify(this.cookware)
1033
+ );
673
1034
  newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
674
1035
  newRecipe.servings = this.servings;
675
1036
  return newRecipe;
@@ -716,7 +1077,7 @@ var ShoppingList = class {
716
1077
  continue;
717
1078
  }
718
1079
  const existingIngredient = this.ingredients.find(
719
- (i) => i.name === ingredient.name
1080
+ (i2) => i2.name === ingredient.name
720
1081
  );
721
1082
  let addSeparate = false;
722
1083
  try {
@@ -826,6 +1187,7 @@ var ShoppingList = class {
826
1187
  export {
827
1188
  AisleConfig,
828
1189
  Recipe,
1190
+ Section,
829
1191
  ShoppingList
830
1192
  };
831
1193
  //# sourceMappingURL=index.js.map