@tmlmt/cooklang-parser 3.0.0-alpha.9 → 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
@@ -97,11 +103,28 @@ declare class Recipe {
97
103
  * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
98
104
  */
99
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;
100
118
  /**
101
119
  * External storage for item count (not a property on instances).
102
120
  * Used for giving ID numbers to items during parsing.
103
121
  */
104
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;
105
128
  /**
106
129
  * Gets the current item count for this recipe.
107
130
  */
@@ -142,7 +165,26 @@ declare class Recipe {
142
165
  * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
143
166
  * @internal
144
167
  */
145
- private _populate_ingredient_quantities;
168
+ private _populateIngredientQuantities;
169
+ /** @internal */
170
+ private collectQuantityGroups;
171
+ /**
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.
176
+ *
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
+ * ```
186
+ */
187
+ getRawQuantityGroups(options?: GetIngredientQuantitiesOptions): RawQuantityGroup[];
146
188
  /**
147
189
  * Gets ingredients with their quantities populated, optionally filtered by section/step
148
190
  * and respecting user choices for alternatives.
@@ -171,6 +213,25 @@ declare class Recipe {
171
213
  * ```
172
214
  */
173
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[];
174
235
  /**
175
236
  * Parses a recipe from a string.
176
237
  * @param content - The recipe content to parse.
@@ -192,6 +253,26 @@ declare class Recipe {
192
253
  * @returns A new Recipe instance with the scaled ingredients.
193
254
  */
194
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;
195
276
  /**
196
277
  * Gets the number of servings for the recipe.
197
278
  * @private
@@ -205,6 +286,55 @@ declare class Recipe {
205
286
  clone(): Recipe;
206
287
  }
207
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;
208
338
  /**
209
339
  * Represents the metadata of a recipe.
210
340
  * @category Types
@@ -214,56 +344,60 @@ interface Metadata {
214
344
  title?: string;
215
345
  /** The tags of the recipe. */
216
346
  tags?: string[];
217
- /** The source of the recipe. */
218
- source?: string;
219
- /** The source name of the recipe. */
220
- "source.name"?: string;
221
- /** The source url of the recipe. */
222
- "source.url"?: string;
223
- /** The source author of the recipe. */
224
- "source.author"?: string;
225
- /** 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). */
226
353
  author?: string;
227
354
  /** The number of servings the recipe makes.
228
- * Should be either a number or a string which starts with a number
229
- * (which will be used for scaling) followed by a comma and then
230
- * 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.
231
357
  *
232
- * Interchangeable with `yield` or `serves`. If multiple ones are defined,
233
- * the prevailance order for the number which will used for scaling
234
- * 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`.
235
361
  *
236
362
  * @example
237
363
  * ```yaml
238
364
  * servings: 4
239
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`.
240
377
  *
241
378
  * @example
242
379
  * ```yaml
243
- * servings: 2, a few
380
+ * yield: 300%g
244
381
  * ```
245
- */
246
- servings?: number | string;
247
- /** The yield of the recipe.
248
- * Should be either a number or a string which starts with a number
249
- * (which will be used for scaling) followed by a comma and then
250
- * whatever you want.
251
382
  *
252
- * Interchangeable with `servings` or `serves`. If multiple ones are defined,
253
- * the prevailance order for the number which will used for scaling
254
- * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
255
- * for examples.
383
+ * @example
384
+ * ```yaml
385
+ * yield: about {{300%g}} of bread
386
+ * ```
256
387
  */
257
- yield?: number | string;
388
+ yield?: Yield;
258
389
  /** The number of people the recipe serves.
259
- * Should be either a number or a string which starts with a number
260
- * (which will be used for scaling) followed by a comma and then
261
- * 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`.
262
396
  *
263
- * Interchangeable with `servings` or `yield`. If multiple ones are defined,
264
- * the prevailance order for the number which will used for scaling
265
- * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
266
- * for examples.
397
+ * @example
398
+ * ```yaml
399
+ * serves: 4
400
+ * ```
267
401
  */
268
402
  serves?: number | string;
269
403
  /** The course of the recipe. */
@@ -273,30 +407,11 @@ interface Metadata {
273
407
  /** The locale of the recipe. */
274
408
  locale?: string;
275
409
  /**
276
- * The preparation time of the recipe.
277
- * Will not be further parsed into any DateTime format nor normalize
278
- */
279
- "prep time"?: string;
280
- /**
281
- * Alias of `prep time`
282
- */
283
- "time.prep"?: string;
284
- /**
285
- * The cooking time of the recipe.
286
- * Will not be further parsed into any DateTime format nor normalize
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.
287
413
  */
288
- "cook time"?: string;
289
- /**
290
- * Alias of `cook time`
291
- */
292
- "time.cook"?: string;
293
- /**
294
- * The total time of the recipe.
295
- * Will not be further parsed into any DateTime format nor normalize
296
- */
297
- "time required"?: string;
298
- time?: string;
299
- duration?: string;
414
+ time?: MetadataTime;
300
415
  /** The difficulty of the recipe. */
301
416
  difficulty?: string;
302
417
  /** The cuisine of the recipe. */
@@ -305,16 +420,25 @@ interface Metadata {
305
420
  diet?: string;
306
421
  /** The description of the recipe. */
307
422
  description?: string;
308
- /** The images of the recipe. Alias of `pictures` */
423
+ /** The images of the recipe. */
309
424
  images?: string[];
310
- /** The images of the recipe. Alias of `images` */
311
- pictures?: string[];
312
- /** The picture of the recipe. Alias of `picture` */
425
+ /** The primary image of the recipe. */
313
426
  image?: string;
314
- /** The picture of the recipe. Alias of `image` */
315
- picture?: string;
316
427
  /** The introduction of the recipe. */
317
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;
318
442
  }
319
443
  /**
320
444
  * Represents a quantity described by text, e.g. "a pinch"
@@ -385,7 +509,7 @@ interface IngredientExtras {
385
509
  * Used if: the ingredient is a recipe
386
510
  *
387
511
  * @example
388
- * ```cooklang
512
+ * ```yaml
389
513
  * Take @./essentials/doughs/pizza dough{1} out of the freezer and let it unfreeze overnight
390
514
  * ```
391
515
  * Would lead to:
@@ -417,29 +541,31 @@ interface AlternativeIngredientRef {
417
541
  interface IngredientQuantityGroup extends QuantityWithPlainUnit {
418
542
  /**
419
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.
420
547
  * If undefined, this group has no alternatives.
421
548
  */
422
- alternatives?: AlternativeIngredientRef[];
549
+ alternatives?: AlternativeIngredientRef[][];
423
550
  }
424
551
  /**
425
552
  * Represents an AND group of quantities when primary units are incompatible but equivalents can be summed.
426
- * For example: 1 large carrot + 2 small carrots, both with cup equivalents 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.
427
554
  * @category Types
428
555
  */
429
- interface IngredientQuantityAndGroup {
430
- /**
431
- * The incompatible primary quantities (e.g., "1 large" and "2 small").
432
- */
433
- and: QuantityWithPlainUnit[];
556
+ interface IngredientQuantityAndGroup extends FlatAndGroup<QuantityWithPlainUnit> {
434
557
  /**
435
558
  * The summed equivalent quantities (e.g., "5 cups" from summing "1.5 cup + 2 cup + 1.5 cup").
436
559
  */
437
560
  equivalents?: QuantityWithPlainUnit[];
438
561
  /**
439
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.
440
566
  * If undefined, this group has no alternatives.
441
567
  */
442
- alternatives?: AlternativeIngredientRef[];
568
+ alternatives?: AlternativeIngredientRef[][];
443
569
  }
444
570
  /**
445
571
  * Represents an ingredient in a recipe.
@@ -475,49 +601,40 @@ interface Ingredient {
475
601
  extras?: IngredientExtras;
476
602
  }
477
603
  /**
478
- * Represents a computed ingredient with its total quantity after applying choices.
479
- * 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
+ *
480
607
  * @category Types
481
608
  */
482
- interface ComputedIngredient {
483
- /** The name of the ingredient. */
484
- name: string;
485
- /** The total quantity of the ingredient after applying choices. */
486
- quantityTotal?: QuantityWithPlainUnit | MaybeNestedGroup<QuantityWithPlainUnit>;
487
- /** The preparation of the ingredient. */
488
- preparation?: string;
489
- /** The list of ingredients mentioned in the preparation as alternatives to this ingredient */
490
- alternatives?: Set<number>;
491
- /** A list of potential state modifiers or other flags for the ingredient */
492
- flags?: IngredientFlag[];
493
- /** The collection of potential additional metadata for the ingredient */
494
- extras?: IngredientExtras;
495
- }
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
+ };
496
616
  /**
497
- * Represents a contributor to an ingredient's total quantity, corresponding
498
- * to a single mention in the recipe text. It can contain multiple
499
- * 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
+ *
500
623
  * @category Types
501
624
  */
502
- interface IngredientItemQuantity extends QuantityWithExtendedUnit {
503
- /**
504
- * A list of equivalent quantities/units for this ingredient mention besides the primary quantity.
505
- * For `@salt{1%tsp|5%g}`, the main quantity is 1 tsp and the equivalents will contain 5 g.
506
- */
507
- equivalents?: QuantityWithExtendedUnit[];
508
- /** Indicates whether this quantity should be scaled when the recipe serving size changes. */
509
- scalable: boolean;
510
- }
625
+ type WithOptionalQuantity<T> = (T & MaybeScalableQuantity) | (T & {
626
+ quantity?: undefined;
627
+ scalable?: never;
628
+ unit?: never;
629
+ equivalents?: never;
630
+ });
511
631
  /**
512
- * Represents a single ingredient choice within a single or a group of `IngredientItem`s. It points
513
- * to a specific ingredient and its corresponding quantity information.
632
+ * Base type of {@link IngredientAlternative}
514
633
  * @category Types
515
634
  */
516
- interface IngredientAlternative {
635
+ type IngredientAlternativeBase = {
517
636
  /** The index of the ingredient within the {@link Recipe.ingredients} array. */
518
637
  index: number;
519
- /** The quantity of this specific mention of the ingredient */
520
- itemQuantity?: IngredientItemQuantity;
521
638
  /** The alias/name of the ingredient as it should be displayed for this occurrence. */
522
639
  displayName: string;
523
640
  /** An optional note for this specific choice (e.g., "for a vegan version"). */
@@ -526,7 +643,16 @@ interface IngredientAlternative {
526
643
  * with group keys: the id of the corresponding ingredient item (e.g. "ingredient-item-2").
527
644
  * Can be useful for creating alternative selection UI elements with anchor links */
528
645
  itemId?: string;
529
- }
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>;
530
656
  /**
531
657
  * Represents an ingredient item in a recipe step.
532
658
  * @category Types
@@ -547,6 +673,12 @@ interface IngredientItem {
547
673
  * `@|group|...` syntax), they represent a single logical choice.
548
674
  */
549
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;
550
682
  }
551
683
  /**
552
684
  * Represents the choices one can make in a recipe
@@ -560,9 +692,18 @@ interface RecipeAlternatives {
560
692
  ingredientItems: Map<string, IngredientAlternative[]>;
561
693
  /** Map of choices that can be made for Grouped Ingredient StepItem's
562
694
  * - Keys are the Group IDs (e.g. "eggs" for `@|eggs|...`)
563
- * - 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.
564
705
  */
565
- ingredientGroups: Map<string, IngredientAlternative[]>;
706
+ variants: string[];
566
707
  }
567
708
  /**
568
709
  * Represents the choices to apply when computing ingredient quantities.
@@ -574,6 +715,8 @@ interface RecipeChoices {
574
715
  ingredientItems?: Map<string, number>;
575
716
  /** Map of choices that can be made for Grouped Ingredient StepItem's */
576
717
  ingredientGroups?: Map<string, number>;
718
+ /** The selected variant name */
719
+ variant?: string;
577
720
  }
578
721
  /**
579
722
  * Options for the {@link Recipe.getIngredientQuantities | getIngredientQuantities()} method.
@@ -596,6 +739,23 @@ interface GetIngredientQuantitiesOptions {
596
739
  */
597
740
  choices?: RecipeChoices;
598
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>)[];
758
+ }
599
759
  /**
600
760
  * Represents a cookware item in a recipe step.
601
761
  * @category Types
@@ -630,6 +790,11 @@ interface Timer {
630
790
  /** The unit of the timer. */
631
791
  unit: string;
632
792
  }
793
+ /**
794
+ * Represents a formatting attribute for a {@link TextItem}.
795
+ * @category Types
796
+ */
797
+ type TextAttribute = "bold" | "italic" | "bold+italic" | "link" | "code";
633
798
  /**
634
799
  * Represents a text item in a recipe step.
635
800
  * @category Types
@@ -639,6 +804,10 @@ interface TextItem {
639
804
  type: "text";
640
805
  /** The content of the text item. */
641
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;
642
811
  }
643
812
  /**
644
813
  * Represents an arbitrary scalable quantity in a recipe.
@@ -675,6 +844,10 @@ interface Step {
675
844
  type: "step";
676
845
  /** The items in the step. */
677
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;
678
851
  }
679
852
  /**
680
853
  * Represents an item in a note (can be text or arbitrary scalable).
@@ -745,6 +918,9 @@ interface RecipeWithServings {
745
918
  type AddedRecipe = RecipeWithFactor | RecipeWithServings;
746
919
  /**
747
920
  * Options for adding a recipe to a shopping list
921
+ *
922
+ * Used in {@link ShoppingList.addRecipe}
923
+ *
748
924
  * @category Types
749
925
  */
750
926
  type AddedRecipeOptions = {
@@ -761,7 +937,7 @@ type AddedRecipeOptions = {
761
937
  * Represents an ingredient that has been added to a shopping list
762
938
  * @category Types
763
939
  */
764
- type AddedIngredient = Pick<ComputedIngredient, "name" | "quantityTotal">;
940
+ type AddedIngredient = Pick<Ingredient, "name" | "quantities">;
765
941
  /**
766
942
  * Represents an ingredient in a category.
767
943
  * @category Types
@@ -821,6 +997,48 @@ type ProductOption = ProductOptionBase & {
821
997
  /** The size(s) of the product. Multiple sizes allow equivalent expressions (e.g., "1%dozen" and "12") */
822
998
  sizes: ProductSize[];
823
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
+ }
824
1042
  /**
825
1043
  * Represents a product selection in a {@link ShoppingCart}
826
1044
  * @category Types
@@ -873,12 +1091,23 @@ type CartMisMatch = ProductMisMatch[];
873
1091
  * Represents the type category of a unit used for quantities
874
1092
  * @category Types
875
1093
  */
876
- 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";
877
1100
  /**
878
1101
  * Represents the measurement system a unit belongs to
879
1102
  * @category Types
880
1103
  */
881
- 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>>;
882
1111
  /**
883
1112
  * Represents a unit used to describe quantities
884
1113
  * @category Types
@@ -898,8 +1127,28 @@ interface UnitDefinition extends Unit {
898
1127
  system: UnitSystem;
899
1128
  /** e.g. ['gram', 'grams'] */
900
1129
  aliases: string[];
901
- /** 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) */
902
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;
903
1152
  }
904
1153
  /**
905
1154
  * Represents a resolved unit definition or a lightweight placeholder for non-standard units
@@ -946,6 +1195,13 @@ interface QuantityWithUnitDef extends QuantityBase {
946
1195
  * @category Types
947
1196
  */
948
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
+ }
949
1205
  /**
950
1206
  * Represents an "or" group of alternative quantities that may contain nested groups (alternatives with nested structure)
951
1207
  * @category Types
@@ -953,6 +1209,13 @@ type QuantityWithUnitLike = QuantityWithPlainUnit | QuantityWithExtendedUnit | Q
953
1209
  interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
954
1210
  or: (T | MaybeNestedGroup<T>)[];
955
1211
  }
1212
+ /**
1213
+ * Represents a flat "and" group of quantities (combined quantities)
1214
+ * @category Types
1215
+ */
1216
+ interface FlatAndGroup<T = QuantityWithUnitLike> {
1217
+ and: T[];
1218
+ }
956
1219
  /**
957
1220
  * Represents an "and" group of quantities that may contain nested groups (combinations with nested structure)
958
1221
  * @category Types
@@ -960,11 +1223,31 @@ interface MaybeNestedOrGroup<T = QuantityWithUnitLike> {
960
1223
  interface MaybeNestedAndGroup<T = QuantityWithUnitLike> {
961
1224
  and: (T | MaybeNestedGroup<T>)[];
962
1225
  }
1226
+ /**
1227
+ * Represents any flat group type ("and" or "or")
1228
+ * @category Types
1229
+ */
1230
+ type FlatGroup<T = QuantityWithUnitLike> = FlatAndGroup<T> | FlatOrGroup<T>;
963
1231
  /**
964
1232
  * Represents any group type that may include nested groups
965
1233
  * @category Types
966
1234
  */
967
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>;
968
1251
 
969
1252
  /**
970
1253
  * Parser for category configurations specified à-la-cooklang.
@@ -1015,6 +1298,127 @@ declare class CategoryConfig {
1015
1298
  parse(config: string): void;
1016
1299
  }
1017
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
+
1018
1422
  /**
1019
1423
  * Product Catalog Manager: used in conjunction with {@link ShoppingCart}
1020
1424
  *
@@ -1041,7 +1445,7 @@ declare class CategoryConfig {
1041
1445
  * 14141 = { name = "Big pack", size = "6%kg", price = 10 }
1042
1446
  * `
1043
1447
  * const catalog = new ProductCatalog(catalog);
1044
- * const eggs = catalog.find("oeuf");
1448
+ * const eggs = catalog.products.find(p => p.ingredientName === "eggs");
1045
1449
  * ```
1046
1450
  */
1047
1451
  declare class ProductCatalog {
@@ -1077,7 +1481,7 @@ declare class ProductCatalog {
1077
1481
  * ## Usage
1078
1482
  *
1079
1483
  * - Create a new ShoppingList instance with an optional category configuration (see {@link ShoppingList."constructor" | constructor})
1080
- * - 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()})
1081
1485
  * - Categorize the ingredients (see {@link ShoppingList.categorize | categorize()})
1082
1486
  *
1083
1487
  * @example
@@ -1089,10 +1493,10 @@ declare class ProductCatalog {
1089
1493
  * const categoryConfig = fs.readFileSync("./myconfig.txt", "utf-8")
1090
1494
  * const recipe1 = new Recipe(fs.readFileSync("./myrecipe.cook", "utf-8"));
1091
1495
  * const shoppingList = new ShoppingList();
1092
- * shoppingList.set_category_config(categoryConfig);
1496
+ * shoppingList.setCategoryConfig(categoryConfig);
1093
1497
  * // Quantities are automatically calculated and ingredients categorized
1094
1498
  * // when adding a recipe
1095
- * shoppingList.add_recipe(recipe1);
1499
+ * shoppingList.addRecipe(recipe1);
1096
1500
  * ```
1097
1501
  *
1098
1502
  * @category Classes
@@ -1109,17 +1513,42 @@ declare class ShoppingList {
1109
1513
  /**
1110
1514
  * The category configuration for the shopping list.
1111
1515
  */
1112
- category_config?: CategoryConfig;
1516
+ categoryConfig?: CategoryConfig;
1113
1517
  /**
1114
1518
  * The categorized ingredients in the shopping list.
1115
1519
  */
1116
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?;
1117
1541
  /**
1118
1542
  * Creates a new ShoppingList instance
1119
- * @param category_config_str - The category configuration to parse.
1543
+ * @param categoryConfigStr - The category configuration to parse.
1120
1544
  */
1121
- constructor(category_config_str?: string | CategoryConfig);
1122
- 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;
1123
1552
  /**
1124
1553
  * Adds a recipe to the shopping list, then automatically
1125
1554
  * recalculates the quantities and recategorize the ingredients.
@@ -1127,7 +1556,7 @@ declare class ShoppingList {
1127
1556
  * @param options - Options for adding the recipe.
1128
1557
  * @throws Error if the recipe has alternatives without corresponding choices.
1129
1558
  */
1130
- add_recipe(recipe: Recipe, options?: AddedRecipeOptions): void;
1559
+ addRecipe(recipe: Recipe, options?: AddedRecipeOptions): void;
1131
1560
  /**
1132
1561
  * Checks if a recipe has unresolved alternatives (alternatives without provided choices).
1133
1562
  * @param recipe - The recipe to check.
@@ -1137,16 +1566,31 @@ declare class ShoppingList {
1137
1566
  private getUnresolvedAlternativesError;
1138
1567
  /**
1139
1568
  * Removes a recipe from the shopping list, then automatically
1140
- * recalculates the quantities and recategorize the ingredients.s
1569
+ * recalculates the quantities and recategorize the ingredients.
1141
1570
  * @param index - The index of the recipe to remove.
1142
1571
  */
1143
- 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;
1144
1587
  /**
1145
1588
  * Sets the category configuration for the shopping list
1146
1589
  * and automatically categorize current ingredients from the list.
1590
+ * Also propagates the configuration to the pantry if one is set.
1147
1591
  * @param config - The category configuration to parse.
1148
1592
  */
1149
- set_category_config(config: string | CategoryConfig): void;
1593
+ setCategoryConfig(config: string | CategoryConfig): void;
1150
1594
  /**
1151
1595
  * Categorizes the ingredients in the shopping list
1152
1596
  * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
@@ -1189,7 +1633,7 @@ interface ShoppingCartSummary {
1189
1633
  * ```ts
1190
1634
  * const shoppingList = new ShoppingList();
1191
1635
  * const recipe = new Recipe("@flour{600%g}");
1192
- * shoppingList.add_recipe(recipe);
1636
+ * shoppingList.addRecipe(recipe);
1193
1637
  *
1194
1638
  * const catalog = new ProductCatalog();
1195
1639
  * catalog.products = [
@@ -1299,6 +1743,149 @@ declare class ShoppingCart {
1299
1743
  summarize(): ShoppingCartSummary;
1300
1744
  }
1301
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;
1302
1889
  /**
1303
1890
  * Determines if a specific alternative in an IngredientItem is selected
1304
1891
  * based on the applied choices.
@@ -1318,7 +1905,7 @@ declare class ShoppingCart {
1318
1905
  * for (const item of step.items) {
1319
1906
  * if (item.type === 'ingredient') {
1320
1907
  * item.alternatives.forEach((alt, idx) => {
1321
- * const isSelected = isAlternativeSelected(item, idx, recipe, choices);
1908
+ * const isSelected = isAlternativeSelected(recipe, choices, item, idx);
1322
1909
  * // Render differently based on isSelected
1323
1910
  * });
1324
1911
  * }
@@ -1326,6 +1913,158 @@ declare class ShoppingCart {
1326
1913
  * ```
1327
1914
  */
1328
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;
1329
2068
 
1330
2069
  /**
1331
2070
  * Error thrown when trying to build a shopping cart without a product catalog
@@ -1342,4 +2081,4 @@ declare class NoShoppingListForCartError extends Error {
1342
2081
  constructor();
1343
2082
  }
1344
2083
 
1345
- 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 GetIngredientQuantitiesOptions, 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, isAlternativeSelected };
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 };