@tmlmt/cooklang-parser 3.0.0-alpha.19 → 3.0.0-alpha.20
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 +180 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -54
- package/dist/index.d.ts +41 -54
- package/dist/index.js +180 -158
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -276,7 +276,7 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
|
276
276
|
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
|
-
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();
|
|
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
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();
|
|
@@ -292,11 +292,18 @@ var tokensRegex = new RegExp(
|
|
|
292
292
|
].map((r2) => r2.source).join("|"),
|
|
293
293
|
"gu"
|
|
294
294
|
);
|
|
295
|
-
var
|
|
296
|
-
var
|
|
297
|
-
var
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
var yieldPrefixPart = d().startAnchor().literal("yield").literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
|
|
296
|
+
var yieldSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
|
|
297
|
+
var yieldMetaValueWithUnitRegex = new RegExp(
|
|
298
|
+
yieldPrefixPart.source + arbitraryScalableRegex.source + yieldSuffixPart.source,
|
|
299
|
+
"m"
|
|
300
|
+
);
|
|
301
|
+
var yieldMetaValueAsQuantityRegex = d().startAnchor().literal("yield:").anyOf(" ").zeroOrMore().startNamedGroup("quantity").notAnyOf("{}|%\\n\\r").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("\\n\\r|}").oneOrMore().endGroup().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
|
|
302
|
+
var yieldMetaValueRegex = new RegExp(
|
|
303
|
+
[
|
|
304
|
+
yieldMetaValueWithUnitRegex.source,
|
|
305
|
+
yieldMetaValueAsQuantityRegex.source
|
|
306
|
+
].join("|"),
|
|
300
307
|
"m"
|
|
301
308
|
);
|
|
302
309
|
var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
|
|
@@ -305,7 +312,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
|
|
|
305
312
|
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
313
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
307
314
|
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("\\]").
|
|
315
|
+
var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").oneOrMore().endGroup().optional().literal("]").whitespace().zeroOrMore().toRegExp();
|
|
309
316
|
var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
|
|
310
317
|
var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
|
|
311
318
|
var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
|
|
@@ -881,7 +888,7 @@ var NoProductMatchError = class extends Error {
|
|
|
881
888
|
constructor(item_name, code) {
|
|
882
889
|
const messageMap = {
|
|
883
890
|
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
884
|
-
noProduct:
|
|
891
|
+
noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
|
|
885
892
|
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
886
893
|
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
887
894
|
textValue_incompatibleUnits: `Multiple alternative quantities were provided for ingredient ${item_name} in the shopping list but they were either text values or no product in catalog were found to have compatible units`
|
|
@@ -1484,7 +1491,7 @@ function stringifyFixedValue(quantity) {
|
|
|
1484
1491
|
return String(quantity.value.decimal);
|
|
1485
1492
|
else return quantity.value.text;
|
|
1486
1493
|
}
|
|
1487
|
-
function
|
|
1494
|
+
function parseQuantityValue(input_str) {
|
|
1488
1495
|
const clean_str = String(input_str).trim();
|
|
1489
1496
|
if (rangeRegex.test(clean_str)) {
|
|
1490
1497
|
const range_parts = clean_str.split("-");
|
|
@@ -1498,12 +1505,12 @@ function parseQuantityWithUnit(input) {
|
|
|
1498
1505
|
const trimmed = input.trim();
|
|
1499
1506
|
const separatorIndex = trimmed.indexOf("%");
|
|
1500
1507
|
if (separatorIndex === -1) {
|
|
1501
|
-
return { value:
|
|
1508
|
+
return { value: parseQuantityValue(trimmed) };
|
|
1502
1509
|
}
|
|
1503
1510
|
const valuePart = trimmed.slice(0, separatorIndex).trim();
|
|
1504
1511
|
const unitPart = trimmed.slice(separatorIndex + 1).trim();
|
|
1505
1512
|
return {
|
|
1506
|
-
value:
|
|
1513
|
+
value: parseQuantityValue(valuePart),
|
|
1507
1514
|
unit: unitPart || void 0
|
|
1508
1515
|
};
|
|
1509
1516
|
}
|
|
@@ -1693,7 +1700,7 @@ function parseArbitraryQuantity(raw) {
|
|
|
1693
1700
|
"Arbitrary quantities must have a numerical value"
|
|
1694
1701
|
);
|
|
1695
1702
|
}
|
|
1696
|
-
const value =
|
|
1703
|
+
const value = parseQuantityValue(quantityMatch.groups.quantity);
|
|
1697
1704
|
const unit = quantityMatch.groups.unit;
|
|
1698
1705
|
if (!value || value.type === "fixed" && value.value.type === "text") {
|
|
1699
1706
|
throw new InvalidQuantityFormat(
|
|
@@ -1707,40 +1714,40 @@ function parseArbitraryQuantity(raw) {
|
|
|
1707
1714
|
if (unit) arbitrary.unit = unit;
|
|
1708
1715
|
return arbitrary;
|
|
1709
1716
|
}
|
|
1710
|
-
function
|
|
1711
|
-
const
|
|
1712
|
-
if (
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
+
function parseServingsMetaVar(content, varName) {
|
|
1718
|
+
const raw = parseSimpleMetaVar(content, varName);
|
|
1719
|
+
if (raw === void 0) return void 0;
|
|
1720
|
+
const num = Number(raw);
|
|
1721
|
+
if (isNaN(num)) {
|
|
1722
|
+
return { numericValue: 1, rawValue: raw };
|
|
1723
|
+
}
|
|
1724
|
+
return { numericValue: num, rawValue: num };
|
|
1725
|
+
}
|
|
1726
|
+
function parseYieldMetaVar(content) {
|
|
1727
|
+
const match = content.match(yieldMetaValueRegex);
|
|
1728
|
+
if (!match) return void 0;
|
|
1729
|
+
if (match.groups?.arbitraryQuantity) {
|
|
1730
|
+
const parsed = parseArbitraryQuantity(match.groups.arbitraryQuantity);
|
|
1731
|
+
const result = {
|
|
1717
1732
|
quantity: parsed.quantity
|
|
1718
1733
|
};
|
|
1719
|
-
if (parsed.unit)
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1734
|
+
if (parsed.unit) result.unit = parsed.unit;
|
|
1735
|
+
if (match.groups.servingsPrefix) {
|
|
1736
|
+
result.textBefore = match.groups.servingsPrefix;
|
|
1722
1737
|
}
|
|
1723
|
-
if (
|
|
1724
|
-
|
|
1738
|
+
if (match.groups.servingsSuffix) {
|
|
1739
|
+
result.textAfter = match.groups.servingsSuffix;
|
|
1725
1740
|
}
|
|
1726
|
-
return
|
|
1727
|
-
}
|
|
1728
|
-
const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
|
|
1729
|
-
if (!varMatch) return void 0;
|
|
1730
|
-
if (isNaN(Number(varMatch[2]?.trim()))) {
|
|
1731
|
-
throw new Error("Scaling variables should be numbers");
|
|
1741
|
+
return result;
|
|
1732
1742
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
};
|
|
1740
|
-
if (varMatch[3]) {
|
|
1741
|
-
result.text = `${varMatch[3].trim()}`;
|
|
1743
|
+
if (match.groups?.quantity) {
|
|
1744
|
+
const result = {
|
|
1745
|
+
quantity: parseQuantityValue(match.groups.quantity)
|
|
1746
|
+
};
|
|
1747
|
+
if (match.groups.unit) result.unit = match.groups.unit;
|
|
1748
|
+
return result;
|
|
1742
1749
|
}
|
|
1743
|
-
return
|
|
1750
|
+
return void 0;
|
|
1744
1751
|
}
|
|
1745
1752
|
function parseListMetaVar(content, varName) {
|
|
1746
1753
|
const listMatch = content.match(
|
|
@@ -1858,12 +1865,12 @@ function parseAnyMetaVar(content, varName) {
|
|
|
1858
1865
|
if (simple) return parseMetadataValue(simple);
|
|
1859
1866
|
return void 0;
|
|
1860
1867
|
}
|
|
1861
|
-
function
|
|
1868
|
+
function getNumericValueFromYield(v) {
|
|
1862
1869
|
if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
|
|
1863
1870
|
return getNumericValue(v.quantity.value);
|
|
1864
1871
|
}
|
|
1865
1872
|
if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
|
|
1866
|
-
return
|
|
1873
|
+
return 1;
|
|
1867
1874
|
}
|
|
1868
1875
|
function extractMetadata(content) {
|
|
1869
1876
|
const metadata = {};
|
|
@@ -1988,11 +1995,16 @@ function extractMetadata(content) {
|
|
|
1988
1995
|
};
|
|
1989
1996
|
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
1990
1997
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1998
|
+
const yieldValue = parseYieldMetaVar(metadataContent);
|
|
1999
|
+
if (yieldValue) {
|
|
2000
|
+
metadata.yield = yieldValue;
|
|
2001
|
+
servings = getNumericValueFromYield(yieldValue);
|
|
2002
|
+
}
|
|
2003
|
+
for (const metaVar of ["serves", "servings"]) {
|
|
2004
|
+
const result = parseServingsMetaVar(metadataContent, metaVar);
|
|
2005
|
+
if (result !== void 0) {
|
|
2006
|
+
metadata[metaVar] = result.rawValue;
|
|
2007
|
+
servings = result.numericValue;
|
|
1996
2008
|
}
|
|
1997
2009
|
}
|
|
1998
2010
|
const tags = parseListMetaVar(metadataContent, "tags");
|
|
@@ -2273,7 +2285,7 @@ var ProductCatalog = class {
|
|
|
2273
2285
|
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
2274
2286
|
const sizes = sizeStrings.map((sizeStr) => {
|
|
2275
2287
|
const sizeAndUnitRaw = sizeStr.split("%");
|
|
2276
|
-
const sizeParsed =
|
|
2288
|
+
const sizeParsed = parseQuantityValue(
|
|
2277
2289
|
sizeAndUnitRaw[0]
|
|
2278
2290
|
);
|
|
2279
2291
|
const productSize = { size: sizeParsed };
|
|
@@ -2439,34 +2451,7 @@ function findCompatibleQuantityWithinList(list, quantity) {
|
|
|
2439
2451
|
}
|
|
2440
2452
|
|
|
2441
2453
|
// src/utils/general.ts
|
|
2442
|
-
var
|
|
2443
|
-
if (v === null || typeof v !== "object") {
|
|
2444
|
-
return v;
|
|
2445
|
-
}
|
|
2446
|
-
if (v instanceof Map) {
|
|
2447
|
-
return new Map(
|
|
2448
|
-
Array.from(v.entries()).map(([k, val]) => [
|
|
2449
|
-
legacyDeepClone(k),
|
|
2450
|
-
legacyDeepClone(val)
|
|
2451
|
-
])
|
|
2452
|
-
);
|
|
2453
|
-
}
|
|
2454
|
-
if (v instanceof Set) {
|
|
2455
|
-
return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
|
|
2456
|
-
}
|
|
2457
|
-
if (v instanceof Date) {
|
|
2458
|
-
return new Date(v.getTime());
|
|
2459
|
-
}
|
|
2460
|
-
if (Array.isArray(v)) {
|
|
2461
|
-
return v.map((item) => legacyDeepClone(item));
|
|
2462
|
-
}
|
|
2463
|
-
const cloned = {};
|
|
2464
|
-
for (const key of Object.keys(v)) {
|
|
2465
|
-
cloned[key] = legacyDeepClone(v[key]);
|
|
2466
|
-
}
|
|
2467
|
-
return cloned;
|
|
2468
|
-
};
|
|
2469
|
-
var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
|
|
2454
|
+
var deepClone = (v) => structuredClone(v);
|
|
2470
2455
|
|
|
2471
2456
|
// src/quantities/alternatives.ts
|
|
2472
2457
|
function getEquivalentUnitsLists(...quantities) {
|
|
@@ -2745,6 +2730,47 @@ function addEquivalentsAndSimplify(quantities, system) {
|
|
|
2745
2730
|
return { and: regrouped.map(toPlainUnit) };
|
|
2746
2731
|
}
|
|
2747
2732
|
}
|
|
2733
|
+
function buildEquivalenceRatioMap(unitsLists) {
|
|
2734
|
+
const ratioMap = {};
|
|
2735
|
+
for (const list of unitsLists) {
|
|
2736
|
+
for (const equiv of list) {
|
|
2737
|
+
const equivValue = getAverageValue(equiv.quantity);
|
|
2738
|
+
for (const primary of list) {
|
|
2739
|
+
if (primary === equiv) continue;
|
|
2740
|
+
const primaryValue = getAverageValue(primary.quantity);
|
|
2741
|
+
const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
|
|
2742
|
+
const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
|
|
2743
|
+
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
2744
|
+
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return ratioMap;
|
|
2749
|
+
}
|
|
2750
|
+
function recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
2751
|
+
const equivalents = [];
|
|
2752
|
+
for (const equivUnit of equivUnits) {
|
|
2753
|
+
const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
|
|
2754
|
+
let total = 0;
|
|
2755
|
+
for (const primary of primaries) {
|
|
2756
|
+
const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
|
|
2757
|
+
const ratio = ratios[pUnit];
|
|
2758
|
+
if (ratio === void 0) continue;
|
|
2759
|
+
const pValue = getAverageValue(primary.quantity);
|
|
2760
|
+
total += pValue * ratio;
|
|
2761
|
+
}
|
|
2762
|
+
if (total > 0) {
|
|
2763
|
+
equivalents.push({
|
|
2764
|
+
quantity: {
|
|
2765
|
+
type: "fixed",
|
|
2766
|
+
value: { type: "decimal", decimal: total }
|
|
2767
|
+
},
|
|
2768
|
+
...equivUnit !== "" && { unit: equivUnit }
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
2773
|
+
}
|
|
2748
2774
|
|
|
2749
2775
|
// src/classes/recipe.ts
|
|
2750
2776
|
import Big4 from "big.js";
|
|
@@ -2869,7 +2895,7 @@ var _Recipe = class _Recipe {
|
|
|
2869
2895
|
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
2870
2896
|
const quantities = [];
|
|
2871
2897
|
while (quantityMatch?.groups) {
|
|
2872
|
-
const value = quantityMatch.groups.quantity ?
|
|
2898
|
+
const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
|
|
2873
2899
|
const unit = quantityMatch.groups.unit;
|
|
2874
2900
|
if (value) {
|
|
2875
2901
|
const newQuantity = { quantity: value };
|
|
@@ -3237,7 +3263,9 @@ var _Recipe = class _Recipe {
|
|
|
3237
3263
|
} else {
|
|
3238
3264
|
const targetSubgroupIndex = 0;
|
|
3239
3265
|
const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
|
|
3240
|
-
isSelected = selectedSubgroup
|
|
3266
|
+
isSelected = selectedSubgroup.some(
|
|
3267
|
+
(alt) => alt.itemId === item.id
|
|
3268
|
+
);
|
|
3241
3269
|
}
|
|
3242
3270
|
} else {
|
|
3243
3271
|
const targetSubgroupIndex = groupChoice ?? 0;
|
|
@@ -3636,16 +3664,10 @@ var _Recipe = class _Recipe {
|
|
|
3636
3664
|
}
|
|
3637
3665
|
sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
|
|
3638
3666
|
}
|
|
3639
|
-
if (
|
|
3640
|
-
section
|
|
3641
|
-
if (sectionVariants) section.variants = sectionVariants;
|
|
3642
|
-
if (sectionOptional) section.optional = true;
|
|
3643
|
-
} else {
|
|
3644
|
-
if (!section.isBlank()) {
|
|
3645
|
-
this.sections.push(section);
|
|
3646
|
-
}
|
|
3647
|
-
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
3667
|
+
if (!section.isBlank()) {
|
|
3668
|
+
this.sections.push(section);
|
|
3648
3669
|
}
|
|
3670
|
+
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
3649
3671
|
blankLineBefore = true;
|
|
3650
3672
|
inNote = false;
|
|
3651
3673
|
continue;
|
|
@@ -3711,7 +3733,7 @@ var _Recipe = class _Recipe {
|
|
|
3711
3733
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
3712
3734
|
flags.push("hidden");
|
|
3713
3735
|
}
|
|
3714
|
-
const quantity = quantityRaw ?
|
|
3736
|
+
const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
|
|
3715
3737
|
const newCookware = {
|
|
3716
3738
|
name
|
|
3717
3739
|
};
|
|
@@ -3743,7 +3765,7 @@ var _Recipe = class _Recipe {
|
|
|
3743
3765
|
throw new Error("Timer missing unit");
|
|
3744
3766
|
}
|
|
3745
3767
|
const name = groups.timerName || void 0;
|
|
3746
|
-
const duration =
|
|
3768
|
+
const duration = parseQuantityValue(durationStr);
|
|
3747
3769
|
const timerObj = {
|
|
3748
3770
|
name,
|
|
3749
3771
|
duration,
|
|
@@ -3876,9 +3898,15 @@ var _Recipe = class _Recipe {
|
|
|
3876
3898
|
}
|
|
3877
3899
|
newRecipe._populateIngredientQuantities();
|
|
3878
3900
|
newRecipe.servings = Big4(originalServings).times(factor).toNumber();
|
|
3879
|
-
for (const metaVar of ["servings", "
|
|
3880
|
-
if (newRecipe.metadata[metaVar]
|
|
3881
|
-
|
|
3901
|
+
for (const metaVar of ["servings", "serves"]) {
|
|
3902
|
+
if (typeof newRecipe.metadata[metaVar] === "number") {
|
|
3903
|
+
newRecipe.metadata[metaVar] = Big4(newRecipe.metadata[metaVar]).times(factor).toNumber();
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
if (newRecipe.metadata.yield && this.metadata.yield) {
|
|
3907
|
+
const original = this.metadata.yield;
|
|
3908
|
+
if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
|
|
3909
|
+
} else {
|
|
3882
3910
|
const scaledQuantity = multiplyQuantityValue(
|
|
3883
3911
|
original.quantity,
|
|
3884
3912
|
factor
|
|
@@ -3893,8 +3921,7 @@ var _Recipe = class _Recipe {
|
|
|
3893
3921
|
if (optimized.unit) scaled.unit = optimized.unit;
|
|
3894
3922
|
if (original.textBefore) scaled.textBefore = original.textBefore;
|
|
3895
3923
|
if (original.textAfter) scaled.textAfter = original.textAfter;
|
|
3896
|
-
|
|
3897
|
-
newRecipe.metadata[metaVar] = scaled;
|
|
3924
|
+
newRecipe.metadata.yield = scaled;
|
|
3898
3925
|
}
|
|
3899
3926
|
}
|
|
3900
3927
|
return newRecipe;
|
|
@@ -4111,7 +4138,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
|
|
|
4111
4138
|
var Recipe = _Recipe;
|
|
4112
4139
|
|
|
4113
4140
|
// src/classes/shopping_list.ts
|
|
4114
|
-
var ShoppingList = class
|
|
4141
|
+
var ShoppingList = class {
|
|
4115
4142
|
/**
|
|
4116
4143
|
* Creates a new ShoppingList instance
|
|
4117
4144
|
* @param categoryConfigStr - The category configuration to parse.
|
|
@@ -4206,7 +4233,7 @@ var ShoppingList = class _ShoppingList {
|
|
|
4206
4233
|
}
|
|
4207
4234
|
}
|
|
4208
4235
|
if (numericEntries.length > 1) {
|
|
4209
|
-
const ratioMap =
|
|
4236
|
+
const ratioMap = buildEquivalenceRatioMap(
|
|
4210
4237
|
getEquivalentUnitsLists(...numericEntries)
|
|
4211
4238
|
);
|
|
4212
4239
|
if (Object.keys(ratioMap).length > 0) {
|
|
@@ -4263,10 +4290,12 @@ var ShoppingList = class _ShoppingList {
|
|
|
4263
4290
|
const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
|
|
4264
4291
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4265
4292
|
const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
|
|
4293
|
+
const leafDef = normalizeUnit(leaf.unit);
|
|
4294
|
+
const pantryDef = normalizeUnit(pantryExtended.unit?.name);
|
|
4266
4295
|
if (unitMismatch) {
|
|
4267
4296
|
const leafUnit = leaf.unit ?? NO_UNIT;
|
|
4268
4297
|
const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
|
|
4269
|
-
const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
|
|
4298
|
+
const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
|
|
4270
4299
|
if (ratioFromPantry !== void 0) {
|
|
4271
4300
|
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4272
4301
|
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
@@ -4281,7 +4310,7 @@ var ShoppingList = class _ShoppingList {
|
|
|
4281
4310
|
type: "fixed",
|
|
4282
4311
|
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4283
4312
|
};
|
|
4284
|
-
const consumedInPantryUnits =
|
|
4313
|
+
const consumedInPantryUnits = subtracted / ratioFromPantry;
|
|
4285
4314
|
const remainingPantryValue = Math.max(
|
|
4286
4315
|
pantryValue - consumedInPantryUnits,
|
|
4287
4316
|
0
|
|
@@ -4298,9 +4327,10 @@ var ShoppingList = class _ShoppingList {
|
|
|
4298
4327
|
};
|
|
4299
4328
|
continue;
|
|
4300
4329
|
}
|
|
4330
|
+
} else {
|
|
4331
|
+
continue;
|
|
4301
4332
|
}
|
|
4302
|
-
}
|
|
4303
|
-
try {
|
|
4333
|
+
} else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
|
|
4304
4334
|
const remaining = subtractQuantities(
|
|
4305
4335
|
ingredientExtended,
|
|
4306
4336
|
pantryExtended,
|
|
@@ -4315,7 +4345,47 @@ var ShoppingList = class _ShoppingList {
|
|
|
4315
4345
|
const updated = toPlainUnit(remaining);
|
|
4316
4346
|
leaf.quantity = updated.quantity;
|
|
4317
4347
|
leaf.unit = updated.unit;
|
|
4318
|
-
}
|
|
4348
|
+
} else if (ratioMap) {
|
|
4349
|
+
const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
|
|
4350
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
4351
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4352
|
+
if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
|
|
4353
|
+
for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
|
|
4354
|
+
const ratio = ratios[canonicalLeaf];
|
|
4355
|
+
if (ratio === void 0) continue;
|
|
4356
|
+
const equivDef = normalizeUnit(equivUnit);
|
|
4357
|
+
if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
|
|
4358
|
+
continue;
|
|
4359
|
+
const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
|
|
4360
|
+
const pantryInLeafUnits = pantryInEquiv / ratio;
|
|
4361
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
4362
|
+
const remainingLeafValue = Math.max(
|
|
4363
|
+
leafValue - pantryInLeafUnits,
|
|
4364
|
+
0
|
|
4365
|
+
);
|
|
4366
|
+
leaf.quantity = {
|
|
4367
|
+
type: "fixed",
|
|
4368
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4369
|
+
};
|
|
4370
|
+
const consumedInEquiv = subtracted * ratio;
|
|
4371
|
+
const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
|
|
4372
|
+
const remainingPantryValue = Math.max(
|
|
4373
|
+
pantryValue - consumedInPantryUnits,
|
|
4374
|
+
0
|
|
4375
|
+
);
|
|
4376
|
+
pantryExtended = {
|
|
4377
|
+
quantity: {
|
|
4378
|
+
type: "fixed",
|
|
4379
|
+
value: {
|
|
4380
|
+
type: "decimal",
|
|
4381
|
+
decimal: remainingPantryValue
|
|
4382
|
+
}
|
|
4383
|
+
},
|
|
4384
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
4385
|
+
};
|
|
4386
|
+
break;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4319
4389
|
}
|
|
4320
4390
|
}
|
|
4321
4391
|
if ("and" in entry) {
|
|
@@ -4326,8 +4396,8 @@ var ShoppingList = class _ShoppingList {
|
|
|
4326
4396
|
entry.and.push(...nonZero);
|
|
4327
4397
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4328
4398
|
if (entry.equivalents && ratioMap) {
|
|
4329
|
-
const equivUnits = entry.equivalents.map((e2) => e2.unit
|
|
4330
|
-
entry.equivalents =
|
|
4399
|
+
const equivUnits = entry.equivalents.map((e2) => e2.unit);
|
|
4400
|
+
entry.equivalents = recomputeEquivalents(
|
|
4331
4401
|
entry.and,
|
|
4332
4402
|
ratioMap,
|
|
4333
4403
|
equivUnits
|
|
@@ -4338,17 +4408,17 @@ var ShoppingList = class _ShoppingList {
|
|
|
4338
4408
|
ingredient.quantities[i2] = {
|
|
4339
4409
|
quantity: single.quantity,
|
|
4340
4410
|
...single.unit && { unit: single.unit },
|
|
4341
|
-
...entry.equivalents && { equivalents: entry.equivalents }
|
|
4342
|
-
...entry.alternatives && { alternatives: entry.alternatives }
|
|
4411
|
+
...entry.equivalents && { equivalents: entry.equivalents }
|
|
4343
4412
|
};
|
|
4344
4413
|
}
|
|
4345
4414
|
} else if ("equivalents" in entry && entry.equivalents) {
|
|
4346
4415
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4347
4416
|
if (ratioMap) {
|
|
4348
4417
|
const equivUnits = entry.equivalents.map(
|
|
4349
|
-
(e2) => e2.unit
|
|
4418
|
+
(e2) => e2.unit
|
|
4419
|
+
// equivalents always have units
|
|
4350
4420
|
);
|
|
4351
|
-
const recomputed =
|
|
4421
|
+
const recomputed = recomputeEquivalents(
|
|
4352
4422
|
[entry],
|
|
4353
4423
|
ratioMap,
|
|
4354
4424
|
equivUnits
|
|
@@ -4371,57 +4441,6 @@ var ShoppingList = class _ShoppingList {
|
|
|
4371
4441
|
}
|
|
4372
4442
|
this.resultingPantry = clonedPantry;
|
|
4373
4443
|
}
|
|
4374
|
-
/**
|
|
4375
|
-
* Builds a ratio map from equivalence lists.
|
|
4376
|
-
* For each equivalence list, stores ratio = equiv_value / primary_value
|
|
4377
|
-
* for every pair of units, so equivalents can be recomputed after
|
|
4378
|
-
* pantry subtraction modifies primary quantities.
|
|
4379
|
-
*/
|
|
4380
|
-
static buildEquivalenceRatioMap(unitsLists) {
|
|
4381
|
-
const ratioMap = {};
|
|
4382
|
-
for (const list of unitsLists) {
|
|
4383
|
-
for (const equiv of list) {
|
|
4384
|
-
const equivValue = getAverageValue(equiv.quantity);
|
|
4385
|
-
for (const primary of list) {
|
|
4386
|
-
if (primary === equiv) continue;
|
|
4387
|
-
const primaryValue = getAverageValue(primary.quantity);
|
|
4388
|
-
const equivUnit = equiv.unit.name;
|
|
4389
|
-
const primaryUnit = primary.unit.name;
|
|
4390
|
-
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
4391
|
-
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
4392
|
-
}
|
|
4393
|
-
}
|
|
4394
|
-
}
|
|
4395
|
-
return ratioMap;
|
|
4396
|
-
}
|
|
4397
|
-
/**
|
|
4398
|
-
* Recomputes equivalent quantities from current primary values and stored ratios.
|
|
4399
|
-
* For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
|
|
4400
|
-
* Returns undefined if all equivalents compute to zero.
|
|
4401
|
-
*/
|
|
4402
|
-
static recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
4403
|
-
const equivalents = [];
|
|
4404
|
-
for (const equivUnit of equivUnits) {
|
|
4405
|
-
const ratios = ratioMap[equivUnit];
|
|
4406
|
-
let total = 0;
|
|
4407
|
-
for (const primary of primaries) {
|
|
4408
|
-
const pUnit = primary.unit ?? NO_UNIT;
|
|
4409
|
-
const ratio = ratios[pUnit];
|
|
4410
|
-
const pValue = getAverageValue(primary.quantity);
|
|
4411
|
-
total += pValue * ratio;
|
|
4412
|
-
}
|
|
4413
|
-
if (total > 0) {
|
|
4414
|
-
equivalents.push({
|
|
4415
|
-
quantity: {
|
|
4416
|
-
type: "fixed",
|
|
4417
|
-
value: { type: "decimal", decimal: total }
|
|
4418
|
-
},
|
|
4419
|
-
...equivUnit !== "" && { unit: equivUnit }
|
|
4420
|
-
});
|
|
4421
|
-
}
|
|
4422
|
-
}
|
|
4423
|
-
return equivalents.length > 0 ? equivalents : void 0;
|
|
4424
|
-
}
|
|
4425
4444
|
/**
|
|
4426
4445
|
* Adds a recipe to the shopping list, then automatically
|
|
4427
4446
|
* recalculates the quantities and recategorize the ingredients.
|
|
@@ -5035,6 +5054,9 @@ export {
|
|
|
5035
5054
|
// v8 ignore if -- @preserve: defensive type guard
|
|
5036
5055
|
/* v8 ignore if -- @preserve */
|
|
5037
5056
|
// v8 ignore next -- @preserve
|
|
5057
|
+
// v8 ignore else -- @preserve: text quantities never reach the equivalence path
|
|
5038
5058
|
// v8 ignore else --@preserve: defensive type guard
|
|
5039
5059
|
// v8 ignore else -- @preserve: detection if
|
|
5060
|
+
/* v8 ignore else -- @preserve: only act when there are matches */
|
|
5061
|
+
/* v8 ignore else -- @preserve: initialization pattern */
|
|
5040
5062
|
//# sourceMappingURL=index.js.map
|