@tmlmt/cooklang-parser 3.0.0-alpha.4 → 3.0.0-alpha.7

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.d.cts CHANGED
@@ -86,6 +86,10 @@ declare class Recipe {
86
86
  * The parsed recipe timers.
87
87
  */
88
88
  timers: Timer[];
89
+ /**
90
+ * The parsed arbitrary quantities.
91
+ */
92
+ arbitraries: ArbitraryScalable[];
89
93
  /**
90
94
  * The parsed recipe servings. Used for scaling. Parsed from one of
91
95
  * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
@@ -112,6 +116,19 @@ declare class Recipe {
112
116
  * @param content - The recipe content to parse.
113
117
  */
114
118
  constructor(content?: string);
119
+ /**
120
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
121
+ * @private
122
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
123
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
124
+ */
125
+ private _parseArbitraryScalable;
126
+ /**
127
+ * Parses text for arbitrary scalables and returns NoteItem array.
128
+ * @param text - The text to parse for arbitrary scalables.
129
+ * @returns Array of NoteItem (text and arbitrary scalable items).
130
+ */
131
+ private _parseNoteText;
115
132
  private _parseQuantityRecursive;
116
133
  private _parseIngredientWithAlternativeRecursive;
117
134
  private _parseIngredientWithGroupKey;
@@ -371,7 +388,7 @@ interface AlternativeIngredientRef {
371
388
  /** The index of the alternative ingredient within the {@link Recipe.ingredients} array. */
372
389
  index: number;
373
390
  /** The quantities of the alternative ingredient. Multiple entries when units are incompatible. */
374
- alternativeQuantities?: QuantityWithPlainUnit[];
391
+ quantities?: QuantityWithPlainUnit[];
375
392
  }
376
393
  /**
377
394
  * Represents a group of summed quantities for an ingredient, optionally with alternatives.
@@ -379,18 +396,12 @@ interface AlternativeIngredientRef {
379
396
  * When units are incompatible, separate IngredientQuantityGroup entries are created instead of merging.
380
397
  * @category Types
381
398
  */
382
- interface IngredientQuantityGroup {
399
+ interface IngredientQuantityGroup extends QuantityWithPlainUnit {
383
400
  /**
384
401
  * References to alternative ingredients for this quantity group.
385
402
  * If undefined, this group has no alternatives.
386
403
  */
387
404
  alternatives?: AlternativeIngredientRef[];
388
- /**
389
- * The summed quantity for this group, potentially with equivalents.
390
- * OR groups from addEquivalentsAndSimplify are converted back to QuantityWithPlainUnit
391
- * (first entry as main, rest as equivalents).
392
- */
393
- groupQuantity: QuantityWithPlainUnit;
394
405
  }
395
406
  /**
396
407
  * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
@@ -525,12 +536,12 @@ interface IngredientItem {
525
536
  * @category Types
526
537
  */
527
538
  interface RecipeAlternatives {
528
- /** Map of choices that can be made at Ingredient Item level
529
- * - Keys are the Ingredient Item IDs (e.g. "ingredient-item-2")
539
+ /** Map of choices that can be made at Ingredient StepItem level
540
+ * - Keys are the Ingredient StepItem IDs (e.g. "ingredient-item-2")
530
541
  * - Values are arrays of IngredientAlternative objects representing the choices available for that item
531
542
  */
532
543
  ingredientItems: Map<string, IngredientAlternative[]>;
533
- /** Map of choices that can be made for Grouped Ingredient Item's
544
+ /** Map of choices that can be made for Grouped Ingredient StepItem's
534
545
  * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
535
546
  * - Values are arrays of IngredientAlternative objects representing the choices available for that group
536
547
  */
@@ -542,9 +553,9 @@ interface RecipeAlternatives {
542
553
  * @category Types
543
554
  */
544
555
  interface RecipeChoices {
545
- /** Map of choices that can be made at Ingredient Item level */
556
+ /** Map of choices that can be made at Ingredient StepItem level */
546
557
  ingredientItems?: Map<string, number>;
547
- /** Map of choices that can be made for Grouped Ingredient Item's */
558
+ /** Map of choices that can be made for Grouped Ingredient StepItem's */
548
559
  ingredientGroups?: Map<string, number>;
549
560
  }
550
561
  /**
@@ -591,11 +602,33 @@ interface TextItem {
591
602
  /** The content of the text item. */
592
603
  value: string;
593
604
  }
605
+ /**
606
+ * Represents an arbitrary scalable quantity in a recipe.
607
+ * @category Types
608
+ */
609
+ interface ArbitraryScalable {
610
+ /** The name of the arbitrary scalable quantity. */
611
+ name?: string;
612
+ /** The numerical value of the arbitrary scalable quantity. */
613
+ quantity: FixedNumericValue;
614
+ /** The unit of the arbitrary scalable quantity. */
615
+ unit?: string;
616
+ }
617
+ /**
618
+ * Represents an arbitrary scalable quantity item in a recipe step.
619
+ * @category Types
620
+ */
621
+ interface ArbitraryScalableItem {
622
+ /** The type of the item. */
623
+ type: "arbitrary";
624
+ /** The index of the arbitrary scalable quantity, within the {@link Recipe.arbitraries | list of arbitrary scalable quantities} */
625
+ index: number;
626
+ }
594
627
  /**
595
628
  * Represents an item in a recipe step.
596
629
  * @category Types
597
630
  */
598
- type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
631
+ type StepItem = TextItem | IngredientItem | CookwareItem | TimerItem | ArbitraryScalableItem;
599
632
  /**
600
633
  * Represents a step in a recipe.
601
634
  * @category Types
@@ -603,16 +636,21 @@ type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
603
636
  interface Step {
604
637
  type: "step";
605
638
  /** The items in the step. */
606
- items: Item[];
639
+ items: StepItem[];
607
640
  }
641
+ /**
642
+ * Represents an item in a note (can be text or arbitrary scalable).
643
+ * @category Types
644
+ */
645
+ type NoteItem = TextItem | ArbitraryScalableItem;
608
646
  /**
609
647
  * Represents a note in a recipe.
610
648
  * @category Types
611
649
  */
612
650
  interface Note {
613
651
  type: "note";
614
- /** The content of the note. */
615
- note: string;
652
+ /** The items in the note. */
653
+ items: NoteItem[];
616
654
  }
617
655
  /**
618
656
  * Represents a possible state modifier or other flag for cookware used in a recipe
@@ -1232,4 +1270,4 @@ declare class NoShoppingListForCartError extends Error {
1232
1270
  constructor();
1233
1271
  }
1234
1272
 
1235
- export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type ComputedIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FractionValue, type Ingredient, type IngredientAlternative, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientItemQuantity, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type Item, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type Metadata, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, ProductCatalog, type ProductMatch, type ProductMisMatch, type ProductOption, type ProductOptionBase, type ProductOptionCore, type ProductSelection, type ProductSize, type QuantityBase, type QuantityWithExtendedUnit, type QuantityWithPlainUnit, type QuantityWithUnitDef, type QuantityWithUnitLike, type Range, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType };
1273
+ export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type ArbitraryScalable, type ArbitraryScalableItem, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type ComputedIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FractionValue, type Ingredient, type IngredientAlternative, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientItemQuantity, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type Metadata, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, type NoteItem, ProductCatalog, type ProductMatch, type ProductMisMatch, type ProductOption, type ProductOptionBase, type ProductOptionCore, type ProductSelection, type ProductSize, type QuantityBase, type QuantityWithExtendedUnit, type QuantityWithPlainUnit, type QuantityWithUnitDef, type QuantityWithUnitLike, type Range, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type Step, type StepItem, type TextItem, type TextValue, type Timer, type TimerItem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType };
package/dist/index.d.ts CHANGED
@@ -86,6 +86,10 @@ declare class Recipe {
86
86
  * The parsed recipe timers.
87
87
  */
88
88
  timers: Timer[];
89
+ /**
90
+ * The parsed arbitrary quantities.
91
+ */
92
+ arbitraries: ArbitraryScalable[];
89
93
  /**
90
94
  * The parsed recipe servings. Used for scaling. Parsed from one of
91
95
  * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
@@ -112,6 +116,19 @@ declare class Recipe {
112
116
  * @param content - The recipe content to parse.
113
117
  */
114
118
  constructor(content?: string);
119
+ /**
120
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
121
+ * @private
122
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
123
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
124
+ */
125
+ private _parseArbitraryScalable;
126
+ /**
127
+ * Parses text for arbitrary scalables and returns NoteItem array.
128
+ * @param text - The text to parse for arbitrary scalables.
129
+ * @returns Array of NoteItem (text and arbitrary scalable items).
130
+ */
131
+ private _parseNoteText;
115
132
  private _parseQuantityRecursive;
116
133
  private _parseIngredientWithAlternativeRecursive;
117
134
  private _parseIngredientWithGroupKey;
@@ -371,7 +388,7 @@ interface AlternativeIngredientRef {
371
388
  /** The index of the alternative ingredient within the {@link Recipe.ingredients} array. */
372
389
  index: number;
373
390
  /** The quantities of the alternative ingredient. Multiple entries when units are incompatible. */
374
- alternativeQuantities?: QuantityWithPlainUnit[];
391
+ quantities?: QuantityWithPlainUnit[];
375
392
  }
376
393
  /**
377
394
  * Represents a group of summed quantities for an ingredient, optionally with alternatives.
@@ -379,18 +396,12 @@ interface AlternativeIngredientRef {
379
396
  * When units are incompatible, separate IngredientQuantityGroup entries are created instead of merging.
380
397
  * @category Types
381
398
  */
382
- interface IngredientQuantityGroup {
399
+ interface IngredientQuantityGroup extends QuantityWithPlainUnit {
383
400
  /**
384
401
  * References to alternative ingredients for this quantity group.
385
402
  * If undefined, this group has no alternatives.
386
403
  */
387
404
  alternatives?: AlternativeIngredientRef[];
388
- /**
389
- * The summed quantity for this group, potentially with equivalents.
390
- * OR groups from addEquivalentsAndSimplify are converted back to QuantityWithPlainUnit
391
- * (first entry as main, rest as equivalents).
392
- */
393
- groupQuantity: QuantityWithPlainUnit;
394
405
  }
395
406
  /**
396
407
  * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
@@ -525,12 +536,12 @@ interface IngredientItem {
525
536
  * @category Types
526
537
  */
527
538
  interface RecipeAlternatives {
528
- /** Map of choices that can be made at Ingredient Item level
529
- * - Keys are the Ingredient Item IDs (e.g. "ingredient-item-2")
539
+ /** Map of choices that can be made at Ingredient StepItem level
540
+ * - Keys are the Ingredient StepItem IDs (e.g. "ingredient-item-2")
530
541
  * - Values are arrays of IngredientAlternative objects representing the choices available for that item
531
542
  */
532
543
  ingredientItems: Map<string, IngredientAlternative[]>;
533
- /** Map of choices that can be made for Grouped Ingredient Item's
544
+ /** Map of choices that can be made for Grouped Ingredient StepItem's
534
545
  * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
535
546
  * - Values are arrays of IngredientAlternative objects representing the choices available for that group
536
547
  */
@@ -542,9 +553,9 @@ interface RecipeAlternatives {
542
553
  * @category Types
543
554
  */
544
555
  interface RecipeChoices {
545
- /** Map of choices that can be made at Ingredient Item level */
556
+ /** Map of choices that can be made at Ingredient StepItem level */
546
557
  ingredientItems?: Map<string, number>;
547
- /** Map of choices that can be made for Grouped Ingredient Item's */
558
+ /** Map of choices that can be made for Grouped Ingredient StepItem's */
548
559
  ingredientGroups?: Map<string, number>;
549
560
  }
550
561
  /**
@@ -591,11 +602,33 @@ interface TextItem {
591
602
  /** The content of the text item. */
592
603
  value: string;
593
604
  }
605
+ /**
606
+ * Represents an arbitrary scalable quantity in a recipe.
607
+ * @category Types
608
+ */
609
+ interface ArbitraryScalable {
610
+ /** The name of the arbitrary scalable quantity. */
611
+ name?: string;
612
+ /** The numerical value of the arbitrary scalable quantity. */
613
+ quantity: FixedNumericValue;
614
+ /** The unit of the arbitrary scalable quantity. */
615
+ unit?: string;
616
+ }
617
+ /**
618
+ * Represents an arbitrary scalable quantity item in a recipe step.
619
+ * @category Types
620
+ */
621
+ interface ArbitraryScalableItem {
622
+ /** The type of the item. */
623
+ type: "arbitrary";
624
+ /** The index of the arbitrary scalable quantity, within the {@link Recipe.arbitraries | list of arbitrary scalable quantities} */
625
+ index: number;
626
+ }
594
627
  /**
595
628
  * Represents an item in a recipe step.
596
629
  * @category Types
597
630
  */
598
- type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
631
+ type StepItem = TextItem | IngredientItem | CookwareItem | TimerItem | ArbitraryScalableItem;
599
632
  /**
600
633
  * Represents a step in a recipe.
601
634
  * @category Types
@@ -603,16 +636,21 @@ type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
603
636
  interface Step {
604
637
  type: "step";
605
638
  /** The items in the step. */
606
- items: Item[];
639
+ items: StepItem[];
607
640
  }
641
+ /**
642
+ * Represents an item in a note (can be text or arbitrary scalable).
643
+ * @category Types
644
+ */
645
+ type NoteItem = TextItem | ArbitraryScalableItem;
608
646
  /**
609
647
  * Represents a note in a recipe.
610
648
  * @category Types
611
649
  */
612
650
  interface Note {
613
651
  type: "note";
614
- /** The content of the note. */
615
- note: string;
652
+ /** The items in the note. */
653
+ items: NoteItem[];
616
654
  }
617
655
  /**
618
656
  * Represents a possible state modifier or other flag for cookware used in a recipe
@@ -1232,4 +1270,4 @@ declare class NoShoppingListForCartError extends Error {
1232
1270
  constructor();
1233
1271
  }
1234
1272
 
1235
- export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type ComputedIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FractionValue, type Ingredient, type IngredientAlternative, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientItemQuantity, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type Item, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type Metadata, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, ProductCatalog, type ProductMatch, type ProductMisMatch, type ProductOption, type ProductOptionBase, type ProductOptionCore, type ProductSelection, type ProductSize, type QuantityBase, type QuantityWithExtendedUnit, type QuantityWithPlainUnit, type QuantityWithUnitDef, type QuantityWithUnitLike, type Range, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType };
1273
+ export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type ArbitraryScalable, type ArbitraryScalableItem, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type ComputedIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FractionValue, type Ingredient, type IngredientAlternative, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientItemQuantity, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type Metadata, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, type NoteItem, ProductCatalog, type ProductMatch, type ProductMisMatch, type ProductOption, type ProductOptionBase, type ProductOptionCore, type ProductSelection, type ProductSize, type QuantityBase, type QuantityWithExtendedUnit, type QuantityWithPlainUnit, type QuantityWithUnitDef, type QuantityWithUnitLike, type Range, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type Step, type StepItem, type TextItem, type TextValue, type Timer, type TimerItem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType };
package/dist/index.js CHANGED
@@ -271,17 +271,19 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
271
271
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
272
272
  var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
273
273
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
274
- var quantityAlternativeRegex = d().startNamedGroup("ingredientQuantityValue").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("ingredientUnit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("ingredientAltQuantity").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
274
+ var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
275
275
  var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
276
276
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
277
277
  var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
278
278
  var timerRegex = 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();
279
+ var arbitraryScalableRegex = d().literal("{{").startGroup().startNamedGroup("arbitraryName").notAnyOf("}:%").oneOrMore().endGroup().literal(":").endGroup().optional().startNamedGroup("arbitraryQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}}").toRegExp();
279
280
  var tokensRegex = new RegExp(
280
281
  [
281
282
  ingredientWithGroupKeyRegex,
282
283
  ingredientWithAlternativeRegex,
283
284
  cookwareRegex,
284
- timerRegex
285
+ timerRegex,
286
+ arbitraryScalableRegex
285
287
  ].map((r2) => r2.source).join("|"),
286
288
  "gu"
287
289
  );
@@ -513,8 +515,8 @@ function multiplyQuantityValue(value, factor) {
513
515
  value.value,
514
516
  Big(factor)
515
517
  );
516
- if (factor === parseInt(factor.toString()) || // e.g. 2 === int
517
- Big(1).div(factor).toNumber() === parseInt(Big(1).div(factor).toString())) {
518
+ if (newValue.type === "fraction" && (Big(factor).toNumber() === parseInt(Big(factor).toString()) || // e.g. 2 === int
519
+ Big(1).div(factor).toNumber() === parseInt(Big(1).div(factor).toString()))) {
518
520
  return {
519
521
  type: "fixed",
520
522
  value: newValue
@@ -601,8 +603,10 @@ var IncompatibleUnitsError = class extends Error {
601
603
  }
602
604
  };
603
605
  var InvalidQuantityFormat = class extends Error {
604
- constructor(value) {
605
- super(`Invalid quantity format found in: ${value}`);
606
+ constructor(value, extra) {
607
+ super(
608
+ `Invalid quantity format found in: ${value}${extra ? ` (${extra})` : ""}`
609
+ );
606
610
  this.name = "InvalidQuantityFormat";
607
611
  }
608
612
  };
@@ -792,7 +796,7 @@ var flattenPlainUnitGroup = (summed) => {
792
796
  }
793
797
  ];
794
798
  } else {
795
- return andEntries.map((entry) => ({ groupQuantity: entry }));
799
+ return andEntries;
796
800
  }
797
801
  }
798
802
  const simpleEntries = entries.filter(
@@ -806,12 +810,10 @@ var flattenPlainUnitGroup = (summed) => {
806
810
  if (simpleEntries.length > 1) {
807
811
  result.equivalents = simpleEntries.slice(1);
808
812
  }
809
- return [{ groupQuantity: result }];
813
+ return [result];
810
814
  } else {
811
815
  const first = entries[0];
812
- return [
813
- { groupQuantity: { quantity: first.quantity, unit: first.unit } }
814
- ];
816
+ return [{ quantity: first.quantity, unit: first.unit }];
815
817
  }
816
818
  } else if (isGroup(summed)) {
817
819
  const andEntries = [];
@@ -836,7 +838,7 @@ var flattenPlainUnitGroup = (summed) => {
836
838
  }
837
839
  }
838
840
  if (equivalentsList.length === 0) {
839
- return andEntries.map((entry) => ({ groupQuantity: entry }));
841
+ return andEntries;
840
842
  }
841
843
  const result = {
842
844
  type: "and",
@@ -845,19 +847,17 @@ var flattenPlainUnitGroup = (summed) => {
845
847
  };
846
848
  return [result];
847
849
  } else {
848
- return [
849
- { groupQuantity: { quantity: summed.quantity, unit: summed.unit } }
850
- ];
850
+ return [{ quantity: summed.quantity, unit: summed.unit }];
851
851
  }
852
852
  };
853
853
 
854
854
  // src/utils/parser_helpers.ts
855
- function flushPendingNote(section, note) {
856
- if (note.length > 0) {
857
- section.content.push({ type: "note", note });
858
- return "";
855
+ function flushPendingNote(section, noteItems) {
856
+ if (noteItems.length > 0) {
857
+ section.content.push({ type: "note", items: [...noteItems] });
858
+ return [];
859
859
  }
860
- return note;
860
+ return noteItems;
861
861
  }
862
862
  function flushPendingItems(section, items) {
863
863
  if (items.length > 0) {
@@ -1630,6 +1630,10 @@ var _Recipe = class _Recipe {
1630
1630
  * The parsed recipe timers.
1631
1631
  */
1632
1632
  __publicField(this, "timers", []);
1633
+ /**
1634
+ * The parsed arbitrary quantities.
1635
+ */
1636
+ __publicField(this, "arbitraries", []);
1633
1637
  /**
1634
1638
  * The parsed recipe servings. Used for scaling. Parsed from one of
1635
1639
  * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
@@ -1657,12 +1661,64 @@ var _Recipe = class _Recipe {
1657
1661
  _Recipe.itemCounts.set(this, current + 1);
1658
1662
  return current;
1659
1663
  }
1664
+ /**
1665
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
1666
+ * @private
1667
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
1668
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
1669
+ */
1670
+ _parseArbitraryScalable(regexMatchGroups, intoArray) {
1671
+ if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
1672
+ const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
1673
+ if (quantityMatch?.groups) {
1674
+ const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
1675
+ const unit = quantityMatch.groups.unit;
1676
+ const name = regexMatchGroups.arbitraryName || void 0;
1677
+ if (!value || value.type === "fixed" && value.value.type === "text") {
1678
+ throw new InvalidQuantityFormat(
1679
+ regexMatchGroups.arbitraryQuantity?.trim(),
1680
+ "Arbitrary quantities must have a numerical value"
1681
+ );
1682
+ }
1683
+ const arbitrary = {
1684
+ quantity: value
1685
+ };
1686
+ if (name) arbitrary.name = name;
1687
+ if (unit) arbitrary.unit = unit;
1688
+ intoArray.push({
1689
+ type: "arbitrary",
1690
+ index: this.arbitraries.push(arbitrary) - 1
1691
+ });
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Parses text for arbitrary scalables and returns NoteItem array.
1696
+ * @param text - The text to parse for arbitrary scalables.
1697
+ * @returns Array of NoteItem (text and arbitrary scalable items).
1698
+ */
1699
+ _parseNoteText(text) {
1700
+ const noteItems = [];
1701
+ let cursor = 0;
1702
+ const globalRegex = new RegExp(arbitraryScalableRegex.source, "g");
1703
+ for (const match of text.matchAll(globalRegex)) {
1704
+ const idx = match.index;
1705
+ if (idx > cursor) {
1706
+ noteItems.push({ type: "text", value: text.slice(cursor, idx) });
1707
+ }
1708
+ this._parseArbitraryScalable(match.groups, noteItems);
1709
+ cursor = idx + match[0].length;
1710
+ }
1711
+ if (cursor < text.length) {
1712
+ noteItems.push({ type: "text", value: text.slice(cursor) });
1713
+ }
1714
+ return noteItems;
1715
+ }
1660
1716
  _parseQuantityRecursive(quantityRaw) {
1661
1717
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
1662
1718
  const quantities = [];
1663
1719
  while (quantityMatch?.groups) {
1664
- const value = quantityMatch.groups.ingredientQuantityValue ? parseQuantityInput(quantityMatch.groups.ingredientQuantityValue) : void 0;
1665
- const unit = quantityMatch.groups.ingredientUnit;
1720
+ const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
1721
+ const unit = quantityMatch.groups.unit;
1666
1722
  if (value) {
1667
1723
  const newQuantity = { quantity: value };
1668
1724
  if (unit) {
@@ -1679,9 +1735,7 @@ var _Recipe = class _Recipe {
1679
1735
  } else {
1680
1736
  throw new InvalidQuantityFormat(quantityRaw);
1681
1737
  }
1682
- quantityMatch = quantityMatch.groups.ingredientAltQuantity ? quantityMatch.groups.ingredientAltQuantity.match(
1683
- quantityAlternativeRegex
1684
- ) : null;
1738
+ quantityMatch = quantityMatch.groups.alternative ? quantityMatch.groups.alternative.match(quantityAlternativeRegex) : null;
1685
1739
  }
1686
1740
  return quantities;
1687
1741
  }
@@ -1984,7 +2038,7 @@ var _Recipe = class _Recipe {
1984
2038
  (eq) => toPlainUnit(eq)
1985
2039
  );
1986
2040
  }
1987
- newRef.alternativeQuantities = [altQty];
2041
+ newRef.quantities = [altQty];
1988
2042
  }
1989
2043
  alternativeRefs.push(newRef);
1990
2044
  }
@@ -2009,7 +2063,7 @@ var _Recipe = class _Recipe {
2009
2063
  }
2010
2064
  alternativeRefs.push({
2011
2065
  index: otherAlt.index,
2012
- alternativeQuantities: [altQty]
2066
+ quantities: [altQty]
2013
2067
  });
2014
2068
  }
2015
2069
  }
@@ -2036,8 +2090,8 @@ var _Recipe = class _Recipe {
2036
2090
  if (!group.alternativeQuantities.has(ref.index)) {
2037
2091
  group.alternativeQuantities.set(ref.index, []);
2038
2092
  }
2039
- if (ref.alternativeQuantities && ref.alternativeQuantities.length > 0) {
2040
- for (const altQty of ref.alternativeQuantities) {
2093
+ if (ref.quantities && ref.quantities.length > 0) {
2094
+ for (const altQty of ref.quantities) {
2041
2095
  if (altQty.equivalents && altQty.equivalents.length > 0) {
2042
2096
  const entries = [
2043
2097
  toExtendedUnit({
@@ -2080,9 +2134,9 @@ var _Recipe = class _Recipe {
2080
2134
  ...altQuantities
2081
2135
  );
2082
2136
  const flattenedAlt = flattenPlainUnitGroup(summedAltQuantity);
2083
- ref.alternativeQuantities = flattenedAlt.flatMap((item) => {
2084
- if ("groupQuantity" in item) {
2085
- return [item.groupQuantity];
2137
+ ref.quantities = flattenedAlt.flatMap((item) => {
2138
+ if ("quantity" in item) {
2139
+ return [item];
2086
2140
  } else {
2087
2141
  return item.entries;
2088
2142
  }
@@ -2213,19 +2267,27 @@ var _Recipe = class _Recipe {
2213
2267
  let blankLineBefore = true;
2214
2268
  let section = new Section();
2215
2269
  const items = [];
2216
- let note = "";
2270
+ let noteText = "";
2217
2271
  let inNote = false;
2218
2272
  for (const line of cleanContent) {
2219
2273
  if (line.trim().length === 0) {
2220
2274
  flushPendingItems(section, items);
2221
- note = flushPendingNote(section, note);
2275
+ flushPendingNote(
2276
+ section,
2277
+ noteText ? this._parseNoteText(noteText) : []
2278
+ );
2279
+ noteText = "";
2222
2280
  blankLineBefore = true;
2223
2281
  inNote = false;
2224
2282
  continue;
2225
2283
  }
2226
2284
  if (line.startsWith("=")) {
2227
2285
  flushPendingItems(section, items);
2228
- note = flushPendingNote(section, note);
2286
+ flushPendingNote(
2287
+ section,
2288
+ noteText ? this._parseNoteText(noteText) : []
2289
+ );
2290
+ noteText = "";
2229
2291
  if (this.sections.length === 0 && section.isBlank()) {
2230
2292
  section.name = line.replace(/^=+|=+$/g, "").trim();
2231
2293
  } else {
@@ -2240,22 +2302,26 @@ var _Recipe = class _Recipe {
2240
2302
  }
2241
2303
  if (blankLineBefore && line.startsWith(">")) {
2242
2304
  flushPendingItems(section, items);
2243
- note = flushPendingNote(section, note);
2244
- note += line.substring(1).trim();
2305
+ flushPendingNote(
2306
+ section,
2307
+ noteText ? this._parseNoteText(noteText) : []
2308
+ );
2309
+ noteText = line.substring(1).trim();
2245
2310
  inNote = true;
2246
2311
  blankLineBefore = false;
2247
2312
  continue;
2248
2313
  }
2249
2314
  if (inNote) {
2250
2315
  if (line.startsWith(">")) {
2251
- note += " " + line.substring(1).trim();
2316
+ noteText += " " + line.substring(1).trim();
2252
2317
  } else {
2253
- note += " " + line.trim();
2318
+ noteText += " " + line.trim();
2254
2319
  }
2255
2320
  blankLineBefore = false;
2256
2321
  continue;
2257
2322
  }
2258
- note = flushPendingNote(section, note);
2323
+ flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
2324
+ noteText = "";
2259
2325
  let cursor = 0;
2260
2326
  for (const match of line.matchAll(tokensRegex)) {
2261
2327
  const idx = match.index;
@@ -2302,6 +2368,8 @@ var _Recipe = class _Recipe {
2302
2368
  newItem.quantity = quantity;
2303
2369
  }
2304
2370
  items.push(newItem);
2371
+ } else if (groups.arbitraryQuantity) {
2372
+ this._parseArbitraryScalable(groups, items);
2305
2373
  } else {
2306
2374
  const durationStr = groups.timerQuantity.trim();
2307
2375
  const unit = (groups.timerUnit || "").trim();
@@ -2325,7 +2393,7 @@ var _Recipe = class _Recipe {
2325
2393
  blankLineBefore = false;
2326
2394
  }
2327
2395
  flushPendingItems(section, items);
2328
- note = flushPendingNote(section, note);
2396
+ flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
2329
2397
  if (!section.isBlank()) {
2330
2398
  this.sections.push(section);
2331
2399
  }
@@ -2340,9 +2408,9 @@ var _Recipe = class _Recipe {
2340
2408
  * @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
2341
2409
  */
2342
2410
  scaleTo(newServings) {
2343
- const originalServings = this.getServings();
2411
+ let originalServings = this.getServings();
2344
2412
  if (originalServings === void 0 || originalServings === 0) {
2345
- throw new Error("Error scaling recipe: no initial servings value set");
2413
+ originalServings = 1;
2346
2414
  }
2347
2415
  const factor = Big4(newServings).div(originalServings);
2348
2416
  return this.scaleBy(factor);
@@ -2355,9 +2423,9 @@ var _Recipe = class _Recipe {
2355
2423
  */
2356
2424
  scaleBy(factor) {
2357
2425
  const newRecipe = this.clone();
2358
- const originalServings = newRecipe.getServings();
2426
+ let originalServings = newRecipe.getServings();
2359
2427
  if (originalServings === void 0 || originalServings === 0) {
2360
- throw new Error("Error scaling recipe: no initial servings value set");
2428
+ originalServings = 1;
2361
2429
  }
2362
2430
  function scaleAlternativesBy(alternatives, factor2) {
2363
2431
  for (const alternative of alternatives) {
@@ -2406,6 +2474,12 @@ var _Recipe = class _Recipe {
2406
2474
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
2407
2475
  scaleAlternativesBy(alternatives, factor);
2408
2476
  }
2477
+ for (const arbitrary of newRecipe.arbitraries) {
2478
+ arbitrary.quantity = multiplyQuantityValue(
2479
+ arbitrary.quantity,
2480
+ factor
2481
+ );
2482
+ }
2409
2483
  newRecipe._populate_ingredient_quantities();
2410
2484
  newRecipe.servings = Big4(originalServings).times(factor).toNumber();
2411
2485
  if (newRecipe.metadata.servings && this.metadata.servings) {
@@ -2468,6 +2542,7 @@ var _Recipe = class _Recipe {
2468
2542
  });
2469
2543
  newRecipe.cookware = deepClone(this.cookware);
2470
2544
  newRecipe.timers = deepClone(this.timers);
2545
+ newRecipe.arbitraries = deepClone(this.arbitraries);
2471
2546
  newRecipe.servings = this.servings;
2472
2547
  return newRecipe;
2473
2548
  }