@tmlmt/cooklang-parser 3.0.0-alpha.17 → 3.0.0-alpha.19

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.cjs CHANGED
@@ -32,11 +32,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
32
32
  // src/index.ts
33
33
  var index_exports = {};
34
34
  __export(index_exports, {
35
- BadIndentationError: () => BadIndentationError,
36
35
  CategoryConfig: () => CategoryConfig,
37
36
  NoProductCatalogForCartError: () => NoProductCatalogForCartError,
38
37
  NoShoppingListForCartError: () => NoShoppingListForCartError,
39
- NoTabAsIndentError: () => NoTabAsIndentError,
40
38
  Pantry: () => Pantry,
41
39
  ProductCatalog: () => ProductCatalog,
42
40
  Recipe: () => Recipe,
@@ -51,11 +49,14 @@ __export(index_exports, {
51
49
  formatQuantityWithUnit: () => formatQuantityWithUnit,
52
50
  formatSingleValue: () => formatSingleValue,
53
51
  formatUnit: () => formatUnit,
52
+ getEffectiveChoices: () => getEffectiveChoices,
54
53
  hasAlternatives: () => hasAlternatives,
55
54
  isAlternativeSelected: () => isAlternativeSelected,
56
55
  isAndGroup: () => isAndGroup,
57
56
  isGroupedItem: () => isGroupedItem,
57
+ isSectionActive: () => isSectionActive,
58
58
  isSimpleGroup: () => isSimpleGroup,
59
+ isStepActive: () => isStepActive,
59
60
  renderFractionAsVulgar: () => renderFractionAsVulgar
60
61
  });
61
62
  module.exports = __toCommonJS(index_exports);
@@ -335,7 +336,7 @@ var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
335
336
  var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
336
337
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
337
338
  var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
338
- var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict + "/").oneOrMore().endGroup().startGroup().literal("/").startNamedGroup("gIngredientSubgroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().endGroup().optional().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
339
+ var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict + "/").oneOrMore().endGroup().startGroup().literal("/").startNamedGroup("gIngredientSubgroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().endGroup().optional().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("gIngredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().toRegExp();
339
340
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
340
341
  var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
341
342
  var timerRegex = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
@@ -363,6 +364,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
363
364
  var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
364
365
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
365
366
  var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
367
+ var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").zeroOrMore().endGroup().literal("]").whitespace().zeroOrMore().toRegExp();
366
368
  var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
367
369
  var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
368
370
  var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
@@ -1417,9 +1419,12 @@ function flushPendingNote(section, noteItems) {
1417
1419
  }
1418
1420
  return noteItems;
1419
1421
  }
1420
- function flushPendingItems(section, items) {
1422
+ function flushPendingItems(section, items, stepVariants, stepOptional) {
1421
1423
  if (items.length > 0) {
1422
- section.content.push({ type: "step", items: [...items] });
1424
+ const step = { type: "step", items: [...items] };
1425
+ if (stepVariants) step.variants = stepVariants;
1426
+ if (stepOptional) step.optional = true;
1427
+ section.content.push(step);
1423
1428
  items.length = 0;
1424
1429
  return true;
1425
1430
  }
@@ -1963,7 +1968,8 @@ function extractMetadata(content) {
1963
1968
  "yield",
1964
1969
  "serves",
1965
1970
  // List fields
1966
- "tags"
1971
+ "tags",
1972
+ "variants"
1967
1973
  ]);
1968
1974
  for (const metaVar of [
1969
1975
  "title",
@@ -2050,6 +2056,8 @@ function extractMetadata(content) {
2050
2056
  }
2051
2057
  const tags = parseListMetaVar(metadataContent, "tags");
2052
2058
  if (tags) metadata.tags = tags;
2059
+ const variants = parseListMetaVar(metadataContent, "variants");
2060
+ if (variants) metadata.variants = variants;
2053
2061
  const allKeys = extractAllMetadataKeys(metadataContent);
2054
2062
  for (const key of allKeys) {
2055
2063
  if (handledKeys.has(key)) continue;
@@ -2437,8 +2445,10 @@ var Section = class {
2437
2445
  /**
2438
2446
  * Creates an instance of Section.
2439
2447
  * @param name - The name of the section. Defaults to an empty string.
2448
+ * @param variants - Optional variant names for this section.
2449
+ * @param optional - Whether the section is optional.
2440
2450
  */
2441
- constructor(name = "") {
2451
+ constructor(name = "", variants, optional) {
2442
2452
  /**
2443
2453
  * The name of the section. Can be an empty string for the default (first) section.
2444
2454
  * @defaultValue `""`
@@ -2446,7 +2456,13 @@ var Section = class {
2446
2456
  __publicField(this, "name");
2447
2457
  /** An array of steps and notes that make up the content of the section. */
2448
2458
  __publicField(this, "content", []);
2459
+ /** Optional list of variant names this section belongs to. */
2460
+ __publicField(this, "variants");
2461
+ /** Whether the section has been marked as optional ([?]) */
2462
+ __publicField(this, "optional");
2449
2463
  this.name = name;
2464
+ if (variants) this.variants = variants;
2465
+ if (optional) this.optional = true;
2450
2466
  }
2451
2467
  /**
2452
2468
  * Checks if the section is blank (has no name and no content).
@@ -2806,7 +2822,8 @@ var _Recipe = class _Recipe {
2806
2822
  */
2807
2823
  __publicField(this, "choices", {
2808
2824
  ingredientItems: /* @__PURE__ */ new Map(),
2809
- ingredientGroups: /* @__PURE__ */ new Map()
2825
+ ingredientGroups: /* @__PURE__ */ new Map(),
2826
+ variants: []
2810
2827
  });
2811
2828
  /**
2812
2829
  * The parsed recipe ingredients.
@@ -3123,6 +3140,10 @@ var _Recipe = class _Recipe {
3123
3140
  if (itemQuantity) {
3124
3141
  Object.assign(alternative, itemQuantity);
3125
3142
  }
3143
+ const note = groups.gIngredientNote?.trim();
3144
+ if (note) {
3145
+ alternative.note = note;
3146
+ }
3126
3147
  const existingSubgroups = this.choices.ingredientGroups.get(groupKey);
3127
3148
  const existingAlternativesFlat = existingSubgroups?.flat();
3128
3149
  function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
@@ -3216,6 +3237,8 @@ var _Recipe = class _Recipe {
3216
3237
  /** @internal */
3217
3238
  collectQuantityGroups(options) {
3218
3239
  const { section, step, choices } = options || {};
3240
+ const activeVariant = choices?.variant;
3241
+ const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
3219
3242
  const sectionsToProcess = section !== void 0 ? (() => {
3220
3243
  const idx = typeof section === "number" ? section : this.sections.indexOf(section);
3221
3244
  return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
@@ -3223,12 +3246,29 @@ var _Recipe = class _Recipe {
3223
3246
  const ingredientGroups = /* @__PURE__ */ new Map();
3224
3247
  const selectedIndices = /* @__PURE__ */ new Set();
3225
3248
  const referencedIndices = /* @__PURE__ */ new Set();
3249
+ const dynamicOptionalIndices = /* @__PURE__ */ new Set();
3226
3250
  for (const currentSection of sectionsToProcess) {
3251
+ if (currentSection.variants) {
3252
+ if (isDefaultVariant) {
3253
+ if (!currentSection.variants.includes("*")) continue;
3254
+ } else {
3255
+ if (!currentSection.variants.includes(activeVariant)) continue;
3256
+ }
3257
+ }
3227
3258
  const allSteps = currentSection.content.filter(
3228
3259
  (item) => item.type === "step"
3229
3260
  );
3261
+ const isOptionalSection = currentSection.optional === true;
3230
3262
  const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
3231
3263
  for (const currentStep of stepsToProcess) {
3264
+ if (currentStep.variants) {
3265
+ if (isDefaultVariant) {
3266
+ if (!currentStep.variants.includes("*")) continue;
3267
+ } else {
3268
+ if (!currentStep.variants.includes(activeVariant)) continue;
3269
+ }
3270
+ }
3271
+ const isOptionalStep = currentStep.optional === true || isOptionalSection;
3232
3272
  for (const item of currentStep.items.filter(
3233
3273
  (item2) => item2.type === "ingredient"
3234
3274
  )) {
@@ -3240,18 +3280,53 @@ var _Recipe = class _Recipe {
3240
3280
  if (isGrouped) {
3241
3281
  const groupChoice = choices?.ingredientGroups?.get(item.group);
3242
3282
  hasExplicitChoice = groupChoice !== void 0;
3243
- const targetSubgroupIndex = groupChoice ?? 0;
3244
- const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3245
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3283
+ if (!hasExplicitChoice && !isDefaultVariant) {
3284
+ const matchingSubgroupIdx = groupSubgroups?.findIndex(
3285
+ (sg) => sg.some(
3286
+ (alt) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
3287
+ )
3288
+ );
3289
+ if (matchingSubgroupIdx !== void 0 && matchingSubgroupIdx >= 0) {
3290
+ const matchedSubgroup = groupSubgroups[matchingSubgroupIdx];
3291
+ isSelected = matchedSubgroup.some(
3292
+ (alt) => alt.itemId === item.id
3293
+ );
3294
+ hasExplicitChoice = true;
3295
+ selectedAltIndex = 0;
3296
+ } else {
3297
+ const targetSubgroupIndex = 0;
3298
+ const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3299
+ isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3300
+ }
3301
+ } else {
3302
+ const targetSubgroupIndex = groupChoice ?? 0;
3303
+ const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3304
+ isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3305
+ }
3246
3306
  } else {
3247
3307
  const itemChoice = choices?.ingredientItems?.get(item.id);
3248
3308
  hasExplicitChoice = itemChoice !== void 0;
3249
- selectedAltIndex = itemChoice ?? 0;
3309
+ if (!hasExplicitChoice && !isDefaultVariant) {
3310
+ const matchingIndices = item.alternatives.map((alt, idx) => ({ alt, idx })).filter(
3311
+ ({ alt }) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
3312
+ ).map(({ idx }) => idx);
3313
+ if (matchingIndices.length > 0) {
3314
+ selectedAltIndex = matchingIndices[0];
3315
+ hasExplicitChoice = true;
3316
+ } else {
3317
+ selectedAltIndex = itemChoice ?? 0;
3318
+ }
3319
+ } else {
3320
+ selectedAltIndex = itemChoice ?? 0;
3321
+ }
3250
3322
  isSelected = true;
3251
3323
  }
3252
3324
  const alternative = item.alternatives[selectedAltIndex];
3253
3325
  if (!alternative || !isSelected) continue;
3254
3326
  selectedIndices.add(alternative.index);
3327
+ if (isOptionalStep) {
3328
+ dynamicOptionalIndices.add(alternative.index);
3329
+ }
3255
3330
  const allAltsFlat = isGrouped ? groupSubgroups.flat() : item.alternatives;
3256
3331
  for (const alt of allAltsFlat) {
3257
3332
  referencedIndices.add(alt.index);
@@ -3357,7 +3432,12 @@ var _Recipe = class _Recipe {
3357
3432
  }
3358
3433
  }
3359
3434
  }
3360
- return { ingredientGroups, selectedIndices, referencedIndices };
3435
+ return {
3436
+ ingredientGroups,
3437
+ selectedIndices,
3438
+ referencedIndices,
3439
+ dynamicOptionalIndices
3440
+ };
3361
3441
  }
3362
3442
  /**
3363
3443
  * Gets the raw (unprocessed) quantity groups for each ingredient, before
@@ -3376,12 +3456,21 @@ var _Recipe = class _Recipe {
3376
3456
  * ```
3377
3457
  */
3378
3458
  getRawQuantityGroups(options) {
3379
- const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
3459
+ const {
3460
+ ingredientGroups,
3461
+ selectedIndices,
3462
+ referencedIndices,
3463
+ dynamicOptionalIndices
3464
+ } = this.collectQuantityGroups(options);
3380
3465
  const result = [];
3381
3466
  for (let index = 0; index < this.ingredients.length; index++) {
3382
3467
  if (!referencedIndices.has(index)) continue;
3383
3468
  const orig = this.ingredients[index];
3384
3469
  const usedAsPrimary = selectedIndices.has(index);
3470
+ let flags = orig.flags;
3471
+ if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
3472
+ flags = [...flags ?? [], "optional"];
3473
+ }
3385
3474
  const quantities = [];
3386
3475
  if (usedAsPrimary) {
3387
3476
  const groupsForIng = ingredientGroups.get(index);
@@ -3394,7 +3483,7 @@ var _Recipe = class _Recipe {
3394
3483
  result.push({
3395
3484
  name: orig.name,
3396
3485
  ...usedAsPrimary && { usedAsPrimary: true },
3397
- ...orig.flags && { flags: orig.flags },
3486
+ ...flags && { flags },
3398
3487
  quantities
3399
3488
  });
3400
3489
  }
@@ -3428,15 +3517,24 @@ var _Recipe = class _Recipe {
3428
3517
  * ```
3429
3518
  */
3430
3519
  getIngredientQuantities(options) {
3431
- const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
3520
+ const {
3521
+ ingredientGroups,
3522
+ selectedIndices,
3523
+ referencedIndices,
3524
+ dynamicOptionalIndices
3525
+ } = this.collectQuantityGroups(options);
3432
3526
  const result = [];
3433
3527
  for (let index = 0; index < this.ingredients.length; index++) {
3434
3528
  if (!referencedIndices.has(index)) continue;
3435
3529
  const orig = this.ingredients[index];
3530
+ let flags = orig.flags;
3531
+ if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
3532
+ flags = [...flags ?? [], "optional"];
3533
+ }
3436
3534
  const ing = {
3437
3535
  name: orig.name,
3438
3536
  ...orig.preparation && { preparation: orig.preparation },
3439
- ...orig.flags && { flags: orig.flags },
3537
+ ...flags && { flags },
3440
3538
  ...orig.extras && { extras: orig.extras }
3441
3539
  };
3442
3540
  if (selectedIndices.has(index)) {
@@ -3487,6 +3585,59 @@ var _Recipe = class _Recipe {
3487
3585
  }
3488
3586
  return result;
3489
3587
  }
3588
+ /**
3589
+ * Returns the list of cookware items that are used in the active variant.
3590
+ * Cookware in steps/sections not matching the active variant are excluded.
3591
+ * Hidden cookware is always excluded.
3592
+ *
3593
+ * @param options - Options for filtering:
3594
+ * - `choices`: The choices to apply (only `variant` is used)
3595
+ * @returns Array of Cookware objects referenced by active steps
3596
+ *
3597
+ * @example
3598
+ * ```typescript
3599
+ * // Get all cookware for the default variant
3600
+ * const cookware = recipe.getCookwareForVariant();
3601
+ *
3602
+ * // Get cookware for a specific variant
3603
+ * const veganCookware = recipe.getCookwareForVariant({ choices: { variant: 'vegan' } });
3604
+ * ```
3605
+ */
3606
+ getCookwareForVariant(options) {
3607
+ const { choices } = options || {};
3608
+ const activeVariant = choices?.variant;
3609
+ const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
3610
+ const cookwareIndices = /* @__PURE__ */ new Set();
3611
+ for (const currentSection of this.sections) {
3612
+ if (currentSection.variants) {
3613
+ if (isDefaultVariant) {
3614
+ if (!currentSection.variants.includes("*")) continue;
3615
+ } else {
3616
+ if (!currentSection.variants.includes(activeVariant)) continue;
3617
+ }
3618
+ }
3619
+ const allSteps = currentSection.content.filter(
3620
+ (item) => item.type === "step"
3621
+ );
3622
+ for (const currentStep of allSteps) {
3623
+ if (currentStep.variants) {
3624
+ if (isDefaultVariant) {
3625
+ if (!currentStep.variants.includes("*")) continue;
3626
+ } else {
3627
+ if (!currentStep.variants.includes(activeVariant)) continue;
3628
+ }
3629
+ }
3630
+ for (const item of currentStep.items) {
3631
+ if (item.type === "cookware") {
3632
+ cookwareIndices.add(item.index);
3633
+ }
3634
+ }
3635
+ }
3636
+ }
3637
+ return this.cookware.filter(
3638
+ (cw, idx) => cookwareIndices.has(idx) && !cw.flags?.includes("hidden")
3639
+ );
3640
+ }
3490
3641
  /**
3491
3642
  * Parses a recipe from a string.
3492
3643
  * @param content - The recipe content to parse.
@@ -3502,9 +3653,14 @@ var _Recipe = class _Recipe {
3502
3653
  const items = [];
3503
3654
  let noteText = "";
3504
3655
  let inNote = false;
3656
+ let stepVariants;
3657
+ let stepOptional;
3658
+ const discoveredVariants = /* @__PURE__ */ new Set();
3505
3659
  for (const line of cleanContent) {
3506
3660
  if (line.trim().length === 0) {
3507
- flushPendingItems(section, items);
3661
+ flushPendingItems(section, items, stepVariants, stepOptional);
3662
+ stepVariants = void 0;
3663
+ stepOptional = void 0;
3508
3664
  flushPendingNote(
3509
3665
  section,
3510
3666
  noteText ? this._parseNoteText(noteText) : []
@@ -3515,26 +3671,48 @@ var _Recipe = class _Recipe {
3515
3671
  continue;
3516
3672
  }
3517
3673
  if (line.startsWith("=")) {
3518
- flushPendingItems(section, items);
3674
+ flushPendingItems(section, items, stepVariants, stepOptional);
3675
+ stepVariants = void 0;
3676
+ stepOptional = void 0;
3519
3677
  flushPendingNote(
3520
3678
  section,
3521
3679
  noteText ? this._parseNoteText(noteText) : []
3522
3680
  );
3523
3681
  noteText = "";
3682
+ let sectionName = line.replace(/^=+|=+$/g, "").trim();
3683
+ let sectionVariants;
3684
+ let sectionOptional;
3685
+ const sectionVarMatch = sectionName.match(variantTagRegex);
3686
+ if (sectionVarMatch?.groups) {
3687
+ const isOptionalPrefix = sectionVarMatch.groups.variantOptionalPrefix === "?";
3688
+ const names = (sectionVarMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
3689
+ if (names.length > 0) {
3690
+ sectionVariants = names;
3691
+ for (const v of names) discoveredVariants.add(v);
3692
+ }
3693
+ if (isOptionalPrefix) {
3694
+ sectionOptional = true;
3695
+ }
3696
+ sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3697
+ }
3524
3698
  if (this.sections.length === 0 && section.isBlank()) {
3525
- section.name = line.replace(/^=+|=+$/g, "").trim();
3699
+ section.name = sectionName;
3700
+ if (sectionVariants) section.variants = sectionVariants;
3701
+ if (sectionOptional) section.optional = true;
3526
3702
  } else {
3527
3703
  if (!section.isBlank()) {
3528
3704
  this.sections.push(section);
3529
3705
  }
3530
- section = new Section(line.replace(/^=+|=+$/g, "").trim());
3706
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3531
3707
  }
3532
3708
  blankLineBefore = true;
3533
3709
  inNote = false;
3534
3710
  continue;
3535
3711
  }
3536
3712
  if (blankLineBefore && line.startsWith(">")) {
3537
- flushPendingItems(section, items);
3713
+ flushPendingItems(section, items, stepVariants, stepOptional);
3714
+ stepVariants = void 0;
3715
+ stepOptional = void 0;
3538
3716
  noteText = line.substring(1).trim();
3539
3717
  inNote = true;
3540
3718
  blankLineBefore = false;
@@ -3549,11 +3727,31 @@ var _Recipe = class _Recipe {
3549
3727
  blankLineBefore = false;
3550
3728
  continue;
3551
3729
  }
3730
+ let currentLine = line;
3731
+ if (items.length === 0) {
3732
+ const varMatch = currentLine.match(variantTagRegex);
3733
+ if (varMatch?.groups) {
3734
+ const isOptionalPrefix = varMatch.groups.variantOptionalPrefix === "?";
3735
+ const names = (varMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
3736
+ if (names.length > 0) {
3737
+ stepVariants = names;
3738
+ for (const v of names) discoveredVariants.add(v);
3739
+ }
3740
+ if (isOptionalPrefix) {
3741
+ stepOptional = true;
3742
+ }
3743
+ currentLine = currentLine.slice(varMatch[0].length);
3744
+ if (currentLine.trim().length === 0) {
3745
+ blankLineBefore = false;
3746
+ continue;
3747
+ }
3748
+ }
3749
+ }
3552
3750
  let cursor = 0;
3553
- for (const match of line.matchAll(tokensRegex)) {
3751
+ for (const match of currentLine.matchAll(tokensRegex)) {
3554
3752
  const idx = match.index;
3555
3753
  if (idx > cursor) {
3556
- items.push(...parseMarkdownSegments(line.slice(cursor, idx)));
3754
+ items.push(...parseMarkdownSegments(currentLine.slice(cursor, idx)));
3557
3755
  }
3558
3756
  const groups = match.groups;
3559
3757
  if (groups.mIngredientName || groups.sIngredientName) {
@@ -3614,16 +3812,21 @@ var _Recipe = class _Recipe {
3614
3812
  }
3615
3813
  cursor = idx + match[0].length;
3616
3814
  }
3617
- if (cursor < line.length) {
3618
- items.push(...parseMarkdownSegments(line.slice(cursor)));
3815
+ if (cursor < currentLine.length) {
3816
+ items.push(...parseMarkdownSegments(currentLine.slice(cursor)));
3619
3817
  }
3620
3818
  blankLineBefore = false;
3621
3819
  }
3622
- flushPendingItems(section, items);
3820
+ flushPendingItems(section, items, stepVariants, stepOptional);
3623
3821
  flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
3624
3822
  if (!section.isBlank()) {
3625
3823
  this.sections.push(section);
3626
3824
  }
3825
+ const metaVariants = this.metadata.variants ?? [];
3826
+ const allVariants = /* @__PURE__ */ new Set([...metaVariants, ...discoveredVariants]);
3827
+ if (allVariants.size > 0) {
3828
+ this.choices.variants = [...allVariants];
3829
+ }
3627
3830
  this._populateIngredientQuantities();
3628
3831
  }
3629
3832
  /**
@@ -3934,7 +4137,11 @@ var _Recipe = class _Recipe {
3934
4137
  newRecipe.metadata = deepClone(this.metadata);
3935
4138
  newRecipe.ingredients = deepClone(this.ingredients);
3936
4139
  newRecipe.sections = this.sections.map((section) => {
3937
- const newSection = new Section(section.name);
4140
+ const newSection = new Section(
4141
+ section.name,
4142
+ section.variants,
4143
+ section.optional
4144
+ );
3938
4145
  newSection.content = deepClone(section.content);
3939
4146
  return newSection;
3940
4147
  });
@@ -4807,13 +5014,53 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
4807
5014
  const selectedIndex = choices?.ingredientItems?.get(item.id);
4808
5015
  return alternativeIndex === selectedIndex;
4809
5016
  }
5017
+ function isSectionActive(section, variant) {
5018
+ if (!section.variants) return true;
5019
+ const isDefault = variant === void 0 || variant === "*";
5020
+ if (isDefault) {
5021
+ return section.variants.includes("*");
5022
+ }
5023
+ return section.variants.includes(variant);
5024
+ }
5025
+ function isStepActive(step, variant) {
5026
+ if (!step.variants) return true;
5027
+ const isDefault = variant === void 0 || variant === "*";
5028
+ if (isDefault) {
5029
+ return step.variants.includes("*");
5030
+ }
5031
+ return step.variants.includes(variant);
5032
+ }
5033
+ function getEffectiveChoices(recipe, variant) {
5034
+ const choices = { variant };
5035
+ if (variant === void 0 || variant === "*") return choices;
5036
+ const variantLower = variant.toLowerCase();
5037
+ for (const [itemId, alternatives] of recipe.choices.ingredientItems) {
5038
+ const matchIdx = alternatives.findIndex(
5039
+ (alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
5040
+ );
5041
+ if (matchIdx >= 0) {
5042
+ if (!choices.ingredientItems) choices.ingredientItems = /* @__PURE__ */ new Map();
5043
+ choices.ingredientItems.set(itemId, matchIdx);
5044
+ }
5045
+ }
5046
+ for (const [groupId, subgroups] of recipe.choices.ingredientGroups) {
5047
+ const matchIdx = subgroups.findIndex(
5048
+ (sg) => sg.some(
5049
+ (alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
5050
+ )
5051
+ );
5052
+ if (matchIdx >= 0) {
5053
+ if (!choices.ingredientGroups) choices.ingredientGroups = /* @__PURE__ */ new Map();
5054
+ choices.ingredientGroups.set(groupId, matchIdx);
5055
+ }
5056
+ }
5057
+ return choices;
5058
+ }
4810
5059
  // Annotate the CommonJS export names for ESM import in node:
4811
5060
  0 && (module.exports = {
4812
- BadIndentationError,
4813
5061
  CategoryConfig,
4814
5062
  NoProductCatalogForCartError,
4815
5063
  NoShoppingListForCartError,
4816
- NoTabAsIndentError,
4817
5064
  Pantry,
4818
5065
  ProductCatalog,
4819
5066
  Recipe,
@@ -4828,11 +5075,14 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
4828
5075
  formatQuantityWithUnit,
4829
5076
  formatSingleValue,
4830
5077
  formatUnit,
5078
+ getEffectiveChoices,
4831
5079
  hasAlternatives,
4832
5080
  isAlternativeSelected,
4833
5081
  isAndGroup,
4834
5082
  isGroupedItem,
5083
+ isSectionActive,
4835
5084
  isSimpleGroup,
5085
+ isStepActive,
4836
5086
  renderFractionAsVulgar
4837
5087
  });
4838
5088
  /* v8 ignore else -- @preserve */