@tmlmt/cooklang-parser 2.1.8 → 3.0.0-alpha.11

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,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,92 @@ declare class Recipe {
89
97
  * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
90
98
  */
91
99
  servings?: number;
100
+ /**
101
+ * Gets the unit system specified in the recipe metadata.
102
+ * Used for resolving ambiguous units like tsp, tbsp, cup, etc.
103
+ *
104
+ * @returns The unit system if specified, or undefined to use defaults
105
+ */
106
+ get unitSystem(): SpecificUnitSystem | undefined;
107
+ /**
108
+ * External storage for unit system (not a property on instances).
109
+ * Used for resolving ambiguous units during quantity addition.
110
+ */
111
+ private static unitSystems;
112
+ /**
113
+ * External storage for item count (not a property on instances).
114
+ * Used for giving ID numbers to items during parsing.
115
+ */
116
+ private static itemCounts;
117
+ /**
118
+ * Gets the current item count for this recipe.
119
+ */
120
+ private getItemCount;
121
+ /**
122
+ * Gets the current item count and increments it.
123
+ */
124
+ private getAndIncrementItemCount;
92
125
  /**
93
126
  * Creates a new Recipe instance.
94
127
  * @param content - The recipe content to parse.
95
128
  */
96
129
  constructor(content?: string);
130
+ /**
131
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
132
+ * @private
133
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
134
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
135
+ */
136
+ private _parseArbitraryScalable;
137
+ /**
138
+ * Parses text for arbitrary scalables and returns NoteItem array.
139
+ * @param text - The text to parse for arbitrary scalables.
140
+ * @returns Array of NoteItem (text and arbitrary scalable items).
141
+ */
142
+ private _parseNoteText;
143
+ private _parseQuantityRecursive;
144
+ private _parseIngredientWithAlternativeRecursive;
145
+ private _parseIngredientWithGroupKey;
146
+ /**
147
+ * Populates the `quantities` property for each ingredient based on
148
+ * how they appear in the recipe preparation. Only primary ingredients
149
+ * get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
150
+ *
151
+ * For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
152
+ * For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
153
+ *
154
+ * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
155
+ * @internal
156
+ */
157
+ private _populate_ingredient_quantities;
158
+ /**
159
+ * Gets ingredients with their quantities populated, optionally filtered by section/step
160
+ * and respecting user choices for alternatives.
161
+ *
162
+ * When no options are provided, returns all recipe ingredients with quantities
163
+ * calculated using primary alternatives (same as after parsing).
164
+ *
165
+ * @param options - Options for filtering and choice selection:
166
+ * - `section`: Filter to a specific section (Section object or 0-based index)
167
+ * - `step`: Filter to a specific step (Step object or 0-based index)
168
+ * - `choices`: Choices for alternative ingredients (defaults to primary)
169
+ * @returns Array of Ingredient objects with quantities populated
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * // Get all ingredients with primary alternatives
174
+ * const ingredients = recipe.getIngredientQuantities();
175
+ *
176
+ * // Get ingredients for a specific section
177
+ * const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
178
+ *
179
+ * // Get ingredients with specific choices applied
180
+ * const withChoices = recipe.getIngredientQuantities({
181
+ * choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
182
+ * });
183
+ * ```
184
+ */
185
+ getIngredientQuantities(options?: GetIngredientQuantitiesOptions): Ingredient[];
97
186
  /**
98
187
  * Parses a recipe from a string.
99
188
  * @param content - The recipe content to parse.
@@ -115,6 +204,26 @@ declare class Recipe {
115
204
  * @returns A new Recipe instance with the scaled ingredients.
116
205
  */
117
206
  scaleBy(factor: number | Big): Recipe;
207
+ /**
208
+ * Converts all ingredient quantities in the recipe to a target unit system.
209
+ *
210
+ * @param system - The target unit system to convert to (metric, US, UK, JP)
211
+ * @param method - How to handle existing quantities:
212
+ * - "keep": Keep all existing equivalents (swap if needed, or add converted)
213
+ * - "replace": Replace primary with target system quantity, discard equivalent used for conversion
214
+ * - "remove": Only keep target system quantity, delete all equivalents
215
+ * @returns A new Recipe instance with converted quantities
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * // Convert a recipe to metric, keeping original units as equivalents
220
+ * const metricRecipe = recipe.convertTo("metric", "keep");
221
+ *
222
+ * // Convert to US units, removing all other equivalents
223
+ * const usRecipe = recipe.convertTo("US", "remove");
224
+ * ```
225
+ */
226
+ convertTo(system: SpecificUnitSystem, method: "keep" | "replace" | "remove"): Recipe;
118
227
  /**
119
228
  * Gets the number of servings for the recipe.
120
229
  * @private
@@ -128,11 +237,6 @@ declare class Recipe {
128
237
  clone(): Recipe;
129
238
  }
130
239
 
131
- interface Quantity {
132
- value: FixedValue | Range;
133
- unit?: string;
134
- }
135
-
136
240
  /**
137
241
  * Represents the metadata of a recipe.
138
242
  * @category Types
@@ -243,6 +347,12 @@ interface Metadata {
243
347
  picture?: string;
244
348
  /** The introduction of the recipe. */
245
349
  introduction?: string;
350
+ /**
351
+ * The unit system used in the recipe for ambiguous units like tsp, tbsp, cup.
352
+ * See [Unit Conversion Guide](/guide-unit-conversion) for more information.
353
+ * This stores the original value as written by the user.
354
+ */
355
+ "unit system"?: string;
246
356
  }
247
357
  /**
248
358
  * Represents a quantity described by text, e.g. "a pinch"
@@ -250,7 +360,7 @@ interface Metadata {
250
360
  */
251
361
  interface TextValue {
252
362
  type: "text";
253
- value: string;
363
+ text: string;
254
364
  }
255
365
  /**
256
366
  * Represents a quantity described by a decimal number, e.g. "1.5"
@@ -258,7 +368,7 @@ interface TextValue {
258
368
  */
259
369
  interface DecimalValue {
260
370
  type: "decimal";
261
- value: number;
371
+ decimal: number;
262
372
  }
263
373
  /**
264
374
  * Represents a quantity described by a fraction, e.g. "1/2"
@@ -280,6 +390,15 @@ interface FixedValue {
280
390
  type: "fixed";
281
391
  value: TextValue | DecimalValue | FractionValue;
282
392
  }
393
+ /**
394
+ * Represents a single, fixed numeric quantity.
395
+ * This can be a decimal or fraction.
396
+ * @category Types
397
+ */
398
+ interface FixedNumericValue {
399
+ type: "fixed";
400
+ value: DecimalValue | FractionValue;
401
+ }
283
402
  /**
284
403
  * Represents a range of quantities, e.g. "1-2"
285
404
  * @category Types
@@ -289,16 +408,6 @@ interface Range {
289
408
  min: DecimalValue | FractionValue;
290
409
  max: DecimalValue | FractionValue;
291
410
  }
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
411
  /**
303
412
  * Represents a possible state modifier or other flag for an ingredient in a recipe
304
413
  * @category Types
@@ -324,6 +433,48 @@ interface IngredientExtras {
324
433
  */
325
434
  path: string;
326
435
  }
436
+ /**
437
+ * Represents a reference to an alternative ingredient along with its quantities.
438
+ *
439
+ * Used in {@link IngredientQuantityGroup} to describe what other ingredients
440
+ * could be used in place of the main ingredient.
441
+ * @category Types
442
+ */
443
+ interface AlternativeIngredientRef {
444
+ /** The index of the alternative ingredient within the {@link Recipe.ingredients} array. */
445
+ index: number;
446
+ /** The quantities of the alternative ingredient. Multiple entries when units are incompatible. */
447
+ quantities?: QuantityWithPlainUnit[];
448
+ }
449
+ /**
450
+ * Represents a group of summed quantities for an ingredient, optionally with alternatives.
451
+ * Quantities with the same alternative signature are summed together into a single group.
452
+ * When units are incompatible, separate IngredientQuantityGroup entries are created instead of merging.
453
+ * @category Types
454
+ */
455
+ interface IngredientQuantityGroup extends QuantityWithPlainUnit {
456
+ /**
457
+ * References to alternative ingredients for this quantity group.
458
+ * If undefined, this group has no alternatives.
459
+ */
460
+ alternatives?: AlternativeIngredientRef[];
461
+ }
462
+ /**
463
+ * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
464
+ * For example: 1 large carrot + 2 small carrots, both with cup equivalents (resp. 2 cup and 1.5 cup) that sum to 5 cups.
465
+ * @category Types
466
+ */
467
+ interface IngredientQuantityAndGroup extends FlatAndGroup<QuantityWithPlainUnit> {
468
+ /**
469
+ * The summed equivalent quantities (e.g., "5 cups" from summing "1.5 cup + 2 cup + 1.5 cup").
470
+ */
471
+ equivalents?: QuantityWithPlainUnit[];
472
+ /**
473
+ * References to alternative ingredients for this quantity group.
474
+ * If undefined, this group has no alternatives.
475
+ */
476
+ alternatives?: AlternativeIngredientRef[];
477
+ }
327
478
  /**
328
479
  * Represents an ingredient in a recipe.
329
480
  * @category Types
@@ -331,40 +482,65 @@ interface IngredientExtras {
331
482
  interface Ingredient {
332
483
  /** The name of the ingredient. */
333
484
  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[];
485
+ /**
486
+ * Represents the quantities list for an ingredient as groups.
487
+ * Each group contains summed quantities that share the same alternative signature.
488
+ * Groups can be either simple (single unit) or AND groups (incompatible primary units with summed equivalents).
489
+ * Only populated for primary ingredients (not alternative-only).
490
+ * Quantities without alternatives are merged opportunistically when units are compatible.
491
+ * Quantities with alternatives are only merged if the alternatives are exactly the same.
492
+ */
493
+ quantities?: (IngredientQuantityGroup | IngredientQuantityAndGroup)[];
340
494
  /** The preparation of the ingredient. */
341
495
  preparation?: string;
496
+ /** The list of indexes of the ingredients mentioned in the preparation as alternatives to this ingredient */
497
+ alternatives?: Set<number>;
498
+ /**
499
+ * True if this ingredient appears as the primary choice (first in an alternatives list).
500
+ * Only primary ingredients have quantities populated directly.
501
+ *
502
+ * Alternative-only ingredients (usedAsPrimary undefined/false) have their quantities
503
+ * available via the {@link Recipe.choices} structure.
504
+ */
505
+ usedAsPrimary?: boolean;
342
506
  /** A list of potential state modifiers or other flags for the ingredient */
343
507
  flags?: IngredientFlag[];
344
508
  /** The collection of potential additional metadata for the ingredient */
345
509
  extras?: IngredientExtras;
346
510
  }
347
511
  /**
348
- * Represents a timer in a recipe.
512
+ * Represents a contributor to an ingredient's total quantity, corresponding
513
+ * to a single mention in the recipe text. It can contain multiple
514
+ * equivalent quantities (e.g., in different units).
349
515
  * @category Types
350
516
  */
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;
517
+ interface IngredientItemQuantity extends QuantityWithExtendedUnit {
518
+ /**
519
+ * A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
520
+ * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g.
521
+ */
522
+ equivalents?: QuantityWithExtendedUnit[];
523
+ /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
524
+ scalable: boolean;
358
525
  }
359
526
  /**
360
- * Represents a text item in a recipe step.
527
+ * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
528
+ * to a specific ingredient and its corresponding quantity information.
361
529
  * @category Types
362
530
  */
363
- interface TextItem {
364
- /** The type of the item. */
365
- type: "text";
366
- /** The content of the text item. */
367
- value: string;
531
+ interface IngredientAlternative {
532
+ /** The index of the ingredient within the {@link Recipe.ingredients} array. */
533
+ index: number;
534
+ /** The quantity of this specific mention of the ingredient */
535
+ itemQuantity?: IngredientItemQuantity;
536
+ /** The alias/name of the ingredient as it should be displayed for this occurrence. */
537
+ displayName: string;
538
+ /** An optional note for this specific choice (e.g., "for a vegan version"). */
539
+ note?: string;
540
+ /** When {@link Recipe.choices} is populated for alternatives ingredients
541
+ * with group keys: the id of the corresponding ingredient item (e.g. "ingredient-item-2").
542
+ * Can be useful for creating alternative selection UI elements with anchor links */
543
+ itemId?: string;
368
544
  }
369
545
  /**
370
546
  * Represents an ingredient item in a recipe step.
@@ -373,14 +549,67 @@ interface TextItem {
373
549
  interface IngredientItem {
374
550
  /** The type of the item. */
375
551
  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;
552
+ /** The item identifier */
553
+ id: string;
554
+ /**
555
+ * A list of alternative ingredient choices. For a standard ingredient,
556
+ * this array will contain a single element.
557
+ */
558
+ alternatives: IngredientAlternative[];
559
+ /**
560
+ * An optional identifier for linking distributed alternatives. If multiple
561
+ * `IngredientItem`s in a recipe share the same `group` ID (e.g., from
562
+ * `@|group|...` syntax), they represent a single logical choice.
563
+ */
564
+ group?: string;
565
+ }
566
+ /**
567
+ * Represents the choices one can make in a recipe
568
+ * @category Types
569
+ */
570
+ interface RecipeAlternatives {
571
+ /** Map of choices that can be made at Ingredient StepItem level
572
+ * - Keys are the Ingredient StepItem IDs (e.g. "ingredient-item-2")
573
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that item
574
+ */
575
+ ingredientItems: Map<string, IngredientAlternative[]>;
576
+ /** Map of choices that can be made for Grouped Ingredient StepItem's
577
+ * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
578
+ * - Values are arrays of IngredientAlternative objects representing the choices available for that group
579
+ */
580
+ ingredientGroups: Map<string, IngredientAlternative[]>;
581
+ }
582
+ /**
583
+ * Represents the choices to apply when computing ingredient quantities.
584
+ * Maps item/group IDs to the index of the selected alternative.
585
+ * @category Types
586
+ */
587
+ interface RecipeChoices {
588
+ /** Map of choices that can be made at Ingredient StepItem level */
589
+ ingredientItems?: Map<string, number>;
590
+ /** Map of choices that can be made for Grouped Ingredient StepItem's */
591
+ ingredientGroups?: Map<string, number>;
592
+ }
593
+ /**
594
+ * Options for the {@link Recipe.getIngredientQuantities | getIngredientQuantities()} method.
595
+ * @category Types
596
+ */
597
+ interface GetIngredientQuantitiesOptions {
598
+ /**
599
+ * Filter ingredients to only those appearing in a specific section.
600
+ * Can be a Section object or section index (0-based).
601
+ */
602
+ section?: Section | number;
603
+ /**
604
+ * Filter ingredients to only those appearing in a specific step.
605
+ * Can be a Step object or step index (0-based within the section, or global if no section specified).
606
+ */
607
+ step?: Step | number;
608
+ /**
609
+ * The choices to apply when computing quantities.
610
+ * If not provided, uses primary alternatives (index 0 for all).
611
+ */
612
+ choices?: RecipeChoices;
384
613
  }
385
614
  /**
386
615
  * Represents a cookware item in a recipe step.
@@ -391,9 +620,8 @@ interface CookwareItem {
391
620
  type: "cookware";
392
621
  /** The index of the cookware, within the {@link Recipe.cookware | list of cookware} */
393
622
  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;
623
+ /** The quantity of this specific mention of the cookware */
624
+ quantity?: FixedValue | Range;
397
625
  }
398
626
  /**
399
627
  * Represents a timer item in a recipe step.
@@ -405,11 +633,55 @@ interface TimerItem {
405
633
  /** The index of the timer, within the {@link Recipe.timers | list of timers} */
406
634
  index: number;
407
635
  }
636
+ /**
637
+ * Represents a timer in a recipe.
638
+ * @category Types
639
+ */
640
+ interface Timer {
641
+ /** The name of the timer. */
642
+ name?: string;
643
+ /** The duration of the timer. */
644
+ duration: FixedValue | Range;
645
+ /** The unit of the timer. */
646
+ unit: string;
647
+ }
648
+ /**
649
+ * Represents a text item in a recipe step.
650
+ * @category Types
651
+ */
652
+ interface TextItem {
653
+ /** The type of the item. */
654
+ type: "text";
655
+ /** The content of the text item. */
656
+ value: string;
657
+ }
658
+ /**
659
+ * Represents an arbitrary scalable quantity in a recipe.
660
+ * @category Types
661
+ */
662
+ interface ArbitraryScalable {
663
+ /** The name of the arbitrary scalable quantity. */
664
+ name?: string;
665
+ /** The numerical value of the arbitrary scalable quantity. */
666
+ quantity: FixedNumericValue;
667
+ /** The unit of the arbitrary scalable quantity. */
668
+ unit?: string;
669
+ }
670
+ /**
671
+ * Represents an arbitrary scalable quantity item in a recipe step.
672
+ * @category Types
673
+ */
674
+ interface ArbitraryScalableItem {
675
+ /** The type of the item. */
676
+ type: "arbitrary";
677
+ /** The index of the arbitrary scalable quantity, within the {@link Recipe.arbitraries | list of arbitrary scalable quantities} */
678
+ index: number;
679
+ }
408
680
  /**
409
681
  * Represents an item in a recipe step.
410
682
  * @category Types
411
683
  */
412
- type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
684
+ type StepItem = TextItem | IngredientItem | CookwareItem | TimerItem | ArbitraryScalableItem;
413
685
  /**
414
686
  * Represents a step in a recipe.
415
687
  * @category Types
@@ -417,16 +689,21 @@ type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
417
689
  interface Step {
418
690
  type: "step";
419
691
  /** The items in the step. */
420
- items: Item[];
692
+ items: StepItem[];
421
693
  }
694
+ /**
695
+ * Represents an item in a note (can be text or arbitrary scalable).
696
+ * @category Types
697
+ */
698
+ type NoteItem = TextItem | ArbitraryScalableItem;
422
699
  /**
423
700
  * Represents a note in a recipe.
424
701
  * @category Types
425
702
  */
426
703
  interface Note {
427
704
  type: "note";
428
- /** The content of the note. */
429
- note: string;
705
+ /** The items in the note. */
706
+ items: NoteItem[];
430
707
  }
431
708
  /**
432
709
  * Represents a possible state modifier or other flag for cookware used in a recipe
@@ -442,17 +719,15 @@ interface Cookware {
442
719
  name: string;
443
720
  /** The quantity of cookware */
444
721
  quantity?: FixedValue | Range;
445
- /** The array of contributors to the cookware's total quantity. */
446
- quantityParts?: (FixedValue | Range)[];
447
722
  /** A list of potential state modifiers or other flags for the cookware */
448
- flags: CookwareFlag[];
723
+ flags?: CookwareFlag[];
449
724
  }
450
725
  /**
451
726
  * Represents categorized ingredients.
452
727
  * @category Types
453
728
  */
454
729
  interface CategorizedIngredients {
455
- [category: string]: Ingredient[];
730
+ [category: string]: AddedIngredient[];
456
731
  }
457
732
  /**
458
733
  * Represents a recipe together with a scaling factor
@@ -463,6 +738,8 @@ interface RecipeWithFactor {
463
738
  recipe: Recipe;
464
739
  /** The factor the recipe is scaled by. */
465
740
  factor: number;
741
+ /** The choices for alternative ingredients. */
742
+ choices?: RecipeChoices;
466
743
  }
467
744
  /**
468
745
  * Represents a recipe together with a servings value for scaling
@@ -473,12 +750,36 @@ interface RecipeWithServings {
473
750
  recipe: Recipe;
474
751
  /** The servings the recipe is scaled to */
475
752
  servings: number;
753
+ /** The choices for alternative ingredients. */
754
+ choices?: RecipeChoices;
476
755
  }
477
756
  /**
478
757
  * Represents a recipe that has been added to a shopping list.
479
758
  * @category Types
480
759
  */
481
760
  type AddedRecipe = RecipeWithFactor | RecipeWithServings;
761
+ /**
762
+ * Options for adding a recipe to a shopping list
763
+ * @category Types
764
+ */
765
+ type AddedRecipeOptions = {
766
+ /** The scaling option for the recipe. Can be either a factor or a number of servings */
767
+ scaling?: {
768
+ factor: number;
769
+ } | {
770
+ servings: number;
771
+ };
772
+ /** The choices for alternative ingredients. */
773
+ choices?: RecipeChoices;
774
+ };
775
+ /**
776
+ * Represents an ingredient that has been added to a shopping list
777
+ * @category Types
778
+ */
779
+ type AddedIngredient = Pick<Ingredient, "name"> & {
780
+ /** The total quantity of the ingredient after applying choices. */
781
+ quantityTotal?: QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit>;
782
+ };
482
783
  /**
483
784
  * Represents an ingredient in a category.
484
785
  * @category Types
@@ -499,6 +800,254 @@ interface Category {
499
800
  /** The ingredients in the category. */
500
801
  ingredients: CategoryIngredient[];
501
802
  }
803
+ /**
804
+ * Represents a single size expression for a product (value + optional unit)
805
+ * @category Types
806
+ */
807
+ interface ProductSize {
808
+ /** The numeric size value */
809
+ size: FixedNumericValue;
810
+ /** The unit of the size (optional) */
811
+ unit?: string;
812
+ }
813
+ /**
814
+ * Core properties for {@link ProductOption}
815
+ * @category Types
816
+ */
817
+ interface ProductOptionCore {
818
+ /** The ID of the product */
819
+ id: string;
820
+ /** The name of the product */
821
+ productName: string;
822
+ /** The name of the ingredient it corresponds to */
823
+ ingredientName: string;
824
+ /** The aliases of the ingredient it also corresponds to */
825
+ ingredientAliases?: string[];
826
+ /** The price of the product */
827
+ price: number;
828
+ }
829
+ /**
830
+ * Base type for {@link ProductOption} allowing arbitrary additional metadata
831
+ * @category Types
832
+ */
833
+ type ProductOptionBase = ProductOptionCore & Record<string, unknown>;
834
+ /**
835
+ * Represents a product option in a {@link ProductCatalog}
836
+ * @category Types
837
+ */
838
+ type ProductOption = ProductOptionBase & {
839
+ /** The size(s) of the product. Multiple sizes allow equivalent expressions (e.g., "1%dozen" and "12") */
840
+ sizes: ProductSize[];
841
+ };
842
+ /**
843
+ * Represents a product selection in a {@link ShoppingCart}
844
+ * @category Types
845
+ */
846
+ interface ProductSelection {
847
+ /** The selected product */
848
+ product: ProductOption;
849
+ /** The quantity of the selected product */
850
+ quantity: number;
851
+ /** The total price for this selected product */
852
+ totalPrice: number;
853
+ }
854
+ /**
855
+ * Represents the content of the actual cart of the {@link ShoppingCart}
856
+ * @category Types
857
+ */
858
+ type CartContent = ProductSelection[];
859
+ /**
860
+ * Represents a successful match between a ingredient and product(s) in the product catalog, in a {@link ShoppingCart}
861
+ * @category Types
862
+ */
863
+ interface ProductMatch {
864
+ ingredient: Ingredient;
865
+ selection: ProductSelection[];
866
+ }
867
+ /**
868
+ * Represents all successful matches between ingredients and the product catalog, in a {@link ShoppingCart}
869
+ * @category Types
870
+ */
871
+ type CartMatch = ProductMatch[];
872
+ /**
873
+ * Represents the error codes for an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
874
+ * @category Types
875
+ */
876
+ type NoProductMatchErrorCode = "incompatibleUnits" | "textValue" | "textValue_incompatibleUnits" | "noProduct" | "noQuantity";
877
+ /**
878
+ * Represents an ingredient which didn't match with any product in the product catalog, in a {@link ShoppingCart}
879
+ * @category Types
880
+ */
881
+ interface ProductMisMatch {
882
+ ingredient: Ingredient;
883
+ reason: NoProductMatchErrorCode;
884
+ }
885
+ /**
886
+ * Represents all ingredients which didn't match with any product in the product catalog, in a {@link ShoppingCart}
887
+ * @category Types
888
+ */
889
+ type CartMisMatch = ProductMisMatch[];
890
+ /**
891
+ * Represents the type category of a unit used for quantities
892
+ * @category Types
893
+ */
894
+ type UnitType = "mass" | "volume" | "count" | "other";
895
+ /**
896
+ * Represents the specific measurement systems
897
+ * @category Types
898
+ */
899
+ type SpecificUnitSystem = "metric" | "US" | "UK" | "JP";
900
+ /**
901
+ * Represents the measurement system a unit belongs to
902
+ * @category Types
903
+ */
904
+ type UnitSystem = SpecificUnitSystem | "ambiguous";
905
+ /**
906
+ * Conversion factors for ambiguous units that can belong to multiple systems.
907
+ * Maps each possible system to its toBase conversion factor.
908
+ * @category Types
909
+ */
910
+ type ToBaseBySystem = Partial<Record<SpecificUnitSystem, number>>;
911
+ /**
912
+ * Represents a unit used to describe quantities
913
+ * @category Types
914
+ */
915
+ interface Unit {
916
+ name: string;
917
+ /** This property is set to true when the unit is prefixed by an `=` sign in the cooklang file, e.g. `=g`
918
+ * Indicates that quantities with this unit should be treated as integers only (no decimal/fractional values). */
919
+ integerProtected?: boolean;
920
+ }
921
+ /**
922
+ * Represents a fully defined unit with conversion and alias information
923
+ * @category Types
924
+ */
925
+ interface UnitDefinition extends Unit {
926
+ type: UnitType;
927
+ system: UnitSystem;
928
+ /** e.g. ['gram', 'grams'] */
929
+ aliases: string[];
930
+ /** Conversion factor to the base unit of its type (uses default system for ambiguous units) */
931
+ toBase: number;
932
+ /** For ambiguous units: conversion factors for each possible system */
933
+ toBaseBySystem?: ToBaseBySystem;
934
+ /** Whether this unit is a candidate for "best unit" selection (default: true) */
935
+ isBestUnit?: boolean;
936
+ /** Maximum value before upgrading to a larger unit (default: 999) */
937
+ maxValue?: number;
938
+ /** Fraction display configuration */
939
+ fractions?: UnitFractionConfig;
940
+ }
941
+ /**
942
+ * Configuration for fraction display on a unit
943
+ * @category Types
944
+ */
945
+ interface UnitFractionConfig {
946
+ /** Whether to approximate decimals as fractions for this unit */
947
+ enabled: boolean;
948
+ /** Allowed denominators (default: [2, 3, 4, 8]) */
949
+ denominators?: number[];
950
+ /** Maximum whole number in mixed fraction before falling back to decimal (default: 4) */
951
+ maxWhole?: number;
952
+ }
953
+ /**
954
+ * Represents a resolved unit definition or a lightweight placeholder for non-standard units
955
+ * @category Types
956
+ */
957
+ type UnitDefinitionLike = UnitDefinition | {
958
+ name: string;
959
+ type: "other";
960
+ system: "none";
961
+ integerProtected?: boolean;
962
+ };
963
+ /**
964
+ * Core quantity container holding a fixed value or a range
965
+ * @category Types
966
+ */
967
+ interface QuantityBase {
968
+ quantity: FixedValue | Range;
969
+ }
970
+ /**
971
+ * Represents a quantity with an optional plain (string) unit
972
+ * @category Types
973
+ */
974
+ interface QuantityWithPlainUnit extends QuantityBase {
975
+ unit?: string;
976
+ /** Optional equivalent quantities in different units (for alternative units like `@flour{100%g|3.5%oz}`) */
977
+ equivalents?: QuantityWithPlainUnit[];
978
+ }
979
+ /**
980
+ * Represents a quantity with an optional extended `Unit` object
981
+ * @category Types
982
+ */
983
+ interface QuantityWithExtendedUnit extends QuantityBase {
984
+ unit?: Unit;
985
+ }
986
+ /**
987
+ * Represents a quantity with a resolved unit definition
988
+ * @category Types
989
+ */
990
+ interface QuantityWithUnitDef extends QuantityBase {
991
+ unit: UnitDefinitionLike;
992
+ }
993
+ /**
994
+ * Represents any quantity shape supported by the parser (plain, extended, or resolved unit)
995
+ * @category Types
996
+ */
997
+ type QuantityWithUnitLike = QuantityWithPlainUnit | QuantityWithExtendedUnit | QuantityWithUnitDef;
998
+ /**
999
+ * Represents a flat "or" group of alternative quantities (for alternative units)
1000
+ * @category Types
1001
+ */
1002
+ interface FlatOrGroup<T = QuantityWithUnitLike> {
1003
+ or: T[];
1004
+ }
1005
+ /**
1006
+ * Represents an "or" group of alternative quantities that may contain nested groups (alternatives with nested structure)
1007
+ * @category Types
1008
+ */
1009
+ interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
1010
+ or: (T | MaybeNestedGroup<T>)[];
1011
+ }
1012
+ /**
1013
+ * Represents a flat "and" group of quantities (combined quantities)
1014
+ * @category Types
1015
+ */
1016
+ interface FlatAndGroup<T = QuantityWithUnitLike> {
1017
+ and: T[];
1018
+ }
1019
+ /**
1020
+ * Represents an "and" group of quantities that may contain nested groups (combinations with nested structure)
1021
+ * @category Types
1022
+ */
1023
+ interface MaybeNestedAndGroup<T = QuantityWithUnitLike> {
1024
+ and: (T | MaybeNestedGroup<T>)[];
1025
+ }
1026
+ /**
1027
+ * Represents any flat group type ("and" or "or")
1028
+ * @category Types
1029
+ */
1030
+ type FlatGroup<T = QuantityWithUnitLike> = FlatAndGroup<T> | FlatOrGroup<T>;
1031
+ /**
1032
+ * Represents any group type that may include nested groups
1033
+ * @category Types
1034
+ */
1035
+ type MaybeNestedGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | MaybeNestedOrGroup<T>;
1036
+ /**
1037
+ * Represents any group type (flat or nested)
1038
+ * @category Types
1039
+ */
1040
+ type Group<T = QuantityWithUnitLike> = MaybeNestedGroup<T> | FlatGroup<T>;
1041
+ /**
1042
+ * Represents any "or" group (flat or nested)
1043
+ * @category Types
1044
+ */
1045
+ type OrGroup<T = QuantityWithUnitLike> = MaybeNestedOrGroup<T> | FlatOrGroup<T>;
1046
+ /**
1047
+ * Represents any "and" group (flat or nested)
1048
+ * @category Types
1049
+ */
1050
+ type AndGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | FlatAndGroup<T>;
502
1051
 
503
1052
  /**
504
1053
  * Parser for category configurations specified à-la-cooklang.
@@ -549,6 +1098,62 @@ declare class CategoryConfig {
549
1098
  parse(config: string): void;
550
1099
  }
551
1100
 
1101
+ /**
1102
+ * Product Catalog Manager: used in conjunction with {@link ShoppingCart}
1103
+ *
1104
+ * ## Usage
1105
+ *
1106
+ * You can either directly populate the products by feeding the {@link ProductCatalog.products | products} property. Alternatively,
1107
+ * you can provide a catalog in TOML format to either the constructor itself or to the {@link ProductCatalog.parse | parse()} method.
1108
+ *
1109
+ * @category Classes
1110
+ *
1111
+ * @example
1112
+ * ```typescript
1113
+ * import { ProductCatalog } from "@tmlmt/cooklang-parser";
1114
+ *
1115
+ * const catalog = `
1116
+ * [eggs]
1117
+ * aliases = ["oeuf", "huevo"]
1118
+ * 01123 = { name = "Single Egg", size = "1", price = 2 }
1119
+ * 11244 = { name = "Pack of 6 eggs", size = "6", price = 10 }
1120
+ *
1121
+ * [flour]
1122
+ * aliases = ["farine", "Mehl"]
1123
+ * 01124 = { name = "Small pack", size = "100%g", price = 1.5 }
1124
+ * 14141 = { name = "Big pack", size = "6%kg", price = 10 }
1125
+ * `
1126
+ * const catalog = new ProductCatalog(catalog);
1127
+ * const eggs = catalog.find("oeuf");
1128
+ * ```
1129
+ */
1130
+ declare class ProductCatalog {
1131
+ products: ProductOption[];
1132
+ constructor(tomlContent?: string);
1133
+ /**
1134
+ * Parses a TOML string into a list of product options.
1135
+ * @param tomlContent - The TOML string to parse.
1136
+ * @returns A parsed list of `ProductOption`.
1137
+ */
1138
+ parse(tomlContent: string): ProductOption[];
1139
+ /**
1140
+ * Stringifies the catalog to a TOML string.
1141
+ * @returns The TOML string representation of the catalog.
1142
+ */
1143
+ stringify(): string;
1144
+ /**
1145
+ * Adds a product to the catalog.
1146
+ * @param productOption - The product to add.
1147
+ */
1148
+ add(productOption: ProductOption): void;
1149
+ /**
1150
+ * Removes a product from the catalog by its ID.
1151
+ * @param productId - The ID of the product to remove.
1152
+ */
1153
+ remove(productId: string): void;
1154
+ private isValidTomlContent;
1155
+ }
1156
+
552
1157
  /**
553
1158
  * Shopping List generator.
554
1159
  *
@@ -579,7 +1184,7 @@ declare class ShoppingList {
579
1184
  /**
580
1185
  * The ingredients in the shopping list.
581
1186
  */
582
- ingredients: Ingredient[];
1187
+ ingredients: AddedIngredient[];
583
1188
  /**
584
1189
  * The recipes in the shopping list.
585
1190
  */
@@ -602,21 +1207,17 @@ declare class ShoppingList {
602
1207
  * Adds a recipe to the shopping list, then automatically
603
1208
  * recalculates the quantities and recategorize the ingredients.
604
1209
  * @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
1210
+ * @param options - Options for adding the recipe.
1211
+ * @throws Error if the recipe has alternatives without corresponding choices.
606
1212
  */
607
- add_recipe(recipe: Recipe, scaling?: {
608
- factor: number;
609
- } | {
610
- servings: number;
611
- }): void;
1213
+ add_recipe(recipe: Recipe, options?: AddedRecipeOptions): void;
612
1214
  /**
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
1215
+ * Checks if a recipe has unresolved alternatives (alternatives without provided choices).
1216
+ * @param recipe - The recipe to check.
1217
+ * @param choices - The choices provided for the recipe.
1218
+ * @returns An error message if there are unresolved alternatives, undefined otherwise.
618
1219
  */
619
- add_recipe(recipe: Recipe, factor?: number): void;
1220
+ private getUnresolvedAlternativesError;
620
1221
  /**
621
1222
  * Removes a recipe from the shopping list, then automatically
622
1223
  * recalculates the quantities and recategorize the ingredients.s
@@ -636,4 +1237,409 @@ declare class ShoppingList {
636
1237
  categorize(): void;
637
1238
  }
638
1239
 
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 };
1240
+ /**
1241
+ * Options for the {@link ShoppingCart} constructor
1242
+ * @category Types
1243
+ */
1244
+ interface ShoppingCartOptions {
1245
+ /**
1246
+ * A product catalog to connect to the cart
1247
+ */
1248
+ catalog?: ProductCatalog;
1249
+ /**
1250
+ * A shopping list to connect to the cart
1251
+ */
1252
+ list?: ShoppingList;
1253
+ }
1254
+ /**
1255
+ * Key information about the {@link ShoppingCart}
1256
+ * @category Types
1257
+ */
1258
+ interface ShoppingCartSummary {
1259
+ /**
1260
+ * The total price of the cart
1261
+ */
1262
+ totalPrice: number;
1263
+ /**
1264
+ * The total number of items in the cart
1265
+ */
1266
+ totalItems: number;
1267
+ }
1268
+ /**
1269
+ * Shopping Cart Manager: a tool to find the best combination of products to buy (defined in a {@link ProductCatalog}) to satisfy a {@link ShoppingList}.
1270
+ *
1271
+ * @example
1272
+ * ```ts
1273
+ * const shoppingList = new ShoppingList();
1274
+ * const recipe = new Recipe("@flour{600%g}");
1275
+ * shoppingList.add_recipe(recipe);
1276
+ *
1277
+ * const catalog = new ProductCatalog();
1278
+ * catalog.products = [
1279
+ * {
1280
+ * id: "flour-1kg",
1281
+ * productName: "Flour (1kg)",
1282
+ * ingredientName: "flour",
1283
+ * price: 10,
1284
+ * size: { type: "fixed", value: { type: "decimal", value: 1000 } },
1285
+ * unit: "g",
1286
+ * },
1287
+ * {
1288
+ * id: "flour-500g",
1289
+ * productName: "Flour (500g)",
1290
+ * ingredientName: "flour",
1291
+ * price: 6,
1292
+ * size: { type: "fixed", value: { type: "decimal", value: 500 } },
1293
+ * unit: "g",
1294
+ * },
1295
+ * ];
1296
+ *
1297
+ * const shoppingCart = new ShoppingCart({list: shoppingList, catalog}))
1298
+ * shoppingCart.buildCart();
1299
+ * ```
1300
+ *
1301
+ * @category Classes
1302
+ */
1303
+ declare class ShoppingCart {
1304
+ /**
1305
+ * The product catalog to use for matching products
1306
+ */
1307
+ productCatalog?: ProductCatalog;
1308
+ /**
1309
+ * The shopping list to build the cart from
1310
+ */
1311
+ shoppingList?: ShoppingList;
1312
+ /**
1313
+ * The content of the cart
1314
+ */
1315
+ cart: CartContent;
1316
+ /**
1317
+ * The ingredients that were successfully matched with products
1318
+ */
1319
+ match: CartMatch;
1320
+ /**
1321
+ * The ingredients that could not be matched with products
1322
+ */
1323
+ misMatch: CartMisMatch;
1324
+ /**
1325
+ * Key information about the shopping cart
1326
+ */
1327
+ summary: ShoppingCartSummary;
1328
+ /**
1329
+ * Creates a new ShoppingCart instance
1330
+ * @param options - {@link ShoppingCartOptions | Options} for the constructor
1331
+ */
1332
+ constructor(options?: ShoppingCartOptions);
1333
+ /**
1334
+ * Sets the product catalog to use for matching products
1335
+ * To use if a catalog was not provided at the creation of the instance
1336
+ * @param catalog - The {@link ProductCatalog} to set
1337
+ */
1338
+ setProductCatalog(catalog: ProductCatalog): void;
1339
+ /**
1340
+ * Sets the shopping list to build the cart from.
1341
+ * To use if a shopping list was not provided at the creation of the instance
1342
+ * @param list - The {@link ShoppingList} to set
1343
+ */
1344
+ setShoppingList(list: ShoppingList): void;
1345
+ /**
1346
+ * Builds the cart from the shopping list and product catalog
1347
+ * @remarks
1348
+ * - 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
1349
+ * in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
1350
+ * - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
1351
+ * - No product is listed in the catalog for that ingredient
1352
+ * - The ingredient has no quantity, a text quantity
1353
+ * - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
1354
+ * @throws {@link NoProductCatalogForCartError} if no product catalog is set
1355
+ * @throws {@link NoShoppingListForCartError} if no shopping list is set
1356
+ * @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
1357
+ */
1358
+ buildCart(): boolean;
1359
+ /**
1360
+ * Gets the product options for a given ingredient
1361
+ * @param ingredient - The ingredient to get the product options for
1362
+ * @returns An array of {@link ProductOption}
1363
+ */
1364
+ private getProductOptions;
1365
+ /**
1366
+ * Gets the optimum match for a given ingredient and product option
1367
+ * @param ingredient - The ingredient to match
1368
+ * @param options - The product options to choose from
1369
+ * @returns An array of {@link ProductSelection}
1370
+ * @throws {@link NoProductMatchError} if no match can be found
1371
+ */
1372
+ private getOptimumMatch;
1373
+ /**
1374
+ * Reset the cart's properties
1375
+ */
1376
+ private resetCart;
1377
+ /**
1378
+ * Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
1379
+ * This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
1380
+ * @returns the total price and number of items in the cart
1381
+ */
1382
+ summarize(): ShoppingCartSummary;
1383
+ }
1384
+
1385
+ /**
1386
+ * Render a fraction using Unicode vulgar fraction characters when available.
1387
+ * Handles improper fractions by extracting the whole part (e.g., 5/4 → "1¼").
1388
+ *
1389
+ * @param num - The numerator
1390
+ * @param den - The denominator
1391
+ * @returns The fraction as a string, using vulgar characters if available
1392
+ * @category Helpers
1393
+ *
1394
+ * @example
1395
+ * ```typescript
1396
+ * renderFractionAsVulgar(1, 2); // "½"
1397
+ * renderFractionAsVulgar(3, 4); // "¾"
1398
+ * renderFractionAsVulgar(5, 4); // "1¼"
1399
+ * renderFractionAsVulgar(7, 3); // "2⅓"
1400
+ * renderFractionAsVulgar(2, 5); // "2/5" (no vulgar character available)
1401
+ * ```
1402
+ */
1403
+ declare function renderFractionAsVulgar(num: number, den: number): string;
1404
+ /**
1405
+ * Format a numeric value (decimal or fraction) to a string.
1406
+ *
1407
+ * @param value - The decimal or fraction value to format
1408
+ * @param useVulgar - Whether to use Unicode vulgar fraction characters (default: false)
1409
+ * @returns The formatted string representation
1410
+ * @category Helpers
1411
+ *
1412
+ * @example
1413
+ * ```typescript
1414
+ * formatNumericValue({ type: "decimal", decimal: 1.5 }); // "1.5"
1415
+ * formatNumericValue({ type: "fraction", num: 1, den: 2 }); // "1/2"
1416
+ * formatNumericValue({ type: "fraction", num: 1, den: 2 }, true); // "½"
1417
+ * formatNumericValue({ type: "fraction", num: 5, den: 4 }, true); // "1¼"
1418
+ * ```
1419
+ */
1420
+ declare function formatNumericValue(value: DecimalValue | FractionValue, useVulgar?: boolean): string;
1421
+ /**
1422
+ * Format a single value (text, decimal, or fraction) to a string.
1423
+ *
1424
+ * @param value - The value to format
1425
+ * @returns The formatted string representation
1426
+ * @category Helpers
1427
+ *
1428
+ * @example
1429
+ * ```typescript
1430
+ * formatSingleValue({ type: "text", text: "a pinch" }); // "a pinch"
1431
+ * formatSingleValue({ type: "decimal", decimal: 2 }); // "2"
1432
+ * formatSingleValue({ type: "fraction", num: 3, den: 4 }); // "3/4"
1433
+ * ```
1434
+ */
1435
+ declare function formatSingleValue(value: TextValue | DecimalValue | FractionValue): string;
1436
+ /**
1437
+ * Format a quantity (fixed value or range) to a string.
1438
+ *
1439
+ * @param quantity - The quantity to format
1440
+ * @returns The formatted string representation
1441
+ * @category Helpers
1442
+ *
1443
+ * @example
1444
+ * ```typescript
1445
+ * formatQuantity({ type: "fixed", value: { type: "decimal", decimal: 100 } }); // "100"
1446
+ * formatQuantity({ type: "range", min: { type: "decimal", decimal: 1 }, max: { type: "decimal", decimal: 2 } }); // "1-2"
1447
+ * ```
1448
+ */
1449
+ declare function formatQuantity(quantity: FixedValue | Range): string;
1450
+ /**
1451
+ * Format a unit to a string. Handles both plain string units and Unit objects.
1452
+ *
1453
+ * @param unit - The unit to format (string, Unit object, or undefined)
1454
+ * @returns The formatted unit string, or empty string if undefined
1455
+ * @category Helpers
1456
+ *
1457
+ * @example
1458
+ * ```typescript
1459
+ * formatUnit("g"); // "g"
1460
+ * formatUnit({ name: "grams" }); // "grams"
1461
+ * formatUnit(undefined); // ""
1462
+ * ```
1463
+ */
1464
+ declare function formatUnit(unit: string | Unit | undefined): string;
1465
+ /**
1466
+ * Format a quantity with its unit to a string.
1467
+ *
1468
+ * @param quantity - The quantity to format
1469
+ * @param unit - The unit to append (string, Unit object, or undefined)
1470
+ * @returns The formatted string with quantity and unit
1471
+ * @category Helpers
1472
+ *
1473
+ * @example
1474
+ * ```typescript
1475
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 100 } }, "g"); // "100 g"
1476
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 2 } }, undefined); // "2"
1477
+ * ```
1478
+ */
1479
+ declare function formatQuantityWithUnit(quantity: FixedValue | Range | undefined, unit: string | Unit | undefined): string;
1480
+ /**
1481
+ * Format a QuantityWithExtendedUnit to a string.
1482
+ *
1483
+ * @param item - The quantity with extended unit to format
1484
+ * @returns The formatted string
1485
+ * @category Helpers
1486
+ */
1487
+ declare function formatExtendedQuantity(item: QuantityWithExtendedUnit): string;
1488
+ /**
1489
+ * Format an IngredientItemQuantity with all its equivalents to a string.
1490
+ *
1491
+ * @param itemQuantity - The ingredient item quantity to format
1492
+ * @param separator - The separator between primary and equivalent quantities (default: " | ")
1493
+ * @returns The formatted string with all quantities
1494
+ * @category Helpers
1495
+ *
1496
+ * @example
1497
+ * ```typescript
1498
+ * // For an ingredient like @flour{100%g|3.5%oz}
1499
+ * formatItemQuantity(itemQuantity); // "100 g | 3.5 oz"
1500
+ * formatItemQuantity(itemQuantity, " / "); // "100 g / 3.5 oz"
1501
+ * ```
1502
+ */
1503
+ declare function formatItemQuantity(itemQuantity: IngredientItemQuantity, separator?: string): string;
1504
+ /**
1505
+ * Check if an ingredient item is a grouped alternative (vs inline alternative).
1506
+ *
1507
+ * Grouped alternatives are ingredients that share a group key (e.g., `@|milk|...`)
1508
+ * and are distributed across multiple tokens in the recipe.
1509
+ *
1510
+ * @param item - The ingredient item to check
1511
+ * @returns true if this is a grouped alternative
1512
+ * @category Helpers
1513
+ *
1514
+ * @example
1515
+ * ```typescript
1516
+ * for (const item of step.items) {
1517
+ * if (item.type === 'ingredient') {
1518
+ * if (isGroupedItem(item)) {
1519
+ * // Handle grouped alternative (e.g., show with strikethrough if not selected)
1520
+ * } else {
1521
+ * // Handle inline alternative (e.g., hide if not selected)
1522
+ * }
1523
+ * }
1524
+ * }
1525
+ * ```
1526
+ */
1527
+ declare function isGroupedItem(item: IngredientItem): boolean;
1528
+ /**
1529
+ * Determines if a specific alternative in an IngredientItem is selected
1530
+ * based on the applied choices.
1531
+ *
1532
+ * Use this in renderers to determine how an ingredient alternative should be displayed.
1533
+ *
1534
+ * @param recipe - The Recipe instance containing choices
1535
+ * @param choices - The choices that have been made
1536
+ * @param item - The IngredientItem to check
1537
+ * @param alternativeIndex - The index within item.alternatives to check (for inline alternatives only)
1538
+ * @returns true if this alternative is the selected one
1539
+ * @category Helpers
1540
+ *
1541
+ * @example
1542
+ * ```typescript
1543
+ * const recipe = new Recipe(cooklangText);
1544
+ * for (const item of step.items) {
1545
+ * if (item.type === 'ingredient') {
1546
+ * item.alternatives.forEach((alt, idx) => {
1547
+ * const isSelected = isAlternativeSelected(item, idx, recipe, choices);
1548
+ * // Render differently based on isSelected
1549
+ * });
1550
+ * }
1551
+ * }
1552
+ * ```
1553
+ */
1554
+ declare function isAlternativeSelected(recipe: Recipe, choices: RecipeChoices, item: IngredientItem, alternativeIndex?: number): boolean;
1555
+
1556
+ /**
1557
+ * Type guard to check if an ingredient quantity-like object is an AND group.
1558
+ * *
1559
+ * @param x - The quantity-like entry to check
1560
+ * @returns true if this is an AND group (has `and` property)
1561
+ * @category Helpers
1562
+ *
1563
+ * @example
1564
+ * ```typescript
1565
+ * for (const entry of ingredient.quantities) {
1566
+ * if (isAndGroup(entry)) {
1567
+ * // entry.and contains the list of quantities in the AND group
1568
+ * }
1569
+ * }
1570
+ * ```
1571
+ */
1572
+ declare function isAndGroup(x: IngredientQuantityGroup | IngredientQuantityAndGroup): x is IngredientQuantityAndGroup;
1573
+ declare function isAndGroup(x: QuantityWithUnitLike | Group): x is AndGroup;
1574
+ /**
1575
+ * Type guard to check if an ingredient quantity entry is a simple group.
1576
+ *
1577
+ * Simple groups have a single quantity with optional unit and equivalents.
1578
+ *
1579
+ * @param entry - The quantity entry to check
1580
+ * @returns true if this is a simple group (has `quantity` property)
1581
+ * @category Helpers
1582
+ *
1583
+ * @example
1584
+ * ```typescript
1585
+ * for (const entry of ingredient.quantities) {
1586
+ * if (isSimpleGroup(entry)) {
1587
+ * // entry.quantity is available
1588
+ * // entry.unit is available
1589
+ * }
1590
+ * }
1591
+ * ```
1592
+ */
1593
+ declare function isSimpleGroup(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is IngredientQuantityGroup;
1594
+ /**
1595
+ * Type guard to check if an ingredient quantity entry has alternatives.
1596
+ *
1597
+ * @param entry - The quantity entry to check
1598
+ * @returns true if this entry has alternatives
1599
+ * @category Helpers
1600
+ *
1601
+ * @example
1602
+ * ```typescript
1603
+ * for (const entry of ingredient.quantities) {
1604
+ * if (hasAlternatives(entry)) {
1605
+ * // entry.alternatives is available and non-empty
1606
+ * for (const alt of entry.alternatives) {
1607
+ * console.log(`Alternative ingredient index: ${alt.index}`);
1608
+ * }
1609
+ * }
1610
+ * }
1611
+ * ```
1612
+ */
1613
+ declare function hasAlternatives(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is (IngredientQuantityGroup | IngredientQuantityAndGroup) & {
1614
+ alternatives: AlternativeIngredientRef[];
1615
+ };
1616
+
1617
+ /**
1618
+ * Converts a quantity to the best unit in a target system.
1619
+ * Returns the converted quantity, or undefined if the unit type is "other" or not convertible.
1620
+ *
1621
+ * @category Helpers
1622
+ *
1623
+ * @param quantity - The quantity to convert
1624
+ * @param system - The target unit system
1625
+ * @returns The converted quantity, or undefined if conversion not possible
1626
+ */
1627
+ declare function convertQuantityToSystem(quantity: QuantityWithPlainUnit, system: SpecificUnitSystem): QuantityWithPlainUnit | undefined;
1628
+ declare function convertQuantityToSystem(quantity: QuantityWithExtendedUnit, system: SpecificUnitSystem): QuantityWithExtendedUnit | undefined;
1629
+
1630
+ /**
1631
+ * Error thrown when trying to build a shopping cart without a product catalog
1632
+ * @category Errors
1633
+ */
1634
+ declare class NoProductCatalogForCartError extends Error {
1635
+ constructor();
1636
+ }
1637
+ /**
1638
+ * Error thrown when trying to build a shopping cart without a shopping list
1639
+ * @category Errors
1640
+ */
1641
+ declare class NoShoppingListForCartError extends Error {
1642
+ constructor();
1643
+ }
1644
+
1645
+ 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 SpecificUnitSystem, type Step, type StepItem, type TextItem, type TextValue, type Timer, type TimerItem, type ToBaseBySystem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup, renderFractionAsVulgar };