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

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
@@ -335,7 +335,7 @@ var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
335
335
  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
336
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
337
337
  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().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();
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
339
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
340
340
  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
341
  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();
@@ -2837,6 +2837,7 @@ var _Recipe = class _Recipe {
2837
2837
  */
2838
2838
  __publicField(this, "servings");
2839
2839
  _Recipe.itemCounts.set(this, 0);
2840
+ _Recipe.subgroupIndices.set(this, /* @__PURE__ */ new Map());
2840
2841
  if (content) {
2841
2842
  this.parse(content);
2842
2843
  }
@@ -3054,6 +3055,7 @@ var _Recipe = class _Recipe {
3054
3055
  if (!match?.groups) return;
3055
3056
  const groups = match.groups;
3056
3057
  const groupKey = groups.gIngredientGroupKey;
3058
+ const subgroupKey = groups.gIngredientSubgroupKey;
3057
3059
  let name = groups.gmIngredientName || groups.gsIngredientName;
3058
3060
  const preparation = groups.gIngredientPreparation;
3059
3061
  const modifiers = groups.gIngredientModifiers;
@@ -3121,7 +3123,8 @@ var _Recipe = class _Recipe {
3121
3123
  if (itemQuantity) {
3122
3124
  Object.assign(alternative, itemQuantity);
3123
3125
  }
3124
- const existingAlternatives = this.choices.ingredientGroups.get(groupKey);
3126
+ const existingSubgroups = this.choices.ingredientGroups.get(groupKey);
3127
+ const existingAlternativesFlat = existingSubgroups?.flat();
3125
3128
  function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
3126
3129
  const ingredient = ingredients[ingredientIdx];
3127
3130
  if (ingredient) {
@@ -3132,8 +3135,8 @@ var _Recipe = class _Recipe {
3132
3135
  }
3133
3136
  }
3134
3137
  }
3135
- if (existingAlternatives) {
3136
- for (const alt of existingAlternatives) {
3138
+ if (existingAlternativesFlat) {
3139
+ for (const alt of existingAlternativesFlat) {
3137
3140
  upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
3138
3141
  upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
3139
3142
  }
@@ -3145,14 +3148,35 @@ var _Recipe = class _Recipe {
3145
3148
  group: groupKey,
3146
3149
  alternatives: [alternative]
3147
3150
  };
3151
+ if (subgroupKey !== void 0) {
3152
+ newItem.subgroup = subgroupKey;
3153
+ }
3148
3154
  items.push(newItem);
3149
3155
  const choiceAlternative = deepClone(alternative);
3150
3156
  choiceAlternative.itemId = id;
3151
3157
  const existingChoice = this.choices.ingredientGroups.get(groupKey);
3158
+ const sgMap = _Recipe.subgroupIndices.get(this);
3152
3159
  if (!existingChoice) {
3153
- this.choices.ingredientGroups.set(groupKey, [choiceAlternative]);
3160
+ this.choices.ingredientGroups.set(groupKey, [[choiceAlternative]]);
3161
+ if (subgroupKey !== void 0) {
3162
+ sgMap.set(groupKey, /* @__PURE__ */ new Map([[subgroupKey, 0]]));
3163
+ }
3164
+ } else if (subgroupKey !== void 0) {
3165
+ const groupSgMap = sgMap.get(groupKey);
3166
+ const existingIdx = groupSgMap?.get(subgroupKey);
3167
+ if (existingIdx !== void 0) {
3168
+ existingChoice[existingIdx].push(choiceAlternative);
3169
+ } else {
3170
+ const newIdx = existingChoice.length;
3171
+ existingChoice.push([choiceAlternative]);
3172
+ if (!groupSgMap) {
3173
+ sgMap.set(groupKey, /* @__PURE__ */ new Map([[subgroupKey, newIdx]]));
3174
+ } else {
3175
+ groupSgMap.set(subgroupKey, newIdx);
3176
+ }
3177
+ }
3154
3178
  } else {
3155
- existingChoice.push(choiceAlternative);
3179
+ existingChoice.push([choiceAlternative]);
3156
3180
  }
3157
3181
  }
3158
3182
  /**
@@ -3209,15 +3233,16 @@ var _Recipe = class _Recipe {
3209
3233
  (item2) => item2.type === "ingredient"
3210
3234
  )) {
3211
3235
  const isGrouped = "group" in item && item.group !== void 0;
3212
- const groupAlternatives = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
3236
+ const groupSubgroups = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
3213
3237
  let selectedAltIndex = 0;
3214
3238
  let isSelected;
3215
3239
  let hasExplicitChoice;
3216
3240
  if (isGrouped) {
3217
3241
  const groupChoice = choices?.ingredientGroups?.get(item.group);
3218
3242
  hasExplicitChoice = groupChoice !== void 0;
3219
- const targetIndex = groupChoice ?? 0;
3220
- isSelected = groupAlternatives?.[targetIndex]?.itemId === item.id;
3243
+ const targetSubgroupIndex = groupChoice ?? 0;
3244
+ const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3245
+ isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3221
3246
  } else {
3222
3247
  const itemChoice = choices?.ingredientItems?.get(item.id);
3223
3248
  hasExplicitChoice = itemChoice !== void 0;
@@ -3227,8 +3252,8 @@ var _Recipe = class _Recipe {
3227
3252
  const alternative = item.alternatives[selectedAltIndex];
3228
3253
  if (!alternative || !isSelected) continue;
3229
3254
  selectedIndices.add(alternative.index);
3230
- const allAlts = isGrouped ? groupAlternatives : item.alternatives;
3231
- for (const alt of allAlts) {
3255
+ const allAltsFlat = isGrouped ? groupSubgroups.flat() : item.alternatives;
3256
+ for (const alt of allAltsFlat) {
3232
3257
  referencedIndices.add(alt.index);
3233
3258
  }
3234
3259
  if (!alternative.quantity) continue;
@@ -3240,10 +3265,34 @@ var _Recipe = class _Recipe {
3240
3265
  };
3241
3266
  const quantityEntry = alternative.equivalents?.length ? { or: [baseQty, ...alternative.equivalents] } : baseQty;
3242
3267
  let alternativeRefs;
3243
- if (!hasExplicitChoice && allAlts.length > 1) {
3244
- alternativeRefs = allAlts.filter(
3245
- (alt) => isGrouped ? alt.itemId !== item.id : alt.index !== alternative.index
3246
- ).map((otherAlt) => {
3268
+ if (!hasExplicitChoice && groupSubgroups && groupSubgroups.length > 1) {
3269
+ const currentSubgroupIdx = groupSubgroups.findIndex(
3270
+ (sg) => sg.some((alt) => alt.itemId === item.id)
3271
+ );
3272
+ alternativeRefs = groupSubgroups.filter((_, idx) => idx !== currentSubgroupIdx).flatMap(
3273
+ (subgroup) => subgroup.map((otherAlt) => {
3274
+ const ref = {
3275
+ index: otherAlt.index
3276
+ };
3277
+ if (otherAlt.quantity) {
3278
+ const altQty = {
3279
+ quantity: otherAlt.quantity,
3280
+ ...otherAlt.unit && {
3281
+ unit: otherAlt.unit.name
3282
+ },
3283
+ ...otherAlt.equivalents && {
3284
+ equivalents: otherAlt.equivalents.map(
3285
+ (eq) => toPlainUnit(eq)
3286
+ )
3287
+ }
3288
+ };
3289
+ ref.quantities = [altQty];
3290
+ }
3291
+ return ref;
3292
+ })
3293
+ );
3294
+ } else if (!hasExplicitChoice && !isGrouped && allAltsFlat.length > 1) {
3295
+ alternativeRefs = allAltsFlat.filter((alt) => alt.index !== alternative.index).map((otherAlt) => {
3247
3296
  const ref = { index: otherAlt.index };
3248
3297
  if (otherAlt.quantity) {
3249
3298
  const altQty = {
@@ -3661,8 +3710,10 @@ var _Recipe = class _Recipe {
3661
3710
  }
3662
3711
  }
3663
3712
  }
3664
- for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
3665
- scaleAlternativesBy(alternatives, factor);
3713
+ for (const subgroups of newRecipe.choices.ingredientGroups.values()) {
3714
+ for (const subgroup of subgroups) {
3715
+ scaleAlternativesBy(subgroup, factor);
3716
+ }
3666
3717
  }
3667
3718
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
3668
3719
  scaleAlternativesBy(alternatives, factor);
@@ -3849,8 +3900,10 @@ var _Recipe = class _Recipe {
3849
3900
  }
3850
3901
  }
3851
3902
  }
3852
- for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
3853
- convertAlternatives(alternatives);
3903
+ for (const subgroups of newRecipe.choices.ingredientGroups.values()) {
3904
+ for (const subgroup of subgroups) {
3905
+ convertAlternatives(subgroup);
3906
+ }
3854
3907
  }
3855
3908
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
3856
3909
  convertAlternatives(alternatives);
@@ -3902,6 +3955,11 @@ __publicField(_Recipe, "unitSystems", /* @__PURE__ */ new WeakMap());
3902
3955
  * Used for giving ID numbers to items during parsing.
3903
3956
  */
3904
3957
  __publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
3958
+ /**
3959
+ * External storage for subgroup index tracking during parsing.
3960
+ * Maps groupKey → subgroupKey → index within the subgroups array.
3961
+ */
3962
+ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
3905
3963
  var Recipe = _Recipe;
3906
3964
 
3907
3965
  // src/classes/shopping_list.ts
@@ -4739,10 +4797,10 @@ function isGroupedItem(item) {
4739
4797
  function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
4740
4798
  if (item.group) {
4741
4799
  const selectedIndex2 = choices?.ingredientGroups?.get(item.group);
4742
- const groupAlternatives = recipe.choices.ingredientGroups.get(item.group);
4743
- if (groupAlternatives && selectedIndex2 !== void 0 && selectedIndex2 < groupAlternatives.length) {
4744
- const selectedItemId = groupAlternatives[selectedIndex2]?.itemId;
4745
- return selectedItemId === item.id;
4800
+ const groupSubgroups = recipe.choices.ingredientGroups.get(item.group);
4801
+ if (groupSubgroups && selectedIndex2 !== void 0 && selectedIndex2 < groupSubgroups.length) {
4802
+ const selectedSubgroup = groupSubgroups[selectedIndex2];
4803
+ return selectedSubgroup?.some((alt) => alt.itemId === item.id);
4746
4804
  }
4747
4805
  return false;
4748
4806
  }