@tmlmt/cooklang-parser 3.0.0-alpha.8 → 3.0.0-elpha.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -16,11 +16,17 @@ declare class Section {
16
16
  name: string;
17
17
  /** An array of steps and notes that make up the content of the section. */
18
18
  content: (Step | Note)[];
19
+ /** Optional list of variant names this section belongs to. */
20
+ variants?: string[];
21
+ /** Whether the section has been marked as optional ([?]) */
22
+ optional?: boolean;
19
23
  /**
20
24
  * Creates an instance of Section.
21
25
  * @param name - The name of the section. Defaults to an empty string.
26
+ * @param variants - Optional variant names for this section.
27
+ * @param optional - Whether the section is optional.
22
28
  */
23
- constructor(name?: string);
29
+ constructor(name?: string, variants?: string[], optional?: boolean);
24
30
  /**
25
31
  * Checks if the section is blank (has no name and no content).
26
32
  * Used during recipe parsing
@@ -66,8 +72,7 @@ declare class Recipe {
66
72
  */
67
73
  metadata: Metadata;
68
74
  /**
69
- * The default or manual choice of alternative ingredients.
70
- * Contains the full context including alternatives list and active selection index.
75
+ * The possible choices of alternative ingredients for this recipe.
71
76
  */
72
77
  choices: RecipeAlternatives;
73
78
  /**
@@ -98,11 +103,28 @@ declare class Recipe {
98
103
  * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
99
104
  */
100
105
  servings?: number;
106
+ /**
107
+ * Gets the unit system specified in the recipe metadata.
108
+ * Used for resolving ambiguous units like tsp, tbsp, cup, etc.
109
+ *
110
+ * @returns The unit system if specified, or undefined to use defaults
111
+ */
112
+ get unitSystem(): SpecificUnitSystem | undefined;
113
+ /**
114
+ * External storage for unit system (not a property on instances).
115
+ * Used for resolving ambiguous units during quantity addition.
116
+ */
117
+ private static unitSystems;
101
118
  /**
102
119
  * External storage for item count (not a property on instances).
103
120
  * Used for giving ID numbers to items during parsing.
104
121
  */
105
122
  private static itemCounts;
123
+ /**
124
+ * External storage for subgroup index tracking during parsing.
125
+ * Maps groupKey → subgroupKey → index within the subgroups array.
126
+ */
127
+ private static subgroupIndices;
106
128
  /**
107
129
  * Gets the current item count for this recipe.
108
130
  */
@@ -143,16 +165,73 @@ declare class Recipe {
143
165
  * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
144
166
  * @internal
145
167
  */
146
- private _populate_ingredient_quantities;
168
+ private _populateIngredientQuantities;
169
+ /** @internal */
170
+ private collectQuantityGroups;
147
171
  /**
148
- * Calculates ingredient quantities based on the provided choices.
149
- * Returns a list of computed ingredients with their total quantities.
172
+ * Gets the raw (unprocessed) quantity groups for each ingredient, before
173
+ * any summation or equivalents simplification. This is useful for cross-recipe
174
+ * aggregation (e.g., in {@link ShoppingList}), where quantities from multiple
175
+ * recipes should be combined before processing.
150
176
  *
151
- * @param choices - The recipe choices to apply when computing quantities.
152
- * If not provided, uses the default choices (first alternative for each item).
153
- * @returns An array of ComputedIngredient with quantityTotal calculated based on choices.
177
+ * @param options - Options for filtering and choice selection (same as {@link getIngredientQuantities}).
178
+ * @returns Array of {@link RawQuantityGroup} objects, one per ingredient with quantities.
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const rawGroups = recipe.getRawQuantityGroups();
183
+ * // Each group has: name, usedAsPrimary, flags, quantities[]
184
+ * // quantities are the raw QuantityWithExtendedUnit or FlatOrGroup entries
185
+ * ```
154
186
  */
155
- calc_ingredient_quantities(choices?: RecipeChoices): ComputedIngredient[];
187
+ getRawQuantityGroups(options?: GetIngredientQuantitiesOptions): RawQuantityGroup[];
188
+ /**
189
+ * Gets ingredients with their quantities populated, optionally filtered by section/step
190
+ * and respecting user choices for alternatives.
191
+ *
192
+ * When no options are provided, returns all recipe ingredients with quantities
193
+ * calculated using primary alternatives (same as after parsing).
194
+ *
195
+ * @param options - Options for filtering and choice selection:
196
+ * - `section`: Filter to a specific section (Section object or 0-based index)
197
+ * - `step`: Filter to a specific step (Step object or 0-based index)
198
+ * - `choices`: Choices for alternative ingredients (defaults to primary)
199
+ * @returns Array of Ingredient objects with quantities populated
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // Get all ingredients with primary alternatives
204
+ * const ingredients = recipe.getIngredientQuantities();
205
+ *
206
+ * // Get ingredients for a specific section
207
+ * const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
208
+ *
209
+ * // Get ingredients with specific choices applied
210
+ * const withChoices = recipe.getIngredientQuantities({
211
+ * choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
212
+ * });
213
+ * ```
214
+ */
215
+ getIngredientQuantities(options?: GetIngredientQuantitiesOptions): Ingredient[];
216
+ /**
217
+ * Returns the list of cookware items that are used in the active variant.
218
+ * Cookware in steps/sections not matching the active variant are excluded.
219
+ * Hidden cookware is always excluded.
220
+ *
221
+ * @param options - Options for filtering:
222
+ * - `choices`: The choices to apply (only `variant` is used)
223
+ * @returns Array of Cookware objects referenced by active steps
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * // Get all cookware for the default variant
228
+ * const cookware = recipe.getCookwareForVariant();
229
+ *
230
+ * // Get cookware for a specific variant
231
+ * const veganCookware = recipe.getCookwareForVariant({ choices: { variant: 'vegan' } });
232
+ * ```
233
+ */
234
+ getCookwareForVariant(options?: Pick<GetIngredientQuantitiesOptions, "choices">): Cookware[];
156
235
  /**
157
236
  * Parses a recipe from a string.
158
237
  * @param content - The recipe content to parse.
@@ -174,6 +253,26 @@ declare class Recipe {
174
253
  * @returns A new Recipe instance with the scaled ingredients.
175
254
  */
176
255
  scaleBy(factor: number | Big): Recipe;
256
+ /**
257
+ * Converts all ingredient quantities in the recipe to a target unit system.
258
+ *
259
+ * @param system - The target unit system to convert to (metric, US, UK, JP)
260
+ * @param method - How to handle existing quantities:
261
+ * - "keep": Keep all existing equivalents (swap if needed, or add converted)
262
+ * - "replace": Replace primary with target system quantity, discard equivalent used for conversion
263
+ * - "remove": Only keep target system quantity, delete all equivalents
264
+ * @returns A new Recipe instance with converted quantities
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * // Convert a recipe to metric, keeping original units as equivalents
269
+ * const metricRecipe = recipe.convertTo("metric", "keep");
270
+ *
271
+ * // Convert to US units, removing all other equivalents
272
+ * const usRecipe = recipe.convertTo("US", "remove");
273
+ * ```
274
+ */
275
+ convertTo(system: SpecificUnitSystem, method: "keep" | "replace" | "remove"): Recipe;
177
276
  /**
178
277
  * Gets the number of servings for the recipe.
179
278
  * @private
@@ -187,6 +286,55 @@ declare class Recipe {
187
286
  clone(): Recipe;
188
287
  }
189
288
 
289
+ /**
290
+ * Represents source attribution information for a recipe.
291
+ * @category Types
292
+ */
293
+ interface MetadataSource {
294
+ /** The name of the source (e.g., "New York Times Cooking"). */
295
+ name?: string;
296
+ /** The URL of the source recipe. */
297
+ url?: string;
298
+ /** The author at the source. */
299
+ author?: string;
300
+ }
301
+ /**
302
+ * Represents a yield metadata value for a recipe.
303
+ * Supports plain quantities (e.g. `yield: 300%g`), complex format with
304
+ * surrounding text (e.g. `yield: about {{300%g}} of bread`), or simple
305
+ * numbers (e.g. `yield: 2`).
306
+ * @category Types
307
+ */
308
+ interface Yield extends QuantityWithPlainUnit {
309
+ /** The text before the scaling variable (complex `{{}}` format). */
310
+ textBefore?: string;
311
+ /** The text after the scaling variable (complex `{{}}` format). */
312
+ textAfter?: string;
313
+ }
314
+ /**
315
+ * Represents time information for a recipe.
316
+ * @category Types
317
+ */
318
+ interface MetadataTime {
319
+ /** The preparation time (not parsed into DateTime format). */
320
+ prep?: string;
321
+ /** The cooking time (not parsed into DateTime format). */
322
+ cook?: string;
323
+ /** The total time required (not parsed into DateTime format). */
324
+ total?: string;
325
+ }
326
+ /**
327
+ * Represents a nested metadata object with arbitrary keys.
328
+ * @category Types
329
+ */
330
+ interface MetadataObject {
331
+ [key: string]: MetadataValue;
332
+ }
333
+ /**
334
+ * Represents any value that can appear in recipe metadata.
335
+ * @category Types
336
+ */
337
+ type MetadataValue = string | number | (string | number)[] | MetadataObject | MetadataSource | MetadataTime | Yield | undefined;
190
338
  /**
191
339
  * Represents the metadata of a recipe.
192
340
  * @category Types
@@ -196,56 +344,60 @@ interface Metadata {
196
344
  title?: string;
197
345
  /** The tags of the recipe. */
198
346
  tags?: string[];
199
- /** The source of the recipe. */
200
- source?: string;
201
- /** The source name of the recipe. */
202
- "source.name"?: string;
203
- /** The source url of the recipe. */
204
- "source.url"?: string;
205
- /** The source author of the recipe. */
206
- "source.author"?: string;
207
- /** The author of the recipe. */
347
+ /**
348
+ * The source of the recipe. Can be a simple URL string or structured attribution.
349
+ * When parsed from YAML, `source.name`, `source.url`, `source.author` keys are merged here.
350
+ */
351
+ source?: string | MetadataSource;
352
+ /** The author of the recipe (separate from source author). */
208
353
  author?: string;
209
354
  /** The number of servings the recipe makes.
210
- * Should be either a number or a string which starts with a number
211
- * (which will be used for scaling) followed by a comma and then
212
- * whatever you want.
355
+ * Stored as a number when the value is numeric, or as a string when non-numeric
356
+ * (e.g. `servings: two`). Non-numeric values default the internal scaling baseline to 1.
213
357
  *
214
- * Interchangeable with `yield` or `serves`. If multiple ones are defined,
215
- * the prevailance order for the number which will used for scaling
216
- * is `servings` \> `yield` \> `serves`.
358
+ * Interchangeable with `yield` or `serves` for the purpose of setting
359
+ * {@link Recipe.servings}. If multiple ones are defined, the prevalence
360
+ * order is `servings` \> `serves` \> `yield`.
217
361
  *
218
362
  * @example
219
363
  * ```yaml
220
364
  * servings: 4
221
365
  * ```
366
+ */
367
+ servings?: number | string;
368
+ /** The yield of the recipe.
369
+ * Can be given as:
370
+ * - a plain quantity with optional unit: `yield: 300%g` or `yield: 2`
371
+ * - a complex format with `{{}}` and optional surrounding text:
372
+ * `yield: about {{300%g}} of bread`
373
+ *
374
+ * Interchangeable with `servings` or `serves` for the purpose of setting
375
+ * {@link Recipe.servings}. If multiple ones are defined, the prevalence
376
+ * order is `servings` \> `serves` \> `yield`.
222
377
  *
223
378
  * @example
224
379
  * ```yaml
225
- * servings: 2, a few
380
+ * yield: 300%g
226
381
  * ```
227
- */
228
- servings?: number | string;
229
- /** The yield of the recipe.
230
- * Should be either a number or a string which starts with a number
231
- * (which will be used for scaling) followed by a comma and then
232
- * whatever you want.
233
382
  *
234
- * Interchangeable with `servings` or `serves`. If multiple ones are defined,
235
- * the prevailance order for the number which will used for scaling
236
- * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
237
- * for examples.
383
+ * @example
384
+ * ```yaml
385
+ * yield: about {{300%g}} of bread
386
+ * ```
238
387
  */
239
- yield?: number | string;
388
+ yield?: Yield;
240
389
  /** The number of people the recipe serves.
241
- * Should be either a number or a string which starts with a number
242
- * (which will be used for scaling) followed by a comma and then
243
- * whatever you want.
390
+ * Stored as a number when the value is numeric, or as a string when non-numeric
391
+ * (e.g. `serves: two`). Non-numeric values default the internal scaling baseline to 1.
392
+ *
393
+ * Interchangeable with `servings` or `yield` for the purpose of setting
394
+ * {@link Recipe.servings}. If multiple ones are defined, the prevalence
395
+ * order is `servings` \> `serves` \> `yield`.
244
396
  *
245
- * Interchangeable with `servings` or `yield`. If multiple ones are defined,
246
- * the prevailance order for the number which will used for scaling
247
- * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
248
- * for examples.
397
+ * @example
398
+ * ```yaml
399
+ * serves: 4
400
+ * ```
249
401
  */
250
402
  serves?: number | string;
251
403
  /** The course of the recipe. */
@@ -255,30 +407,11 @@ interface Metadata {
255
407
  /** The locale of the recipe. */
256
408
  locale?: string;
257
409
  /**
258
- * The preparation time of the recipe.
259
- * Will not be further parsed into any DateTime format nor normalize
260
- */
261
- "prep time"?: string;
262
- /**
263
- * Alias of `prep time`
264
- */
265
- "time.prep"?: string;
266
- /**
267
- * The cooking time of the recipe.
268
- * Will not be further parsed into any DateTime format nor normalize
269
- */
270
- "cook time"?: string;
271
- /**
272
- * Alias of `cook time`
410
+ * Time information for the recipe.
411
+ * When parsed from YAML, `prep time`, `time.prep`, `cook time`, `time.cook`,
412
+ * `time required`, `time`, `duration` keys are merged here.
273
413
  */
274
- "time.cook"?: string;
275
- /**
276
- * The total time of the recipe.
277
- * Will not be further parsed into any DateTime format nor normalize
278
- */
279
- "time required"?: string;
280
- time?: string;
281
- duration?: string;
414
+ time?: MetadataTime;
282
415
  /** The difficulty of the recipe. */
283
416
  difficulty?: string;
284
417
  /** The cuisine of the recipe. */
@@ -287,16 +420,25 @@ interface Metadata {
287
420
  diet?: string;
288
421
  /** The description of the recipe. */
289
422
  description?: string;
290
- /** The images of the recipe. Alias of `pictures` */
423
+ /** The images of the recipe. */
291
424
  images?: string[];
292
- /** The images of the recipe. Alias of `images` */
293
- pictures?: string[];
294
- /** The picture of the recipe. Alias of `picture` */
425
+ /** The primary image of the recipe. */
295
426
  image?: string;
296
- /** The picture of the recipe. Alias of `image` */
297
- picture?: string;
298
427
  /** The introduction of the recipe. */
299
428
  introduction?: string;
429
+ /**
430
+ * The unit system used in the recipe for ambiguous units like tsp, tbsp, cup.
431
+ * See [Unit Conversion Guide](/guide-unit-conversion) for more information.
432
+ * This stores the original value as written by the user.
433
+ */
434
+ unitSystem?: string;
435
+ /** The list of available variant names for this recipe. */
436
+ variants?: string[];
437
+ /**
438
+ * Index signature for additional metadata fields not explicitly typed.
439
+ * Any metadata key in the frontmatter will be captured here.
440
+ */
441
+ [key: string]: MetadataValue;
300
442
  }
301
443
  /**
302
444
  * Represents a quantity described by text, e.g. "a pinch"
@@ -367,7 +509,7 @@ interface IngredientExtras {
367
509
  * Used if: the ingredient is a recipe
368
510
  *
369
511
  * @example
370
- * ```cooklang
512
+ * ```yaml
371
513
  * Take @./essentials/doughs/pizza dough{1} out of the freezer and let it unfreeze overnight
372
514
  * ```
373
515
  * Would lead to:
@@ -399,30 +541,31 @@ interface AlternativeIngredientRef {
399
541
  interface IngredientQuantityGroup extends QuantityWithPlainUnit {
400
542
  /**
401
543
  * References to alternative ingredients for this quantity group.
544
+ * Each inner array represents one alternative choice option (subgroup).
545
+ * Items within the same inner array are combined with "+" (AND),
546
+ * while different inner arrays represent "or" alternatives.
402
547
  * If undefined, this group has no alternatives.
403
548
  */
404
- alternatives?: AlternativeIngredientRef[];
549
+ alternatives?: AlternativeIngredientRef[][];
405
550
  }
406
551
  /**
407
552
  * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
408
- * For example: 1 large carrot + 2 small carrots, both with cup equivalents that sum to 5 cups.
553
+ * For example: 1 large carrot + 2 small carrots, both with cup equivalents (resp. 2 cup and 1.5 cup) that sum to 5 cups.
409
554
  * @category Types
410
555
  */
411
- interface IngredientQuantityAndGroup {
412
- type: "and";
413
- /**
414
- * The incompatible primary quantities (e.g., "1 large" and "2 small").
415
- */
416
- entries: QuantityWithPlainUnit[];
556
+ interface IngredientQuantityAndGroup extends FlatAndGroup<QuantityWithPlainUnit> {
417
557
  /**
418
558
  * The summed equivalent quantities (e.g., "5 cups" from summing "1.5 cup + 2 cup + 1.5 cup").
419
559
  */
420
560
  equivalents?: QuantityWithPlainUnit[];
421
561
  /**
422
562
  * References to alternative ingredients for this quantity group.
563
+ * Each inner array represents one alternative choice option (subgroup).
564
+ * Items within the same inner array are combined with "+" (AND),
565
+ * while different inner arrays represent "or" alternatives.
423
566
  * If undefined, this group has no alternatives.
424
567
  */
425
- alternatives?: AlternativeIngredientRef[];
568
+ alternatives?: AlternativeIngredientRef[][];
426
569
  }
427
570
  /**
428
571
  * Represents an ingredient in a recipe.
@@ -458,49 +601,40 @@ interface Ingredient {
458
601
  extras?: IngredientExtras;
459
602
  }
460
603
  /**
461
- * Represents a computed ingredient with its total quantity after applying choices.
462
- * Used as the return type of {@link Recipe.calc_ingredient_quantities}.
604
+ * Represents a quantity with extended unit and additional information
605
+ * about its scalability and potential equivalents
606
+ *
463
607
  * @category Types
464
608
  */
465
- interface ComputedIngredient {
466
- /** The name of the ingredient. */
467
- name: string;
468
- /** The total quantity of the ingredient after applying choices. */
469
- quantityTotal?: QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit>;
470
- /** The preparation of the ingredient. */
471
- preparation?: string;
472
- /** The list of ingredients mentioned in the preparation as alternatives to this ingredient */
473
- alternatives?: Set<number>;
474
- /** A list of potential state modifiers or other flags for the ingredient */
475
- flags?: IngredientFlag[];
476
- /** The collection of potential additional metadata for the ingredient */
477
- extras?: IngredientExtras;
478
- }
609
+ type MaybeScalableQuantity = QuantityWithExtendedUnit & {
610
+ /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
611
+ scalable: boolean;
612
+ /** A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
613
+ * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g. */
614
+ equivalents?: QuantityWithExtendedUnit[];
615
+ };
479
616
  /**
480
- * Represents a contributor to an ingredient's total quantity, corresponding
481
- * to a single mention in the recipe text. It can contain multiple
482
- * equivalent quantities (e.g., in different units).
617
+ * Defines a type containing an optional {@link QuantityWithExtendedUnit} with scalability and potential info about equivalents
618
+ *
619
+ * Used in {@link IngredientAlternative}
620
+ *
621
+ * @param T - The base type to which the optional quantity properties will be added.
622
+ *
483
623
  * @category Types
484
624
  */
485
- interface IngredientItemQuantity extends QuantityWithExtendedUnit {
486
- /**
487
- * A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
488
- * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g.
489
- */
490
- equivalents?: QuantityWithExtendedUnit[];
491
- /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
492
- scalable: boolean;
493
- }
625
+ type WithOptionalQuantity<T> = (T & MaybeScalableQuantity) | (T & {
626
+ quantity?: undefined;
627
+ scalable?: never;
628
+ unit?: never;
629
+ equivalents?: never;
630
+ });
494
631
  /**
495
- * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
496
- * to a specific ingredient and its corresponding quantity information.
632
+ * Base type of {@link IngredientAlternative}
497
633
  * @category Types
498
634
  */
499
- interface IngredientAlternative {
635
+ type IngredientAlternativeBase = {
500
636
  /** The index of the ingredient within the {@link Recipe.ingredients} array. */
501
637
  index: number;
502
- /** The quantity of this specific mention of the ingredient */
503
- itemQuantity?: IngredientItemQuantity;
504
638
  /** The alias/name of the ingredient as it should be displayed for this occurrence. */
505
639
  displayName: string;
506
640
  /** An optional note for this specific choice (e.g., "for a vegan version"). */
@@ -509,7 +643,16 @@ interface IngredientAlternative {
509
643
  * with group keys: the id of the corresponding ingredient item (e.g. "ingredient-item-2").
510
644
  * Can be useful for creating alternative selection UI elements with anchor links */
511
645
  itemId?: string;
512
- }
646
+ };
647
+ /**
648
+ * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
649
+ * to a specific ingredient and its corresponding quantity information.
650
+ *
651
+ * Used in {@link IngredientItem} and for the various maps of {@link RecipeAlternatives}
652
+ *
653
+ * @category Types
654
+ */
655
+ type IngredientAlternative = WithOptionalQuantity<IngredientAlternativeBase>;
513
656
  /**
514
657
  * Represents an ingredient item in a recipe step.
515
658
  * @category Types
@@ -530,6 +673,12 @@ interface IngredientItem {
530
673
  * `@|group|...` syntax), they represent a single logical choice.
531
674
  */
532
675
  group?: string;
676
+ /**
677
+ * An optional subgroup identifier for binding multiple ingredients together
678
+ * within a group. Ingredients sharing the same `group` and `subgroup` are
679
+ * selected together as a unit (e.g., from `@|group/1|...` syntax).
680
+ */
681
+ subgroup?: string;
533
682
  }
534
683
  /**
535
684
  * Represents the choices one can make in a recipe
@@ -543,9 +692,18 @@ interface RecipeAlternatives {
543
692
  ingredientItems: Map<string, IngredientAlternative[]>;
544
693
  /** Map of choices that can be made for Grouped Ingredient StepItem's
545
694
  * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
546
- * - Values are arrays of IngredientAlternative objects representing the choices available for that group
695
+ * - Values are arrays of subgroups, where each subgroup is an array of
696
+ * bound IngredientAlternative objects that are selected together.
697
+ * Items sharing the same subgroup key (e.g., `@|group/1|...`) are
698
+ * in the same inner array. Items without a subgroup key each form
699
+ * their own single-element subgroup.
700
+ */
701
+ ingredientGroups: Map<string, IngredientAlternative[][]>;
702
+ /**
703
+ * All variant names discovered in the recipe (from metadata, step tags,
704
+ * and section tags). Includes `*` if any step/section uses the default tag.
547
705
  */
548
- ingredientGroups: Map<string, IngredientAlternative[]>;
706
+ variants: string[];
549
707
  }
550
708
  /**
551
709
  * Represents the choices to apply when computing ingredient quantities.
@@ -557,6 +715,46 @@ interface RecipeChoices {
557
715
  ingredientItems?: Map<string, number>;
558
716
  /** Map of choices that can be made for Grouped Ingredient StepItem's */
559
717
  ingredientGroups?: Map<string, number>;
718
+ /** The selected variant name */
719
+ variant?: string;
720
+ }
721
+ /**
722
+ * Options for the {@link Recipe.getIngredientQuantities | getIngredientQuantities()} method.
723
+ * @category Types
724
+ */
725
+ interface GetIngredientQuantitiesOptions {
726
+ /**
727
+ * Filter ingredients to only those appearing in a specific section.
728
+ * Can be a Section object or section index (0-based).
729
+ */
730
+ section?: Section | number;
731
+ /**
732
+ * Filter ingredients to only those appearing in a specific step.
733
+ * Can be a Step object or step index (0-based within the section, or global if no section specified).
734
+ */
735
+ step?: Step | number;
736
+ /**
737
+ * The choices to apply when computing quantities.
738
+ * If not provided, uses primary alternatives (index 0 for all).
739
+ */
740
+ choices?: RecipeChoices;
741
+ }
742
+ /**
743
+ * Represents a raw (unprocessed) group of quantities for a single ingredient.
744
+ * Returned by {@link Recipe.getRawQuantityGroups},
745
+ * these are the pre-addition quantities that are fed internally
746
+ * to another non-public helper function for cross-recipe aggregation.
747
+ * @category Types
748
+ */
749
+ interface RawQuantityGroup {
750
+ /** The name of the ingredient. */
751
+ name: string;
752
+ /** Whether this ingredient is used as a primary choice. */
753
+ usedAsPrimary?: boolean;
754
+ /** Flags on the ingredient (e.g., "hidden", "optional"). */
755
+ flags?: IngredientFlag[];
756
+ /** The raw, unprocessed quantities for this ingredient across all its mentions. */
757
+ quantities: (QuantityWithExtendedUnit | FlatOrGroup<QuantityWithExtendedUnit>)[];
560
758
  }
561
759
  /**
562
760
  * Represents a cookware item in a recipe step.
@@ -592,6 +790,11 @@ interface Timer {
592
790
  /** The unit of the timer. */
593
791
  unit: string;
594
792
  }
793
+ /**
794
+ * Represents a formatting attribute for a {@link TextItem}.
795
+ * @category Types
796
+ */
797
+ type TextAttribute = "bold" | "italic" | "bold+italic" | "link" | "code";
595
798
  /**
596
799
  * Represents a text item in a recipe step.
597
800
  * @category Types
@@ -601,6 +804,10 @@ interface TextItem {
601
804
  type: "text";
602
805
  /** The content of the text item. */
603
806
  value: string;
807
+ /** The formatting attribute of the text item, if any. */
808
+ attribute?: TextAttribute;
809
+ /** The URL target, only present when attribute is "link". */
810
+ href?: string;
604
811
  }
605
812
  /**
606
813
  * Represents an arbitrary scalable quantity in a recipe.
@@ -637,6 +844,10 @@ interface Step {
637
844
  type: "step";
638
845
  /** The items in the step. */
639
846
  items: StepItem[];
847
+ /** Optional list of variant names this step belongs to */
848
+ variants?: string[];
849
+ /** Whether the step has been marked as optional ("[?]") */
850
+ optional?: boolean;
640
851
  }
641
852
  /**
642
853
  * Represents an item in a note (can be text or arbitrary scalable).
@@ -707,6 +918,9 @@ interface RecipeWithServings {
707
918
  type AddedRecipe = RecipeWithFactor | RecipeWithServings;
708
919
  /**
709
920
  * Options for adding a recipe to a shopping list
921
+ *
922
+ * Used in {@link ShoppingList.addRecipe}
923
+ *
710
924
  * @category Types
711
925
  */
712
926
  type AddedRecipeOptions = {
@@ -723,7 +937,7 @@ type AddedRecipeOptions = {
723
937
  * Represents an ingredient that has been added to a shopping list
724
938
  * @category Types
725
939
  */
726
- type AddedIngredient = Pick<ComputedIngredient, "name" | "quantityTotal">;
940
+ type AddedIngredient = Pick<Ingredient, "name" | "quantities">;
727
941
  /**
728
942
  * Represents an ingredient in a category.
729
943
  * @category Types
@@ -783,6 +997,48 @@ type ProductOption = ProductOptionBase & {
783
997
  /** The size(s) of the product. Multiple sizes allow equivalent expressions (e.g., "1%dozen" and "12") */
784
998
  sizes: ProductSize[];
785
999
  };
1000
+ /**
1001
+ * Represents a pantry item entry in the TOML file.
1002
+ * Can be a simple quantity string (e.g. `"500%g"`) or an object with details.
1003
+ * @category Types
1004
+ */
1005
+ type PantryItemToml = string | {
1006
+ quantity?: string;
1007
+ bought?: string;
1008
+ expire?: string;
1009
+ low?: string;
1010
+ };
1011
+ /**
1012
+ * Represents a parsed pantry item.
1013
+ * @category Types
1014
+ */
1015
+ interface PantryItem {
1016
+ /** The name of the item. */
1017
+ name: string;
1018
+ /** The storage location (TOML section name, e.g. "freezer", "fridge"). */
1019
+ location: string;
1020
+ /** The quantity value of the item. */
1021
+ quantity?: FixedValue | Range;
1022
+ /** The unit of the item's quantity. */
1023
+ unit?: string;
1024
+ /** The date when the item was purchased. */
1025
+ bought?: Date;
1026
+ /** The expiration date of the item. */
1027
+ expire?: Date;
1028
+ /** The low stock threshold value. */
1029
+ low?: FixedValue | Range;
1030
+ /** The unit of the low stock threshold. */
1031
+ lowUnit?: string;
1032
+ }
1033
+ /**
1034
+ * Options for configuring a {@link Pantry}.
1035
+ * @category Types
1036
+ */
1037
+ interface PantryOptions {
1038
+ /** Date format pattern for parsing date strings (e.g. `"DD.MM.YYYY"`, `"MM/DD/YYYY"`, `"YYYY-MM-DD"`).
1039
+ * If not provided, dates are parsed with fuzzy detection defaulting to day-first when ambiguous. */
1040
+ dateFormat?: string;
1041
+ }
786
1042
  /**
787
1043
  * Represents a product selection in a {@link ShoppingCart}
788
1044
  * @category Types
@@ -835,12 +1091,23 @@ type CartMisMatch = ProductMisMatch[];
835
1091
  * Represents the type category of a unit used for quantities
836
1092
  * @category Types
837
1093
  */
838
- type UnitType = "mass" | "volume" | "count";
1094
+ type UnitType = "mass" | "volume" | "count" | "other";
1095
+ /**
1096
+ * Represents the specific measurement systems
1097
+ * @category Types
1098
+ */
1099
+ type SpecificUnitSystem = "metric" | "US" | "UK" | "JP";
839
1100
  /**
840
1101
  * Represents the measurement system a unit belongs to
841
1102
  * @category Types
842
1103
  */
843
- type UnitSystem = "metric" | "imperial";
1104
+ type UnitSystem = SpecificUnitSystem | "ambiguous";
1105
+ /**
1106
+ * Conversion factors for ambiguous units that can belong to multiple systems.
1107
+ * Maps each possible system to its toBase conversion factor.
1108
+ * @category Types
1109
+ */
1110
+ type ToBaseBySystem = Partial<Record<SpecificUnitSystem, number>>;
844
1111
  /**
845
1112
  * Represents a unit used to describe quantities
846
1113
  * @category Types
@@ -860,8 +1127,28 @@ interface UnitDefinition extends Unit {
860
1127
  system: UnitSystem;
861
1128
  /** e.g. ['gram', 'grams'] */
862
1129
  aliases: string[];
863
- /** Conversion factor to the base unit of its type */
1130
+ /** Conversion factor to the base unit of its type (uses default system for ambiguous units) */
864
1131
  toBase: number;
1132
+ /** For ambiguous units: conversion factors for each possible system */
1133
+ toBaseBySystem?: ToBaseBySystem;
1134
+ /** Whether this unit is a candidate for "best unit" selection (default: true) */
1135
+ isBestUnit?: boolean;
1136
+ /** Maximum value before upgrading to a larger unit (default: 999) */
1137
+ maxValue?: number;
1138
+ /** Fraction display configuration */
1139
+ fractions?: UnitFractionConfig;
1140
+ }
1141
+ /**
1142
+ * Configuration for fraction display on a unit
1143
+ * @category Types
1144
+ */
1145
+ interface UnitFractionConfig {
1146
+ /** Whether to approximate decimals as fractions for this unit */
1147
+ enabled: boolean;
1148
+ /** Allowed denominators (default: [2, 3, 4, 8]) */
1149
+ denominators?: number[];
1150
+ /** Maximum whole number in mixed fraction before falling back to decimal (default: 4) */
1151
+ maxWhole?: number;
865
1152
  }
866
1153
  /**
867
1154
  * Represents a resolved unit definition or a lightweight placeholder for non-standard units
@@ -908,27 +1195,59 @@ interface QuantityWithUnitDef extends QuantityBase {
908
1195
  * @category Types
909
1196
  */
910
1197
  type QuantityWithUnitLike = QuantityWithPlainUnit | QuantityWithExtendedUnit | QuantityWithUnitDef;
1198
+ /**
1199
+ * Represents a flat "or" group of alternative quantities (for alternative units)
1200
+ * @category Types
1201
+ */
1202
+ interface FlatOrGroup<T = QuantityWithUnitLike> {
1203
+ or: T[];
1204
+ }
911
1205
  /**
912
1206
  * Represents an "or" group of alternative quantities that may contain nested groups (alternatives with nested structure)
913
1207
  * @category Types
914
1208
  */
915
1209
  interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
916
- type: "or";
917
- entries: (T | MaybeNestedGroup<T>)[];
1210
+ or: (T | MaybeNestedGroup<T>)[];
1211
+ }
1212
+ /**
1213
+ * Represents a flat "and" group of quantities (combined quantities)
1214
+ * @category Types
1215
+ */
1216
+ interface FlatAndGroup<T = QuantityWithUnitLike> {
1217
+ and: T[];
918
1218
  }
919
1219
  /**
920
1220
  * Represents an "and" group of quantities that may contain nested groups (combinations with nested structure)
921
1221
  * @category Types
922
1222
  */
923
1223
  interface MaybeNestedAndGroup<T = QuantityWithUnitLike> {
924
- type: "and";
925
- entries: (T | MaybeNestedGroup<T>)[];
1224
+ and: (T | MaybeNestedGroup<T>)[];
926
1225
  }
1226
+ /**
1227
+ * Represents any flat group type ("and" or "or")
1228
+ * @category Types
1229
+ */
1230
+ type FlatGroup<T = QuantityWithUnitLike> = FlatAndGroup<T> | FlatOrGroup<T>;
927
1231
  /**
928
1232
  * Represents any group type that may include nested groups
929
1233
  * @category Types
930
1234
  */
931
1235
  type MaybeNestedGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | MaybeNestedOrGroup<T>;
1236
+ /**
1237
+ * Represents any group type (flat or nested)
1238
+ * @category Types
1239
+ */
1240
+ type Group<T = QuantityWithUnitLike> = MaybeNestedGroup<T> | FlatGroup<T>;
1241
+ /**
1242
+ * Represents any "or" group (flat or nested)
1243
+ * @category Types
1244
+ */
1245
+ type OrGroup<T = QuantityWithUnitLike> = MaybeNestedOrGroup<T> | FlatOrGroup<T>;
1246
+ /**
1247
+ * Represents any "and" group (flat or nested)
1248
+ * @category Types
1249
+ */
1250
+ type AndGroup<T = QuantityWithUnitLike> = MaybeNestedAndGroup<T> | FlatAndGroup<T>;
932
1251
 
933
1252
  /**
934
1253
  * Parser for category configurations specified à-la-cooklang.
@@ -979,6 +1298,127 @@ declare class CategoryConfig {
979
1298
  parse(config: string): void;
980
1299
  }
981
1300
 
1301
+ /**
1302
+ * Pantry Inventory Manager: parses and queries a pantry inventory file.
1303
+ *
1304
+ * ## Usage
1305
+ *
1306
+ * Create a new Pantry instance with optional TOML content and options
1307
+ * (see {@link Pantry."constructor" | constructor}), then query items
1308
+ * using {@link Pantry.getDepletedItems | getDepletedItems()},
1309
+ * {@link Pantry.getExpiredItems | getExpiredItems()},
1310
+ * {@link Pantry.isLow | isLow()}, or {@link Pantry.isExpired | isExpired()}.
1311
+ *
1312
+ * A Pantry can also be attached to a {@link ShoppingList} via
1313
+ * {@link ShoppingList.addPantry | addPantry()} so that on-hand stock
1314
+ * is subtracted from recipe ingredient needs.
1315
+ *
1316
+ * @example
1317
+ * ```typescript
1318
+ * import { Pantry } from "@tmlmt/cooklang-parser";
1319
+ *
1320
+ * const pantryToml = `
1321
+ * [fridge]
1322
+ * milk = { expire = "10.05.2024", quantity = "1%L" }
1323
+ *
1324
+ * [freezer]
1325
+ * spinach = { quantity = "1%kg", low = "200%g" }
1326
+ * `;
1327
+ *
1328
+ * const pantry = new Pantry(pantryToml);
1329
+ * console.log(pantry.getExpiredItems());
1330
+ * console.log(pantry.isLow("spinach"));
1331
+ * ```
1332
+ *
1333
+ * @see [Pantry Configuration](https://cooklang.org/docs/spec/#pantry-configuration) section of the cooklang specs
1334
+ *
1335
+ * @category Classes
1336
+ */
1337
+ declare class Pantry {
1338
+ /**
1339
+ * The parsed pantry items.
1340
+ */
1341
+ items: PantryItem[];
1342
+ /**
1343
+ * Options for date parsing and other configuration.
1344
+ */
1345
+ private options;
1346
+ /**
1347
+ * Optional category configuration for alias-based lookups.
1348
+ */
1349
+ private categoryConfig?;
1350
+ /**
1351
+ * Creates a new Pantry instance.
1352
+ * @param tomlContent - Optional TOML content to parse.
1353
+ * @param options - Optional configuration options.
1354
+ */
1355
+ constructor(tomlContent?: string, options?: PantryOptions);
1356
+ /**
1357
+ * Parses a TOML string into pantry items.
1358
+ * @param tomlContent - The TOML string to parse.
1359
+ * @returns The parsed list of pantry items.
1360
+ */
1361
+ parse(tomlContent: string): PantryItem[];
1362
+ /**
1363
+ * Parses a single pantry item from its TOML representation.
1364
+ */
1365
+ private parseItem;
1366
+ /**
1367
+ * Parses a date string using the configured format or fuzzy detection.
1368
+ */
1369
+ private parseDate;
1370
+ /**
1371
+ * Sets a category configuration for alias-based item lookups.
1372
+ * @param config - The category configuration to use.
1373
+ */
1374
+ setCategoryConfig(config: CategoryConfig): void;
1375
+ /**
1376
+ * Finds a pantry item by name, using exact match first, then alias lookup
1377
+ * via the stored CategoryConfig.
1378
+ * @param name - The name to search for.
1379
+ * @returns The matching pantry item, or undefined if not found.
1380
+ */
1381
+ findItem(name: string): PantryItem | undefined;
1382
+ /**
1383
+ * Gets the numeric value of a pantry item's quantity, optionally converted to base units.
1384
+ * Returns undefined if the quantity has a text value or is not set.
1385
+ */
1386
+ private getItemNumericValue;
1387
+ /**
1388
+ * Returns all items that are depleted (quantity = 0) or below their low threshold.
1389
+ * @returns An array of depleted pantry items.
1390
+ */
1391
+ getDepletedItems(): PantryItem[];
1392
+ /**
1393
+ * Returns all items whose expiration date is within `nbDays` days from today
1394
+ * (or already passed).
1395
+ * @param nbDays - Number of days ahead to check. Defaults to 0 (already expired).
1396
+ * @returns An array of expired pantry items.
1397
+ */
1398
+ getExpiredItems(nbDays?: number): PantryItem[];
1399
+ /**
1400
+ * Checks if a specific item is low (quantity = 0 or below `low` threshold).
1401
+ * @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
1402
+ * @returns true if the item is low, false otherwise. Returns false if item not found.
1403
+ */
1404
+ isLow(itemName: string): boolean;
1405
+ /**
1406
+ * Checks if a specific item is expired or expires within `nbDays` days.
1407
+ * @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
1408
+ * @param nbDays - Number of days ahead to check. Defaults to 0.
1409
+ * @returns true if the item is expired, false otherwise. Returns false if item not found.
1410
+ */
1411
+ isExpired(itemName: string, nbDays?: number): boolean;
1412
+ /**
1413
+ * Internal: checks if a pantry item is low.
1414
+ */
1415
+ private isItemLow;
1416
+ /**
1417
+ * Internal: checks if a pantry item is expired.
1418
+ */
1419
+ private isItemExpired;
1420
+ }
1421
+
982
1422
  /**
983
1423
  * Product Catalog Manager: used in conjunction with {@link ShoppingCart}
984
1424
  *
@@ -1005,7 +1445,7 @@ declare class CategoryConfig {
1005
1445
  * 14141 = { name = "Big pack", size = "6%kg", price = 10 }
1006
1446
  * `
1007
1447
  * const catalog = new ProductCatalog(catalog);
1008
- * const eggs = catalog.find("oeuf");
1448
+ * const eggs = catalog.products.find(p => p.ingredientName === "eggs");
1009
1449
  * ```
1010
1450
  */
1011
1451
  declare class ProductCatalog {
@@ -1041,7 +1481,7 @@ declare class ProductCatalog {
1041
1481
  * ## Usage
1042
1482
  *
1043
1483
  * - Create a new ShoppingList instance with an optional category configuration (see {@link ShoppingList."constructor" | constructor})
1044
- * - Add recipes, scaling them as needed (see {@link ShoppingList.add_recipe | add_recipe()})
1484
+ * - Add recipes, scaling them as needed (see {@link ShoppingList.addRecipe | addRecipe()})
1045
1485
  * - Categorize the ingredients (see {@link ShoppingList.categorize | categorize()})
1046
1486
  *
1047
1487
  * @example
@@ -1053,10 +1493,10 @@ declare class ProductCatalog {
1053
1493
  * const categoryConfig = fs.readFileSync("./myconfig.txt", "utf-8")
1054
1494
  * const recipe1 = new Recipe(fs.readFileSync("./myrecipe.cook", "utf-8"));
1055
1495
  * const shoppingList = new ShoppingList();
1056
- * shoppingList.set_category_config(categoryConfig);
1496
+ * shoppingList.setCategoryConfig(categoryConfig);
1057
1497
  * // Quantities are automatically calculated and ingredients categorized
1058
1498
  * // when adding a recipe
1059
- * shoppingList.add_recipe(recipe1);
1499
+ * shoppingList.addRecipe(recipe1);
1060
1500
  * ```
1061
1501
  *
1062
1502
  * @category Classes
@@ -1073,36 +1513,84 @@ declare class ShoppingList {
1073
1513
  /**
1074
1514
  * The category configuration for the shopping list.
1075
1515
  */
1076
- category_config?: CategoryConfig;
1516
+ categoryConfig?: CategoryConfig;
1077
1517
  /**
1078
1518
  * The categorized ingredients in the shopping list.
1079
1519
  */
1080
1520
  categories?: CategorizedIngredients;
1521
+ /**
1522
+ * The unit system to use for quantity simplification.
1523
+ * When set, overrides per-recipe unit systems.
1524
+ */
1525
+ unitSystem?: SpecificUnitSystem;
1526
+ /**
1527
+ * Per-ingredient equivalence ratio maps for recomputing equivalents
1528
+ * after pantry subtraction. Keyed by ingredient name.
1529
+ * @internal
1530
+ */
1531
+ private equivalenceRatios;
1532
+ /**
1533
+ * The original pantry (never mutated by recipe calculations).
1534
+ */
1535
+ pantry?: Pantry;
1536
+ /**
1537
+ * The pantry with quantities updated after subtracting recipe needs.
1538
+ * Recomputed on every {@link ShoppingList.calculateIngredients | calculateIngredients()} call.
1539
+ */
1540
+ private resultingPantry?;
1081
1541
  /**
1082
1542
  * Creates a new ShoppingList instance
1083
- * @param category_config_str - The category configuration to parse.
1543
+ * @param categoryConfigStr - The category configuration to parse.
1084
1544
  */
1085
- constructor(category_config_str?: string | CategoryConfig);
1086
- private calculate_ingredients;
1545
+ constructor(categoryConfigStr?: string | CategoryConfig);
1546
+ private calculateIngredients;
1547
+ /**
1548
+ * Subtracts pantry item quantities from calculated ingredient quantities
1549
+ * and updates the resultingPantry to reflect consumed stock.
1550
+ */
1551
+ private applyPantrySubtraction;
1087
1552
  /**
1088
1553
  * Adds a recipe to the shopping list, then automatically
1089
1554
  * recalculates the quantities and recategorize the ingredients.
1090
1555
  * @param recipe - The recipe to add.
1091
1556
  * @param options - Options for adding the recipe.
1557
+ * @throws Error if the recipe has alternatives without corresponding choices.
1558
+ */
1559
+ addRecipe(recipe: Recipe, options?: AddedRecipeOptions): void;
1560
+ /**
1561
+ * Checks if a recipe has unresolved alternatives (alternatives without provided choices).
1562
+ * @param recipe - The recipe to check.
1563
+ * @param choices - The choices provided for the recipe.
1564
+ * @returns An error message if there are unresolved alternatives, undefined otherwise.
1092
1565
  */
1093
- add_recipe(recipe: Recipe, options?: AddedRecipeOptions): void;
1566
+ private getUnresolvedAlternativesError;
1094
1567
  /**
1095
1568
  * Removes a recipe from the shopping list, then automatically
1096
- * recalculates the quantities and recategorize the ingredients.s
1569
+ * recalculates the quantities and recategorize the ingredients.
1097
1570
  * @param index - The index of the recipe to remove.
1098
1571
  */
1099
- remove_recipe(index: number): void;
1572
+ removeRecipe(index: number): void;
1573
+ /**
1574
+ * Adds a pantry to the shopping list. On-hand pantry quantities will be
1575
+ * subtracted from recipe ingredient needs on each recalculation.
1576
+ * @param pantry - A Pantry instance or a TOML string to parse.
1577
+ * @param options - Options for pantry parsing (only used when providing a TOML string).
1578
+ */
1579
+ addPantry(pantry: Pantry | string, options?: PantryOptions): void;
1580
+ /**
1581
+ * Returns the resulting pantry with quantities updated to reflect
1582
+ * what was consumed by the shopping list's recipes.
1583
+ * Returns undefined if no pantry was added.
1584
+ * @returns The resulting Pantry, or undefined.
1585
+ */
1586
+ getPantry(): Pantry | undefined;
1100
1587
  /**
1101
1588
  * Sets the category configuration for the shopping list
1102
1589
  * and automatically categorize current ingredients from the list.
1590
+ * Also propagates the configuration to the pantry if one is set.
1103
1591
  * @param config - The category configuration to parse.
1104
1592
  */
1105
- set_category_config(config: string | CategoryConfig): void;
1593
+ setCategoryConfig(config: string | CategoryConfig): void;
1106
1594
  /**
1107
1595
  * Categorizes the ingredients in the shopping list
1108
1596
  * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
@@ -1145,7 +1633,7 @@ interface ShoppingCartSummary {
1145
1633
  * ```ts
1146
1634
  * const shoppingList = new ShoppingList();
1147
1635
  * const recipe = new Recipe("@flour{600%g}");
1148
- * shoppingList.add_recipe(recipe);
1636
+ * shoppingList.addRecipe(recipe);
1149
1637
  *
1150
1638
  * const catalog = new ProductCatalog();
1151
1639
  * catalog.products = [
@@ -1255,6 +1743,329 @@ declare class ShoppingCart {
1255
1743
  summarize(): ShoppingCartSummary;
1256
1744
  }
1257
1745
 
1746
+ /**
1747
+ * Render a fraction using Unicode vulgar fraction characters when available.
1748
+ * Handles improper fractions by extracting the whole part (e.g., 5/4 → "1¼").
1749
+ *
1750
+ * @param num - The numerator
1751
+ * @param den - The denominator
1752
+ * @returns The fraction as a string, using vulgar characters if available
1753
+ * @category Helpers
1754
+ *
1755
+ * @example
1756
+ * ```typescript
1757
+ * renderFractionAsVulgar(1, 2); // "½"
1758
+ * renderFractionAsVulgar(3, 4); // "¾"
1759
+ * renderFractionAsVulgar(5, 4); // "1¼"
1760
+ * renderFractionAsVulgar(7, 3); // "2⅓"
1761
+ * renderFractionAsVulgar(2, 5); // "2/5" (no vulgar character available)
1762
+ * ```
1763
+ */
1764
+ declare function renderFractionAsVulgar(num: number, den: number): string;
1765
+ /**
1766
+ * Format a numeric value (decimal or fraction) to a string.
1767
+ *
1768
+ * @param value - The decimal or fraction value to format
1769
+ * @param useVulgar - Whether to use Unicode vulgar fraction characters (default: true)
1770
+ * @returns The formatted string representation
1771
+ * @category Helpers
1772
+ *
1773
+ * @example
1774
+ * ```typescript
1775
+ * formatNumericValue({ type: "decimal", decimal: 1.5 }); // "1.5"
1776
+ * formatNumericValue({ type: "fraction", num: 1, den: 2 }); // "½"
1777
+ * formatNumericValue({ type: "fraction", num: 1, den: 2 }, false); // "1/2"
1778
+ * formatNumericValue({ type: "fraction", num: 5, den: 4 }, true); // "1¼"
1779
+ * ```
1780
+ */
1781
+ declare function formatNumericValue(value: DecimalValue | FractionValue, useVulgar?: boolean): string;
1782
+ /**
1783
+ * Format a single value (text, decimal, or fraction) to a string.
1784
+ *
1785
+ * @param value - The value to format
1786
+ * @returns The formatted string representation
1787
+ * @category Helpers
1788
+ *
1789
+ * @example
1790
+ * ```typescript
1791
+ * formatSingleValue({ type: "text", text: "a pinch" }); // "a pinch"
1792
+ * formatSingleValue({ type: "decimal", decimal: 2 }); // "2"
1793
+ * formatSingleValue({ type: "fraction", num: 3, den: 4 }); // "3/4"
1794
+ * ```
1795
+ */
1796
+ declare function formatSingleValue(value: TextValue | DecimalValue | FractionValue): string;
1797
+ /**
1798
+ * Format a quantity (fixed value or range) to a string.
1799
+ *
1800
+ * @param quantity - The quantity to format
1801
+ * @returns The formatted string representation
1802
+ * @category Helpers
1803
+ *
1804
+ * @example
1805
+ * ```typescript
1806
+ * formatQuantity({ type: "fixed", value: { type: "decimal", decimal: 100 } }); // "100"
1807
+ * formatQuantity({ type: "range", min: { type: "decimal", decimal: 1 }, max: { type: "decimal", decimal: 2 } }); // "1-2"
1808
+ * ```
1809
+ */
1810
+ declare function formatQuantity(quantity: FixedValue | Range): string;
1811
+ /**
1812
+ * Format a unit to a string. Handles both plain string units and Unit objects.
1813
+ *
1814
+ * @param unit - The unit to format (string, Unit object, or undefined)
1815
+ * @returns The formatted unit string, or empty string if undefined
1816
+ * @category Helpers
1817
+ *
1818
+ * @example
1819
+ * ```typescript
1820
+ * formatUnit("g"); // "g"
1821
+ * formatUnit({ name: "grams" }); // "grams"
1822
+ * formatUnit(undefined); // ""
1823
+ * ```
1824
+ */
1825
+ declare function formatUnit(unit: string | Unit | undefined): string;
1826
+ /**
1827
+ * Format a quantity with its unit to a string.
1828
+ *
1829
+ * @param quantity - The quantity to format
1830
+ * @param unit - The unit to append (string, Unit object, or undefined)
1831
+ * @returns The formatted string with quantity and unit
1832
+ * @category Helpers
1833
+ *
1834
+ * @example
1835
+ * ```typescript
1836
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 100 } }, "g"); // "100 g"
1837
+ * formatQuantityWithUnit({ type: "fixed", value: { type: "decimal", decimal: 2 } }, undefined); // "2"
1838
+ * ```
1839
+ */
1840
+ declare function formatQuantityWithUnit(quantity: FixedValue | Range | undefined, unit: string | Unit | undefined): string;
1841
+ /**
1842
+ * Format a QuantityWithExtendedUnit to a string.
1843
+ *
1844
+ * @param item - The quantity with extended unit to format
1845
+ * @returns The formatted string
1846
+ * @category Helpers
1847
+ */
1848
+ declare function formatExtendedQuantity(item: QuantityWithExtendedUnit): string;
1849
+ /**
1850
+ * Format an IngredientItemQuantity with all its equivalents to a string.
1851
+ *
1852
+ * @param itemQuantity - The ingredient item quantity to format
1853
+ * @param separator - The separator between primary and equivalent quantities (default: " | ")
1854
+ * @returns The formatted string with all quantities
1855
+ * @category Helpers
1856
+ *
1857
+ * @example
1858
+ * ```typescript
1859
+ * // For an ingredient like @flour{100%g|3.5%oz}
1860
+ * formatItemQuantity(itemQuantity); // "100 g | 3.5 oz"
1861
+ * formatItemQuantity(itemQuantity, " / "); // "100 g / 3.5 oz"
1862
+ * ```
1863
+ */
1864
+ declare function formatItemQuantity(itemQuantity: MaybeScalableQuantity, separator?: string): string;
1865
+ /**
1866
+ * Check if an ingredient item is a grouped alternative (vs inline alternative).
1867
+ *
1868
+ * Grouped alternatives are ingredients that share a group key (e.g., `@|milk|...`)
1869
+ * and are distributed across multiple tokens in the recipe.
1870
+ *
1871
+ * @param item - The ingredient item to check
1872
+ * @returns true if this is a grouped alternative
1873
+ * @category Helpers
1874
+ *
1875
+ * @example
1876
+ * ```typescript
1877
+ * for (const item of step.items) {
1878
+ * if (item.type === 'ingredient') {
1879
+ * if (isGroupedItem(item)) {
1880
+ * // Handle grouped alternative (e.g., show with strikethrough if not selected)
1881
+ * } else {
1882
+ * // Handle inline alternative (e.g., hide if not selected)
1883
+ * }
1884
+ * }
1885
+ * }
1886
+ * ```
1887
+ */
1888
+ declare function isGroupedItem(item: IngredientItem): boolean;
1889
+ /**
1890
+ * Determines if a specific alternative in an IngredientItem is selected
1891
+ * based on the applied choices.
1892
+ *
1893
+ * Use this in renderers to determine how an ingredient alternative should be displayed.
1894
+ *
1895
+ * @param recipe - The Recipe instance containing choices
1896
+ * @param choices - The choices that have been made
1897
+ * @param item - The IngredientItem to check
1898
+ * @param alternativeIndex - The index within item.alternatives to check (for inline alternatives only)
1899
+ * @returns true if this alternative is the selected one
1900
+ * @category Helpers
1901
+ *
1902
+ * @example
1903
+ * ```typescript
1904
+ * const recipe = new Recipe(cooklangText);
1905
+ * for (const item of step.items) {
1906
+ * if (item.type === 'ingredient') {
1907
+ * item.alternatives.forEach((alt, idx) => {
1908
+ * const isSelected = isAlternativeSelected(recipe, choices, item, idx);
1909
+ * // Render differently based on isSelected
1910
+ * });
1911
+ * }
1912
+ * }
1913
+ * ```
1914
+ */
1915
+ declare function isAlternativeSelected(recipe: Recipe, choices: RecipeChoices, item: IngredientItem, alternativeIndex?: number): boolean;
1916
+ /**
1917
+ * Determines if a section is active (should be displayed or processed) for a given variant.
1918
+ *
1919
+ * - Sections with no `variants` property are always active.
1920
+ * - When no variant is selected (default), sections tagged `[*]` are active,
1921
+ * and sections tagged with named variants are not.
1922
+ * - When a named variant is selected, sections whose `variants` array includes
1923
+ * that name are active.
1924
+ *
1925
+ * @param section - The Section to check
1926
+ * @param variant - The active variant name, or `undefined`/`*` for the default variant
1927
+ * @returns `true` if the section should be displayed
1928
+ * @category Helpers
1929
+ *
1930
+ * @example
1931
+ * ```typescript
1932
+ * const recipe = new Recipe(cooklangText);
1933
+ * for (const section of recipe.sections) {
1934
+ * if (isSectionActive(section, choices.variant)) {
1935
+ * // render section
1936
+ * }
1937
+ * }
1938
+ * ```
1939
+ */
1940
+ declare function isSectionActive(section: Section, variant?: string): boolean;
1941
+ /**
1942
+ * Determines if a step is active (should be displayed) for a given variant.
1943
+ *
1944
+ * - Steps with no `variants` property are always active.
1945
+ * - When no variant is selected (default), steps tagged `[*]` are active,
1946
+ * and steps tagged with named variants are not.
1947
+ * - When a named variant is selected, steps whose `variants` array includes
1948
+ * that name are active.
1949
+ *
1950
+ * @param step - The Step to check
1951
+ * @param variant - The active variant name, or `undefined`/`*` for the default variant
1952
+ * @returns `true` if the step should be displayed
1953
+ * @category Helpers
1954
+ *
1955
+ * @example
1956
+ * ```typescript
1957
+ * for (const item of section.content) {
1958
+ * if (item.type === 'step' && isStepActive(item, choices.variant)) {
1959
+ * // render step
1960
+ * }
1961
+ * }
1962
+ * ```
1963
+ */
1964
+ declare function isStepActive(step: Step, variant?: string): boolean;
1965
+ /**
1966
+ * Returns the effective choices for a recipe given a variant selection.
1967
+ *
1968
+ * When a named variant is active, this scans ingredient alternatives whose
1969
+ * `note` contains the variant name (case-insensitive substring match) and
1970
+ * returns a `RecipeChoices` object with auto-selected alternatives.
1971
+ *
1972
+ * For inline alternatives: auto-selects the first alternative whose note
1973
+ * matches the variant name.
1974
+ *
1975
+ * For grouped alternatives: auto-selects the first subgroup that has any
1976
+ * alternative whose note matches the variant name.
1977
+ *
1978
+ * @param recipe - The Recipe instance
1979
+ * @param variant - The active variant name, or `undefined`/`*` for defaults
1980
+ * @returns A `RecipeChoices` with the `variant` set and auto-selected alternatives
1981
+ * @category Helpers
1982
+ *
1983
+ * @example
1984
+ * ```typescript
1985
+ * const recipe = new Recipe(cooklangText);
1986
+ * const choices = getEffectiveChoices(recipe, "vegan");
1987
+ * const ingredients = recipe.getIngredientQuantities({ choices });
1988
+ * ```
1989
+ */
1990
+ declare function getEffectiveChoices(recipe: Recipe, variant?: string): RecipeChoices;
1991
+
1992
+ /**
1993
+ * Type guard to check if an ingredient quantity-like object is an AND group.
1994
+ * *
1995
+ * @param x - The quantity-like entry to check
1996
+ * @returns true if this is an AND group (has `and` property)
1997
+ * @category Helpers
1998
+ *
1999
+ * @example
2000
+ * ```typescript
2001
+ * for (const entry of ingredient.quantities) {
2002
+ * if (isAndGroup(entry)) {
2003
+ * // entry.and contains the list of quantities in the AND group
2004
+ * }
2005
+ * }
2006
+ * ```
2007
+ */
2008
+ declare function isAndGroup(x: IngredientQuantityGroup | IngredientQuantityAndGroup): x is IngredientQuantityAndGroup;
2009
+ declare function isAndGroup(x: QuantityWithUnitLike | Group): x is AndGroup;
2010
+ /**
2011
+ * Type guard to check if an ingredient quantity entry is a simple group.
2012
+ *
2013
+ * Simple groups have a single quantity with optional unit and equivalents.
2014
+ *
2015
+ * @param entry - The quantity entry to check
2016
+ * @returns true if this is a simple group (has `quantity` property)
2017
+ * @category Helpers
2018
+ *
2019
+ * @example
2020
+ * ```typescript
2021
+ * for (const entry of ingredient.quantities) {
2022
+ * if (isSimpleGroup(entry)) {
2023
+ * // entry.quantity is available
2024
+ * // entry.unit is available
2025
+ * }
2026
+ * }
2027
+ * ```
2028
+ */
2029
+ declare function isSimpleGroup(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is IngredientQuantityGroup;
2030
+ /**
2031
+ * Type guard to check if an ingredient quantity entry has alternatives.
2032
+ *
2033
+ * @param entry - The quantity entry to check
2034
+ * @returns true if this entry has alternatives
2035
+ * @category Helpers
2036
+ *
2037
+ * @example
2038
+ * ```typescript
2039
+ * for (const entry of ingredient.quantities) {
2040
+ * if (hasAlternatives(entry)) {
2041
+ * // entry.alternatives is available and non-empty
2042
+ * for (const subgroup of entry.alternatives) {
2043
+ * // Each subgroup is one "or" choice; items within are combined with "+"
2044
+ * for (const alt of subgroup) {
2045
+ * console.log(`Alternative ingredient index: ${alt.index}`);
2046
+ * }
2047
+ * }
2048
+ * }
2049
+ * }
2050
+ * ```
2051
+ */
2052
+ declare function hasAlternatives(entry: IngredientQuantityGroup | IngredientQuantityAndGroup): entry is (IngredientQuantityGroup | IngredientQuantityAndGroup) & {
2053
+ alternatives: AlternativeIngredientRef[][];
2054
+ };
2055
+
2056
+ /**
2057
+ * Converts a quantity to the best unit in a target system.
2058
+ * Returns the converted quantity, or undefined if the unit type is "other" or not convertible.
2059
+ *
2060
+ * @category Helpers
2061
+ *
2062
+ * @param quantity - The quantity to convert
2063
+ * @param system - The target unit system
2064
+ * @returns The converted quantity, or undefined if conversion not possible
2065
+ */
2066
+ declare function convertQuantityToSystem(quantity: QuantityWithPlainUnit, system: SpecificUnitSystem): QuantityWithPlainUnit | undefined;
2067
+ declare function convertQuantityToSystem(quantity: QuantityWithExtendedUnit, system: SpecificUnitSystem): QuantityWithExtendedUnit | undefined;
2068
+
1258
2069
  /**
1259
2070
  * Error thrown when trying to build a shopping cart without a product catalog
1260
2071
  * @category Errors
@@ -1270,4 +2081,4 @@ declare class NoShoppingListForCartError extends Error {
1270
2081
  constructor();
1271
2082
  }
1272
2083
 
1273
- export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type ArbitraryScalable, type ArbitraryScalableItem, type CartContent, type CartMatch, type CartMisMatch, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type ComputedIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedNumericValue, type FixedValue, type FractionValue, type Ingredient, type IngredientAlternative, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientItemQuantity, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type Metadata, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, type NoteItem, ProductCatalog, type ProductMatch, type ProductMisMatch, type ProductOption, type ProductOptionBase, type ProductOptionCore, type ProductSelection, type ProductSize, type QuantityBase, type QuantityWithExtendedUnit, type QuantityWithPlainUnit, type QuantityWithUnitDef, type QuantityWithUnitLike, type Range, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type Step, type StepItem, type TextItem, type TextValue, type Timer, type TimerItem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitSystem, type UnitType };
2084
+ 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 IngredientAlternativeBase, type IngredientExtras, type IngredientFlag, type IngredientItem, type IngredientQuantityAndGroup, type IngredientQuantityGroup, type MaybeNestedAndGroup, type MaybeNestedGroup, type MaybeNestedOrGroup, type MaybeScalableQuantity, type Metadata, type MetadataObject, type MetadataSource, type MetadataTime, type MetadataValue, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, type Note, type NoteItem, type OrGroup, Pantry, type PantryItem, type PantryItemToml, type PantryOptions, 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, type RawQuantityGroup, Recipe, type RecipeAlternatives, type RecipeChoices, type RecipeWithFactor, type RecipeWithServings, Section, ShoppingCart, type ShoppingCartOptions, type ShoppingCartSummary, ShoppingList, type SpecificUnitSystem, type Step, type StepItem, type TextAttribute, type TextItem, type TextValue, type Timer, type TimerItem, type ToBaseBySystem, type Unit, type UnitDefinition, type UnitDefinitionLike, type UnitFractionConfig, type UnitSystem, type UnitType, type WithOptionalQuantity, type Yield, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, getEffectiveChoices, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSectionActive, isSimpleGroup, isStepActive, renderFractionAsVulgar };