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

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
@@ -268,6 +268,18 @@ interface MetadataSource {
268
268
  /** The author at the source. */
269
269
  author?: string;
270
270
  }
271
+ /**
272
+ * Represents scaling variable information for a recipe.
273
+ * @category Types
274
+ */
275
+ interface MetadataScalingVar extends QuantityWithPlainUnit {
276
+ /** The text before the scaling variable. */
277
+ textBefore?: string;
278
+ /** The text after the scaling variable. */
279
+ textAfter?: string;
280
+ /** The text precising a numerical scaling variable. */
281
+ text?: string;
282
+ }
271
283
  /**
272
284
  * Represents time information for a recipe.
273
285
  * @category Types
@@ -291,7 +303,7 @@ interface MetadataObject {
291
303
  * Represents any value that can appear in recipe metadata.
292
304
  * @category Types
293
305
  */
294
- type MetadataValue = string | number | (string | number)[] | MetadataObject | MetadataSource | MetadataTime | undefined;
306
+ type MetadataValue = string | number | (string | number)[] | MetadataObject | MetadataSource | MetadataTime | MetadataScalingVar | undefined;
295
307
  /**
296
308
  * Represents the metadata of a recipe.
297
309
  * @category Types
@@ -309,9 +321,10 @@ interface Metadata {
309
321
  /** The author of the recipe (separate from source author). */
310
322
  author?: string;
311
323
  /** The number of servings the recipe makes.
312
- * Should be either a number or a string which starts with a number
313
- * (which will be used for scaling) followed by a comma and then
314
- * whatever you want.
324
+ * Can be given either as:
325
+ * - a number
326
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
327
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
315
328
  *
316
329
  * Interchangeable with `yield` or `serves`. If multiple ones are defined,
317
330
  * the prevailance order for the number which will used for scaling
@@ -326,30 +339,37 @@ interface Metadata {
326
339
  * ```yaml
327
340
  * servings: 2, a few
328
341
  * ```
342
+ *
343
+ * * @example
344
+ * ```yaml
345
+ * servings: {{1.5%kg}} of bread
346
+ * ```
329
347
  */
330
- servings?: number | string;
348
+ servings?: MetadataScalingVar;
331
349
  /** The yield of the recipe.
332
- * Should be either a number or a string which starts with a number
333
- * (which will be used for scaling) followed by a comma and then
334
- * whatever you want.
350
+ * Can be given either as:
351
+ * - a number
352
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
353
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
335
354
  *
336
355
  * Interchangeable with `servings` or `serves`. If multiple ones are defined,
337
356
  * the prevailance order for the number which will used for scaling
338
357
  * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
339
358
  * for examples.
340
359
  */
341
- yield?: number | string;
360
+ yield?: MetadataScalingVar;
342
361
  /** The number of people the recipe serves.
343
- * Should be either a number or a string which starts with a number
344
- * (which will be used for scaling) followed by a comma and then
345
- * whatever you want.
362
+ * Can be given either as:
363
+ * - a number
364
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
365
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
346
366
  *
347
367
  * Interchangeable with `servings` or `yield`. If multiple ones are defined,
348
368
  * the prevailance order for the number which will used for scaling
349
369
  * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
350
370
  * for examples.
351
371
  */
352
- serves?: number | string;
372
+ serves?: MetadataScalingVar;
353
373
  /** The course of the recipe. */
354
374
  course?: string;
355
375
  /** The category of the recipe. */
@@ -1918,4 +1938,4 @@ declare class BadIndentationError extends Error {
1918
1938
  constructor();
1919
1939
  }
1920
1940
 
1921
- export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type AndGroup, type ArbitraryScalable, type ArbitraryScalableItem, BadIndentationError, 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, NoTabAsIndentError, 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, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup, renderFractionAsVulgar };
1941
+ export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type AndGroup, type ArbitraryScalable, type ArbitraryScalableItem, BadIndentationError, 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 MetadataScalingVar, type MetadataSource, type MetadataTime, type MetadataValue, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, NoTabAsIndentError, 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, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup, renderFractionAsVulgar };
package/dist/index.d.ts CHANGED
@@ -268,6 +268,18 @@ interface MetadataSource {
268
268
  /** The author at the source. */
269
269
  author?: string;
270
270
  }
271
+ /**
272
+ * Represents scaling variable information for a recipe.
273
+ * @category Types
274
+ */
275
+ interface MetadataScalingVar extends QuantityWithPlainUnit {
276
+ /** The text before the scaling variable. */
277
+ textBefore?: string;
278
+ /** The text after the scaling variable. */
279
+ textAfter?: string;
280
+ /** The text precising a numerical scaling variable. */
281
+ text?: string;
282
+ }
271
283
  /**
272
284
  * Represents time information for a recipe.
273
285
  * @category Types
@@ -291,7 +303,7 @@ interface MetadataObject {
291
303
  * Represents any value that can appear in recipe metadata.
292
304
  * @category Types
293
305
  */
294
- type MetadataValue = string | number | (string | number)[] | MetadataObject | MetadataSource | MetadataTime | undefined;
306
+ type MetadataValue = string | number | (string | number)[] | MetadataObject | MetadataSource | MetadataTime | MetadataScalingVar | undefined;
295
307
  /**
296
308
  * Represents the metadata of a recipe.
297
309
  * @category Types
@@ -309,9 +321,10 @@ interface Metadata {
309
321
  /** The author of the recipe (separate from source author). */
310
322
  author?: string;
311
323
  /** The number of servings the recipe makes.
312
- * Should be either a number or a string which starts with a number
313
- * (which will be used for scaling) followed by a comma and then
314
- * whatever you want.
324
+ * Can be given either as:
325
+ * - a number
326
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
327
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
315
328
  *
316
329
  * Interchangeable with `yield` or `serves`. If multiple ones are defined,
317
330
  * the prevailance order for the number which will used for scaling
@@ -326,30 +339,37 @@ interface Metadata {
326
339
  * ```yaml
327
340
  * servings: 2, a few
328
341
  * ```
342
+ *
343
+ * * @example
344
+ * ```yaml
345
+ * servings: {{1.5%kg}} of bread
346
+ * ```
329
347
  */
330
- servings?: number | string;
348
+ servings?: MetadataScalingVar;
331
349
  /** The yield of the recipe.
332
- * Should be either a number or a string which starts with a number
333
- * (which will be used for scaling) followed by a comma and then
334
- * whatever you want.
350
+ * Can be given either as:
351
+ * - a number
352
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
353
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
335
354
  *
336
355
  * Interchangeable with `servings` or `serves`. If multiple ones are defined,
337
356
  * the prevailance order for the number which will used for scaling
338
357
  * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
339
358
  * for examples.
340
359
  */
341
- yield?: number | string;
360
+ yield?: MetadataScalingVar;
342
361
  /** The number of people the recipe serves.
343
- * Should be either a number or a string which starts with a number
344
- * (which will be used for scaling) followed by a comma and then
345
- * whatever you want.
362
+ * Can be given either as:
363
+ * - a number
364
+ * - a string which starts with a number (which will be used for scaling) followed by a comma and then whatever you want
365
+ * - or an arbitrary scalable number, optionally preceded and/or followed by text.
346
366
  *
347
367
  * Interchangeable with `servings` or `yield`. If multiple ones are defined,
348
368
  * the prevailance order for the number which will used for scaling
349
369
  * is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
350
370
  * for examples.
351
371
  */
352
- serves?: number | string;
372
+ serves?: MetadataScalingVar;
353
373
  /** The course of the recipe. */
354
374
  course?: string;
355
375
  /** The category of the recipe. */
@@ -1918,4 +1938,4 @@ declare class BadIndentationError extends Error {
1918
1938
  constructor();
1919
1939
  }
1920
1940
 
1921
- export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type AndGroup, type ArbitraryScalable, type ArbitraryScalableItem, BadIndentationError, 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, NoTabAsIndentError, 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, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup, renderFractionAsVulgar };
1941
+ export { type AddedIngredient, type AddedRecipe, type AddedRecipeOptions, type AlternativeIngredientRef, type AndGroup, type ArbitraryScalable, type ArbitraryScalableItem, BadIndentationError, 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 MetadataScalingVar, type MetadataSource, type MetadataTime, type MetadataValue, NoProductCatalogForCartError, type NoProductMatchErrorCode, NoShoppingListForCartError, NoTabAsIndentError, 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, convertQuantityToSystem, formatExtendedQuantity, formatItemQuantity, formatNumericValue, formatQuantity, formatQuantityWithUnit, formatSingleValue, formatUnit, hasAlternatives, isAlternativeSelected, isAndGroup, isGroupedItem, isSimpleGroup, renderFractionAsVulgar };
package/dist/index.js CHANGED
@@ -272,7 +272,6 @@ var nestedMetaVarRegex = (varName) => new RegExp(
272
272
  "m"
273
273
  );
274
274
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
275
- var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
276
275
  var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
277
276
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
278
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();
@@ -293,6 +292,13 @@ var tokensRegex = new RegExp(
293
292
  ].map((r2) => r2.source).join("|"),
294
293
  "gu"
295
294
  );
295
+ var servingsPrefixPart = (varName) => d().startAnchor().literal(varName).literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
296
+ var servingsSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
297
+ var scalingSimpleMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").anyOf("\\t ").zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().optional().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
298
+ var scalingMetaValueWithUnitRegex = (varName) => new RegExp(
299
+ servingsPrefixPart(varName).source + arbitraryScalableRegex.source + servingsSuffixPart.source,
300
+ "m"
301
+ );
296
302
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
297
303
  var blockCommentRegex = d().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
298
304
  var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
@@ -1276,17 +1282,21 @@ var flattenPlainUnitGroup = (summed) => {
1276
1282
  }
1277
1283
  };
1278
1284
  function applyBestUnit(q, system) {
1279
- if (!q.unit?.name) {
1285
+ const extended = { quantity: q.quantity };
1286
+ if (q.unit) {
1287
+ extended.unit = typeof q.unit === "string" ? { name: q.unit } : q.unit;
1288
+ }
1289
+ if (!extended.unit?.name) {
1280
1290
  return q;
1281
1291
  }
1282
- const unitDef = resolveUnit(q.unit.name);
1292
+ const unitDef = resolveUnit(extended.unit.name);
1283
1293
  if (unitDef.type === "other") {
1284
1294
  return q;
1285
1295
  }
1286
- if (q.quantity.type === "fixed" && q.quantity.value.type === "text") {
1296
+ if (extended.quantity.type === "fixed" && extended.quantity.value.type === "text") {
1287
1297
  return q;
1288
1298
  }
1289
- const avgValue = getAverageValue(q.quantity);
1299
+ const avgValue = getAverageValue(extended.quantity);
1290
1300
  const effectiveSystem = system ?? (["metric", "JP"].includes(unitDef.system) ? unitDef.system : "US");
1291
1301
  const toBase = getToBase(unitDef, effectiveSystem);
1292
1302
  const valueInBase = avgValue * toBase;
@@ -1296,22 +1306,22 @@ function applyBestUnit(q, system) {
1296
1306
  effectiveSystem,
1297
1307
  [unitDef]
1298
1308
  );
1299
- const originalCanonicalName = normalizeUnit(q.unit.name)?.name;
1309
+ const originalCanonicalName = normalizeUnit(extended.unit.name)?.name;
1300
1310
  if (bestUnit.name === originalCanonicalName) {
1301
1311
  return q;
1302
1312
  }
1303
1313
  const formattedValue = formatOutputValue(bestValue, bestUnit);
1304
- if (q.quantity.type === "range") {
1314
+ if (extended.quantity.type === "range") {
1305
1315
  const bestToBase = getToBase(bestUnit, effectiveSystem);
1306
- const minValue = getNumericValue(q.quantity.min) * toBase / bestToBase;
1307
- const maxValue = getNumericValue(q.quantity.max) * toBase / bestToBase;
1316
+ const minValue = getNumericValue(extended.quantity.min) * toBase / bestToBase;
1317
+ const maxValue = getNumericValue(extended.quantity.max) * toBase / bestToBase;
1308
1318
  return {
1309
1319
  quantity: {
1310
1320
  type: "range",
1311
1321
  min: formatOutputValue(minValue, bestUnit),
1312
1322
  max: formatOutputValue(maxValue, bestUnit)
1313
1323
  },
1314
- unit: { name: bestUnit.name }
1324
+ unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
1315
1325
  };
1316
1326
  }
1317
1327
  return {
@@ -1319,7 +1329,7 @@ function applyBestUnit(q, system) {
1319
1329
  type: "fixed",
1320
1330
  value: formattedValue
1321
1331
  },
1322
- unit: { name: bestUnit.name }
1332
+ unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
1323
1333
  };
1324
1334
  }
1325
1335
  function subtractQuantities(q1, q2, options = {}) {
@@ -1671,13 +1681,62 @@ function parseBlockScalarMetaVar(content, varName) {
1671
1681
  }
1672
1682
  return stripped.replace(/\n\n/g, "\0").replace(/\n/g, " ").replace(/\0/g, "\n");
1673
1683
  }
1684
+ function parseArbitraryQuantity(raw) {
1685
+ const quantityMatch = raw.trim().match(quantityAlternativeRegex);
1686
+ if (!quantityMatch?.groups) {
1687
+ throw new InvalidQuantityFormat(
1688
+ raw,
1689
+ "Arbitrary quantities must have a numerical value"
1690
+ );
1691
+ }
1692
+ const value = parseQuantityInput(quantityMatch.groups.quantity);
1693
+ const unit = quantityMatch.groups.unit;
1694
+ if (!value || value.type === "fixed" && value.value.type === "text") {
1695
+ throw new InvalidQuantityFormat(
1696
+ raw,
1697
+ "Arbitrary quantities must have a numerical value"
1698
+ );
1699
+ }
1700
+ const arbitrary = {
1701
+ quantity: value
1702
+ };
1703
+ if (unit) arbitrary.unit = unit;
1704
+ return arbitrary;
1705
+ }
1674
1706
  function parseScalingMetaVar(content, varName) {
1675
- const varMatch = content.match(scalingMetaValueRegex(varName));
1707
+ const complexMatch = content.match(scalingMetaValueWithUnitRegex(varName));
1708
+ if (complexMatch?.groups?.arbitraryQuantity) {
1709
+ const parsed = parseArbitraryQuantity(
1710
+ complexMatch.groups.arbitraryQuantity
1711
+ );
1712
+ const result2 = {
1713
+ quantity: parsed.quantity
1714
+ };
1715
+ if (parsed.unit) result2.unit = parsed.unit;
1716
+ if (complexMatch.groups.servingsPrefix) {
1717
+ result2.textBefore = complexMatch.groups.servingsPrefix;
1718
+ }
1719
+ if (complexMatch.groups.servingsSuffix) {
1720
+ result2.textAfter = complexMatch.groups.servingsSuffix;
1721
+ }
1722
+ return result2;
1723
+ }
1724
+ const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
1676
1725
  if (!varMatch) return void 0;
1677
1726
  if (isNaN(Number(varMatch[2]?.trim()))) {
1678
1727
  throw new Error("Scaling variables should be numbers");
1679
1728
  }
1680
- return [Number(varMatch[2]?.trim()), varMatch[1].trim()];
1729
+ const numericValue = Number(varMatch[2]?.trim());
1730
+ const result = {
1731
+ quantity: {
1732
+ type: "fixed",
1733
+ value: { type: "decimal", decimal: numericValue }
1734
+ }
1735
+ };
1736
+ if (varMatch[3]) {
1737
+ result.text = `${varMatch[3].trim()}`;
1738
+ }
1739
+ return result;
1681
1740
  }
1682
1741
  function parseListMetaVar(content, varName) {
1683
1742
  const listMatch = content.match(
@@ -1795,6 +1854,13 @@ function parseAnyMetaVar(content, varName) {
1795
1854
  if (simple) return parseMetadataValue(simple);
1796
1855
  return void 0;
1797
1856
  }
1857
+ function getNumericValueFromMetaVar(v) {
1858
+ if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
1859
+ return getNumericValue(v.quantity.value);
1860
+ }
1861
+ if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
1862
+ return 0;
1863
+ }
1798
1864
  function extractMetadata(content) {
1799
1865
  const metadata = {};
1800
1866
  let servings = void 0;
@@ -1919,9 +1985,9 @@ function extractMetadata(content) {
1919
1985
  }
1920
1986
  for (const metaVar of ["servings", "yield", "serves"]) {
1921
1987
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
1922
- if (scalingMetaValue && scalingMetaValue[1]) {
1923
- metadata[metaVar] = scalingMetaValue[1];
1924
- servings = scalingMetaValue[0];
1988
+ if (scalingMetaValue) {
1989
+ metadata[metaVar] = scalingMetaValue;
1990
+ servings = getNumericValueFromMetaVar(scalingMetaValue);
1925
1991
  }
1926
1992
  }
1927
1993
  const tags = parseListMetaVar(metadataContent, "tags");
@@ -2748,27 +2814,17 @@ var _Recipe = class _Recipe {
2748
2814
  */
2749
2815
  _parseArbitraryScalable(regexMatchGroups, intoArray) {
2750
2816
  if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
2751
- const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
2752
- if (quantityMatch?.groups) {
2753
- const value = parseQuantityInput(quantityMatch.groups.quantity);
2754
- const unit = quantityMatch.groups.unit;
2755
- const name = regexMatchGroups.arbitraryName || void 0;
2756
- if (!value || value.type === "fixed" && value.value.type === "text") {
2757
- throw new InvalidQuantityFormat(
2758
- regexMatchGroups.arbitraryQuantity?.trim(),
2759
- "Arbitrary quantities must have a numerical value"
2760
- );
2761
- }
2762
- const arbitrary = {
2763
- quantity: value
2764
- };
2765
- if (name) arbitrary.name = name;
2766
- if (unit) arbitrary.unit = unit;
2767
- intoArray.push({
2768
- type: "arbitrary",
2769
- index: this.arbitraries.push(arbitrary) - 1
2770
- });
2771
- }
2817
+ const parsed = parseArbitraryQuantity(regexMatchGroups.arbitraryQuantity);
2818
+ const name = regexMatchGroups.arbitraryName || void 0;
2819
+ const arbitrary = {
2820
+ quantity: parsed.quantity
2821
+ };
2822
+ if (name) arbitrary.name = name;
2823
+ if (parsed.unit) arbitrary.unit = parsed.unit;
2824
+ intoArray.push({
2825
+ type: "arbitrary",
2826
+ index: this.arbitraries.push(arbitrary) - 1
2827
+ });
2772
2828
  }
2773
2829
  /**
2774
2830
  * Parses text for arbitrary scalables and returns NoteItem array.
@@ -3097,8 +3153,8 @@ var _Recipe = class _Recipe {
3097
3153
  const isGrouped = "group" in item && item.group !== void 0;
3098
3154
  const groupAlternatives = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
3099
3155
  let selectedAltIndex = 0;
3100
- let isSelected = false;
3101
- let hasExplicitChoice = false;
3156
+ let isSelected;
3157
+ let hasExplicitChoice;
3102
3158
  if (isGrouped) {
3103
3159
  const groupChoice = choices?.ingredientGroups?.get(item.group);
3104
3160
  hasExplicitChoice = groupChoice !== void 0;
@@ -3558,37 +3614,34 @@ var _Recipe = class _Recipe {
3558
3614
  arbitrary.quantity,
3559
3615
  factor
3560
3616
  );
3617
+ const optimized = applyBestUnit(
3618
+ { quantity: arbitrary.quantity, unit: arbitrary.unit },
3619
+ unitSystem
3620
+ );
3621
+ arbitrary.quantity = optimized.quantity;
3622
+ arbitrary.unit = optimized.unit;
3561
3623
  }
3562
3624
  newRecipe._populateIngredientQuantities();
3563
3625
  newRecipe.servings = Big4(originalServings).times(factor).toNumber();
3564
- if (newRecipe.metadata.servings && this.metadata.servings) {
3565
- if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
3566
- const servingsValue = parseFloat(
3567
- String(this.metadata.servings).replace(",", ".")
3568
- );
3569
- newRecipe.metadata.servings = String(
3570
- Big4(servingsValue).times(factor).toNumber()
3571
- );
3572
- }
3573
- }
3574
- if (newRecipe.metadata.yield && this.metadata.yield) {
3575
- if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
3576
- const yieldValue = parseFloat(
3577
- String(this.metadata.yield).replace(",", ".")
3626
+ for (const metaVar of ["servings", "yield", "serves"]) {
3627
+ if (newRecipe.metadata[metaVar] && this.metadata[metaVar]) {
3628
+ const original = this.metadata[metaVar];
3629
+ const scaledQuantity = multiplyQuantityValue(
3630
+ original.quantity,
3631
+ factor
3578
3632
  );
3579
- newRecipe.metadata.yield = String(
3580
- Big4(yieldValue).times(factor).toNumber()
3581
- );
3582
- }
3583
- }
3584
- if (newRecipe.metadata.serves && this.metadata.serves) {
3585
- if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
3586
- const servesValue = parseFloat(
3587
- String(this.metadata.serves).replace(",", ".")
3588
- );
3589
- newRecipe.metadata.serves = String(
3590
- Big4(servesValue).times(factor).toNumber()
3633
+ const optimized = applyBestUnit(
3634
+ { quantity: scaledQuantity, unit: original.unit },
3635
+ unitSystem
3591
3636
  );
3637
+ const scaled = {
3638
+ quantity: optimized.quantity
3639
+ };
3640
+ if (optimized.unit) scaled.unit = optimized.unit;
3641
+ if (original.textBefore) scaled.textBefore = original.textBefore;
3642
+ if (original.textAfter) scaled.textAfter = original.textAfter;
3643
+ if (original.text) scaled.text = original.text;
3644
+ newRecipe.metadata[metaVar] = scaled;
3592
3645
  }
3593
3646
  }
3594
3647
  return newRecipe;
@@ -4671,6 +4724,7 @@ export {
4671
4724
  // v8 ignore else -- @preserve
4672
4725
  // v8 ignore if -- @preserve
4673
4726
  /* v8 ignore else -- expliciting error type -- @preserve */
4727
+ /* v8 ignore next 4 -- @preserve: defensive guard; regex always matches */
4674
4728
  // v8 ignore if -- @preserve: defensive type guard
4675
4729
  /* v8 ignore if -- @preserve */
4676
4730
  // v8 ignore next -- @preserve