@tmlmt/cooklang-parser 2.1.7 → 3.0.0-alpha.10

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
@@ -65,6 +65,10 @@ declare class Recipe {
65
65
  * The parsed recipe metadata.
66
66
  */
67
67
  metadata: Metadata;
68
+ /**
69
+ * The possible choices of alternative ingredients for this recipe.
70
+ */
71
+ choices: RecipeAlternatives;
68
72
  /**
69
73
  * The parsed recipe ingredients.
70
74
  */
@@ -81,6 +85,10 @@ declare class Recipe {
81
85
  * The parsed recipe timers.
82
86
  */
83
87
  timers: Timer[];
88
+ /**
89
+ * The parsed arbitrary quantities.
90
+ */
91
+ arbitraries: ArbitraryScalable[];
84
92
  /**
85
93
  * The parsed recipe servings. Used for scaling. Parsed from one of
86
94
  * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
@@ -89,11 +97,80 @@ declare class Recipe {
89
97
  * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
90
98
  */
91
99
  servings?: number;
100
+ /**
101
+ * External storage for item count (not a property on instances).
102
+ * Used for giving ID numbers to items during parsing.
103
+ */
104
+ private static itemCounts;
105
+ /**
106
+ * Gets the current item count for this recipe.
107
+ */
108
+ private getItemCount;
109
+ /**
110
+ * Gets the current item count and increments it.
111
+ */
112
+ private getAndIncrementItemCount;
92
113
  /**
93
114
  * Creates a new Recipe instance.
94
115
  * @param content - The recipe content to parse.
95
116
  */
96
117
  constructor(content?: string);
118
+ /**
119
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
120
+ * @private
121
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
122
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
123
+ */
124
+ private _parseArbitraryScalable;
125
+ /**
126
+ * Parses text for arbitrary scalables and returns NoteItem array.
127
+ * @param text - The text to parse for arbitrary scalables.
128
+ * @returns Array of NoteItem (text and arbitrary scalable items).
129
+ */
130
+ private _parseNoteText;
131
+ private _parseQuantityRecursive;
132
+ private _parseIngredientWithAlternativeRecursive;
133
+ private _parseIngredientWithGroupKey;
134
+ /**
135
+ * Populates the `quantities` property for each ingredient based on
136
+ * how they appear in the recipe preparation. Only primary ingredients
137
+ * get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
138
+ *
139
+ * For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
140
+ * For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
141
+ *
142
+ * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
143
+ * @internal
144
+ */
145
+ private _populate_ingredient_quantities;
146
+ /**
147
+ * Gets ingredients with their quantities populated, optionally filtered by section/step
148
+ * and respecting user choices for alternatives.
149
+ *
150
+ * When no options are provided, returns all recipe ingredients with quantities
151
+ * calculated using primary alternatives (same as after parsing).
152
+ *
153
+ * @param options - Options for filtering and choice selection:
154
+ * - `section`: Filter to a specific section (Section object or 0-based index)
155
+ * - `step`: Filter to a specific step (Step object or 0-based index)
156
+ * - `choices`: Choices for alternative ingredients (defaults to primary)
157
+ * @returns Array of Ingredient objects with quantities populated
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * // Get all ingredients with primary alternatives
162
+ * const ingredients = recipe.getIngredientQuantities();
163
+ *
164
+ * // Get ingredients for a specific section
165
+ * const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
166
+ *
167
+ * // Get ingredients with specific choices applied
168
+ * const withChoices = recipe.getIngredientQuantities({
169
+ * choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
170
+ * });
171
+ * ```
172
+ */
173
+ getIngredientQuantities(options?: GetIngredientQuantitiesOptions): Ingredient[];
97
174
  /**
98
175
  * Parses a recipe from a string.
99
176
  * @param content - The recipe content to parse.
@@ -128,11 +205,6 @@ declare class Recipe {
128
205
  clone(): Recipe;
129
206
  }
130
207
 
131
- interface Quantity {
132
- value: FixedValue | Range;
133
- unit?: string;
134
- }
135
-
136
208
  /**
137
209
  * Represents the metadata of a recipe.
138
210
  * @category Types
@@ -250,7 +322,7 @@ interface Metadata {
250
322
  */
251
323
  interface TextValue {
252
324
  type: "text";
253
- value: string;
325
+ text: string;
254
326
  }
255
327
  /**
256
328
  * Represents a quantity described by a decimal number, e.g. "1.5"
@@ -258,7 +330,7 @@ interface TextValue {
258
330
  */
259
331
  interface DecimalValue {
260
332
  type: "decimal";
261
- value: number;
333
+ decimal: number;
262
334
  }
263
335
  /**
264
336
  * Represents a quantity described by a fraction, e.g. "1/2"
@@ -280,6 +352,15 @@ interface FixedValue {
280
352
  type: "fixed";
281
353
  value: TextValue | DecimalValue | FractionValue;
282
354
  }
355
+ /**
356
+ * Represents a single, fixed numeric quantity.
357
+ * This can be a decimal or fraction.
358
+ * @category Types
359
+ */
360
+ interface FixedNumericValue {
361
+ type: "fixed";
362
+ value: DecimalValue | FractionValue;
363
+ }
283
364
  /**
284
365
  * Represents a range of quantities, e.g. "1-2"
285
366
  * @category Types
@@ -289,16 +370,6 @@ interface Range {
289
370
  min: DecimalValue | FractionValue;
290
371
  max: DecimalValue | FractionValue;
291
372
  }
292
- /**
293
- * Represents a contributor to an ingredient's total quantity
294
- * @category Types
295
- */
296
- interface QuantityPart extends Quantity {
297
- /** - If _true_, the quantity will scale
298
- * - If _false_, the quantity is fixed
299
- */
300
- scalable: boolean;
301
- }
302
373
  /**
303
374
  * Represents a possible state modifier or other flag for an ingredient in a recipe
304
375
  * @category Types
@@ -324,6 +395,48 @@ interface IngredientExtras {
324
395
  */
325
396
  path: string;
326
397
  }
398
+ /**
399
+ * Represents a reference to an alternative ingredient along with its quantities.
400
+ *
401
+ * Used in {@link IngredientQuantityGroup} to describe what other ingredients
402
+ * could be used in place of the main ingredient.
403
+ * @category Types
404
+ */
405
+ interface AlternativeIngredientRef {
406
+ /** The index of the alternative ingredient within the {@link Recipe.ingredients} array. */
407
+ index: number;
408
+ /** The quantities of the alternative ingredient. Multiple entries when units are incompatible. */
409
+ quantities?: QuantityWithPlainUnit[];
410
+ }
411
+ /**
412
+ * Represents a group of summed quantities for an ingredient, optionally with alternatives.
413
+ * Quantities with the same alternative signature are summed together into a single group.
414
+ * When units are incompatible, separate IngredientQuantityGroup entries are created instead of merging.
415
+ * @category Types
416
+ */
417
+ interface IngredientQuantityGroup extends QuantityWithPlainUnit {
418
+ /**
419
+ * References to alternative ingredients for this quantity group.
420
+ * If undefined, this group has no alternatives.
421
+ */
422
+ alternatives?: AlternativeIngredientRef[];
423
+ }
424
+ /**
425
+ * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
426
+ * For example: 1 large carrot + 2 small carrots, both with cup equivalents (resp. 2 cup and 1.5 cup) that sum to 5 cups.
427
+ * @category Types
428
+ */
429
+ interface IngredientQuantityAndGroup extends FlatAndGroup<QuantityWithPlainUnit> {
430
+ /**
431
+ * The summed equivalent quantities (e.g., "5 cups" from summing "1.5 cup + 2 cup + 1.5 cup").
432
+ */
433
+ equivalents?: QuantityWithPlainUnit[];
434
+ /**
435
+ * References to alternative ingredients for this quantity group.
436
+ * If undefined, this group has no alternatives.
437
+ */
438
+ alternatives?: AlternativeIngredientRef[];
439
+ }
327
440
  /**
328
441
  * Represents an ingredient in a recipe.
329
442
  * @category Types
@@ -331,40 +444,65 @@ interface IngredientExtras {
331
444
  interface Ingredient {
332
445
  /** The name of the ingredient. */
333
446
  name: string;
334
- /** The quantity of the ingredient. */
335
- quantity?: FixedValue | Range;
336
- /** The unit of the ingredient. */
337
- unit?: string;
338
- /** The array of contributors to the ingredient's total quantity. */
339
- quantityParts?: QuantityPart[];
447
+ /**
448
+ * Represents the quantities list for an ingredient as groups.
449
+ * Each group contains summed quantities that share the same alternative signature.
450
+ * Groups can be either simple (single unit) or AND groups (incompatible primary units with summed equivalents).
451
+ * Only populated for primary ingredients (not alternative-only).
452
+ * Quantities without alternatives are merged opportunistically when units are compatible.
453
+ * Quantities with alternatives are only merged if the alternatives are exactly the same.
454
+ */
455
+ quantities?: (IngredientQuantityGroup | IngredientQuantityAndGroup)[];
340
456
  /** The preparation of the ingredient. */
341
457
  preparation?: string;
458
+ /** The list of indexes of the ingredients mentioned in the preparation as alternatives to this ingredient */
459
+ alternatives?: Set<number>;
460
+ /**
461
+ * True if this ingredient appears as the primary choice (first in an alternatives list).
462
+ * Only primary ingredients have quantities populated directly.
463
+ *
464
+ * Alternative-only ingredients (usedAsPrimary undefined/false) have their quantities
465
+ * available via the {@link Recipe.choices} structure.
466
+ */
467
+ usedAsPrimary?: boolean;
342
468
  /** A list of potential state modifiers or other flags for the ingredient */
343
469
  flags?: IngredientFlag[];
344
470
  /** The collection of potential additional metadata for the ingredient */
345
471
  extras?: IngredientExtras;
346
472
  }
347
473
  /**
348
- * Represents a timer in a recipe.
474
+ * Represents a contributor to an ingredient's total quantity, corresponding
475
+ * to a single mention in the recipe text. It can contain multiple
476
+ * equivalent quantities (e.g., in different units).
349
477
  * @category Types
350
478
  */
351
- interface Timer {
352
- /** The name of the timer. */
353
- name?: string;
354
- /** The duration of the timer. */
355
- duration: FixedValue | Range;
356
- /** The unit of the timer. */
357
- unit: string;
479
+ interface IngredientItemQuantity extends QuantityWithExtendedUnit {
480
+ /**
481
+ * A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
482
+ * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g.
483
+ */
484
+ equivalents?: QuantityWithExtendedUnit[];
485
+ /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
486
+ scalable: boolean;
358
487
  }
359
488
  /**
360
- * Represents a text item in a recipe step.
489
+ * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
490
+ * to a specific ingredient and its corresponding quantity information.
361
491
  * @category Types
362
492
  */
363
- interface TextItem {
364
- /** The type of the item. */
365
- type: "text";
366
- /** The content of the text item. */
367
- value: string;
493
+ interface IngredientAlternative {
494
+ /** The index of the ingredient within the {@link Recipe.ingredients} array. */
495
+ index: number;
496
+ /** The quantity of this specific mention of the ingredient */
497
+ itemQuantity?: IngredientItemQuantity;
498
+ /** The alias/name of the ingredient as it should be displayed for this occurrence. */
499
+ displayName: string;
500
+ /** An optional note for this specific choice (e.g., "for a vegan version"). */
501
+ note?: string;
502
+ /** When {@link Recipe.choices} is populated for alternatives ingredients
503
+ * with group keys: the id of the corresponding ingredient item (e.g. "ingredient-item-2").
504
+ * Can be useful for creating alternative selection UI elements with anchor links */
505
+ itemId?: string;
368
506
  }
369
507
  /**
370
508
  * Represents an ingredient item in a recipe step.
@@ -373,14 +511,67 @@ interface TextItem {
373
511
  interface IngredientItem {
374
512
  /** The type of the item. */
375
513
  type: "ingredient";
376
- /** The index of the ingredient, within the {@link Recipe.ingredients | list of ingredients} */
377
- index: number;
378
- /** Index of the quantity part corresponding to this item / this occurence
379
- * of the ingredient, which may be referenced elsewhere. */
380
- quantityPartIndex?: number;
381
- /** The alias/name of the ingredient as it should be displayed in the preparation
382
- * for this occurence */
383
- displayName: string;
514
+ /** The item identifier */
515
+ id: string;
516
+ /**
517
+ * A list of alternative ingredient choices. For a standard ingredient,
518
+ * this array will contain a single element.
519
+ */
520
+ alternatives: IngredientAlternative[];
521
+ /**
522
+ * An optional identifier for linking distributed alternatives. If multiple
523
+ * `IngredientItem`s in a recipe share the same `group` ID (e.g., from
524
+ * `@|group|...` syntax), they represent a single logical choice.
525
+ */
526
+ group?: string;
527
+ }
528
+ /**
529
+ * Represents the choices one can make in a recipe
530
+ * @category Types
531
+ */
532
+ interface RecipeAlternatives {
533
+ /** Map of choices that can be made at Ingredient StepItem level
534
+ * - Keys are the Ingredient StepItem IDs (e.g. "ingredient-item-2")
535
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that item
536
+ */
537
+ ingredientItems: Map<string, IngredientAlternative[]>;
538
+ /** Map of choices that can be made for Grouped Ingredient StepItem's
539
+ * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
540
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that group
541
+ */
542
+ ingredientGroups: Map<string, IngredientAlternative[]>;
543
+ }
544
+ /**
545
+ * Represents the choices to apply when computing ingredient quantities.
546
+ * Maps item/group IDs to the index of the selected alternative.
547
+ * @category Types
548
+ */
549
+ interface RecipeChoices {
550
+ /** Map of choices that can be made at Ingredient StepItem level */
551
+ ingredientItems?: Map<string, number>;
552
+ /** Map of choices that can be made for Grouped Ingredient StepItem's */
553
+ ingredientGroups?: Map<string, number>;
554
+ }
555
+ /**
556
+ * Options for the {@link Recipe.getIngredientQuantities | getIngredientQuantities()} method.
557
+ * @category Types
558
+ */
559
+ interface GetIngredientQuantitiesOptions {
560
+ /**
561
+ * Filter ingredients to only those appearing in a specific section.
562
+ * Can be a Section object or section index (0-based).
563
+ */
564
+ section?: Section | number;
565
+ /**
566
+ * Filter ingredients to only those appearing in a specific step.
567
+ * Can be a Step object or step index (0-based within the section, or global if no section specified).
568
+ */
569
+ step?: Step | number;
570
+ /**
571
+ * The choices to apply when computing quantities.
572
+ * If not provided, uses primary alternatives (index 0 for all).
573
+ */
574
+ choices?: RecipeChoices;
384
575
  }
385
576
  /**
386
577
  * Represents a cookware item in a recipe step.
@@ -391,9 +582,8 @@ interface CookwareItem {
391
582
  type: "cookware";
392
583
  /** The index of the cookware, within the {@link Recipe.cookware | list of cookware} */
393
584
  index: number;
394
- /** Index of the quantity part corresponding to this item / this occurence
395
- * of the cookware, which may be referenced elsewhere. */
396
- quantityPartIndex?: number;
585
+ /** The quantity of this specific mention of the cookware */
586
+ quantity?: FixedValue | Range;
397
587
  }
398
588
  /**
399
589
  * Represents a timer item in a recipe step.
@@ -405,11 +595,55 @@ interface TimerItem {
405
595
  /** The index of the timer, within the {@link Recipe.timers | list of timers} */
406
596
  index: number;
407
597
  }
598
+ /**
599
+ * Represents a timer in a recipe.
600
+ * @category Types
601
+ */
602
+ interface Timer {
603
+ /** The name of the timer. */
604
+ name?: string;
605
+ /** The duration of the timer. */
606
+ duration: FixedValue | Range;
607
+ /** The unit of the timer. */
608
+ unit: string;
609
+ }
610
+ /**
611
+ * Represents a text item in a recipe step.
612
+ * @category Types
613
+ */
614
+ interface TextItem {
615
+ /** The type of the item. */
616
+ type: "text";
617
+ /** The content of the text item. */
618
+ value: string;
619
+ }
620
+ /**
621
+ * Represents an arbitrary scalable quantity in a recipe.
622
+ * @category Types
623
+ */
624
+ interface ArbitraryScalable {
625
+ /** The name of the arbitrary scalable quantity. */
626
+ name?: string;
627
+ /** The numerical value of the arbitrary scalable quantity. */
628
+ quantity: FixedNumericValue;
629
+ /** The unit of the arbitrary scalable quantity. */
630
+ unit?: string;
631
+ }
632
+ /**
633
+ * Represents an arbitrary scalable quantity item in a recipe step.
634
+ * @category Types
635
+ */
636
+ interface ArbitraryScalableItem {
637
+ /** The type of the item. */
638
+ type: "arbitrary";
639
+ /** The index of the arbitrary scalable quantity, within the {@link Recipe.arbitraries | list of arbitrary scalable quantities} */
640
+ index: number;
641
+ }
408
642
  /**
409
643
  * Represents an item in a recipe step.
410
644
  * @category Types
411
645
  */
412
- type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
646
+ type StepItem = TextItem | IngredientItem | CookwareItem | TimerItem | ArbitraryScalableItem;
413
647
  /**
414
648
  * Represents a step in a recipe.
415
649
  * @category Types
@@ -417,16 +651,21 @@ type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
417
651
  interface Step {
418
652
  type: "step";
419
653
  /** The items in the step. */
420
- items: Item[];
654
+ items: StepItem[];
421
655
  }
656
+ /**
657
+ * Represents an item in a note (can be text or arbitrary scalable).
658
+ * @category Types
659
+ */
660
+ type NoteItem = TextItem | ArbitraryScalableItem;
422
661
  /**
423
662
  * Represents a note in a recipe.
424
663
  * @category Types
425
664
  */
426
665
  interface Note {
427
666
  type: "note";
428
- /** The content of the note. */
429
- note: string;
667
+ /** The items in the note. */
668
+ items: NoteItem[];
430
669
  }
431
670
  /**
432
671
  * Represents a possible state modifier or other flag for cookware used in a recipe
@@ -442,17 +681,15 @@ interface Cookware {
442
681
  name: string;
443
682
  /** The quantity of cookware */
444
683
  quantity?: FixedValue | Range;
445
- /** The array of contributors to the cookware's total quantity. */
446
- quantityParts?: (FixedValue | Range)[];
447
684
  /** A list of potential state modifiers or other flags for the cookware */
448
- flags: CookwareFlag[];
685
+ flags?: CookwareFlag[];
449
686
  }
450
687
  /**
451
688
  * Represents categorized ingredients.
452
689
  * @category Types
453
690
  */
454
691
  interface CategorizedIngredients {
455
- [category: string]: Ingredient[];
692
+ [category: string]: AddedIngredient[];
456
693
  }
457
694
  /**
458
695
  * Represents a recipe together with a scaling factor
@@ -463,6 +700,8 @@ interface RecipeWithFactor {
463
700
  recipe: Recipe;
464
701
  /** The factor the recipe is scaled by. */
465
702
  factor: number;
703
+ /** The choices for alternative ingredients. */
704
+ choices?: RecipeChoices;
466
705
  }
467
706
  /**
468
707
  * Represents a recipe together with a servings value for scaling
@@ -473,12 +712,36 @@ interface RecipeWithServings {
473
712
  recipe: Recipe;
474
713
  /** The servings the recipe is scaled to */
475
714
  servings: number;
715
+ /** The choices for alternative ingredients. */
716
+ choices?: RecipeChoices;
476
717
  }
477
718
  /**
478
719
  * Represents a recipe that has been added to a shopping list.
479
720
  * @category Types
480
721
  */
481
722
  type AddedRecipe = RecipeWithFactor | RecipeWithServings;
723
+ /**
724
+ * Options for adding a recipe to a shopping list
725
+ * @category Types
726
+ */
727
+ type AddedRecipeOptions = {
728
+ /** The scaling option for the recipe. Can be either a factor or a number of servings */
729
+ scaling?: {
730
+ factor: number;
731
+ } | {
732
+ servings: number;
733
+ };
734
+ /** The choices for alternative ingredients. */
735
+ choices?: RecipeChoices;
736
+ };
737
+ /**
738
+ * Represents an ingredient that has been added to a shopping list
739
+ * @category Types
740
+ */
741
+ type AddedIngredient = Pick<Ingredient, "name"> & {
742
+ /** The total quantity of the ingredient after applying choices. */
743
+ quantityTotal?: QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit>;
744
+ };
482
745
  /**
483
746
  * Represents an ingredient in a category.
484
747
  * @category Types
@@ -499,6 +762,223 @@ interface Category {
499
762
  /** The ingredients in the category. */
500
763
  ingredients: CategoryIngredient[];
501
764
  }
765
+ /**
766
+ * Represents a single size expression for a product (value + optional unit)
767
+ * @category Types
768
+ */
769
+ interface ProductSize {
770
+ /** The numeric size value */
771
+ size: FixedNumericValue;
772
+ /** The unit of the size (optional) */
773
+ unit?: string;
774
+ }
775
+ /**
776
+ * Core properties for {@link ProductOption}
777
+ * @category Types
778
+ */
779
+ interface ProductOptionCore {
780
+ /** The ID of the product */
781
+ id: string;
782
+ /** The name of the product */
783
+ productName: string;
784
+ /** The name of the ingredient it corresponds to */
785
+ ingredientName: string;
786
+ /** The aliases of the ingredient it also corresponds to */
787
+ ingredientAliases?: string[];
788
+ /** The price of the product */
789
+ price: number;
790
+ }
791
+ /**
792
+ * Base type for {@link ProductOption} allowing arbitrary additional metadata
793
+ * @category Types
794
+ */
795
+ type ProductOptionBase = ProductOptionCore & Record<string, unknown>;
796
+ /**
797
+ * Represents a product option in a {@link ProductCatalog}
798
+ * @category Types
799
+ */
800
+ type ProductOption = ProductOptionBase & {
801
+ /** The size(s) of the product. Multiple sizes allow equivalent expressions (e.g., "1%dozen" and "12") */
802
+ sizes: ProductSize[];
803
+ };
804
+ /**
805
+ * Represents a product selection in a {@link ShoppingCart}
806
+ * @category Types
807
+ */
808
+ interface ProductSelection {
809
+ /** The selected product */
810
+ product: ProductOption;
811
+ /** The quantity of the selected product */
812
+ quantity: number;
813
+ /** The total price for this selected product */
814
+ totalPrice: number;
815
+ }
816
+ /**
817
+ * Represents the content of the actual cart of the {@link ShoppingCart}
818
+ * @category Types
819
+ */
820
+ type CartContent = ProductSelection[];
821
+ /**
822
+ * Represents a successful match between a ingredient and product(s) in the product catalog, in a {@link ShoppingCart}
823
+ * @category Types
824
+ */
825
+ interface ProductMatch {
826
+ ingredient: Ingredient;
827
+ selection: ProductSelection[];
828
+ }
829
+ /**
830
+ * Represents all successful matches between ingredients and the product catalog, in a {@link ShoppingCart}
831
+ * @category Types
832
+ */
833
+ type CartMatch = ProductMatch[];
834
+ /**
835
+ * Represents the error codes for an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
836
+ * @category Types
837
+ */
838
+ type NoProductMatchErrorCode = "incompatibleUnits" | "textValue" | "textValue_incompatibleUnits" | "noProduct" | "noQuantity";
839
+ /**
840
+ * Represents an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
841
+ * @category Types
842
+ */
843
+ interface ProductMisMatch {
844
+ ingredient: Ingredient;
845
+ reason: NoProductMatchErrorCode;
846
+ }
847
+ /**
848
+ * Represents all ingredients which didn't match with any product in the product catalog, in a {@link ShoppingCart}
849
+ * @category Types
850
+ */
851
+ type CartMisMatch = ProductMisMatch[];
852
+ /**
853
+ * Represents the type category of a unit used for quantities
854
+ * @category Types
855
+ */
856
+ type UnitType = "mass" | "volume" | "count";
857
+ /**
858
+ * Represents the measurement system a unit belongs to
859
+ * @category Types
860
+ */
861
+ type UnitSystem = "metric" | "imperial";
862
+ /**
863
+ * Represents a unit used to describe quantities
864
+ * @category Types
865
+ */
866
+ interface Unit {
867
+ name: string;
868
+ /** This property is set to true when the unit is prefixed by an `=` sign in the cooklang file, e.g. `=g`
869
+ * Indicates that quantities with this unit should be treated as integers only (no decimal/fractional values). */
870
+ integerProtected?: boolean;
871
+ }
872
+ /**
873
+ * Represents a fully defined unit with conversion and alias information
874
+ * @category Types
875
+ */
876
+ interface UnitDefinition extends Unit {
877
+ type: UnitType;
878
+ system: UnitSystem;
879
+ /** e.g. ['gram', 'grams'] */
880
+ aliases: string[];
881
+ /** Conversion factor to the base unit of its type */
882
+ toBase: number;
883
+ }
884
+ /**
885
+ * Represents a resolved unit definition or a lightweight placeholder for non-standard units
886
+ * @category Types
887
+ */
888
+ type UnitDefinitionLike = UnitDefinition | {
889
+ name: string;
890
+ type: "other";
891
+ system: "none";
892
+ integerProtected?: boolean;
893
+ };
894
+ /**
895
+ * Core quantity container holding a fixed value or a range
896
+ * @category Types
897
+ */
898
+ interface QuantityBase {
899
+ quantity: FixedValue | Range;
900
+ }
901
+ /**
902
+ * Represents a quantity with an optional plain (string) unit
903
+ * @category Types
904
+ */
905
+ interface QuantityWithPlainUnit extends QuantityBase {
906
+ unit?: string;
907
+ /** Optional equivalent quantities in different units (for alternative units like `@flour{100%g|3.5%oz}`) */
908
+ equivalents?: QuantityWithPlainUnit[];
909
+ }
910
+ /**
911
+ * Represents a quantity with an optional extended `Unit` object
912
+ * @category Types
913
+ */
914
+ interface QuantityWithExtendedUnit extends QuantityBase {
915
+ unit?: Unit;
916
+ }
917
+ /**
918
+ * Represents a quantity with a resolved unit definition
919
+ * @category Types
920
+ */
921
+ interface QuantityWithUnitDef extends QuantityBase {
922
+ unit: UnitDefinitionLike;
923
+ }
924
+ /**
925
+ * Represents any quantity shape supported by the parser (plain, extended, or resolved unit)
926
+ * @category Types
927
+ */
928
+ type QuantityWithUnitLike = QuantityWithPlainUnit | QuantityWithExtendedUnit | QuantityWithUnitDef;
929
+ /**
930
+ * Represents a flat "or" group of alternative quantities (for alternative units)
931
+ * @category Types
932
+ */
933
+ interface FlatOrGroup<T = QuantityWithUnitLike> {
934
+ or: T[];
935
+ }
936
+ /**
937
+ * Represents an "or" group of alternative quantities that may contain nested groups (alternatives with nested structure)
938
+ * @category Types
939
+ */
940
+ interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
941
+ or: (T | MaybeNestedGroup<T>)[];
942
+ }
943
+ /**
944
+ * Represents a flat "and" group of quantities (combined quantities)
945
+ * @category Types
946
+ */
947
+ interface FlatAndGroup<T = QuantityWithUnitLike> {
948
+ and: T[];
949
+ }
950
+ /**
951
+ * Represents an "and" group of quantities that may contain nested groups (combinations with nested structure)
952
+ * @category Types
953
+ */
954
+ interface MaybeNestedAndGroup<T = QuantityWithUnitLike> {
955
+ and: (T | MaybeNestedGroup<T>)[];
956
+ }
957
+ /**
958
+ * Represents any flat group type ("and" or "or")
959
+ * @category Types
960
+ */
961
+ type FlatGroup<T = QuantityWithUnitLike> = FlatAndGroup<T> | FlatOrGroup<T>;
962
+ /**
963
+ * Represents any group type that may include nested groups
964
+ * @category Types
965
+ */
966
+ type MaybeNestedGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | MaybeNestedOrGroup<T>;
967
+ /**
968
+ * Represents any group type (flat or nested)
969
+ * @category Types
970
+ */
971
+ type Group<T = QuantityWithUnitLike> = MaybeNestedGroup<T> | FlatGroup<T>;
972
+ /**
973
+ * Represents any "or" group (flat or nested)
974
+ * @category Types
975
+ */
976
+ type OrGroup<T = QuantityWithUnitLike> = MaybeNestedOrGroup<T> | FlatOrGroup<T>;
977
+ /**
978
+ * Represents any "and" group (flat or nested)
979
+ * @category Types
980
+ */
981
+ type AndGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | FlatAndGroup<T>;
502
982
 
503
983
  /**
504
984
  * Parser for category configurations specified à-la-cooklang.
@@ -549,6 +1029,62 @@ declare class CategoryConfig {
549
1029
  parse(config: string): void;
550
1030
  }
551
1031
 
1032
+ /**
1033
+ * Product Catalog Manager: used in conjunction with {@link ShoppingCart}
1034
+ *
1035
+ * ## Usage
1036
+ *
1037
+ * You can either directly populate the products by feeding the {@link ProductCatalog.products | products} property. Alternatively,
1038
+ * you can provide a catalog in TOML format to either the constructor itself or to the {@link ProductCatalog.parse | parse()} method.
1039
+ *
1040
+ * @category Classes
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * import { ProductCatalog } from "@tmlmt/cooklang-parser";
1045
+ *
1046
+ * const catalog = `
1047
+ * [eggs]
1048
+ * aliases = ["oeuf", "huevo"]
1049
+ * 01123 = { name = "Single Egg", size = "1", price = 2 }
1050
+ * 11244 = { name = "Pack of 6 eggs", size = "6", price = 10 }
1051
+ *
1052
+ * [flour]
1053
+ * aliases = ["farine", "Mehl"]
1054
+ * 01124 = { name = "Small pack", size = "100%g", price = 1.5 }
1055
+ * 14141 = { name = "Big pack", size = "6%kg", price = 10 }
1056
+ * `
1057
+ * const catalog = new ProductCatalog(catalog);
1058
+ * const eggs = catalog.find("oeuf");
1059
+ * ```
1060
+ */
1061
+ declare class ProductCatalog {
1062
+ products: ProductOption[];
1063
+ constructor(tomlContent?: string);
1064
+ /**
1065
+ * Parses a TOML string into a list of product options.
1066
+ * @param tomlContent - The TOML string to parse.
1067
+ * @returns A parsed list of `ProductOption`.
1068
+ */
1069
+ parse(tomlContent: string): ProductOption[];
1070
+ /**
1071
+ * Stringifies the catalog to a TOML string.
1072
+ * @returns The TOML string representation of the catalog.
1073
+ */
1074
+ stringify(): string;
1075
+ /**
1076
+ * Adds a product to the catalog.
1077
+ * @param productOption - The product to add.
1078
+ */
1079
+ add(productOption: ProductOption): void;
1080
+ /**
1081
+ * Removes a product from the catalog by its ID.
1082
+ * @param productId - The ID of the product to remove.
1083
+ */
1084
+ remove(productId: string): void;
1085
+ private isValidTomlContent;
1086
+ }
1087
+
552
1088
  /**
553
1089
  * Shopping List generator.
554
1090
  *
@@ -579,7 +1115,7 @@ declare class ShoppingList {
579
1115
  /**
580
1116
  * The ingredients in the shopping list.
581
1117
  */
582
- ingredients: Ingredient[];
1118
+ ingredients: AddedIngredient[];
583
1119
  /**
584
1120
  * The recipes in the shopping list.
585
1121
  */
@@ -602,21 +1138,17 @@ declare class ShoppingList {
602
1138
  * Adds a recipe to the shopping list, then automatically
603
1139
  * recalculates the quantities and recategorize the ingredients.
604
1140
  * @param recipe - The recipe to add.
605
- * @param scaling - The scaling option for the recipe. Can be either a factor or a number of servings
1141
+ * @param options - Options for adding the recipe.
1142
+ * @throws Error if the recipe has alternatives without corresponding choices.
606
1143
  */
607
- add_recipe(recipe: Recipe, scaling?: {
608
- factor: number;
609
- } | {
610
- servings: number;
611
- }): void;
1144
+ add_recipe(recipe: Recipe, options?: AddedRecipeOptions): void;
612
1145
  /**
613
- * Adds a recipe to the shopping list, then automatically
614
- * recalculates the quantities and recategorize the ingredients.
615
- * @param recipe - The recipe to add.
616
- * @param factor - The factor to scale the recipe by.
617
- * @deprecated since v2.0.3. Use the other call signature with `scaling` instead. Will be removed in v3
1146
+ * Checks if a recipe has unresolved alternatives (alternatives without provided choices).
1147
+ * @param recipe - The recipe to check.
1148
+ * @param choices - The choices provided for the recipe.
1149
+ * @returns An error message if there are unresolved alternatives, undefined otherwise.
618
1150
  */
619
- add_recipe(recipe: Recipe, factor?: number): void;
1151
+ private getUnresolvedAlternativesError;
620
1152
  /**
621
1153
  * Removes a recipe from the shopping list, then automatically
622
1154
  * recalculates the quantities and recategorize the ingredients.s
@@ -636,4 +1168,374 @@ declare class ShoppingList {
636
1168
  categorize(): void;
637
1169
  }
638
1170
 
639
- export { type AddedRecipe, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedValue, type FractionValue, type Ingredient, type IngredientExtras, type IngredientFlag, type IngredientItem, type Item, type Metadata, type Note, type QuantityPart, type Range, Recipe, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem };
1171
+ /**
1172
+ * Options for the {@link ShoppingCart} constructor
1173
+ * @category Types
1174
+ */
1175
+ interface ShoppingCartOptions {
1176
+ /**
1177
+ * A product catalog to connect to the cart
1178
+ */
1179
+ catalog?: ProductCatalog;
1180
+ /**
1181
+ * A shopping list to connect to the cart
1182
+ */
1183
+ list?: ShoppingList;
1184
+ }
1185
+ /**
1186
+ * Key information about the {@link ShoppingCart}
1187
+ * @category Types
1188
+ */
1189
+ interface ShoppingCartSummary {
1190
+ /**
1191
+ * The total price of the cart
1192
+ */
1193
+ totalPrice: number;
1194
+ /**
1195
+ * The total number of items in the cart
1196
+ */
1197
+ totalItems: number;
1198
+ }
1199
+ /**
1200
+ * Shopping Cart Manager: a tool to find the best combination of products to buy (defined in a {@link ProductCatalog}) to satisfy a {@link ShoppingList}.
1201
+ *
1202
+ * @example
1203
+ * ```ts
1204
+ * const shoppingList = new ShoppingList();
1205
+ * const recipe = new Recipe("@flour{600%g}");
1206
+ * shoppingList.add_recipe(recipe);
1207
+ *
1208
+ * const catalog = new ProductCatalog();
1209
+ * catalog.products = [
1210
+ * {
1211
+ * id: "flour-1kg",
1212
+ * productName: "Flour (1kg)",
1213
+ * ingredientName: "flour",
1214
+ * price: 10,
1215
+ * size: { type: "fixed", value: { type: "decimal", value: 1000 } },
1216
+ * unit: "g",
1217
+ * },
1218
+ * {
1219
+ * id: "flour-500g",
1220
+ * productName: "Flour (500g)",
1221
+ * ingredientName: "flour",
1222
+ * price: 6,
1223
+ * size: { type: "fixed", value: { type: "decimal", value: 500 } },
1224
+ * unit: "g",
1225
+ * },
1226
+ * ];
1227
+ *
1228
+ * const shoppingCart = new ShoppingCart({list: shoppingList, catalog}))
1229
+ * shoppingCart.buildCart();
1230
+ * ```
1231
+ *
1232
+ * @category Classes
1233
+ */
1234
+ declare class ShoppingCart {
1235
+ /**
1236
+ * The product catalog to use for matching products
1237
+ */
1238
+ productCatalog?: ProductCatalog;
1239
+ /**
1240
+ * The shopping list to build the cart from
1241
+ */
1242
+ shoppingList?: ShoppingList;
1243
+ /**
1244
+ * The content of the cart
1245
+ */
1246
+ cart: CartContent;
1247
+ /**
1248
+ * The ingredients that were successfully matched with products
1249
+ */
1250
+ match: CartMatch;
1251
+ /**
1252
+ * The ingredients that could not be matched with products
1253
+ */
1254
+ misMatch: CartMisMatch;
1255
+ /**
1256
+ * Key information about the shopping cart
1257
+ */
1258
+ summary: ShoppingCartSummary;
1259
+ /**
1260
+ * Creates a new ShoppingCart instance
1261
+ * @param options - {@link ShoppingCartOptions | Options} for the constructor
1262
+ */
1263
+ constructor(options?: ShoppingCartOptions);
1264
+ /**
1265
+ * Sets the product catalog to use for matching products
1266
+ * To use if a catalog was not provided at the creation of the instance
1267
+ * @param catalog - The {@link ProductCatalog} to set
1268
+ */
1269
+ setProductCatalog(catalog: ProductCatalog): void;
1270
+ /**
1271
+ * Sets the shopping list to build the cart from.
1272
+ * To use if a shopping list was not provided at the creation of the instance
1273
+ * @param list - The {@link ShoppingList} to set
1274
+ */
1275
+ setShoppingList(list: ShoppingList): void;
1276
+ /**
1277
+ * Builds the cart from the shopping list and product catalog
1278
+ * @remarks
1279
+ * - If a combination of product(s) is successfully found for a given ingredient, the latter will be listed in the {@link ShoppingCart.match | match} array
1280
+ * in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
1281
+ * - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
1282
+ * - No product is listed in the catalog for that ingredient
1283
+ * - The ingredient has no quantity, a text quantity
1284
+ * - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
1285
+ * @throws {@link NoProductCatalogForCartError} if no product catalog is set
1286
+ * @throws {@link NoShoppingListForCartError} if no shopping list is set
1287
+ * @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
1288
+ */
1289
+ buildCart(): boolean;
1290
+ /**
1291
+ * Gets the product options for a given ingredient
1292
+ * @param ingredient - The ingredient to get the product options for
1293
+ * @returns An array of {@link ProductOption}
1294
+ */
1295
+ private getProductOptions;
1296
+ /**
1297
+ * Gets the optimum match for a given ingredient and product option
1298
+ * @param ingredient - The ingredient to match
1299
+ * @param options - The product options to choose from
1300
+ * @returns An array of {@link ProductSelection}
1301
+ * @throws {@link NoProductMatchError} if no match can be found
1302
+ */
1303
+ private getOptimumMatch;
1304
+ /**
1305
+ * Reset the cart's properties
1306
+ */
1307
+ private resetCart;
1308
+ /**
1309
+ * Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
1310
+ * This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
1311
+ * @returns the total price and number of items in the cart
1312
+ */
1313
+ summarize(): ShoppingCartSummary;
1314
+ }
1315
+
1316
+ /**
1317
+ * Format a numeric value (decimal or fraction) to a string.
1318
+ *
1319
+ * @param value - The decimal or fraction value to format
1320
+ * @returns The formatted string representation
1321
+ * @category Helpers
1322
+ *
1323
+ * @example
1324
+ * ```typescript
1325
+ * formatNumericValue({ type: "decimal", decimal: 1.5 }); // "1.5"
1326
+ * formatNumericValue({ type: "fraction", num: 1, den: 2 }); // "1/2"
1327
+ * ```
1328
+ */
1329
+ declare function formatNumericValue(value: DecimalValue | FractionValue): string;
1330
+ /**
1331
+ * Format a single value (text, decimal, or fraction) to a string.
1332
+ *
1333
+ * @param value - The value to format
1334
+ * @returns The formatted string representation
1335
+ * @category Helpers
1336
+ *
1337
+ * @example
1338
+ * ```typescript
1339
+ * formatSingleValue({ type: "text", text: "a pinch" }); // "a pinch"
1340
+ * formatSingleValue({ type: "decimal", decimal: 2 }); // "2"
1341
+ * formatSingleValue({ type: "fraction", num: 3, den: 4 }); // "3/4"
1342
+ * ```
1343
+ */
1344
+ declare function formatSingleValue(value: TextValue | DecimalValue | FractionValue): string;
1345
+ /**
1346
+ * Format a quantity (fixed value or range) to a string.
1347
+ *
1348
+ * @param quantity - The quantity to format
1349
+ * @returns The formatted string representation
1350
+ * @category Helpers
1351
+ *
1352
+ * @example
1353
+ * ```typescript
1354
+ * formatQuantity({ type: "fixed", value: { type: "decimal", decimal: 100 } }); // "100"
1355
+ * formatQuantity({ type: "range", min: { type: "decimal", decimal: 1 }, max: { type: "decimal", decimal: 2 } }); // "1-2"
1356
+ * ```
1357
+ */
1358
+ declare function formatQuantity(quantity: FixedValue | Range): string;
1359
+ /**
1360
+ * Format a unit to a string. Handles both plain string units and Unit objects.
1361
+ *
1362
+ * @param unit - The unit to format (string, Unit object, or undefined)
1363
+ * @returns The formatted unit string, or empty string if undefined
1364
+ * @category Helpers
1365
+ *
1366
+ * @example
1367
+ * ```typescript
1368
+ * formatUnit("g"); // "g"
1369
+ * formatUnit({ name: "grams" }); // "grams"
1370
+ * formatUnit(undefined); // ""
1371
+ * ```
1372
+ */
1373
+ declare function formatUnit(unit: string | Unit | undefined): string;
1374
+ /**
1375
+ * Format a quantity with its unit to a string.
1376
+ *
1377
+ * @param quantity - The quantity to format
1378
+ * @param unit - The unit to append (string, Unit object, or undefined)
1379
+ * @returns The formatted string with quantity and unit
1380
+ * @category Helpers
1381
+ *
1382
+ * @example
1383
+ * ```typescript
1384
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 100 } }, "g"); // "100 g"
1385
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 2 } }, undefined); // "2"
1386
+ * ```
1387
+ */
1388
+ declare function formatQuantityWithUnit(quantity: FixedValue | Range | undefined, unit: string | Unit | undefined): string;
1389
+ /**
1390
+ * Format a QuantityWithExtendedUnit to a string.
1391
+ *
1392
+ * @param item - The quantity with extended unit to format
1393
+ * @returns The formatted string
1394
+ * @category Helpers
1395
+ */
1396
+ declare function formatExtendedQuantity(item: QuantityWithExtendedUnit): string;
1397
+ /**
1398
+ * Format an IngredientItemQuantity with all its equivalents to a string.
1399
+ *
1400
+ * @param itemQuantity - The ingredient item quantity to format
1401
+ * @param separator - The separator between primary and equivalent quantities (default: " | ")
1402
+ * @returns The formatted string with all quantities
1403
+ * @category Helpers
1404
+ *
1405
+ * @example
1406
+ * ```typescript
1407
+ * // For an ingredient like @flour{100%g|3.5%oz}
1408
+ * formatItemQuantity(itemQuantity); // "100 g | 3.5 oz"
1409
+ * formatItemQuantity(itemQuantity, " / "); // "100 g / 3.5 oz"
1410
+ * ```
1411
+ */
1412
+ declare function formatItemQuantity(itemQuantity: IngredientItemQuantity, separator?: string): string;
1413
+ /**
1414
+ * Check if an ingredient item is a grouped alternative (vs inline alternative).
1415
+ *
1416
+ * Grouped alternatives are ingredients that share a group key (e.g., `@|milk|...`)
1417
+ * and are distributed across multiple tokens in the recipe.
1418
+ *
1419
+ * @param item - The ingredient item to check
1420
+ * @returns true if this is a grouped alternative
1421
+ * @category Helpers
1422
+ *
1423
+ * @example
1424
+ * ```typescript
1425
+ * for (const item of step.items) {
1426
+ * if (item.type === 'ingredient') {
1427
+ * if (isGroupedItem(item)) {
1428
+ * // Handle grouped alternative (e.g., show with strikethrough if not selected)
1429
+ * } else {
1430
+ * // Handle inline alternative (e.g., hide if not selected)
1431
+ * }
1432
+ * }
1433
+ * }
1434
+ * ```
1435
+ */
1436
+ declare function isGroupedItem(item: IngredientItem): boolean;
1437
+ /**
1438
+ * Determines if a specific alternative in an IngredientItem is selected
1439
+ * based on the applied choices.
1440
+ *
1441
+ * Use this in renderers to determine how an ingredient alternative should be displayed.
1442
+ *
1443
+ * @param recipe - The Recipe instance containing choices
1444
+ * @param choices - The choices that have been made
1445
+ * @param item - The IngredientItem to check
1446
+ * @param alternativeIndex - The index within item.alternatives to check (for inline alternatives only)
1447
+ * @returns true if this alternative is the selected one
1448
+ * @category Helpers
1449
+ *
1450
+ * @example
1451
+ * ```typescript
1452
+ * const recipe = new Recipe(cooklangText);
1453
+ * for (const item of step.items) {
1454
+ * if (item.type === 'ingredient') {
1455
+ * item.alternatives.forEach((alt, idx) => {
1456
+ * const isSelected = isAlternativeSelected(item, idx, recipe, choices);
1457
+ * // Render differently based on isSelected
1458
+ * });
1459
+ * }
1460
+ * }
1461
+ * ```
1462
+ */
1463
+ declare function isAlternativeSelected(recipe: Recipe, choices: RecipeChoices, item: IngredientItem, alternativeIndex?: number): boolean;
1464
+
1465
+ /**
1466
+ * Type guard to check if an ingredient quantity-like object is an AND group.
1467
+ * *
1468
+ * @param x - The quantity-like entry to check
1469
+ * @returns true if this is an AND group (has `and` property)
1470
+ * @category Helpers
1471
+ *
1472
+ * @example
1473
+ * ```typescript
1474
+ * for (const entry of ingredient.quantities) {
1475
+ * if (isAndGroup(entry)) {
1476
+ * // entry.and contains the list of quantities in the AND group
1477
+ * }
1478
+ * }
1479
+ * ```
1480
+ */
1481
+ declare function isAndGroup(x: IngredientQuantityGroup | IngredientQuantityAndGroup): x is IngredientQuantityAndGroup;
1482
+ declare function isAndGroup(x: QuantityWithUnitLike | Group): x is AndGroup;
1483
+ /**
1484
+ * Type guard to check if an ingredient quantity entry is a simple group.
1485
+ *
1486
+ * Simple groups have a single quantity with optional unit and equivalents.
1487
+ *
1488
+ * @param entry - The quantity entry to check
1489
+ * @returns true if this is a simple group (has `quantity` property)
1490
+ * @category Helpers
1491
+ *
1492
+ * @example
1493
+ * ```typescript
1494
+ * for (const entry of ingredient.quantities) {
1495
+ * if (isSimpleGroup(entry)) {
1496
+ * // entry.quantity is available
1497
+ * // entry.unit is available
1498
+ * }
1499
+ * }
1500
+ * ```
1501
+ */
1502
+ declare function isSimpleGroup(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is IngredientQuantityGroup;
1503
+ /**
1504
+ * Type guard to check if an ingredient quantity entry has alternatives.
1505
+ *
1506
+ * @param entry - The quantity entry to check
1507
+ * @returns true if this entry has alternatives
1508
+ * @category Helpers
1509
+ *
1510
+ * @example
1511
+ * ```typescript
1512
+ * for (const entry of ingredient.quantities) {
1513
+ * if (hasAlternatives(entry)) {
1514
+ * // entry.alternatives is available and non-empty
1515
+ * for (const alt of entry.alternatives) {
1516
+ * console.log(`Alternative ingredient index: ${alt.index}`);
1517
+ * }
1518
+ * }
1519
+ * }
1520
+ * ```
1521
+ */
1522
+ declare function hasAlternatives(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is (IngredientQuantityGroup | IngredientQuantityAndGroup) & {
1523
+ alternatives: AlternativeIngredientRef[];
1524
+ };
1525
+
1526
+ /**
1527
+ * Error thrown when trying to build a shopping cart without a product catalog
1528
+ * @category Errors
1529
+ */
1530
+ declare class NoProductCatalogForCartError extends Error {
1531
+ constructor();
1532
+ }
1533
+ /**
1534
+ * Error thrown when trying to build a shopping cart without a shopping list
1535
+ * @category Errors
1536
+ */
1537
+ declare class NoShoppingListForCartError extends Error {
1538
+ constructor();
1539
+ }
1540
+
1541
+ export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type AndGroup, type ArbitraryScalable, type ArbitraryScalableItem, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FlatAndGroup, type FlatGroup, type FlatOrGroup, type FractionValue, type GetIngredientQuantitiesOptions, type Group, 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, type OrGroup, 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, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup };