@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.cjs CHANGED
@@ -24,6 +24,7 @@ var index_exports = {};
24
24
  __export(index_exports, {
25
25
  AisleConfig: () => AisleConfig,
26
26
  Recipe: () => Recipe,
27
+ Section: () => Section,
27
28
  ShoppingList: () => ShoppingList
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
@@ -91,23 +92,234 @@ var AisleConfig = class {
91
92
 
92
93
  // src/classes/section.ts
93
94
  var Section = class {
95
+ /**
96
+ * Creates an instance of Section.
97
+ * @param name - The name of the section. Defaults to an empty string.
98
+ */
94
99
  constructor(name = "") {
100
+ /** The name of the section. Can be an empty string for the default (first) section. */
95
101
  __publicField(this, "name");
102
+ /** An array of steps and notes that make up the content of the section. */
96
103
  __publicField(this, "content", []);
97
104
  this.name = name;
98
105
  }
106
+ /**
107
+ * Checks if the section is blank (has no name and no content).
108
+ * Used during recipe parsing
109
+ * @returns `true` if the section is blank, otherwise `false`.
110
+ */
99
111
  isBlank() {
100
112
  return this.name === "" && this.content.length === 0;
101
113
  }
102
114
  };
103
115
 
116
+ // node_modules/.pnpm/human-regex@2.1.5_patch_hash=6d6bd9e233f99785a7c2187fd464edc114b76d47001dbb4eb6b5d72168de7460/node_modules/human-regex/dist/human-regex.esm.js
117
+ var t = /* @__PURE__ */ new Map();
118
+ var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
119
+ var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
120
+ var n = Object.freeze({ zeroOrMore: "*", oneOrMore: "+", optional: "?" });
121
+ var a = class {
122
+ constructor() {
123
+ this.parts = [], this.flags = /* @__PURE__ */ new Set();
124
+ }
125
+ digit() {
126
+ return this.add("\\d");
127
+ }
128
+ special() {
129
+ return this.add("(?=.*[!@#$%^&*])");
130
+ }
131
+ word() {
132
+ return this.add("\\w");
133
+ }
134
+ whitespace() {
135
+ return this.add("\\s");
136
+ }
137
+ nonWhitespace() {
138
+ return this.add("\\S");
139
+ }
140
+ literal(r2) {
141
+ return this.add((function(r3) {
142
+ t.has(r3) || t.set(r3, r3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
143
+ return t.get(r3);
144
+ })(r2));
145
+ }
146
+ or() {
147
+ return this.add("|");
148
+ }
149
+ range(t2) {
150
+ const r2 = e[t2];
151
+ if (!r2) throw new Error(`Unknown range: ${t2}`);
152
+ return this.add(`[${r2}]`);
153
+ }
154
+ notRange(t2) {
155
+ const r2 = e[t2];
156
+ if (!r2) throw new Error(`Unknown range: ${t2}`);
157
+ return this.add(`[^${r2}]`);
158
+ }
159
+ anyOf(t2) {
160
+ return this.add(`[${t2}]`);
161
+ }
162
+ notAnyOf(t2) {
163
+ return this.add(`[^${t2}]`);
164
+ }
165
+ lazy() {
166
+ const t2 = this.parts.pop();
167
+ if (!t2) throw new Error("No quantifier to make lazy");
168
+ return this.add(`${t2}?`);
169
+ }
170
+ letter() {
171
+ return this.add("[a-zA-Z]");
172
+ }
173
+ anyCharacter() {
174
+ return this.add(".");
175
+ }
176
+ newline() {
177
+ return this.add("(?:\\r\\n|\\r|\\n)");
178
+ }
179
+ negativeLookahead(t2) {
180
+ return this.add(`(?!${t2})`);
181
+ }
182
+ positiveLookahead(t2) {
183
+ return this.add(`(?=${t2})`);
184
+ }
185
+ positiveLookbehind(t2) {
186
+ return this.add(`(?<=${t2})`);
187
+ }
188
+ negativeLookbehind(t2) {
189
+ return this.add(`(?<!${t2})`);
190
+ }
191
+ hasSpecialCharacter() {
192
+ return this.add("(?=.*[!@#$%^&*])");
193
+ }
194
+ hasDigit() {
195
+ return this.add("(?=.*\\d)");
196
+ }
197
+ hasLetter() {
198
+ return this.add("(?=.*[a-zA-Z])");
199
+ }
200
+ optional() {
201
+ return this.add(n.optional);
202
+ }
203
+ exactly(t2) {
204
+ return this.add(`{${t2}}`);
205
+ }
206
+ atLeast(t2) {
207
+ return this.add(`{${t2},}`);
208
+ }
209
+ atMost(t2) {
210
+ return this.add(`{0,${t2}}`);
211
+ }
212
+ between(t2, r2) {
213
+ return this.add(`{${t2},${r2}}`);
214
+ }
215
+ oneOrMore() {
216
+ return this.add(n.oneOrMore);
217
+ }
218
+ zeroOrMore() {
219
+ return this.add(n.zeroOrMore);
220
+ }
221
+ startNamedGroup(t2) {
222
+ return this.add(`(?<${t2}>`);
223
+ }
224
+ startGroup() {
225
+ return this.add("(?:");
226
+ }
227
+ startCaptureGroup() {
228
+ return this.add("(");
229
+ }
230
+ wordBoundary() {
231
+ return this.add("\\b");
232
+ }
233
+ nonWordBoundary() {
234
+ return this.add("\\B");
235
+ }
236
+ endGroup() {
237
+ return this.add(")");
238
+ }
239
+ startAnchor() {
240
+ return this.add("^");
241
+ }
242
+ endAnchor() {
243
+ return this.add("$");
244
+ }
245
+ global() {
246
+ return this.flags.add(r.GLOBAL), this;
247
+ }
248
+ nonSensitive() {
249
+ return this.flags.add(r.NON_SENSITIVE), this;
250
+ }
251
+ multiline() {
252
+ return this.flags.add(r.MULTILINE), this;
253
+ }
254
+ dotAll() {
255
+ return this.flags.add(r.DOT_ALL), this;
256
+ }
257
+ sticky() {
258
+ return this.flags.add(r.STICKY), this;
259
+ }
260
+ unicodeChar(t2) {
261
+ this.flags.add(r.UNICODE);
262
+ const e2 = /* @__PURE__ */ new Set(["u", "l", "t", "m", "o"]);
263
+ if (void 0 !== t2 && !e2.has(t2)) throw new Error(`Invalid Unicode letter variant: ${t2}`);
264
+ return this.add(`\\p{L${null != t2 ? t2 : ""}}`);
265
+ }
266
+ unicodeDigit() {
267
+ return this.flags.add(r.UNICODE), this.add("\\p{N}");
268
+ }
269
+ unicodePunctuation() {
270
+ return this.flags.add(r.UNICODE), this.add("\\p{P}");
271
+ }
272
+ unicodeSymbol() {
273
+ return this.flags.add(r.UNICODE), this.add("\\p{S}");
274
+ }
275
+ repeat(t2) {
276
+ if (0 === this.parts.length) throw new Error("No pattern to repeat");
277
+ const r2 = this.parts.pop();
278
+ return this.parts.push(`(${r2}){${t2}}`), this;
279
+ }
280
+ ipv4Octet() {
281
+ return this.add("(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)");
282
+ }
283
+ protocol() {
284
+ return this.add("https?://");
285
+ }
286
+ www() {
287
+ return this.add("(www\\.)?");
288
+ }
289
+ tld() {
290
+ return this.add("(com|org|net)");
291
+ }
292
+ path() {
293
+ return this.add("(/\\w+)*");
294
+ }
295
+ add(t2) {
296
+ return this.parts.push(t2), this;
297
+ }
298
+ toString() {
299
+ return this.parts.join("");
300
+ }
301
+ toRegExp() {
302
+ return new RegExp(this.toString(), [...this.flags].join(""));
303
+ }
304
+ };
305
+ var d = () => new a();
306
+ var i = (() => {
307
+ const t2 = (t3) => {
308
+ const r2 = t3().toRegExp();
309
+ return () => new RegExp(r2.source, r2.flags);
310
+ };
311
+ 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())) };
312
+ })();
313
+
104
314
  // src/regex.ts
105
- var metadataRegex = /---\n(.*?)\n---/s;
106
- var multiwordIngredient = /@(?<mIngredientModifier>[@\-&+*!?])?(?<mIngredientName>(?:[^\s@#~\[\]{(.,;:!?]+(?:\s+[^\s@#~\[\]{(.,;:!?]+)+))(?=\s*(?:\{|\}|\(\s*[^)]*\s*\)))(?:\{(?<mIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))?(?:%(?<mIngredientUnits>[^}]+?))?\})?(?:\((?<mIngredientPreparation>[^)]*?)\))?/gu;
107
- var singleWordIngredient = /@(?<sIngredientModifier>[@\-&+*!?])?(?<sIngredientName>[^\s@#~\[\]{(.,;:!?]+)(?:\{(?:(?<sIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))(?:%(?<sIngredientUnits>[^}]+?))?)?\})?(?:\((?<sIngredientPreparation>[^)]*?)\))?/gu;
108
- var multiwordCookware = /#(?<mCookwareModifier>[\-&+*!?])?(?<mCookwareName>(?:[^\s@#~\[\]{(.,;:!?]+(?:\s+[^\s@#~\[\]{(.,;:!?]+)+))(?=\s*(?:\{|\}|\(\s*[^)]*\s*\)))\{(?<mCookwareQuantity>.*?)\}/;
109
- var singleWordCookware = /#(?<sCookwareModifier>[\-&+*!?])?(?<sCookwareName>[^\s@#~\[\]{(.,;:!?]+)(?:\{(?<sCookwareQuantity>.*?)\})?/u;
110
- var timer = /~(?<timerName>.*?)(?:\{(?<timerQuantity>.*?)(?:%(?<timerUnits>.+?))?\})/;
315
+ var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
316
+ var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
317
+ 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();
318
+ 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();
319
+ var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
320
+ 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();
321
+ 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();
322
+ 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();
111
323
  var tokensRegex = new RegExp(
112
324
  [
113
325
  multiwordIngredient,
@@ -115,11 +327,14 @@ var tokensRegex = new RegExp(
115
327
  multiwordCookware,
116
328
  singleWordCookware,
117
329
  timer
118
- ].map((r) => r.source).join("|"),
330
+ ].map((r2) => r2.source).join("|"),
119
331
  "gu"
120
332
  );
121
- var commentRegex = /--.*/g;
122
- var blockCommentRegex = /\s*\[\-.*?\-\]\s*/g;
333
+ var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
334
+ var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
335
+ 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();
336
+ 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();
337
+ var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
123
338
 
124
339
  // src/units.ts
125
340
  var units = [
@@ -237,46 +452,144 @@ for (const unit of units) {
237
452
  function normalizeUnit(unit) {
238
453
  return unitMap.get(unit.toLowerCase().trim());
239
454
  }
455
+ var CannotAddTextValueError = class extends Error {
456
+ constructor() {
457
+ super("Cannot add a quantity with a text value.");
458
+ this.name = "CannotAddTextValueError";
459
+ }
460
+ };
461
+ var IncompatibleUnitsError = class extends Error {
462
+ constructor(unit1, unit2) {
463
+ super(
464
+ `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
465
+ );
466
+ this.name = "IncompatibleUnitsError";
467
+ }
468
+ };
469
+ function gcd(a2, b) {
470
+ return b === 0 ? a2 : gcd(b, a2 % b);
471
+ }
472
+ function simplifyFraction(num, den) {
473
+ if (den === 0) {
474
+ throw new Error("Denominator cannot be zero.");
475
+ }
476
+ const commonDivisor = gcd(Math.abs(num), Math.abs(den));
477
+ let simplifiedNum = num / commonDivisor;
478
+ let simplifiedDen = den / commonDivisor;
479
+ if (simplifiedDen < 0) {
480
+ simplifiedNum = -simplifiedNum;
481
+ simplifiedDen = -simplifiedDen;
482
+ }
483
+ if (simplifiedDen === 1) {
484
+ return { type: "decimal", value: simplifiedNum };
485
+ } else {
486
+ return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
487
+ }
488
+ }
489
+ function multiplyNumericValue(v, factor) {
490
+ if (v.type === "decimal") {
491
+ return { type: "decimal", value: v.value * factor };
492
+ }
493
+ return simplifyFraction(v.num * factor, v.den);
494
+ }
495
+ function addNumericValues(val1, val2) {
496
+ let num1;
497
+ let den1;
498
+ let num2;
499
+ let den2;
500
+ if (val1.type === "decimal") {
501
+ num1 = val1.value;
502
+ den1 = 1;
503
+ } else {
504
+ num1 = val1.num;
505
+ den1 = val1.den;
506
+ }
507
+ if (val2.type === "decimal") {
508
+ num2 = val2.value;
509
+ den2 = 1;
510
+ } else {
511
+ num2 = val2.num;
512
+ den2 = val2.den;
513
+ }
514
+ if (val1.type === "fraction" && val2.type === "fraction") {
515
+ const commonDen = den1 * den2;
516
+ const sumNum = num1 * den2 + num2 * den1;
517
+ return simplifyFraction(sumNum, commonDen);
518
+ } else {
519
+ return { type: "decimal", value: num1 / den1 + num2 / den2 };
520
+ }
521
+ }
522
+ var toRoundedDecimal = (v) => {
523
+ const value = v.type === "decimal" ? v.value : v.num / v.den;
524
+ return { type: "decimal", value: Math.floor(value * 100) / 100 };
525
+ };
526
+ function multiplyQuantityValue(value, factor) {
527
+ if (value.type === "fixed") {
528
+ return {
529
+ type: "fixed",
530
+ value: toRoundedDecimal(
531
+ multiplyNumericValue(
532
+ value.value,
533
+ factor
534
+ )
535
+ )
536
+ };
537
+ }
538
+ return {
539
+ type: "range",
540
+ min: toRoundedDecimal(multiplyNumericValue(value.min, factor)),
541
+ max: toRoundedDecimal(multiplyNumericValue(value.max, factor))
542
+ };
543
+ }
544
+ var convertQuantityValue = (value, def, targetDef) => {
545
+ if (def.name === targetDef.name) return value;
546
+ const factor = def.toBase / targetDef.toBase;
547
+ return multiplyQuantityValue(value, factor);
548
+ };
240
549
  function addQuantities(q1, q2) {
550
+ const v1 = q1.value;
551
+ const v2 = q2.value;
552
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
553
+ throw new CannotAddTextValueError();
554
+ }
241
555
  const unit1Def = normalizeUnit(q1.unit);
242
556
  const unit2Def = normalizeUnit(q2.unit);
243
- if (isNaN(Number(q1.value))) {
244
- throw new Error(
245
- `Cannot add quantity to string-quantified value: ${q1.value}`
557
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
558
+ if (val1.type === "fixed" && val2.type === "fixed") {
559
+ const res = addNumericValues(
560
+ val1.value,
561
+ val2.value
562
+ );
563
+ return { value: { type: "fixed", value: res }, unit };
564
+ }
565
+ const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
566
+ const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
567
+ const newMin = addNumericValues(
568
+ r1.min,
569
+ r2.min
246
570
  );
247
- }
248
- if (isNaN(Number(q2.value))) {
249
- throw new Error(
250
- `Cannot add quantity to string-quantified value: ${q2.value}`
571
+ const newMax = addNumericValues(
572
+ r1.max,
573
+ r2.max
251
574
  );
252
- }
575
+ return { value: { type: "range", min: newMin, max: newMax }, unit };
576
+ };
253
577
  if (q1.unit === "" && unit2Def) {
254
- return {
255
- value: Math.round((q1.value + q2.value) * 100) / 100,
256
- unit: q2.unit
257
- };
578
+ return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
258
579
  }
259
580
  if (q2.unit === "" && unit1Def) {
260
- return {
261
- value: Math.round((q1.value + q2.value) * 100) / 100,
262
- unit: q1.unit
263
- };
581
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
264
582
  }
265
583
  if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
266
- return {
267
- value: Math.round((q1.value + q2.value) * 100) / 100,
268
- unit: q1.unit
269
- };
584
+ return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
270
585
  }
271
586
  if (unit1Def && unit2Def) {
272
587
  if (unit1Def.type !== unit2Def.type) {
273
- throw new Error(
274
- `Cannot add quantities of different types: ${unit1Def.type} (${q1.unit}) and ${unit2Def.type} (${q2.unit})`
588
+ throw new IncompatibleUnitsError(
589
+ `${unit1Def.type} (${q1.unit})`,
590
+ `${unit2Def.type} (${q2.unit})`
275
591
  );
276
592
  }
277
- const baseValue1 = q1.value * unit1Def.toBase;
278
- const baseValue2 = q2.value * unit2Def.toBase;
279
- const totalBaseValue = baseValue1 + baseValue2;
280
593
  let targetUnitDef;
281
594
  if (unit1Def.system !== unit2Def.system) {
282
595
  const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
@@ -286,15 +599,15 @@ function addQuantities(q1, q2) {
286
599
  } else {
287
600
  targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
288
601
  }
289
- const finalValue = totalBaseValue / targetUnitDef.toBase;
290
- return {
291
- value: Math.round(finalValue * 100) / 100,
292
- unit: targetUnitDef.name
293
- };
602
+ const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
603
+ const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
604
+ return addQuantityValuesAndSetUnit(
605
+ convertedV1,
606
+ convertedV2,
607
+ targetUnitDef.name
608
+ );
294
609
  }
295
- throw new Error(
296
- `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`
297
- );
610
+ throw new IncompatibleUnitsError(q1.unit, q2.unit);
298
611
  }
299
612
 
300
613
  // src/parser_helpers.ts
@@ -307,14 +620,14 @@ function findOrPush(list, finder, creator) {
307
620
  }
308
621
  function flushPendingNote(section, note) {
309
622
  if (note.length > 0) {
310
- section.content.push({ note });
623
+ section.content.push({ type: "note", note });
311
624
  return "";
312
625
  }
313
626
  return note;
314
627
  }
315
628
  function flushPendingItems(section, items) {
316
629
  if (items.length > 0) {
317
- section.content.push({ items: [...items] });
630
+ section.content.push({ type: "step", items: [...items] });
318
631
  items.length = 0;
319
632
  return true;
320
633
  }
@@ -324,7 +637,7 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
324
637
  const { name, quantity, unit } = newIngredient;
325
638
  if (isReference) {
326
639
  const index = ingredients.findIndex(
327
- (i) => i.name.toLowerCase() === name.toLowerCase()
640
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
328
641
  );
329
642
  if (index === -1) {
330
643
  throw new Error(
@@ -334,13 +647,22 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
334
647
  const existingIngredient = ingredients[index];
335
648
  if (quantity !== void 0) {
336
649
  const currentQuantity = {
337
- value: existingIngredient.quantity ?? 0,
650
+ value: existingIngredient.quantity ?? {
651
+ type: "fixed",
652
+ value: { type: "decimal", value: 0 }
653
+ },
338
654
  unit: existingIngredient.unit ?? ""
339
655
  };
340
656
  const newQuantity = { value: quantity, unit: unit ?? "" };
341
- const total = addQuantities(currentQuantity, newQuantity);
342
- existingIngredient.quantity = total.value;
343
- existingIngredient.unit = total.unit || void 0;
657
+ try {
658
+ const total = addQuantities(currentQuantity, newQuantity);
659
+ existingIngredient.quantity = total.value;
660
+ existingIngredient.unit = total.unit || void 0;
661
+ } catch (e2) {
662
+ if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
663
+ return ingredients.push(newIngredient) - 1;
664
+ }
665
+ }
344
666
  }
345
667
  return index;
346
668
  }
@@ -350,7 +672,7 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
350
672
  const { name } = newCookware;
351
673
  if (isReference) {
352
674
  const index = cookware.findIndex(
353
- (i) => i.name.toLowerCase() === name.toLowerCase()
675
+ (i2) => i2.name.toLowerCase() === name.toLowerCase()
354
676
  );
355
677
  if (index === -1) {
356
678
  throw new Error(
@@ -361,13 +683,28 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
361
683
  }
362
684
  return cookware.push(newCookware) - 1;
363
685
  }
364
- function parseNumber(input_str) {
365
- const clean_str = String(input_str).replace(",", ".");
366
- if (!clean_str.startsWith("/") && clean_str.includes("/")) {
367
- const [num, den] = clean_str.split("/").map(Number);
368
- return num / den;
686
+ var parseFixedValue = (input_str) => {
687
+ if (!numberLikeRegex.test(input_str)) {
688
+ return { type: "text", value: input_str };
689
+ }
690
+ const s = input_str.trim().replace(",", ".");
691
+ if (s.includes("/")) {
692
+ const parts = s.split("/");
693
+ const num = Number(parts[0]);
694
+ const den = Number(parts[1]);
695
+ return { type: "fraction", num, den };
696
+ }
697
+ return { type: "decimal", value: Number(s) };
698
+ };
699
+ function parseQuantityInput(input_str) {
700
+ const clean_str = String(input_str).trim();
701
+ if (rangeRegex.test(clean_str)) {
702
+ const range_parts = clean_str.split("-");
703
+ const min = parseFixedValue(range_parts[0].trim());
704
+ const max = parseFixedValue(range_parts[1].trim());
705
+ return { type: "range", min, max };
369
706
  }
370
- return Number(clean_str);
707
+ return { type: "fixed", value: parseFixedValue(clean_str) };
371
708
  }
372
709
  function parseSimpleMetaVar(content, varName) {
373
710
  const varMatch = content.match(
@@ -560,11 +897,20 @@ var Recipe = class _Recipe {
560
897
  const hidden = modifier === "-";
561
898
  const reference = modifier === "&";
562
899
  const isRecipe = modifier === "@";
563
- const quantity = quantityRaw ? parseNumber(quantityRaw) : void 0;
900
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
901
+ const aliasMatch = name.match(ingredientAliasRegex);
902
+ let listName, displayName;
903
+ if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
904
+ listName = aliasMatch.groups.ingredientListName.trim();
905
+ displayName = aliasMatch.groups.ingredientDisplayName.trim();
906
+ } else {
907
+ listName = name;
908
+ displayName = name;
909
+ }
564
910
  const idxInList = findAndUpsertIngredient(
565
911
  this.ingredients,
566
912
  {
567
- name,
913
+ name: listName,
568
914
  quantity,
569
915
  unit: units2,
570
916
  optional,
@@ -576,26 +922,30 @@ var Recipe = class _Recipe {
576
922
  );
577
923
  const newItem = {
578
924
  type: "ingredient",
579
- value: idxInList
925
+ value: idxInList,
926
+ itemQuantity: quantity,
927
+ itemUnit: units2,
928
+ displayName
580
929
  };
581
- if (reference) {
582
- newItem.partialQuantity = quantity;
583
- newItem.partialUnit = units2;
584
- newItem.partialPreparation = preparation;
585
- }
586
930
  items.push(newItem);
587
931
  } else if (groups.mCookwareName || groups.sCookwareName) {
588
932
  const name = groups.mCookwareName || groups.sCookwareName;
589
933
  const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
934
+ const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
590
935
  const optional = modifier === "?";
591
936
  const hidden = modifier === "-";
592
937
  const reference = modifier === "&";
938
+ const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
593
939
  const idxInList = findAndUpsertCookware(
594
940
  this.cookware,
595
- { name, optional, hidden },
941
+ { name, quantity, optional, hidden },
596
942
  reference
597
943
  );
598
- items.push({ type: "cookware", value: idxInList });
944
+ items.push({
945
+ type: "cookware",
946
+ value: idxInList,
947
+ itemQuantity: quantity
948
+ });
599
949
  } else if (groups.timerQuantity !== void 0) {
600
950
  const durationStr = groups.timerQuantity.trim();
601
951
  const unit = (groups.timerUnits || "").trim();
@@ -603,7 +953,7 @@ var Recipe = class _Recipe {
603
953
  throw new Error("Timer missing units");
604
954
  }
605
955
  const name = groups.timerName || void 0;
606
- const duration = parseNumber(durationStr);
956
+ const duration = parseQuantityInput(durationStr);
607
957
  const timerObj = {
608
958
  name,
609
959
  duration,
@@ -611,7 +961,7 @@ var Recipe = class _Recipe {
611
961
  };
612
962
  const idxInList = findOrPush(
613
963
  this.timers,
614
- (t) => t.name === timerObj.name && t.duration === timerObj.duration && t.unit === timerObj.unit,
964
+ (t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
615
965
  () => timerObj
616
966
  );
617
967
  items.push({ type: "timer", value: idxInList });
@@ -654,8 +1004,11 @@ var Recipe = class _Recipe {
654
1004
  throw new Error("Error scaling recipe: no initial servings value set");
655
1005
  }
656
1006
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
657
- if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {
658
- ingredient.quantity *= factor;
1007
+ if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
1008
+ ingredient.quantity = multiplyQuantityValue(
1009
+ ingredient.quantity,
1010
+ factor
1011
+ );
659
1012
  }
660
1013
  return ingredient;
661
1014
  }).filter((ingredient) => ingredient.quantity !== null);
@@ -680,6 +1033,11 @@ var Recipe = class _Recipe {
680
1033
  }
681
1034
  return newRecipe;
682
1035
  }
1036
+ /**
1037
+ * Gets the number of servings for the recipe.
1038
+ * @private
1039
+ * @returns The number of servings, or undefined if not set.
1040
+ */
683
1041
  getServings() {
684
1042
  if (this.servings) {
685
1043
  return this.servings;
@@ -693,9 +1051,13 @@ var Recipe = class _Recipe {
693
1051
  clone() {
694
1052
  const newRecipe = new _Recipe();
695
1053
  newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
696
- newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));
1054
+ newRecipe.ingredients = JSON.parse(
1055
+ JSON.stringify(this.ingredients)
1056
+ );
697
1057
  newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
698
- newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));
1058
+ newRecipe.cookware = JSON.parse(
1059
+ JSON.stringify(this.cookware)
1060
+ );
699
1061
  newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
700
1062
  newRecipe.servings = this.servings;
701
1063
  return newRecipe;
@@ -742,7 +1104,7 @@ var ShoppingList = class {
742
1104
  continue;
743
1105
  }
744
1106
  const existingIngredient = this.ingredients.find(
745
- (i) => i.name === ingredient.name
1107
+ (i2) => i2.name === ingredient.name
746
1108
  );
747
1109
  let addSeparate = false;
748
1110
  try {
@@ -853,6 +1215,7 @@ var ShoppingList = class {
853
1215
  0 && (module.exports = {
854
1216
  AisleConfig,
855
1217
  Recipe,
1218
+ Section,
856
1219
  ShoppingList
857
1220
  });
858
1221
  //# sourceMappingURL=index.cjs.map