@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.js CHANGED
@@ -277,7 +277,7 @@ var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
277
277
  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();
278
278
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
279
279
  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();
280
- 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();
280
+ 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();
281
281
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
282
282
  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();
283
283
  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();
@@ -305,6 +305,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
305
305
  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();
306
306
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
307
307
  var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
308
+ var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").zeroOrMore().endGroup().literal("]").whitespace().zeroOrMore().toRegExp();
308
309
  var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
309
310
  var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
310
311
  var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
@@ -1359,9 +1360,12 @@ function flushPendingNote(section, noteItems) {
1359
1360
  }
1360
1361
  return noteItems;
1361
1362
  }
1362
- function flushPendingItems(section, items) {
1363
+ function flushPendingItems(section, items, stepVariants, stepOptional) {
1363
1364
  if (items.length > 0) {
1364
- section.content.push({ type: "step", items: [...items] });
1365
+ const step = { type: "step", items: [...items] };
1366
+ if (stepVariants) step.variants = stepVariants;
1367
+ if (stepOptional) step.optional = true;
1368
+ section.content.push(step);
1365
1369
  items.length = 0;
1366
1370
  return true;
1367
1371
  }
@@ -1905,7 +1909,8 @@ function extractMetadata(content) {
1905
1909
  "yield",
1906
1910
  "serves",
1907
1911
  // List fields
1908
- "tags"
1912
+ "tags",
1913
+ "variants"
1909
1914
  ]);
1910
1915
  for (const metaVar of [
1911
1916
  "title",
@@ -1992,6 +1997,8 @@ function extractMetadata(content) {
1992
1997
  }
1993
1998
  const tags = parseListMetaVar(metadataContent, "tags");
1994
1999
  if (tags) metadata.tags = tags;
2000
+ const variants = parseListMetaVar(metadataContent, "variants");
2001
+ if (variants) metadata.variants = variants;
1995
2002
  const allKeys = extractAllMetadataKeys(metadataContent);
1996
2003
  for (const key of allKeys) {
1997
2004
  if (handledKeys.has(key)) continue;
@@ -2379,8 +2386,10 @@ var Section = class {
2379
2386
  /**
2380
2387
  * Creates an instance of Section.
2381
2388
  * @param name - The name of the section. Defaults to an empty string.
2389
+ * @param variants - Optional variant names for this section.
2390
+ * @param optional - Whether the section is optional.
2382
2391
  */
2383
- constructor(name = "") {
2392
+ constructor(name = "", variants, optional) {
2384
2393
  /**
2385
2394
  * The name of the section. Can be an empty string for the default (first) section.
2386
2395
  * @defaultValue `""`
@@ -2388,7 +2397,13 @@ var Section = class {
2388
2397
  __publicField(this, "name");
2389
2398
  /** An array of steps and notes that make up the content of the section. */
2390
2399
  __publicField(this, "content", []);
2400
+ /** Optional list of variant names this section belongs to. */
2401
+ __publicField(this, "variants");
2402
+ /** Whether the section has been marked as optional ([?]) */
2403
+ __publicField(this, "optional");
2391
2404
  this.name = name;
2405
+ if (variants) this.variants = variants;
2406
+ if (optional) this.optional = true;
2392
2407
  }
2393
2408
  /**
2394
2409
  * Checks if the section is blank (has no name and no content).
@@ -2748,7 +2763,8 @@ var _Recipe = class _Recipe {
2748
2763
  */
2749
2764
  __publicField(this, "choices", {
2750
2765
  ingredientItems: /* @__PURE__ */ new Map(),
2751
- ingredientGroups: /* @__PURE__ */ new Map()
2766
+ ingredientGroups: /* @__PURE__ */ new Map(),
2767
+ variants: []
2752
2768
  });
2753
2769
  /**
2754
2770
  * The parsed recipe ingredients.
@@ -3065,6 +3081,10 @@ var _Recipe = class _Recipe {
3065
3081
  if (itemQuantity) {
3066
3082
  Object.assign(alternative, itemQuantity);
3067
3083
  }
3084
+ const note = groups.gIngredientNote?.trim();
3085
+ if (note) {
3086
+ alternative.note = note;
3087
+ }
3068
3088
  const existingSubgroups = this.choices.ingredientGroups.get(groupKey);
3069
3089
  const existingAlternativesFlat = existingSubgroups?.flat();
3070
3090
  function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
@@ -3158,6 +3178,8 @@ var _Recipe = class _Recipe {
3158
3178
  /** @internal */
3159
3179
  collectQuantityGroups(options) {
3160
3180
  const { section, step, choices } = options || {};
3181
+ const activeVariant = choices?.variant;
3182
+ const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
3161
3183
  const sectionsToProcess = section !== void 0 ? (() => {
3162
3184
  const idx = typeof section === "number" ? section : this.sections.indexOf(section);
3163
3185
  return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
@@ -3165,12 +3187,29 @@ var _Recipe = class _Recipe {
3165
3187
  const ingredientGroups = /* @__PURE__ */ new Map();
3166
3188
  const selectedIndices = /* @__PURE__ */ new Set();
3167
3189
  const referencedIndices = /* @__PURE__ */ new Set();
3190
+ const dynamicOptionalIndices = /* @__PURE__ */ new Set();
3168
3191
  for (const currentSection of sectionsToProcess) {
3192
+ if (currentSection.variants) {
3193
+ if (isDefaultVariant) {
3194
+ if (!currentSection.variants.includes("*")) continue;
3195
+ } else {
3196
+ if (!currentSection.variants.includes(activeVariant)) continue;
3197
+ }
3198
+ }
3169
3199
  const allSteps = currentSection.content.filter(
3170
3200
  (item) => item.type === "step"
3171
3201
  );
3202
+ const isOptionalSection = currentSection.optional === true;
3172
3203
  const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
3173
3204
  for (const currentStep of stepsToProcess) {
3205
+ if (currentStep.variants) {
3206
+ if (isDefaultVariant) {
3207
+ if (!currentStep.variants.includes("*")) continue;
3208
+ } else {
3209
+ if (!currentStep.variants.includes(activeVariant)) continue;
3210
+ }
3211
+ }
3212
+ const isOptionalStep = currentStep.optional === true || isOptionalSection;
3174
3213
  for (const item of currentStep.items.filter(
3175
3214
  (item2) => item2.type === "ingredient"
3176
3215
  )) {
@@ -3182,18 +3221,53 @@ var _Recipe = class _Recipe {
3182
3221
  if (isGrouped) {
3183
3222
  const groupChoice = choices?.ingredientGroups?.get(item.group);
3184
3223
  hasExplicitChoice = groupChoice !== void 0;
3185
- const targetSubgroupIndex = groupChoice ?? 0;
3186
- const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3187
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3224
+ if (!hasExplicitChoice && !isDefaultVariant) {
3225
+ const matchingSubgroupIdx = groupSubgroups?.findIndex(
3226
+ (sg) => sg.some(
3227
+ (alt) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
3228
+ )
3229
+ );
3230
+ if (matchingSubgroupIdx !== void 0 && matchingSubgroupIdx >= 0) {
3231
+ const matchedSubgroup = groupSubgroups[matchingSubgroupIdx];
3232
+ isSelected = matchedSubgroup.some(
3233
+ (alt) => alt.itemId === item.id
3234
+ );
3235
+ hasExplicitChoice = true;
3236
+ selectedAltIndex = 0;
3237
+ } else {
3238
+ const targetSubgroupIndex = 0;
3239
+ const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3240
+ isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3241
+ }
3242
+ } else {
3243
+ const targetSubgroupIndex = groupChoice ?? 0;
3244
+ const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3245
+ isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3246
+ }
3188
3247
  } else {
3189
3248
  const itemChoice = choices?.ingredientItems?.get(item.id);
3190
3249
  hasExplicitChoice = itemChoice !== void 0;
3191
- selectedAltIndex = itemChoice ?? 0;
3250
+ if (!hasExplicitChoice && !isDefaultVariant) {
3251
+ const matchingIndices = item.alternatives.map((alt, idx) => ({ alt, idx })).filter(
3252
+ ({ alt }) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
3253
+ ).map(({ idx }) => idx);
3254
+ if (matchingIndices.length > 0) {
3255
+ selectedAltIndex = matchingIndices[0];
3256
+ hasExplicitChoice = true;
3257
+ } else {
3258
+ selectedAltIndex = itemChoice ?? 0;
3259
+ }
3260
+ } else {
3261
+ selectedAltIndex = itemChoice ?? 0;
3262
+ }
3192
3263
  isSelected = true;
3193
3264
  }
3194
3265
  const alternative = item.alternatives[selectedAltIndex];
3195
3266
  if (!alternative || !isSelected) continue;
3196
3267
  selectedIndices.add(alternative.index);
3268
+ if (isOptionalStep) {
3269
+ dynamicOptionalIndices.add(alternative.index);
3270
+ }
3197
3271
  const allAltsFlat = isGrouped ? groupSubgroups.flat() : item.alternatives;
3198
3272
  for (const alt of allAltsFlat) {
3199
3273
  referencedIndices.add(alt.index);
@@ -3299,7 +3373,12 @@ var _Recipe = class _Recipe {
3299
3373
  }
3300
3374
  }
3301
3375
  }
3302
- return { ingredientGroups, selectedIndices, referencedIndices };
3376
+ return {
3377
+ ingredientGroups,
3378
+ selectedIndices,
3379
+ referencedIndices,
3380
+ dynamicOptionalIndices
3381
+ };
3303
3382
  }
3304
3383
  /**
3305
3384
  * Gets the raw (unprocessed) quantity groups for each ingredient, before
@@ -3318,12 +3397,21 @@ var _Recipe = class _Recipe {
3318
3397
  * ```
3319
3398
  */
3320
3399
  getRawQuantityGroups(options) {
3321
- const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
3400
+ const {
3401
+ ingredientGroups,
3402
+ selectedIndices,
3403
+ referencedIndices,
3404
+ dynamicOptionalIndices
3405
+ } = this.collectQuantityGroups(options);
3322
3406
  const result = [];
3323
3407
  for (let index = 0; index < this.ingredients.length; index++) {
3324
3408
  if (!referencedIndices.has(index)) continue;
3325
3409
  const orig = this.ingredients[index];
3326
3410
  const usedAsPrimary = selectedIndices.has(index);
3411
+ let flags = orig.flags;
3412
+ if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
3413
+ flags = [...flags ?? [], "optional"];
3414
+ }
3327
3415
  const quantities = [];
3328
3416
  if (usedAsPrimary) {
3329
3417
  const groupsForIng = ingredientGroups.get(index);
@@ -3336,7 +3424,7 @@ var _Recipe = class _Recipe {
3336
3424
  result.push({
3337
3425
  name: orig.name,
3338
3426
  ...usedAsPrimary && { usedAsPrimary: true },
3339
- ...orig.flags && { flags: orig.flags },
3427
+ ...flags && { flags },
3340
3428
  quantities
3341
3429
  });
3342
3430
  }
@@ -3370,15 +3458,24 @@ var _Recipe = class _Recipe {
3370
3458
  * ```
3371
3459
  */
3372
3460
  getIngredientQuantities(options) {
3373
- const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
3461
+ const {
3462
+ ingredientGroups,
3463
+ selectedIndices,
3464
+ referencedIndices,
3465
+ dynamicOptionalIndices
3466
+ } = this.collectQuantityGroups(options);
3374
3467
  const result = [];
3375
3468
  for (let index = 0; index < this.ingredients.length; index++) {
3376
3469
  if (!referencedIndices.has(index)) continue;
3377
3470
  const orig = this.ingredients[index];
3471
+ let flags = orig.flags;
3472
+ if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
3473
+ flags = [...flags ?? [], "optional"];
3474
+ }
3378
3475
  const ing = {
3379
3476
  name: orig.name,
3380
3477
  ...orig.preparation && { preparation: orig.preparation },
3381
- ...orig.flags && { flags: orig.flags },
3478
+ ...flags && { flags },
3382
3479
  ...orig.extras && { extras: orig.extras }
3383
3480
  };
3384
3481
  if (selectedIndices.has(index)) {
@@ -3429,6 +3526,59 @@ var _Recipe = class _Recipe {
3429
3526
  }
3430
3527
  return result;
3431
3528
  }
3529
+ /**
3530
+ * Returns the list of cookware items that are used in the active variant.
3531
+ * Cookware in steps/sections not matching the active variant are excluded.
3532
+ * Hidden cookware is always excluded.
3533
+ *
3534
+ * @param options - Options for filtering:
3535
+ * - `choices`: The choices to apply (only `variant` is used)
3536
+ * @returns Array of Cookware objects referenced by active steps
3537
+ *
3538
+ * @example
3539
+ * ```typescript
3540
+ * // Get all cookware for the default variant
3541
+ * const cookware = recipe.getCookwareForVariant();
3542
+ *
3543
+ * // Get cookware for a specific variant
3544
+ * const veganCookware = recipe.getCookwareForVariant({ choices: { variant: 'vegan' } });
3545
+ * ```
3546
+ */
3547
+ getCookwareForVariant(options) {
3548
+ const { choices } = options || {};
3549
+ const activeVariant = choices?.variant;
3550
+ const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
3551
+ const cookwareIndices = /* @__PURE__ */ new Set();
3552
+ for (const currentSection of this.sections) {
3553
+ if (currentSection.variants) {
3554
+ if (isDefaultVariant) {
3555
+ if (!currentSection.variants.includes("*")) continue;
3556
+ } else {
3557
+ if (!currentSection.variants.includes(activeVariant)) continue;
3558
+ }
3559
+ }
3560
+ const allSteps = currentSection.content.filter(
3561
+ (item) => item.type === "step"
3562
+ );
3563
+ for (const currentStep of allSteps) {
3564
+ if (currentStep.variants) {
3565
+ if (isDefaultVariant) {
3566
+ if (!currentStep.variants.includes("*")) continue;
3567
+ } else {
3568
+ if (!currentStep.variants.includes(activeVariant)) continue;
3569
+ }
3570
+ }
3571
+ for (const item of currentStep.items) {
3572
+ if (item.type === "cookware") {
3573
+ cookwareIndices.add(item.index);
3574
+ }
3575
+ }
3576
+ }
3577
+ }
3578
+ return this.cookware.filter(
3579
+ (cw, idx) => cookwareIndices.has(idx) && !cw.flags?.includes("hidden")
3580
+ );
3581
+ }
3432
3582
  /**
3433
3583
  * Parses a recipe from a string.
3434
3584
  * @param content - The recipe content to parse.
@@ -3444,9 +3594,14 @@ var _Recipe = class _Recipe {
3444
3594
  const items = [];
3445
3595
  let noteText = "";
3446
3596
  let inNote = false;
3597
+ let stepVariants;
3598
+ let stepOptional;
3599
+ const discoveredVariants = /* @__PURE__ */ new Set();
3447
3600
  for (const line of cleanContent) {
3448
3601
  if (line.trim().length === 0) {
3449
- flushPendingItems(section, items);
3602
+ flushPendingItems(section, items, stepVariants, stepOptional);
3603
+ stepVariants = void 0;
3604
+ stepOptional = void 0;
3450
3605
  flushPendingNote(
3451
3606
  section,
3452
3607
  noteText ? this._parseNoteText(noteText) : []
@@ -3457,26 +3612,48 @@ var _Recipe = class _Recipe {
3457
3612
  continue;
3458
3613
  }
3459
3614
  if (line.startsWith("=")) {
3460
- flushPendingItems(section, items);
3615
+ flushPendingItems(section, items, stepVariants, stepOptional);
3616
+ stepVariants = void 0;
3617
+ stepOptional = void 0;
3461
3618
  flushPendingNote(
3462
3619
  section,
3463
3620
  noteText ? this._parseNoteText(noteText) : []
3464
3621
  );
3465
3622
  noteText = "";
3623
+ let sectionName = line.replace(/^=+|=+$/g, "").trim();
3624
+ let sectionVariants;
3625
+ let sectionOptional;
3626
+ const sectionVarMatch = sectionName.match(variantTagRegex);
3627
+ if (sectionVarMatch?.groups) {
3628
+ const isOptionalPrefix = sectionVarMatch.groups.variantOptionalPrefix === "?";
3629
+ const names = (sectionVarMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
3630
+ if (names.length > 0) {
3631
+ sectionVariants = names;
3632
+ for (const v of names) discoveredVariants.add(v);
3633
+ }
3634
+ if (isOptionalPrefix) {
3635
+ sectionOptional = true;
3636
+ }
3637
+ sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3638
+ }
3466
3639
  if (this.sections.length === 0 && section.isBlank()) {
3467
- section.name = line.replace(/^=+|=+$/g, "").trim();
3640
+ section.name = sectionName;
3641
+ if (sectionVariants) section.variants = sectionVariants;
3642
+ if (sectionOptional) section.optional = true;
3468
3643
  } else {
3469
3644
  if (!section.isBlank()) {
3470
3645
  this.sections.push(section);
3471
3646
  }
3472
- section = new Section(line.replace(/^=+|=+$/g, "").trim());
3647
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3473
3648
  }
3474
3649
  blankLineBefore = true;
3475
3650
  inNote = false;
3476
3651
  continue;
3477
3652
  }
3478
3653
  if (blankLineBefore && line.startsWith(">")) {
3479
- flushPendingItems(section, items);
3654
+ flushPendingItems(section, items, stepVariants, stepOptional);
3655
+ stepVariants = void 0;
3656
+ stepOptional = void 0;
3480
3657
  noteText = line.substring(1).trim();
3481
3658
  inNote = true;
3482
3659
  blankLineBefore = false;
@@ -3491,11 +3668,31 @@ var _Recipe = class _Recipe {
3491
3668
  blankLineBefore = false;
3492
3669
  continue;
3493
3670
  }
3671
+ let currentLine = line;
3672
+ if (items.length === 0) {
3673
+ const varMatch = currentLine.match(variantTagRegex);
3674
+ if (varMatch?.groups) {
3675
+ const isOptionalPrefix = varMatch.groups.variantOptionalPrefix === "?";
3676
+ const names = (varMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
3677
+ if (names.length > 0) {
3678
+ stepVariants = names;
3679
+ for (const v of names) discoveredVariants.add(v);
3680
+ }
3681
+ if (isOptionalPrefix) {
3682
+ stepOptional = true;
3683
+ }
3684
+ currentLine = currentLine.slice(varMatch[0].length);
3685
+ if (currentLine.trim().length === 0) {
3686
+ blankLineBefore = false;
3687
+ continue;
3688
+ }
3689
+ }
3690
+ }
3494
3691
  let cursor = 0;
3495
- for (const match of line.matchAll(tokensRegex)) {
3692
+ for (const match of currentLine.matchAll(tokensRegex)) {
3496
3693
  const idx = match.index;
3497
3694
  if (idx > cursor) {
3498
- items.push(...parseMarkdownSegments(line.slice(cursor, idx)));
3695
+ items.push(...parseMarkdownSegments(currentLine.slice(cursor, idx)));
3499
3696
  }
3500
3697
  const groups = match.groups;
3501
3698
  if (groups.mIngredientName || groups.sIngredientName) {
@@ -3556,16 +3753,21 @@ var _Recipe = class _Recipe {
3556
3753
  }
3557
3754
  cursor = idx + match[0].length;
3558
3755
  }
3559
- if (cursor < line.length) {
3560
- items.push(...parseMarkdownSegments(line.slice(cursor)));
3756
+ if (cursor < currentLine.length) {
3757
+ items.push(...parseMarkdownSegments(currentLine.slice(cursor)));
3561
3758
  }
3562
3759
  blankLineBefore = false;
3563
3760
  }
3564
- flushPendingItems(section, items);
3761
+ flushPendingItems(section, items, stepVariants, stepOptional);
3565
3762
  flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
3566
3763
  if (!section.isBlank()) {
3567
3764
  this.sections.push(section);
3568
3765
  }
3766
+ const metaVariants = this.metadata.variants ?? [];
3767
+ const allVariants = /* @__PURE__ */ new Set([...metaVariants, ...discoveredVariants]);
3768
+ if (allVariants.size > 0) {
3769
+ this.choices.variants = [...allVariants];
3770
+ }
3569
3771
  this._populateIngredientQuantities();
3570
3772
  }
3571
3773
  /**
@@ -3876,7 +4078,11 @@ var _Recipe = class _Recipe {
3876
4078
  newRecipe.metadata = deepClone(this.metadata);
3877
4079
  newRecipe.ingredients = deepClone(this.ingredients);
3878
4080
  newRecipe.sections = this.sections.map((section) => {
3879
- const newSection = new Section(section.name);
4081
+ const newSection = new Section(
4082
+ section.name,
4083
+ section.variants,
4084
+ section.optional
4085
+ );
3880
4086
  newSection.content = deepClone(section.content);
3881
4087
  return newSection;
3882
4088
  });
@@ -4749,12 +4955,52 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
4749
4955
  const selectedIndex = choices?.ingredientItems?.get(item.id);
4750
4956
  return alternativeIndex === selectedIndex;
4751
4957
  }
4958
+ function isSectionActive(section, variant) {
4959
+ if (!section.variants) return true;
4960
+ const isDefault = variant === void 0 || variant === "*";
4961
+ if (isDefault) {
4962
+ return section.variants.includes("*");
4963
+ }
4964
+ return section.variants.includes(variant);
4965
+ }
4966
+ function isStepActive(step, variant) {
4967
+ if (!step.variants) return true;
4968
+ const isDefault = variant === void 0 || variant === "*";
4969
+ if (isDefault) {
4970
+ return step.variants.includes("*");
4971
+ }
4972
+ return step.variants.includes(variant);
4973
+ }
4974
+ function getEffectiveChoices(recipe, variant) {
4975
+ const choices = { variant };
4976
+ if (variant === void 0 || variant === "*") return choices;
4977
+ const variantLower = variant.toLowerCase();
4978
+ for (const [itemId, alternatives] of recipe.choices.ingredientItems) {
4979
+ const matchIdx = alternatives.findIndex(
4980
+ (alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
4981
+ );
4982
+ if (matchIdx >= 0) {
4983
+ if (!choices.ingredientItems) choices.ingredientItems = /* @__PURE__ */ new Map();
4984
+ choices.ingredientItems.set(itemId, matchIdx);
4985
+ }
4986
+ }
4987
+ for (const [groupId, subgroups] of recipe.choices.ingredientGroups) {
4988
+ const matchIdx = subgroups.findIndex(
4989
+ (sg) => sg.some(
4990
+ (alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
4991
+ )
4992
+ );
4993
+ if (matchIdx >= 0) {
4994
+ if (!choices.ingredientGroups) choices.ingredientGroups = /* @__PURE__ */ new Map();
4995
+ choices.ingredientGroups.set(groupId, matchIdx);
4996
+ }
4997
+ }
4998
+ return choices;
4999
+ }
4752
5000
  export {
4753
- BadIndentationError,
4754
5001
  CategoryConfig,
4755
5002
  NoProductCatalogForCartError,
4756
5003
  NoShoppingListForCartError,
4757
- NoTabAsIndentError,
4758
5004
  Pantry,
4759
5005
  ProductCatalog,
4760
5006
  Recipe,
@@ -4769,11 +5015,14 @@ export {
4769
5015
  formatQuantityWithUnit,
4770
5016
  formatSingleValue,
4771
5017
  formatUnit,
5018
+ getEffectiveChoices,
4772
5019
  hasAlternatives,
4773
5020
  isAlternativeSelected,
4774
5021
  isAndGroup,
4775
5022
  isGroupedItem,
5023
+ isSectionActive,
4776
5024
  isSimpleGroup,
5025
+ isStepActive,
4777
5026
  renderFractionAsVulgar
4778
5027
  };
4779
5028
  /* v8 ignore else -- @preserve */