@tmlmt/cooklang-parser 2.1.6 → 3.0.0-alpha.3

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.ts CHANGED
@@ -65,6 +65,11 @@ declare class Recipe {
65
65
  * The parsed recipe metadata.
66
66
  */
67
67
  metadata: Metadata;
68
+ /**
69
+ * The default or manual choice of alternative ingredients.
70
+ * Contains the full context including alternatives list and active selection index.
71
+ */
72
+ choices: RecipeAlternatives;
68
73
  /**
69
74
  * The parsed recipe ingredients.
70
75
  */
@@ -89,11 +94,48 @@ declare class Recipe {
89
94
  * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
90
95
  */
91
96
  servings?: number;
97
+ /**
98
+ * External storage for item count (not a property on instances).
99
+ * Used for giving ID numbers to items during parsing.
100
+ */
101
+ private static itemCounts;
102
+ /**
103
+ * Gets the current item count for this recipe.
104
+ */
105
+ private getItemCount;
106
+ /**
107
+ * Gets the current item count and increments it.
108
+ */
109
+ private getAndIncrementItemCount;
92
110
  /**
93
111
  * Creates a new Recipe instance.
94
112
  * @param content - The recipe content to parse.
95
113
  */
96
114
  constructor(content?: string);
115
+ private _parseQuantityRecursive;
116
+ private _parseIngredientWithAlternativeRecursive;
117
+ private _parseIngredientWithGroupKey;
118
+ /**
119
+ * Populates the `quantities` property for each ingredient based on
120
+ * how they appear in the recipe preparation. Only primary ingredients
121
+ * get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
122
+ *
123
+ * For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
124
+ * For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
125
+ *
126
+ * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
127
+ * @internal
128
+ */
129
+ private _populate_ingredient_quantities;
130
+ /**
131
+ * Calculates ingredient quantities based on the provided choices.
132
+ * Returns a list of computed ingredients with their total quantities.
133
+ *
134
+ * @param choices - The recipe choices to apply when computing quantities.
135
+ * If not provided, uses the default choices (first alternative for each item).
136
+ * @returns An array of ComputedIngredient with quantityTotal calculated based on choices.
137
+ */
138
+ calc_ingredient_quantities(choices?: RecipeChoices): ComputedIngredient[];
97
139
  /**
98
140
  * Parses a recipe from a string.
99
141
  * @param content - The recipe content to parse.
@@ -128,11 +170,6 @@ declare class Recipe {
128
170
  clone(): Recipe;
129
171
  }
130
172
 
131
- interface Quantity {
132
- value: FixedValue | Range;
133
- unit?: string;
134
- }
135
-
136
173
  /**
137
174
  * Represents the metadata of a recipe.
138
175
  * @category Types
@@ -250,7 +287,7 @@ interface Metadata {
250
287
  */
251
288
  interface TextValue {
252
289
  type: "text";
253
- value: string;
290
+ text: string;
254
291
  }
255
292
  /**
256
293
  * Represents a quantity described by a decimal number, e.g. "1.5"
@@ -258,7 +295,7 @@ interface TextValue {
258
295
  */
259
296
  interface DecimalValue {
260
297
  type: "decimal";
261
- value: number;
298
+ decimal: number;
262
299
  }
263
300
  /**
264
301
  * Represents a quantity described by a fraction, e.g. "1/2"
@@ -280,6 +317,15 @@ interface FixedValue {
280
317
  type: "fixed";
281
318
  value: TextValue | DecimalValue | FractionValue;
282
319
  }
320
+ /**
321
+ * Represents a single, fixed numeric quantity.
322
+ * This can be a decimal or fraction.
323
+ * @category Types
324
+ */
325
+ interface FixedNumericValue {
326
+ type: "fixed";
327
+ value: DecimalValue | FractionValue;
328
+ }
283
329
  /**
284
330
  * Represents a range of quantities, e.g. "1-2"
285
331
  * @category Types
@@ -289,16 +335,6 @@ interface Range {
289
335
  min: DecimalValue | FractionValue;
290
336
  max: DecimalValue | FractionValue;
291
337
  }
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
338
  /**
303
339
  * Represents a possible state modifier or other flag for an ingredient in a recipe
304
340
  * @category Types
@@ -324,6 +360,59 @@ interface IngredientExtras {
324
360
  */
325
361
  path: string;
326
362
  }
363
+ /**
364
+ * Represents a reference to an alternative ingredient along with its quantities.
365
+ *
366
+ * Used in {@link IngredientQuantityGroup} to describe what other ingredients
367
+ * could be used in place of the main ingredient.
368
+ * @category Types
369
+ */
370
+ interface AlternativeIngredientRef {
371
+ /** The index of the alternative ingredient within the {@link Recipe.ingredients} array. */
372
+ index: number;
373
+ /** The quantities of the alternative ingredient. Multiple entries when units are incompatible. */
374
+ alternativeQuantities?: QuantityWithPlainUnit[];
375
+ }
376
+ /**
377
+ * Represents a group of summed quantities for an ingredient, optionally with alternatives.
378
+ * Quantities with the same alternative signature are summed together into a single group.
379
+ * When units are incompatible, separate IngredientQuantityGroup entries are created instead of merging.
380
+ * @category Types
381
+ */
382
+ interface IngredientQuantityGroup {
383
+ /**
384
+ * References to alternative ingredients for this quantity group.
385
+ * If undefined, this group has no alternatives.
386
+ */
387
+ 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
+ }
395
+ /**
396
+ * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
397
+ * For example: 1 large carrot + 2 small carrots, both with cup equivalents that sum to 5 cups.
398
+ * @category Types
399
+ */
400
+ interface IngredientQuantityAndGroup {
401
+ type: "and";
402
+ /**
403
+ * The incompatible primary quantities (e.g., "1 large" and "2 small").
404
+ */
405
+ entries: QuantityWithPlainUnit[];
406
+ /**
407
+ * The summed equivalent quantities (e.g., "5 cups" from summing "1.5 cup + 2 cup + 1.5 cup").
408
+ */
409
+ equivalents?: QuantityWithPlainUnit[];
410
+ /**
411
+ * References to alternative ingredients for this quantity group.
412
+ * If undefined, this group has no alternatives.
413
+ */
414
+ alternatives?: AlternativeIngredientRef[];
415
+ }
327
416
  /**
328
417
  * Represents an ingredient in a recipe.
329
418
  * @category Types
@@ -331,40 +420,84 @@ interface IngredientExtras {
331
420
  interface Ingredient {
332
421
  /** The name of the ingredient. */
333
422
  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[];
423
+ /**
424
+ * Represents the quantities list for an ingredient as groups.
425
+ * Each group contains summed quantities that share the same alternative signature.
426
+ * Groups can be either simple (single unit) or AND groups (incompatible primary units with summed equivalents).
427
+ * Only populated for primary ingredients (not alternative-only).
428
+ * Quantities without alternatives are merged opportunistically when units are compatible.
429
+ * Quantities with alternatives are only merged if the alternatives are exactly the same.
430
+ */
431
+ quantities?: (IngredientQuantityGroup | IngredientQuantityAndGroup)[];
340
432
  /** The preparation of the ingredient. */
341
433
  preparation?: string;
434
+ /** The list of indexes of the ingredients mentioned in the preparation as alternatives to this ingredient */
435
+ alternatives?: Set<number>;
436
+ /**
437
+ * True if this ingredient appears as the primary choice (first in an alternatives list).
438
+ * Only primary ingredients have quantities populated directly.
439
+ *
440
+ * Alternative-only ingredients (usedAsPrimary undefined/false) have their quantities
441
+ * available via the {@link Recipe.choices} structure.
442
+ */
443
+ usedAsPrimary?: boolean;
342
444
  /** A list of potential state modifiers or other flags for the ingredient */
343
445
  flags?: IngredientFlag[];
344
446
  /** The collection of potential additional metadata for the ingredient */
345
447
  extras?: IngredientExtras;
346
448
  }
347
449
  /**
348
- * Represents a timer in a recipe.
450
+ * Represents a computed ingredient with its total quantity after applying choices.
451
+ * Used as the return type of {@link Recipe.calc_ingredient_quantities}.
349
452
  * @category Types
350
453
  */
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;
454
+ interface ComputedIngredient {
455
+ /** The name of the ingredient. */
456
+ name: string;
457
+ /** The total quantity of the ingredient after applying choices. */
458
+ quantityTotal?: QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit>;
459
+ /** The preparation of the ingredient. */
460
+ preparation?: string;
461
+ /** The list of ingredients mentioned in the preparation as alternatives to this ingredient */
462
+ alternatives?: Set<number>;
463
+ /** A list of potential state modifiers or other flags for the ingredient */
464
+ flags?: IngredientFlag[];
465
+ /** The collection of potential additional metadata for the ingredient */
466
+ extras?: IngredientExtras;
358
467
  }
359
468
  /**
360
- * Represents a text item in a recipe step.
469
+ * Represents a contributor to an ingredient's total quantity, corresponding
470
+ * to a single mention in the recipe text. It can contain multiple
471
+ * equivalent quantities (e.g., in different units).
361
472
  * @category Types
362
473
  */
363
- interface TextItem {
364
- /** The type of the item. */
365
- type: "text";
366
- /** The content of the text item. */
367
- value: string;
474
+ interface IngredientItemQuantity extends QuantityWithExtendedUnit {
475
+ /**
476
+ * A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
477
+ * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g.
478
+ */
479
+ equivalents?: QuantityWithExtendedUnit[];
480
+ /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
481
+ scalable: boolean;
482
+ }
483
+ /**
484
+ * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
485
+ * to a specific ingredient and its corresponding quantity information.
486
+ * @category Types
487
+ */
488
+ interface IngredientAlternative {
489
+ /** The index of the ingredient within the {@link Recipe.ingredients} array. */
490
+ index: number;
491
+ /** The quantity of this specific mention of the ingredient */
492
+ itemQuantity?: IngredientItemQuantity;
493
+ /** The alias/name of the ingredient as it should be displayed for this occurrence. */
494
+ displayName: string;
495
+ /** An optional note for this specific choice (e.g., "for a vegan version"). */
496
+ note?: string;
497
+ /** When {@link Recipe.choices} is populated for alternatives ingredients
498
+ * with group keys: the id of the corresponding ingredient item (e.g. "ingredient-item-2").
499
+ * Can be useful for creating alternative selection UI elements with anchor links */
500
+ itemId?: string;
368
501
  }
369
502
  /**
370
503
  * Represents an ingredient item in a recipe step.
@@ -373,14 +506,46 @@ interface TextItem {
373
506
  interface IngredientItem {
374
507
  /** The type of the item. */
375
508
  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;
509
+ /** The item identifier */
510
+ id: string;
511
+ /**
512
+ * A list of alternative ingredient choices. For a standard ingredient,
513
+ * this array will contain a single element.
514
+ */
515
+ alternatives: IngredientAlternative[];
516
+ /**
517
+ * An optional identifier for linking distributed alternatives. If multiple
518
+ * `IngredientItem`s in a recipe share the same `group` ID (e.g., from
519
+ * `@|group|...` syntax), they represent a single logical choice.
520
+ */
521
+ group?: string;
522
+ }
523
+ /**
524
+ * Represents the choices one can make in a recipe
525
+ * @category Types
526
+ */
527
+ 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")
530
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that item
531
+ */
532
+ ingredientItems: Map<string, IngredientAlternative[]>;
533
+ /** Map of choices that can be made for Grouped Ingredient Item's
534
+ * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
535
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that group
536
+ */
537
+ ingredientGroups: Map<string, IngredientAlternative[]>;
538
+ }
539
+ /**
540
+ * Represents the choices to apply when computing ingredient quantities.
541
+ * Maps item/group IDs to the index of the selected alternative.
542
+ * @category Types
543
+ */
544
+ interface RecipeChoices {
545
+ /** Map of choices that can be made at Ingredient Item level */
546
+ ingredientItems?: Map<string, number>;
547
+ /** Map of choices that can be made for Grouped Ingredient Item's */
548
+ ingredientGroups?: Map<string, number>;
384
549
  }
385
550
  /**
386
551
  * Represents a cookware item in a recipe step.
@@ -391,9 +556,8 @@ interface CookwareItem {
391
556
  type: "cookware";
392
557
  /** The index of the cookware, within the {@link Recipe.cookware | list of cookware} */
393
558
  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;
559
+ /** The quantity of this specific mention of the cookware */
560
+ quantity?: FixedValue | Range;
397
561
  }
398
562
  /**
399
563
  * Represents a timer item in a recipe step.
@@ -405,6 +569,28 @@ interface TimerItem {
405
569
  /** The index of the timer, within the {@link Recipe.timers | list of timers} */
406
570
  index: number;
407
571
  }
572
+ /**
573
+ * Represents a timer in a recipe.
574
+ * @category Types
575
+ */
576
+ interface Timer {
577
+ /** The name of the timer. */
578
+ name?: string;
579
+ /** The duration of the timer. */
580
+ duration: FixedValue | Range;
581
+ /** The unit of the timer. */
582
+ unit: string;
583
+ }
584
+ /**
585
+ * Represents a text item in a recipe step.
586
+ * @category Types
587
+ */
588
+ interface TextItem {
589
+ /** The type of the item. */
590
+ type: "text";
591
+ /** The content of the text item. */
592
+ value: string;
593
+ }
408
594
  /**
409
595
  * Represents an item in a recipe step.
410
596
  * @category Types
@@ -442,17 +628,15 @@ interface Cookware {
442
628
  name: string;
443
629
  /** The quantity of cookware */
444
630
  quantity?: FixedValue | Range;
445
- /** The array of contributors to the cookware's total quantity. */
446
- quantityParts?: (FixedValue | Range)[];
447
631
  /** A list of potential state modifiers or other flags for the cookware */
448
- flags: CookwareFlag[];
632
+ flags?: CookwareFlag[];
449
633
  }
450
634
  /**
451
635
  * Represents categorized ingredients.
452
636
  * @category Types
453
637
  */
454
638
  interface CategorizedIngredients {
455
- [category: string]: Ingredient[];
639
+ [category: string]: AddedIngredient[];
456
640
  }
457
641
  /**
458
642
  * Represents a recipe together with a scaling factor
@@ -463,6 +647,8 @@ interface RecipeWithFactor {
463
647
  recipe: Recipe;
464
648
  /** The factor the recipe is scaled by. */
465
649
  factor: number;
650
+ /** The choices for alternative ingredients. */
651
+ choices?: RecipeChoices;
466
652
  }
467
653
  /**
468
654
  * Represents a recipe together with a servings value for scaling
@@ -473,12 +659,33 @@ interface RecipeWithServings {
473
659
  recipe: Recipe;
474
660
  /** The servings the recipe is scaled to */
475
661
  servings: number;
662
+ /** The choices for alternative ingredients. */
663
+ choices?: RecipeChoices;
476
664
  }
477
665
  /**
478
666
  * Represents a recipe that has been added to a shopping list.
479
667
  * @category Types
480
668
  */
481
669
  type AddedRecipe = RecipeWithFactor | RecipeWithServings;
670
+ /**
671
+ * Options for adding a recipe to a shopping list
672
+ * @category Types
673
+ */
674
+ type AddedRecipeOptions = {
675
+ /** The scaling option for the recipe. Can be either a factor or a number of servings */
676
+ scaling?: {
677
+ factor: number;
678
+ } | {
679
+ servings: number;
680
+ };
681
+ /** The choices for alternative ingredients. */
682
+ choices?: RecipeChoices;
683
+ };
684
+ /**
685
+ * Represents an ingredient that has been added to a shopping list
686
+ * @category Types
687
+ */
688
+ type AddedIngredient = Pick<ComputedIngredient, "name" | "quantityTotal">;
482
689
  /**
483
690
  * Represents an ingredient in a category.
484
691
  * @category Types
@@ -499,6 +706,191 @@ interface Category {
499
706
  /** The ingredients in the category. */
500
707
  ingredients: CategoryIngredient[];
501
708
  }
709
+ /**
710
+ * Represents a single size expression for a product (value + optional unit)
711
+ * @category Types
712
+ */
713
+ interface ProductSize {
714
+ /** The numeric size value */
715
+ size: FixedNumericValue;
716
+ /** The unit of the size (optional) */
717
+ unit?: string;
718
+ }
719
+ /**
720
+ * Core properties for {@link ProductOption}
721
+ * @category Types
722
+ */
723
+ interface ProductOptionCore {
724
+ /** The ID of the product */
725
+ id: string;
726
+ /** The name of the product */
727
+ productName: string;
728
+ /** The name of the ingredient it corresponds to */
729
+ ingredientName: string;
730
+ /** The aliases of the ingredient it also corresponds to */
731
+ ingredientAliases?: string[];
732
+ /** The price of the product */
733
+ price: number;
734
+ }
735
+ /**
736
+ * Base type for {@link ProductOption} allowing arbitrary additional metadata
737
+ * @category Types
738
+ */
739
+ type ProductOptionBase = ProductOptionCore & Record<string, unknown>;
740
+ /**
741
+ * Represents a product option in a {@link ProductCatalog}
742
+ * @category Types
743
+ */
744
+ type ProductOption = ProductOptionBase & {
745
+ /** The size(s) of the product. Multiple sizes allow equivalent expressions (e.g., "1%dozen" and "12") */
746
+ sizes: ProductSize[];
747
+ };
748
+ /**
749
+ * Represents a product selection in a {@link ShoppingCart}
750
+ * @category Types
751
+ */
752
+ interface ProductSelection {
753
+ /** The selected product */
754
+ product: ProductOption;
755
+ /** The quantity of the selected product */
756
+ quantity: number;
757
+ /** The total price for this selected product */
758
+ totalPrice: number;
759
+ }
760
+ /**
761
+ * Represents the content of the actual cart of the {@link ShoppingCart}
762
+ * @category Types
763
+ */
764
+ type CartContent = ProductSelection[];
765
+ /**
766
+ * Represents a successful match between a ingredient and product(s) in the product catalog, in a {@link ShoppingCart}
767
+ * @category Types
768
+ */
769
+ interface ProductMatch {
770
+ ingredient: Ingredient;
771
+ selection: ProductSelection[];
772
+ }
773
+ /**
774
+ * Represents all successful matches between ingredients and the product catalog, in a {@link ShoppingCart}
775
+ * @category Types
776
+ */
777
+ type CartMatch = ProductMatch[];
778
+ /**
779
+ * Represents the error codes for an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
780
+ * @category Types
781
+ */
782
+ type NoProductMatchErrorCode = "incompatibleUnits" | "textValue" | "textValue_incompatibleUnits" | "noProduct" | "noQuantity";
783
+ /**
784
+ * Represents an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
785
+ * @category Types
786
+ */
787
+ interface ProductMisMatch {
788
+ ingredient: Ingredient;
789
+ reason: NoProductMatchErrorCode;
790
+ }
791
+ /**
792
+ * Represents all ingredients which didn't match with any product in the product catalog, in a {@link ShoppingCart}
793
+ * @category Types
794
+ */
795
+ type CartMisMatch = ProductMisMatch[];
796
+ /**
797
+ * Represents the type category of a unit used for quantities
798
+ * @category Types
799
+ */
800
+ type UnitType = "mass" | "volume" | "count";
801
+ /**
802
+ * Represents the measurement system a unit belongs to
803
+ * @category Types
804
+ */
805
+ type UnitSystem = "metric" | "imperial";
806
+ /**
807
+ * Represents a unit used to describe quantities
808
+ * @category Types
809
+ */
810
+ interface Unit {
811
+ name: string;
812
+ /** This property is set to true when the unit is prefixed by an `=` sign in the cooklang file, e.g. `=g`
813
+ * Indicates that quantities with this unit should be treated as integers only (no decimal/fractional values). */
814
+ integerProtected?: boolean;
815
+ }
816
+ /**
817
+ * Represents a fully defined unit with conversion and alias information
818
+ * @category Types
819
+ */
820
+ interface UnitDefinition extends Unit {
821
+ type: UnitType;
822
+ system: UnitSystem;
823
+ /** e.g. ['gram', 'grams'] */
824
+ aliases: string[];
825
+ /** Conversion factor to the base unit of its type */
826
+ toBase: number;
827
+ }
828
+ /**
829
+ * Represents a resolved unit definition or a lightweight placeholder for non-standard units
830
+ * @category Types
831
+ */
832
+ type UnitDefinitionLike = UnitDefinition | {
833
+ name: string;
834
+ type: "other";
835
+ system: "none";
836
+ integerProtected?: boolean;
837
+ };
838
+ /**
839
+ * Core quantity container holding a fixed value or a range
840
+ * @category Types
841
+ */
842
+ interface QuantityBase {
843
+ quantity: FixedValue | Range;
844
+ }
845
+ /**
846
+ * Represents a quantity with an optional plain (string) unit
847
+ * @category Types
848
+ */
849
+ interface QuantityWithPlainUnit extends QuantityBase {
850
+ unit?: string;
851
+ /** Optional equivalent quantities in different units (for alternative units like `@flour{100%g|3.5%oz}`) */
852
+ equivalents?: QuantityWithPlainUnit[];
853
+ }
854
+ /**
855
+ * Represents a quantity with an optional extended `Unit` object
856
+ * @category Types
857
+ */
858
+ interface QuantityWithExtendedUnit extends QuantityBase {
859
+ unit?: Unit;
860
+ }
861
+ /**
862
+ * Represents a quantity with a resolved unit definition
863
+ * @category Types
864
+ */
865
+ interface QuantityWithUnitDef extends QuantityBase {
866
+ unit: UnitDefinitionLike;
867
+ }
868
+ /**
869
+ * Represents any quantity shape supported by the parser (plain, extended, or resolved unit)
870
+ * @category Types
871
+ */
872
+ type QuantityWithUnitLike = QuantityWithPlainUnit | QuantityWithExtendedUnit | QuantityWithUnitDef;
873
+ /**
874
+ * Represents an "or" group of alternative quantities that may contain nested groups (alternatives with nested structure)
875
+ * @category Types
876
+ */
877
+ interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
878
+ type: "or";
879
+ entries: (T | MaybeNestedGroup<T>)[];
880
+ }
881
+ /**
882
+ * Represents an "and" group of quantities that may contain nested groups (combinations with nested structure)
883
+ * @category Types
884
+ */
885
+ interface MaybeNestedAndGroup<T = QuantityWithUnitLike> {
886
+ type: "and";
887
+ entries: (T | MaybeNestedGroup<T>)[];
888
+ }
889
+ /**
890
+ * Represents any group type that may include nested groups
891
+ * @category Types
892
+ */
893
+ type MaybeNestedGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | MaybeNestedOrGroup<T>;
502
894
 
503
895
  /**
504
896
  * Parser for category configurations specified à-la-cooklang.
@@ -549,6 +941,62 @@ declare class CategoryConfig {
549
941
  parse(config: string): void;
550
942
  }
551
943
 
944
+ /**
945
+ * Product Catalog Manager: used in conjunction with {@link ShoppingCart}
946
+ *
947
+ * ## Usage
948
+ *
949
+ * You can either directly populate the products by feeding the {@link ProductCatalog.products | products} property. Alternatively,
950
+ * you can provide a catalog in TOML format to either the constructor itself or to the {@link ProductCatalog.parse | parse()} method.
951
+ *
952
+ * @category Classes
953
+ *
954
+ * @example
955
+ * ```typescript
956
+ * import { ProductCatalog } from "@tmlmt/cooklang-parser";
957
+ *
958
+ * const catalog = `
959
+ * [eggs]
960
+ * aliases = ["oeuf", "huevo"]
961
+ * 01123 = { name = "Single Egg", size = "1", price = 2 }
962
+ * 11244 = { name = "Pack of 6 eggs", size = "6", price = 10 }
963
+ *
964
+ * [flour]
965
+ * aliases = ["farine", "Mehl"]
966
+ * 01124 = { name = "Small pack", size = "100%g", price = 1.5 }
967
+ * 14141 = { name = "Big pack", size = "6%kg", price = 10 }
968
+ * `
969
+ * const catalog = new ProductCatalog(catalog);
970
+ * const eggs = catalog.find("oeuf");
971
+ * ```
972
+ */
973
+ declare class ProductCatalog {
974
+ products: ProductOption[];
975
+ constructor(tomlContent?: string);
976
+ /**
977
+ * Parses a TOML string into a list of product options.
978
+ * @param tomlContent - The TOML string to parse.
979
+ * @returns A parsed list of `ProductOption`.
980
+ */
981
+ parse(tomlContent: string): ProductOption[];
982
+ /**
983
+ * Stringifies the catalog to a TOML string.
984
+ * @returns The TOML string representation of the catalog.
985
+ */
986
+ stringify(): string;
987
+ /**
988
+ * Adds a product to the catalog.
989
+ * @param productOption - The product to add.
990
+ */
991
+ add(productOption: ProductOption): void;
992
+ /**
993
+ * Removes a product from the catalog by its ID.
994
+ * @param productId - The ID of the product to remove.
995
+ */
996
+ remove(productId: string): void;
997
+ private isValidTomlContent;
998
+ }
999
+
552
1000
  /**
553
1001
  * Shopping List generator.
554
1002
  *
@@ -579,7 +1027,7 @@ declare class ShoppingList {
579
1027
  /**
580
1028
  * The ingredients in the shopping list.
581
1029
  */
582
- ingredients: Ingredient[];
1030
+ ingredients: AddedIngredient[];
583
1031
  /**
584
1032
  * The recipes in the shopping list.
585
1033
  */
@@ -602,21 +1050,9 @@ declare class ShoppingList {
602
1050
  * Adds a recipe to the shopping list, then automatically
603
1051
  * recalculates the quantities and recategorize the ingredients.
604
1052
  * @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
1053
+ * @param options - Options for adding the recipe.
606
1054
  */
607
- add_recipe(recipe: Recipe, scaling?: {
608
- factor: number;
609
- } | {
610
- servings: number;
611
- }): void;
612
- /**
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
618
- */
619
- add_recipe(recipe: Recipe, factor?: number): void;
1055
+ add_recipe(recipe: Recipe, options?: AddedRecipeOptions): void;
620
1056
  /**
621
1057
  * Removes a recipe from the shopping list, then automatically
622
1058
  * recalculates the quantities and recategorize the ingredients.s
@@ -636,4 +1072,164 @@ declare class ShoppingList {
636
1072
  categorize(): void;
637
1073
  }
638
1074
 
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 };
1075
+ /**
1076
+ * Options for the {@link ShoppingCart} constructor
1077
+ * @category Types
1078
+ */
1079
+ interface ShoppingCartOptions {
1080
+ /**
1081
+ * A product catalog to connect to the cart
1082
+ */
1083
+ catalog?: ProductCatalog;
1084
+ /**
1085
+ * A shopping list to connect to the cart
1086
+ */
1087
+ list?: ShoppingList;
1088
+ }
1089
+ /**
1090
+ * Key information about the {@link ShoppingCart}
1091
+ * @category Types
1092
+ */
1093
+ interface ShoppingCartSummary {
1094
+ /**
1095
+ * The total price of the cart
1096
+ */
1097
+ totalPrice: number;
1098
+ /**
1099
+ * The total number of items in the cart
1100
+ */
1101
+ totalItems: number;
1102
+ }
1103
+ /**
1104
+ * Shopping Cart Manager: a tool to find the best combination of products to buy (defined in a {@link ProductCatalog}) to satisfy a {@link ShoppingList}.
1105
+ *
1106
+ * @example
1107
+ * ```ts
1108
+ * const shoppingList = new ShoppingList();
1109
+ * const recipe = new Recipe("@flour{600%g}");
1110
+ * shoppingList.add_recipe(recipe);
1111
+ *
1112
+ * const catalog = new ProductCatalog();
1113
+ * catalog.products = [
1114
+ * {
1115
+ * id: "flour-1kg",
1116
+ * productName: "Flour (1kg)",
1117
+ * ingredientName: "flour",
1118
+ * price: 10,
1119
+ * size: { type: "fixed", value: { type: "decimal", value: 1000 } },
1120
+ * unit: "g",
1121
+ * },
1122
+ * {
1123
+ * id: "flour-500g",
1124
+ * productName: "Flour (500g)",
1125
+ * ingredientName: "flour",
1126
+ * price: 6,
1127
+ * size: { type: "fixed", value: { type: "decimal", value: 500 } },
1128
+ * unit: "g",
1129
+ * },
1130
+ * ];
1131
+ *
1132
+ * const shoppingCart = new ShoppingCart({list: shoppingList, catalog}))
1133
+ * shoppingCart.buildCart();
1134
+ * ```
1135
+ *
1136
+ * @category Classes
1137
+ */
1138
+ declare class ShoppingCart {
1139
+ /**
1140
+ * The product catalog to use for matching products
1141
+ */
1142
+ productCatalog?: ProductCatalog;
1143
+ /**
1144
+ * The shopping list to build the cart from
1145
+ */
1146
+ shoppingList?: ShoppingList;
1147
+ /**
1148
+ * The content of the cart
1149
+ */
1150
+ cart: CartContent;
1151
+ /**
1152
+ * The ingredients that were successfully matched with products
1153
+ */
1154
+ match: CartMatch;
1155
+ /**
1156
+ * The ingredients that could not be matched with products
1157
+ */
1158
+ misMatch: CartMisMatch;
1159
+ /**
1160
+ * Key information about the shopping cart
1161
+ */
1162
+ summary: ShoppingCartSummary;
1163
+ /**
1164
+ * Creates a new ShoppingCart instance
1165
+ * @param options - {@link ShoppingCartOptions | Options} for the constructor
1166
+ */
1167
+ constructor(options?: ShoppingCartOptions);
1168
+ /**
1169
+ * Sets the product catalog to use for matching products
1170
+ * To use if a catalog was not provided at the creation of the instance
1171
+ * @param catalog - The {@link ProductCatalog} to set
1172
+ */
1173
+ setProductCatalog(catalog: ProductCatalog): void;
1174
+ /**
1175
+ * Sets the shopping list to build the cart from.
1176
+ * To use if a shopping list was not provided at the creation of the instance
1177
+ * @param list - The {@link ShoppingList} to set
1178
+ */
1179
+ setShoppingList(list: ShoppingList): void;
1180
+ /**
1181
+ * Builds the cart from the shopping list and product catalog
1182
+ * @remarks
1183
+ * - 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
1184
+ * in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
1185
+ * - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
1186
+ * - No product is listed in the catalog for that ingredient
1187
+ * - The ingredient has no quantity, a text quantity
1188
+ * - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
1189
+ * @throws {@link NoProductCatalogForCartError} if no product catalog is set
1190
+ * @throws {@link NoShoppingListForCartError} if no shopping list is set
1191
+ * @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
1192
+ */
1193
+ buildCart(): boolean;
1194
+ /**
1195
+ * Gets the product options for a given ingredient
1196
+ * @param ingredient - The ingredient to get the product options for
1197
+ * @returns An array of {@link ProductOption}
1198
+ */
1199
+ private getProductOptions;
1200
+ /**
1201
+ * Gets the optimum match for a given ingredient and product option
1202
+ * @param ingredient - The ingredient to match
1203
+ * @param options - The product options to choose from
1204
+ * @returns An array of {@link ProductSelection}
1205
+ * @throws {@link NoProductMatchError} if no match can be found
1206
+ */
1207
+ private getOptimumMatch;
1208
+ /**
1209
+ * Reset the cart's properties
1210
+ */
1211
+ private resetCart;
1212
+ /**
1213
+ * Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
1214
+ * This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
1215
+ * @returns the total price and number of items in the cart
1216
+ */
1217
+ summarize(): ShoppingCartSummary;
1218
+ }
1219
+
1220
+ /**
1221
+ * Error thrown when trying to build a shopping cart without a product catalog
1222
+ * @category Errors
1223
+ */
1224
+ declare class NoProductCatalogForCartError extends Error {
1225
+ constructor();
1226
+ }
1227
+ /**
1228
+ * Error thrown when trying to build a shopping cart without a shopping list
1229
+ * @category Errors
1230
+ */
1231
+ declare class NoShoppingListForCartError extends Error {
1232
+ constructor();
1233
+ }
1234
+
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 };