@tmlmt/cooklang-parser 2.0.0 → 2.0.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
@@ -2,16 +2,15 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
- // src/classes/aisle_config.ts
6
- var AisleConfig = class {
5
+ // src/classes/category_config.ts
6
+ var CategoryConfig = class {
7
7
  /**
8
- * Creates a new AisleConfig instance.
9
- * @param config - The aisle configuration to parse.
8
+ * Creates a new CategoryConfig instance.
9
+ * @param config - The category configuration to parse.
10
10
  */
11
11
  constructor(config) {
12
12
  /**
13
- * The categories of aisles.
14
- * @see {@link AisleCategory}
13
+ * The parsed categories of ingredients.
15
14
  */
16
15
  __publicField(this, "categories", []);
17
16
  if (config) {
@@ -19,8 +18,9 @@ var AisleConfig = class {
19
18
  }
20
19
  }
21
20
  /**
22
- * Parses an aisle configuration from a string.
23
- * @param config - The aisle configuration to parse.
21
+ * Parses a category configuration from a string into property
22
+ * {@link CategoryConfig.categories | categories}
23
+ * @param config - The category configuration to parse.
24
24
  */
25
25
  parse(config) {
26
26
  let currentCategory = null;
@@ -70,7 +70,10 @@ var Section = class {
70
70
  * @param name - The name of the section. Defaults to an empty string.
71
71
  */
72
72
  constructor(name = "") {
73
- /** The name of the section. Can be an empty string for the default (first) section. */
73
+ /**
74
+ * The name of the section. Can be an empty string for the default (first) section.
75
+ * @defaultValue `""`
76
+ */
74
77
  __publicField(this, "name");
75
78
  /** An array of steps and notes that make up the content of the section. */
76
79
  __publicField(this, "content", []);
@@ -286,13 +289,14 @@ var i = (() => {
286
289
 
287
290
  // src/regex.ts
288
291
  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 scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
293
+ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
294
+ var multiwordIngredient = d().literal("@").startNamedGroup("mIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("mIngredientRecipeAnchor").literal("./").endGroup().optional().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").startGroup().literal("{").startNamedGroup("mIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("mIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("mIngredientUnit").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("mIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
295
+ var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("sIngredientRecipeAnchor").literal("./").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("sIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("sIngredientUnit").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("sIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
292
296
  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();
297
+ var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().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();
298
+ var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
299
+ var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
296
300
  var tokensRegex = new RegExp(
297
301
  [
298
302
  multiwordIngredient,
@@ -306,8 +310,9 @@ var tokensRegex = new RegExp(
306
310
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
307
311
  var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
308
312
  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();
313
+ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
310
314
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
315
+ var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
311
316
 
312
317
  // src/units.ts
313
318
  var units = [
@@ -316,14 +321,14 @@ var units = [
316
321
  name: "g",
317
322
  type: "mass",
318
323
  system: "metric",
319
- aliases: ["gram", "grams"],
324
+ aliases: ["gram", "grams", "grammes"],
320
325
  toBase: 1
321
326
  },
322
327
  {
323
328
  name: "kg",
324
329
  type: "mass",
325
330
  system: "metric",
326
- aliases: ["kilogram", "kilograms"],
331
+ aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
327
332
  toBase: 1e3
328
333
  },
329
334
  // Mass (Imperial)
@@ -346,7 +351,7 @@ var units = [
346
351
  name: "ml",
347
352
  type: "volume",
348
353
  system: "metric",
349
- aliases: ["milliliter", "milliliters", "millilitre", "millilitres"],
354
+ aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
350
355
  toBase: 1
351
356
  },
352
357
  {
@@ -411,7 +416,7 @@ var units = [
411
416
  name: "piece",
412
417
  type: "count",
413
418
  system: "metric",
414
- aliases: ["pieces"],
419
+ aliases: ["pieces", "pc"],
415
420
  toBase: 1
416
421
  }
417
422
  ];
@@ -422,7 +427,7 @@ for (const unit of units) {
422
427
  unitMap.set(alias.toLowerCase(), unit);
423
428
  }
424
429
  }
425
- function normalizeUnit(unit) {
430
+ function normalizeUnit(unit = "") {
426
431
  return unitMap.get(unit.toLowerCase().trim());
427
432
  }
428
433
  var CannotAddTextValueError = class extends Error {
@@ -484,7 +489,10 @@ function addNumericValues(val1, val2) {
484
489
  num2 = val2.num;
485
490
  den2 = val2.den;
486
491
  }
487
- if (val1.type === "fraction" && val2.type === "fraction") {
492
+ if (num1 === 0 && num2 === 0) {
493
+ return { type: "decimal", value: 0 };
494
+ }
495
+ if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.value === 0 || val2.type === "fraction" && val1.type === "decimal" && val1.value === 0) {
488
496
  const commonDen = den1 * den2;
489
497
  const sumNum = num1 * den2 + num2 * den1;
490
498
  return simplifyFraction(sumNum, commonDen);
@@ -498,24 +506,26 @@ var toRoundedDecimal = (v) => {
498
506
  };
499
507
  function multiplyQuantityValue(value, factor) {
500
508
  if (value.type === "fixed") {
509
+ const newValue = multiplyNumericValue(
510
+ value.value,
511
+ factor
512
+ );
513
+ if (factor === parseInt(factor.toString()) || // e.g. 2 === int
514
+ 1 / factor === parseInt((1 / factor).toString())) {
515
+ return {
516
+ type: "fixed",
517
+ value: newValue
518
+ };
519
+ }
501
520
  return {
502
521
  type: "fixed",
503
- value: toRoundedDecimal(
504
- multiplyNumericValue(
505
- value.value,
506
- factor
507
- )
508
- )
522
+ value: toRoundedDecimal(newValue)
509
523
  };
510
524
  }
511
525
  return {
512
526
  type: "range",
513
- min: toRoundedDecimal(
514
- multiplyNumericValue(value.min, factor)
515
- ),
516
- max: toRoundedDecimal(
517
- multiplyNumericValue(value.max, factor)
518
- )
527
+ min: toRoundedDecimal(multiplyNumericValue(value.min, factor)),
528
+ max: toRoundedDecimal(multiplyNumericValue(value.max, factor))
519
529
  };
520
530
  }
521
531
  var convertQuantityValue = (value, def, targetDef) => {
@@ -523,6 +533,32 @@ var convertQuantityValue = (value, def, targetDef) => {
523
533
  const factor = def.toBase / targetDef.toBase;
524
534
  return multiplyQuantityValue(value, factor);
525
535
  };
536
+ function getDefaultQuantityValue() {
537
+ return { type: "fixed", value: { type: "decimal", value: 0 } };
538
+ }
539
+ function addQuantityValues(v1, v2) {
540
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
541
+ throw new CannotAddTextValueError();
542
+ }
543
+ if (v1.type === "fixed" && v2.type === "fixed") {
544
+ const res = addNumericValues(
545
+ v1.value,
546
+ v2.value
547
+ );
548
+ return { type: "fixed", value: res };
549
+ }
550
+ const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
551
+ const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
552
+ const newMin = addNumericValues(
553
+ r1.min,
554
+ r2.min
555
+ );
556
+ const newMax = addNumericValues(
557
+ r1.max,
558
+ r2.max
559
+ );
560
+ return { type: "range", min: newMin, max: newMax };
561
+ }
526
562
  function addQuantities(q1, q2) {
527
563
  const v1 = q1.value;
528
564
  const v2 = q2.value;
@@ -531,33 +567,14 @@ function addQuantities(q1, q2) {
531
567
  }
532
568
  const unit1Def = normalizeUnit(q1.unit);
533
569
  const unit2Def = normalizeUnit(q2.unit);
534
- const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
535
- if (val1.type === "fixed" && val2.type === "fixed") {
536
- const res = addNumericValues(
537
- val1.value,
538
- val2.value
539
- );
540
- return { value: { type: "fixed", value: res }, unit };
541
- }
542
- const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
543
- const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
544
- const newMin = addNumericValues(
545
- r1.min,
546
- r2.min
547
- );
548
- const newMax = addNumericValues(
549
- r1.max,
550
- r2.max
551
- );
552
- return { value: { type: "range", min: newMin, max: newMax }, unit };
553
- };
554
- if (q1.unit === "" && unit2Def) {
570
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({ value: addQuantityValues(val1, val2), unit });
571
+ if ((q1.unit === "" || q1.unit === void 0) && q2.unit !== void 0) {
555
572
  return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
556
573
  }
557
- if (q2.unit === "" && unit1Def) {
574
+ if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
558
575
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
559
576
  }
560
- if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
577
+ if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
561
578
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
562
579
  }
563
580
  if (unit1Def && unit2Def) {
@@ -587,14 +604,18 @@ function addQuantities(q1, q2) {
587
604
  throw new IncompatibleUnitsError(q1.unit, q2.unit);
588
605
  }
589
606
 
590
- // src/parser_helpers.ts
591
- function findOrPush(list, finder, creator) {
592
- let index = list.findIndex(finder);
593
- if (index === -1) {
594
- index = list.push(creator()) - 1;
607
+ // src/errors.ts
608
+ var ReferencedItemCannotBeRedefinedError = class extends Error {
609
+ constructor(item_type, item_name, new_modifier) {
610
+ super(
611
+ `The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
612
+ You can either remove the reference to create a new ${item_type} defined as ${new_modifier} or add the ${new_modifier} flag to the original definition of the ${item_type}`
613
+ );
614
+ this.name = "ReferencedItemCannotBeRedefinedError";
595
615
  }
596
- return index;
597
- }
616
+ };
617
+
618
+ // src/parser_helpers.ts
598
619
  function flushPendingNote(section, note) {
599
620
  if (note.length > 0) {
600
621
  section.content.push({ type: "note", note });
@@ -613,21 +634,28 @@ function flushPendingItems(section, items) {
613
634
  function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
614
635
  const { name, quantity, unit } = newIngredient;
615
636
  if (isReference) {
616
- const index = ingredients.findIndex(
637
+ const indexFind = ingredients.findIndex(
617
638
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
618
639
  );
619
- if (index === -1) {
640
+ if (indexFind === -1) {
620
641
  throw new Error(
621
642
  `Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
622
643
  );
623
644
  }
624
- const existingIngredient = ingredients[index];
645
+ const existingIngredient = ingredients[indexFind];
646
+ for (const flag of newIngredient.flags) {
647
+ if (!existingIngredient.flags.includes(flag)) {
648
+ throw new ReferencedItemCannotBeRedefinedError(
649
+ "ingredient",
650
+ existingIngredient.name,
651
+ flag
652
+ );
653
+ }
654
+ }
655
+ let quantityPartIndex = void 0;
625
656
  if (quantity !== void 0) {
626
657
  const currentQuantity = {
627
- value: existingIngredient.quantity ?? {
628
- type: "fixed",
629
- value: { type: "decimal", value: 0 }
630
- },
658
+ value: existingIngredient.quantity ?? getDefaultQuantityValue(),
631
659
  unit: existingIngredient.unit ?? ""
632
660
  };
633
661
  const newQuantity = { value: quantity, unit: unit ?? "" };
@@ -635,18 +663,35 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
635
663
  const total = addQuantities(currentQuantity, newQuantity);
636
664
  existingIngredient.quantity = total.value;
637
665
  existingIngredient.unit = total.unit || void 0;
666
+ if (existingIngredient.quantityParts) {
667
+ existingIngredient.quantityParts.push(
668
+ ...newIngredient.quantityParts
669
+ );
670
+ } else {
671
+ existingIngredient.quantityParts = newIngredient.quantityParts;
672
+ }
673
+ quantityPartIndex = existingIngredient.quantityParts.length - 1;
638
674
  } catch (e2) {
639
675
  if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
640
- return ingredients.push(newIngredient) - 1;
676
+ return {
677
+ ingredientIndex: ingredients.push(newIngredient) - 1,
678
+ quantityPartIndex: 0
679
+ };
641
680
  }
642
681
  }
643
682
  }
644
- return index;
683
+ return {
684
+ ingredientIndex: indexFind,
685
+ quantityPartIndex
686
+ };
645
687
  }
646
- return ingredients.push(newIngredient) - 1;
688
+ return {
689
+ ingredientIndex: ingredients.push(newIngredient) - 1,
690
+ quantityPartIndex: 0
691
+ };
647
692
  }
648
693
  function findAndUpsertCookware(cookware, newCookware, isReference) {
649
- const { name } = newCookware;
694
+ const { name, quantity } = newCookware;
650
695
  if (isReference) {
651
696
  const index = cookware.findIndex(
652
697
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
@@ -656,9 +701,55 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
656
701
  `Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
657
702
  );
658
703
  }
659
- return index;
704
+ const existingCookware = cookware[index];
705
+ for (const flag of newCookware.flags) {
706
+ if (!existingCookware.flags.includes(flag)) {
707
+ throw new ReferencedItemCannotBeRedefinedError(
708
+ "cookware",
709
+ existingCookware.name,
710
+ flag
711
+ );
712
+ }
713
+ }
714
+ let quantityPartIndex = void 0;
715
+ if (quantity !== void 0) {
716
+ if (!existingCookware.quantity) {
717
+ existingCookware.quantity = quantity;
718
+ existingCookware.quantityParts = newCookware.quantityParts;
719
+ quantityPartIndex = 0;
720
+ } else {
721
+ try {
722
+ existingCookware.quantity = addQuantityValues(
723
+ existingCookware.quantity,
724
+ quantity
725
+ );
726
+ if (!existingCookware.quantityParts) {
727
+ existingCookware.quantityParts = newCookware.quantityParts;
728
+ quantityPartIndex = 0;
729
+ } else {
730
+ quantityPartIndex = existingCookware.quantityParts.push(
731
+ ...newCookware.quantityParts
732
+ ) - 1;
733
+ }
734
+ } catch (e2) {
735
+ if (e2 instanceof CannotAddTextValueError) {
736
+ return {
737
+ cookwareIndex: cookware.push(newCookware) - 1,
738
+ quantityPartIndex: 0
739
+ };
740
+ }
741
+ }
742
+ }
743
+ }
744
+ return {
745
+ cookwareIndex: index,
746
+ quantityPartIndex
747
+ };
660
748
  }
661
- return cookware.push(newCookware) - 1;
749
+ return {
750
+ cookwareIndex: cookware.push(newCookware) - 1,
751
+ quantityPartIndex: quantity ? 0 : void 0
752
+ };
662
753
  }
663
754
  var parseFixedValue = (input_str) => {
664
755
  if (!numberLikeRegex.test(input_str)) {
@@ -690,14 +781,12 @@ function parseSimpleMetaVar(content, varName) {
690
781
  return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
691
782
  }
692
783
  function parseScalingMetaVar(content, varName) {
693
- const varMatch = content.match(
694
- new RegExp(`^${varName}:[\\t ]*(([^,\\n]*),? ?(?:.*)?)`, "m")
695
- );
784
+ const varMatch = content.match(scalingMetaValueRegex(varName));
696
785
  if (!varMatch) return void 0;
697
786
  if (isNaN(Number(varMatch[2]?.trim()))) {
698
787
  throw new Error("Scaling variables should be numbers");
699
788
  }
700
- return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];
789
+ return [Number(varMatch[2]?.trim()), varMatch[1].trim()];
701
790
  }
702
791
  function parseListMetaVar(content, varName) {
703
792
  const listMatch = content.match(
@@ -748,7 +837,7 @@ function extractMetadata(content) {
748
837
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
749
838
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
750
839
  }
751
- for (const metaVar of ["servings", "yield", "serves"]) {
840
+ for (const metaVar of ["serves", "yield", "servings"]) {
752
841
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
753
842
  if (scalingMetaValue && scalingMetaValue[1]) {
754
843
  metadata[metaVar] = scalingMetaValue[1];
@@ -770,32 +859,31 @@ var Recipe = class _Recipe {
770
859
  */
771
860
  constructor(content) {
772
861
  /**
773
- * The recipe's metadata.
774
- * @see {@link Metadata}
862
+ * The parsed recipe metadata.
775
863
  */
776
864
  __publicField(this, "metadata", {});
777
865
  /**
778
- * The recipe's ingredients.
779
- * @see {@link Ingredient}
866
+ * The parsed recipe ingredients.
780
867
  */
781
868
  __publicField(this, "ingredients", []);
782
869
  /**
783
- * The recipe's sections.
784
- * @see {@link Section}
870
+ * The parsed recipe sections.
785
871
  */
786
872
  __publicField(this, "sections", []);
787
873
  /**
788
- * The recipe's cookware.
789
- * @see {@link Cookware}
874
+ * The parsed recipe cookware.
790
875
  */
791
876
  __publicField(this, "cookware", []);
792
877
  /**
793
- * The recipe's timers.
794
- * @see {@link Timer}
878
+ * The parsed recipe timers.
795
879
  */
796
880
  __publicField(this, "timers", []);
797
881
  /**
798
- * The recipe's servings. Used for scaling
882
+ * The parsed recipe servings. Used for scaling. Parsed from one of
883
+ * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
884
+ * metadata fields.
885
+ *
886
+ * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
799
887
  */
800
888
  __publicField(this, "servings");
801
889
  if (content) {
@@ -865,15 +953,28 @@ var Recipe = class _Recipe {
865
953
  }
866
954
  const groups = match.groups;
867
955
  if (groups.mIngredientName || groups.sIngredientName) {
868
- const name = groups.mIngredientName || groups.sIngredientName;
956
+ let name = groups.mIngredientName || groups.sIngredientName;
957
+ const scalableQuantity = (groups.mIngredientQuantityModifier || groups.sIngredientQuantityModifier) !== "=";
869
958
  const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
870
- const units2 = groups.mIngredientUnits || groups.sIngredientUnits;
959
+ const unit = groups.mIngredientUnit || groups.sIngredientUnit;
871
960
  const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
872
- const modifier = groups.mIngredientModifier || groups.sIngredientModifier;
873
- const optional = modifier === "?";
874
- const hidden = modifier === "-";
875
- const reference = modifier === "&";
876
- const isRecipe = modifier === "@";
961
+ const modifiers = groups.mIngredientModifiers || groups.sIngredientModifiers;
962
+ const reference = modifiers !== void 0 && modifiers.includes("&");
963
+ const flags = [];
964
+ if (modifiers !== void 0 && modifiers.includes("?")) {
965
+ flags.push("optional");
966
+ }
967
+ if (modifiers !== void 0 && modifiers.includes("-")) {
968
+ flags.push("hidden");
969
+ }
970
+ if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
971
+ flags.push("recipe");
972
+ }
973
+ let extras = void 0;
974
+ if (flags.includes("recipe")) {
975
+ extras = { path: `${name}.cook` };
976
+ name = name.substring(name.lastIndexOf("/") + 1);
977
+ }
877
978
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
878
979
  const aliasMatch = name.match(ingredientAliasRegex);
879
980
  let listName, displayName;
@@ -884,50 +985,70 @@ var Recipe = class _Recipe {
884
985
  listName = name;
885
986
  displayName = name;
886
987
  }
887
- const idxInList = findAndUpsertIngredient(
988
+ const newIngredient = {
989
+ name: listName,
990
+ quantity,
991
+ quantityParts: quantity ? [
992
+ {
993
+ value: quantity,
994
+ unit,
995
+ scalable: scalableQuantity
996
+ }
997
+ ] : void 0,
998
+ unit,
999
+ preparation,
1000
+ flags
1001
+ };
1002
+ if (extras) {
1003
+ newIngredient.extras = extras;
1004
+ }
1005
+ const idxsInList = findAndUpsertIngredient(
888
1006
  this.ingredients,
889
- {
890
- name: listName,
891
- quantity,
892
- unit: units2,
893
- optional,
894
- hidden,
895
- preparation,
896
- isRecipe
897
- },
1007
+ newIngredient,
898
1008
  reference
899
1009
  );
900
1010
  const newItem = {
901
1011
  type: "ingredient",
902
- value: idxInList,
903
- itemQuantity: quantity,
904
- itemUnit: units2,
1012
+ index: idxsInList.ingredientIndex,
905
1013
  displayName
906
1014
  };
1015
+ if (idxsInList.quantityPartIndex !== void 0) {
1016
+ newItem.quantityPartIndex = idxsInList.quantityPartIndex;
1017
+ }
907
1018
  items.push(newItem);
908
1019
  } else if (groups.mCookwareName || groups.sCookwareName) {
909
1020
  const name = groups.mCookwareName || groups.sCookwareName;
910
- const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
1021
+ const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
911
1022
  const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
912
- const optional = modifier === "?";
913
- const hidden = modifier === "-";
914
- const reference = modifier === "&";
1023
+ const reference = modifiers !== void 0 && modifiers.includes("&");
1024
+ const flags = [];
1025
+ if (modifiers !== void 0 && modifiers.includes("?")) {
1026
+ flags.push("optional");
1027
+ }
1028
+ if (modifiers !== void 0 && modifiers.includes("-")) {
1029
+ flags.push("hidden");
1030
+ }
915
1031
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
916
- const idxInList = findAndUpsertCookware(
1032
+ const idxsInList = findAndUpsertCookware(
917
1033
  this.cookware,
918
- { name, quantity, optional, hidden },
1034
+ {
1035
+ name,
1036
+ quantity,
1037
+ quantityParts: quantity ? [quantity] : void 0,
1038
+ flags
1039
+ },
919
1040
  reference
920
1041
  );
921
1042
  items.push({
922
1043
  type: "cookware",
923
- value: idxInList,
924
- itemQuantity: quantity
1044
+ index: idxsInList.cookwareIndex,
1045
+ quantityPartIndex: idxsInList.quantityPartIndex
925
1046
  });
926
- } else if (groups.timerQuantity !== void 0) {
1047
+ } else {
927
1048
  const durationStr = groups.timerQuantity.trim();
928
- const unit = (groups.timerUnits || "").trim();
1049
+ const unit = (groups.timerUnit || "").trim();
929
1050
  if (!unit) {
930
- throw new Error("Timer missing units");
1051
+ throw new Error("Timer missing unit");
931
1052
  }
932
1053
  const name = groups.timerName || void 0;
933
1054
  const duration = parseQuantityInput(durationStr);
@@ -936,12 +1057,7 @@ var Recipe = class _Recipe {
936
1057
  duration,
937
1058
  unit
938
1059
  };
939
- const idxInList = findOrPush(
940
- this.timers,
941
- (t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
942
- () => timerObj
943
- );
944
- items.push({ type: "timer", value: idxInList });
1060
+ items.push({ type: "timer", index: this.timers.push(timerObj) - 1 });
945
1061
  }
946
1062
  cursor = idx + match[0].length;
947
1063
  }
@@ -957,9 +1073,12 @@ var Recipe = class _Recipe {
957
1073
  }
958
1074
  }
959
1075
  /**
960
- * Scales the recipe to a new number of servings.
1076
+ * Scales the recipe to a new number of servings. In practice, it calls
1077
+ * {@link Recipe.scaleBy | scaleBy} with a factor corresponding to the ratio between `newServings`
1078
+ * and the recipe's {@link Recipe.servings | servings} value.
961
1079
  * @param newServings - The new number of servings.
962
1080
  * @returns A new Recipe instance with the scaled ingredients.
1081
+ * @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
963
1082
  */
964
1083
  scaleTo(newServings) {
965
1084
  const originalServings = this.getServings();
@@ -981,30 +1100,57 @@ var Recipe = class _Recipe {
981
1100
  throw new Error("Error scaling recipe: no initial servings value set");
982
1101
  }
983
1102
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
984
- if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
985
- ingredient.quantity = multiplyQuantityValue(
986
- ingredient.quantity,
987
- factor
1103
+ if (ingredient.quantityParts) {
1104
+ ingredient.quantityParts = ingredient.quantityParts.map(
1105
+ (quantityPart) => {
1106
+ if (quantityPart.value.type === "fixed" && quantityPart.value.value.type === "text") {
1107
+ return quantityPart;
1108
+ }
1109
+ return {
1110
+ ...quantityPart,
1111
+ value: multiplyQuantityValue(
1112
+ quantityPart.value,
1113
+ quantityPart.scalable ? factor : 1
1114
+ )
1115
+ };
1116
+ }
988
1117
  );
1118
+ if (ingredient.quantityParts.length === 1) {
1119
+ ingredient.quantity = ingredient.quantityParts[0].value;
1120
+ ingredient.unit = ingredient.quantityParts[0].unit;
1121
+ } else {
1122
+ const totalQuantity = ingredient.quantityParts.reduce(
1123
+ (acc, val) => addQuantities(acc, { value: val.value, unit: val.unit }),
1124
+ { value: getDefaultQuantityValue() }
1125
+ );
1126
+ ingredient.quantity = totalQuantity.value;
1127
+ ingredient.unit = totalQuantity.unit;
1128
+ }
989
1129
  }
990
1130
  return ingredient;
991
1131
  }).filter((ingredient) => ingredient.quantity !== null);
992
1132
  newRecipe.servings = originalServings * factor;
993
1133
  if (newRecipe.metadata.servings && this.metadata.servings) {
994
- const servingsValue = parseFloat(this.metadata.servings);
995
- if (!isNaN(servingsValue)) {
1134
+ if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
1135
+ const servingsValue = parseFloat(
1136
+ String(this.metadata.servings).replace(",", ".")
1137
+ );
996
1138
  newRecipe.metadata.servings = String(servingsValue * factor);
997
1139
  }
998
1140
  }
999
1141
  if (newRecipe.metadata.yield && this.metadata.yield) {
1000
- const yieldValue = parseFloat(this.metadata.yield);
1001
- if (!isNaN(yieldValue)) {
1142
+ if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
1143
+ const yieldValue = parseFloat(
1144
+ String(this.metadata.yield).replace(",", ".")
1145
+ );
1002
1146
  newRecipe.metadata.yield = String(yieldValue * factor);
1003
1147
  }
1004
1148
  }
1005
1149
  if (newRecipe.metadata.serves && this.metadata.serves) {
1006
- const servesValue = parseFloat(this.metadata.serves);
1007
- if (!isNaN(servesValue)) {
1150
+ if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
1151
+ const servesValue = parseFloat(
1152
+ String(this.metadata.serves).replace(",", ".")
1153
+ );
1008
1154
  newRecipe.metadata.serves = String(servesValue * factor);
1009
1155
  }
1010
1156
  }
@@ -1028,9 +1174,13 @@ var Recipe = class _Recipe {
1028
1174
  clone() {
1029
1175
  const newRecipe = new _Recipe();
1030
1176
  newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
1031
- newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));
1177
+ newRecipe.ingredients = JSON.parse(
1178
+ JSON.stringify(this.ingredients)
1179
+ );
1032
1180
  newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
1033
- newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));
1181
+ newRecipe.cookware = JSON.parse(
1182
+ JSON.stringify(this.cookware)
1183
+ );
1034
1184
  newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
1035
1185
  newRecipe.servings = this.servings;
1036
1186
  return newRecipe;
@@ -1040,32 +1190,28 @@ var Recipe = class _Recipe {
1040
1190
  // src/classes/shopping_list.ts
1041
1191
  var ShoppingList = class {
1042
1192
  /**
1043
- * Creates a new ShoppingList instance.
1044
- * @param aisle_config_str - The aisle configuration to parse.
1193
+ * Creates a new ShoppingList instance
1194
+ * @param category_config_str - The category configuration to parse.
1045
1195
  */
1046
- constructor(aisle_config_str) {
1196
+ constructor(category_config_str) {
1047
1197
  /**
1048
1198
  * The ingredients in the shopping list.
1049
- * @see {@link Ingredient}
1050
1199
  */
1051
1200
  __publicField(this, "ingredients", []);
1052
1201
  /**
1053
1202
  * The recipes in the shopping list.
1054
- * @see {@link AddedRecipe}
1055
1203
  */
1056
1204
  __publicField(this, "recipes", []);
1057
1205
  /**
1058
- * The aisle configuration for the shopping list.
1059
- * @see {@link AisleConfig}
1206
+ * The category configuration for the shopping list.
1060
1207
  */
1061
- __publicField(this, "aisle_config");
1208
+ __publicField(this, "category_config");
1062
1209
  /**
1063
1210
  * The categorized ingredients in the shopping list.
1064
- * @see {@link CategorizedIngredients}
1065
1211
  */
1066
1212
  __publicField(this, "categories");
1067
- if (aisle_config_str) {
1068
- this.set_aisle_config(aisle_config_str);
1213
+ if (category_config_str) {
1214
+ this.set_category_config(category_config_str);
1069
1215
  }
1070
1216
  }
1071
1217
  calculate_ingredients() {
@@ -1073,7 +1219,7 @@ var ShoppingList = class {
1073
1219
  for (const { recipe, factor } of this.recipes) {
1074
1220
  const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);
1075
1221
  for (const ingredient of scaledRecipe.ingredients) {
1076
- if (ingredient.hidden) {
1222
+ if (ingredient.flags && ingredient.flags.includes("hidden")) {
1077
1223
  continue;
1078
1224
  }
1079
1225
  const existingIngredient = this.ingredients.find(
@@ -1081,8 +1227,8 @@ var ShoppingList = class {
1081
1227
  );
1082
1228
  let addSeparate = false;
1083
1229
  try {
1084
- if (existingIngredient) {
1085
- if (existingIngredient.quantity && ingredient.quantity) {
1230
+ if (existingIngredient && ingredient.quantity) {
1231
+ if (existingIngredient.quantity) {
1086
1232
  const newQuantity = addQuantities(
1087
1233
  {
1088
1234
  value: existingIngredient.quantity,
@@ -1097,7 +1243,7 @@ var ShoppingList = class {
1097
1243
  if (newQuantity.unit) {
1098
1244
  existingIngredient.unit = newQuantity.unit;
1099
1245
  }
1100
- } else if (ingredient.quantity) {
1246
+ } else {
1101
1247
  existingIngredient.quantity = ingredient.quantity;
1102
1248
  if (ingredient.unit) {
1103
1249
  existingIngredient.unit = ingredient.unit;
@@ -1121,7 +1267,8 @@ var ShoppingList = class {
1121
1267
  }
1122
1268
  }
1123
1269
  /**
1124
- * Adds a recipe to the shopping list.
1270
+ * Adds a recipe to the shopping list, then automatically
1271
+ * recalculates the quantities and recategorize the ingredients.
1125
1272
  * @param recipe - The recipe to add.
1126
1273
  * @param factor - The factor to scale the recipe by.
1127
1274
  */
@@ -1131,7 +1278,8 @@ var ShoppingList = class {
1131
1278
  this.categorize();
1132
1279
  }
1133
1280
  /**
1134
- * Removes a recipe from the shopping list.
1281
+ * Removes a recipe from the shopping list, then automatically
1282
+ * recalculates the quantities and recategorize the ingredients.s
1135
1283
  * @param index - The index of the recipe to remove.
1136
1284
  */
1137
1285
  remove_recipe(index) {
@@ -1143,31 +1291,35 @@ var ShoppingList = class {
1143
1291
  this.categorize();
1144
1292
  }
1145
1293
  /**
1146
- * Sets the aisle configuration for the shopping list.
1147
- * @param config - The aisle configuration to parse.
1294
+ * Sets the category configuration for the shopping list
1295
+ * and automatically categorize current ingredients from the list.
1296
+ * @param config - The category configuration to parse.
1148
1297
  */
1149
- set_aisle_config(config) {
1150
- this.aisle_config = new AisleConfig(config);
1298
+ set_category_config(config) {
1299
+ if (typeof config === "string")
1300
+ this.category_config = new CategoryConfig(config);
1301
+ else if (config instanceof CategoryConfig) this.category_config = config;
1302
+ else throw new Error("Invalid category configuration");
1151
1303
  this.categorize();
1152
1304
  }
1153
1305
  /**
1154
1306
  * Categorizes the ingredients in the shopping list
1155
- * Will use the aisle config if any, otherwise all ingredients will be placed in the "other" category
1307
+ * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
1156
1308
  */
1157
1309
  categorize() {
1158
- if (!this.aisle_config) {
1310
+ if (!this.category_config) {
1159
1311
  this.categories = { other: this.ingredients };
1160
1312
  return;
1161
1313
  }
1162
1314
  const categories = { other: [] };
1163
- for (const category of this.aisle_config.categories) {
1315
+ for (const category of this.category_config.categories) {
1164
1316
  categories[category.name] = [];
1165
1317
  }
1166
1318
  for (const ingredient of this.ingredients) {
1167
1319
  let found = false;
1168
- for (const category of this.aisle_config.categories) {
1169
- for (const aisleIngredient of category.ingredients) {
1170
- if (aisleIngredient.aliases.includes(ingredient.name)) {
1320
+ for (const category of this.category_config.categories) {
1321
+ for (const categoryIngredient of category.ingredients) {
1322
+ if (categoryIngredient.aliases.includes(ingredient.name)) {
1171
1323
  categories[category.name].push(ingredient);
1172
1324
  found = true;
1173
1325
  break;
@@ -1185,9 +1337,13 @@ var ShoppingList = class {
1185
1337
  }
1186
1338
  };
1187
1339
  export {
1188
- AisleConfig,
1340
+ CategoryConfig,
1189
1341
  Recipe,
1190
1342
  Section,
1191
1343
  ShoppingList
1192
1344
  };
1345
+ /* v8 ignore else -- @preserve */
1346
+ /* v8 ignore else -- expliciting error types -- @preserve */
1347
+ /* v8 ignore else -- expliciting error type -- @preserve */
1348
+ /* v8 ignore else -- only set unit if it is given -- @preserve */
1193
1349
  //# sourceMappingURL=index.js.map