@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.cjs
CHANGED
|
@@ -335,7 +335,7 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
|
335
335
|
var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
|
|
336
336
|
var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
|
|
337
337
|
var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
|
|
338
|
-
var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
|
|
338
|
+
var 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();
|
|
339
339
|
var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict + "/").oneOrMore().endGroup().startGroup().literal("/").startNamedGroup("gIngredientSubgroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().endGroup().optional().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("gIngredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().toRegExp();
|
|
340
340
|
var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
|
|
341
341
|
var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
|
|
@@ -351,11 +351,18 @@ var tokensRegex = new RegExp(
|
|
|
351
351
|
].map((r2) => r2.source).join("|"),
|
|
352
352
|
"gu"
|
|
353
353
|
);
|
|
354
|
-
var
|
|
355
|
-
var
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
354
|
+
var yieldPrefixPart = d().startAnchor().literal("yield").literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
|
|
355
|
+
var yieldSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
|
|
356
|
+
var yieldMetaValueWithUnitRegex = new RegExp(
|
|
357
|
+
yieldPrefixPart.source + arbitraryScalableRegex.source + yieldSuffixPart.source,
|
|
358
|
+
"m"
|
|
359
|
+
);
|
|
360
|
+
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();
|
|
361
|
+
var yieldMetaValueRegex = new RegExp(
|
|
362
|
+
[
|
|
363
|
+
yieldMetaValueWithUnitRegex.source,
|
|
364
|
+
yieldMetaValueAsQuantityRegex.source
|
|
365
|
+
].join("|"),
|
|
359
366
|
"m"
|
|
360
367
|
);
|
|
361
368
|
var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
|
|
@@ -364,7 +371,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
|
|
|
364
371
|
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();
|
|
365
372
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
366
373
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
367
|
-
var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").
|
|
374
|
+
var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").oneOrMore().endGroup().optional().literal("]").whitespace().zeroOrMore().toRegExp();
|
|
368
375
|
var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
|
|
369
376
|
var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
|
|
370
377
|
var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
|
|
@@ -940,7 +947,7 @@ var NoProductMatchError = class extends Error {
|
|
|
940
947
|
constructor(item_name, code) {
|
|
941
948
|
const messageMap = {
|
|
942
949
|
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
943
|
-
noProduct:
|
|
950
|
+
noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
|
|
944
951
|
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
945
952
|
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
946
953
|
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`
|
|
@@ -1543,7 +1550,7 @@ function stringifyFixedValue(quantity) {
|
|
|
1543
1550
|
return String(quantity.value.decimal);
|
|
1544
1551
|
else return quantity.value.text;
|
|
1545
1552
|
}
|
|
1546
|
-
function
|
|
1553
|
+
function parseQuantityValue(input_str) {
|
|
1547
1554
|
const clean_str = String(input_str).trim();
|
|
1548
1555
|
if (rangeRegex.test(clean_str)) {
|
|
1549
1556
|
const range_parts = clean_str.split("-");
|
|
@@ -1557,12 +1564,12 @@ function parseQuantityWithUnit(input) {
|
|
|
1557
1564
|
const trimmed = input.trim();
|
|
1558
1565
|
const separatorIndex = trimmed.indexOf("%");
|
|
1559
1566
|
if (separatorIndex === -1) {
|
|
1560
|
-
return { value:
|
|
1567
|
+
return { value: parseQuantityValue(trimmed) };
|
|
1561
1568
|
}
|
|
1562
1569
|
const valuePart = trimmed.slice(0, separatorIndex).trim();
|
|
1563
1570
|
const unitPart = trimmed.slice(separatorIndex + 1).trim();
|
|
1564
1571
|
return {
|
|
1565
|
-
value:
|
|
1572
|
+
value: parseQuantityValue(valuePart),
|
|
1566
1573
|
unit: unitPart || void 0
|
|
1567
1574
|
};
|
|
1568
1575
|
}
|
|
@@ -1752,7 +1759,7 @@ function parseArbitraryQuantity(raw) {
|
|
|
1752
1759
|
"Arbitrary quantities must have a numerical value"
|
|
1753
1760
|
);
|
|
1754
1761
|
}
|
|
1755
|
-
const value =
|
|
1762
|
+
const value = parseQuantityValue(quantityMatch.groups.quantity);
|
|
1756
1763
|
const unit = quantityMatch.groups.unit;
|
|
1757
1764
|
if (!value || value.type === "fixed" && value.value.type === "text") {
|
|
1758
1765
|
throw new InvalidQuantityFormat(
|
|
@@ -1766,40 +1773,40 @@ function parseArbitraryQuantity(raw) {
|
|
|
1766
1773
|
if (unit) arbitrary.unit = unit;
|
|
1767
1774
|
return arbitrary;
|
|
1768
1775
|
}
|
|
1769
|
-
function
|
|
1770
|
-
const
|
|
1771
|
-
if (
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
+
function parseServingsMetaVar(content, varName) {
|
|
1777
|
+
const raw = parseSimpleMetaVar(content, varName);
|
|
1778
|
+
if (raw === void 0) return void 0;
|
|
1779
|
+
const num = Number(raw);
|
|
1780
|
+
if (isNaN(num)) {
|
|
1781
|
+
return { numericValue: 1, rawValue: raw };
|
|
1782
|
+
}
|
|
1783
|
+
return { numericValue: num, rawValue: num };
|
|
1784
|
+
}
|
|
1785
|
+
function parseYieldMetaVar(content) {
|
|
1786
|
+
const match = content.match(yieldMetaValueRegex);
|
|
1787
|
+
if (!match) return void 0;
|
|
1788
|
+
if (match.groups?.arbitraryQuantity) {
|
|
1789
|
+
const parsed = parseArbitraryQuantity(match.groups.arbitraryQuantity);
|
|
1790
|
+
const result = {
|
|
1776
1791
|
quantity: parsed.quantity
|
|
1777
1792
|
};
|
|
1778
|
-
if (parsed.unit)
|
|
1779
|
-
if (
|
|
1780
|
-
|
|
1793
|
+
if (parsed.unit) result.unit = parsed.unit;
|
|
1794
|
+
if (match.groups.servingsPrefix) {
|
|
1795
|
+
result.textBefore = match.groups.servingsPrefix;
|
|
1781
1796
|
}
|
|
1782
|
-
if (
|
|
1783
|
-
|
|
1797
|
+
if (match.groups.servingsSuffix) {
|
|
1798
|
+
result.textAfter = match.groups.servingsSuffix;
|
|
1784
1799
|
}
|
|
1785
|
-
return
|
|
1786
|
-
}
|
|
1787
|
-
const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
|
|
1788
|
-
if (!varMatch) return void 0;
|
|
1789
|
-
if (isNaN(Number(varMatch[2]?.trim()))) {
|
|
1790
|
-
throw new Error("Scaling variables should be numbers");
|
|
1800
|
+
return result;
|
|
1791
1801
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
};
|
|
1799
|
-
if (varMatch[3]) {
|
|
1800
|
-
result.text = `${varMatch[3].trim()}`;
|
|
1802
|
+
if (match.groups?.quantity) {
|
|
1803
|
+
const result = {
|
|
1804
|
+
quantity: parseQuantityValue(match.groups.quantity)
|
|
1805
|
+
};
|
|
1806
|
+
if (match.groups.unit) result.unit = match.groups.unit;
|
|
1807
|
+
return result;
|
|
1801
1808
|
}
|
|
1802
|
-
return
|
|
1809
|
+
return void 0;
|
|
1803
1810
|
}
|
|
1804
1811
|
function parseListMetaVar(content, varName) {
|
|
1805
1812
|
const listMatch = content.match(
|
|
@@ -1917,12 +1924,12 @@ function parseAnyMetaVar(content, varName) {
|
|
|
1917
1924
|
if (simple) return parseMetadataValue(simple);
|
|
1918
1925
|
return void 0;
|
|
1919
1926
|
}
|
|
1920
|
-
function
|
|
1927
|
+
function getNumericValueFromYield(v) {
|
|
1921
1928
|
if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
|
|
1922
1929
|
return getNumericValue(v.quantity.value);
|
|
1923
1930
|
}
|
|
1924
1931
|
if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
|
|
1925
|
-
return
|
|
1932
|
+
return 1;
|
|
1926
1933
|
}
|
|
1927
1934
|
function extractMetadata(content) {
|
|
1928
1935
|
const metadata = {};
|
|
@@ -2047,11 +2054,16 @@ function extractMetadata(content) {
|
|
|
2047
2054
|
};
|
|
2048
2055
|
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
2049
2056
|
}
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2057
|
+
const yieldValue = parseYieldMetaVar(metadataContent);
|
|
2058
|
+
if (yieldValue) {
|
|
2059
|
+
metadata.yield = yieldValue;
|
|
2060
|
+
servings = getNumericValueFromYield(yieldValue);
|
|
2061
|
+
}
|
|
2062
|
+
for (const metaVar of ["serves", "servings"]) {
|
|
2063
|
+
const result = parseServingsMetaVar(metadataContent, metaVar);
|
|
2064
|
+
if (result !== void 0) {
|
|
2065
|
+
metadata[metaVar] = result.rawValue;
|
|
2066
|
+
servings = result.numericValue;
|
|
2055
2067
|
}
|
|
2056
2068
|
}
|
|
2057
2069
|
const tags = parseListMetaVar(metadataContent, "tags");
|
|
@@ -2332,7 +2344,7 @@ var ProductCatalog = class {
|
|
|
2332
2344
|
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
2333
2345
|
const sizes = sizeStrings.map((sizeStr) => {
|
|
2334
2346
|
const sizeAndUnitRaw = sizeStr.split("%");
|
|
2335
|
-
const sizeParsed =
|
|
2347
|
+
const sizeParsed = parseQuantityValue(
|
|
2336
2348
|
sizeAndUnitRaw[0]
|
|
2337
2349
|
);
|
|
2338
2350
|
const productSize = { size: sizeParsed };
|
|
@@ -2498,34 +2510,7 @@ function findCompatibleQuantityWithinList(list, quantity) {
|
|
|
2498
2510
|
}
|
|
2499
2511
|
|
|
2500
2512
|
// src/utils/general.ts
|
|
2501
|
-
var
|
|
2502
|
-
if (v === null || typeof v !== "object") {
|
|
2503
|
-
return v;
|
|
2504
|
-
}
|
|
2505
|
-
if (v instanceof Map) {
|
|
2506
|
-
return new Map(
|
|
2507
|
-
Array.from(v.entries()).map(([k, val]) => [
|
|
2508
|
-
legacyDeepClone(k),
|
|
2509
|
-
legacyDeepClone(val)
|
|
2510
|
-
])
|
|
2511
|
-
);
|
|
2512
|
-
}
|
|
2513
|
-
if (v instanceof Set) {
|
|
2514
|
-
return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
|
|
2515
|
-
}
|
|
2516
|
-
if (v instanceof Date) {
|
|
2517
|
-
return new Date(v.getTime());
|
|
2518
|
-
}
|
|
2519
|
-
if (Array.isArray(v)) {
|
|
2520
|
-
return v.map((item) => legacyDeepClone(item));
|
|
2521
|
-
}
|
|
2522
|
-
const cloned = {};
|
|
2523
|
-
for (const key of Object.keys(v)) {
|
|
2524
|
-
cloned[key] = legacyDeepClone(v[key]);
|
|
2525
|
-
}
|
|
2526
|
-
return cloned;
|
|
2527
|
-
};
|
|
2528
|
-
var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
|
|
2513
|
+
var deepClone = (v) => structuredClone(v);
|
|
2529
2514
|
|
|
2530
2515
|
// src/quantities/alternatives.ts
|
|
2531
2516
|
function getEquivalentUnitsLists(...quantities) {
|
|
@@ -2804,6 +2789,47 @@ function addEquivalentsAndSimplify(quantities, system) {
|
|
|
2804
2789
|
return { and: regrouped.map(toPlainUnit) };
|
|
2805
2790
|
}
|
|
2806
2791
|
}
|
|
2792
|
+
function buildEquivalenceRatioMap(unitsLists) {
|
|
2793
|
+
const ratioMap = {};
|
|
2794
|
+
for (const list of unitsLists) {
|
|
2795
|
+
for (const equiv of list) {
|
|
2796
|
+
const equivValue = getAverageValue(equiv.quantity);
|
|
2797
|
+
for (const primary of list) {
|
|
2798
|
+
if (primary === equiv) continue;
|
|
2799
|
+
const primaryValue = getAverageValue(primary.quantity);
|
|
2800
|
+
const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
|
|
2801
|
+
const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
|
|
2802
|
+
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
2803
|
+
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
return ratioMap;
|
|
2808
|
+
}
|
|
2809
|
+
function recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
2810
|
+
const equivalents = [];
|
|
2811
|
+
for (const equivUnit of equivUnits) {
|
|
2812
|
+
const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
|
|
2813
|
+
let total = 0;
|
|
2814
|
+
for (const primary of primaries) {
|
|
2815
|
+
const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
|
|
2816
|
+
const ratio = ratios[pUnit];
|
|
2817
|
+
if (ratio === void 0) continue;
|
|
2818
|
+
const pValue = getAverageValue(primary.quantity);
|
|
2819
|
+
total += pValue * ratio;
|
|
2820
|
+
}
|
|
2821
|
+
if (total > 0) {
|
|
2822
|
+
equivalents.push({
|
|
2823
|
+
quantity: {
|
|
2824
|
+
type: "fixed",
|
|
2825
|
+
value: { type: "decimal", decimal: total }
|
|
2826
|
+
},
|
|
2827
|
+
...equivUnit !== "" && { unit: equivUnit }
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
2832
|
+
}
|
|
2807
2833
|
|
|
2808
2834
|
// src/classes/recipe.ts
|
|
2809
2835
|
var import_big4 = __toESM(require("big.js"), 1);
|
|
@@ -2928,7 +2954,7 @@ var _Recipe = class _Recipe {
|
|
|
2928
2954
|
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
2929
2955
|
const quantities = [];
|
|
2930
2956
|
while (quantityMatch?.groups) {
|
|
2931
|
-
const value = quantityMatch.groups.quantity ?
|
|
2957
|
+
const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
|
|
2932
2958
|
const unit = quantityMatch.groups.unit;
|
|
2933
2959
|
if (value) {
|
|
2934
2960
|
const newQuantity = { quantity: value };
|
|
@@ -3296,7 +3322,9 @@ var _Recipe = class _Recipe {
|
|
|
3296
3322
|
} else {
|
|
3297
3323
|
const targetSubgroupIndex = 0;
|
|
3298
3324
|
const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
|
|
3299
|
-
isSelected = selectedSubgroup
|
|
3325
|
+
isSelected = selectedSubgroup.some(
|
|
3326
|
+
(alt) => alt.itemId === item.id
|
|
3327
|
+
);
|
|
3300
3328
|
}
|
|
3301
3329
|
} else {
|
|
3302
3330
|
const targetSubgroupIndex = groupChoice ?? 0;
|
|
@@ -3695,16 +3723,10 @@ var _Recipe = class _Recipe {
|
|
|
3695
3723
|
}
|
|
3696
3724
|
sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
|
|
3697
3725
|
}
|
|
3698
|
-
if (
|
|
3699
|
-
section
|
|
3700
|
-
if (sectionVariants) section.variants = sectionVariants;
|
|
3701
|
-
if (sectionOptional) section.optional = true;
|
|
3702
|
-
} else {
|
|
3703
|
-
if (!section.isBlank()) {
|
|
3704
|
-
this.sections.push(section);
|
|
3705
|
-
}
|
|
3706
|
-
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
3726
|
+
if (!section.isBlank()) {
|
|
3727
|
+
this.sections.push(section);
|
|
3707
3728
|
}
|
|
3729
|
+
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
3708
3730
|
blankLineBefore = true;
|
|
3709
3731
|
inNote = false;
|
|
3710
3732
|
continue;
|
|
@@ -3770,7 +3792,7 @@ var _Recipe = class _Recipe {
|
|
|
3770
3792
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
3771
3793
|
flags.push("hidden");
|
|
3772
3794
|
}
|
|
3773
|
-
const quantity = quantityRaw ?
|
|
3795
|
+
const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
|
|
3774
3796
|
const newCookware = {
|
|
3775
3797
|
name
|
|
3776
3798
|
};
|
|
@@ -3802,7 +3824,7 @@ var _Recipe = class _Recipe {
|
|
|
3802
3824
|
throw new Error("Timer missing unit");
|
|
3803
3825
|
}
|
|
3804
3826
|
const name = groups.timerName || void 0;
|
|
3805
|
-
const duration =
|
|
3827
|
+
const duration = parseQuantityValue(durationStr);
|
|
3806
3828
|
const timerObj = {
|
|
3807
3829
|
name,
|
|
3808
3830
|
duration,
|
|
@@ -3935,9 +3957,15 @@ var _Recipe = class _Recipe {
|
|
|
3935
3957
|
}
|
|
3936
3958
|
newRecipe._populateIngredientQuantities();
|
|
3937
3959
|
newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
|
|
3938
|
-
for (const metaVar of ["servings", "
|
|
3939
|
-
if (newRecipe.metadata[metaVar]
|
|
3940
|
-
|
|
3960
|
+
for (const metaVar of ["servings", "serves"]) {
|
|
3961
|
+
if (typeof newRecipe.metadata[metaVar] === "number") {
|
|
3962
|
+
newRecipe.metadata[metaVar] = (0, import_big4.default)(newRecipe.metadata[metaVar]).times(factor).toNumber();
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
if (newRecipe.metadata.yield && this.metadata.yield) {
|
|
3966
|
+
const original = this.metadata.yield;
|
|
3967
|
+
if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
|
|
3968
|
+
} else {
|
|
3941
3969
|
const scaledQuantity = multiplyQuantityValue(
|
|
3942
3970
|
original.quantity,
|
|
3943
3971
|
factor
|
|
@@ -3952,8 +3980,7 @@ var _Recipe = class _Recipe {
|
|
|
3952
3980
|
if (optimized.unit) scaled.unit = optimized.unit;
|
|
3953
3981
|
if (original.textBefore) scaled.textBefore = original.textBefore;
|
|
3954
3982
|
if (original.textAfter) scaled.textAfter = original.textAfter;
|
|
3955
|
-
|
|
3956
|
-
newRecipe.metadata[metaVar] = scaled;
|
|
3983
|
+
newRecipe.metadata.yield = scaled;
|
|
3957
3984
|
}
|
|
3958
3985
|
}
|
|
3959
3986
|
return newRecipe;
|
|
@@ -4170,7 +4197,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
|
|
|
4170
4197
|
var Recipe = _Recipe;
|
|
4171
4198
|
|
|
4172
4199
|
// src/classes/shopping_list.ts
|
|
4173
|
-
var ShoppingList = class
|
|
4200
|
+
var ShoppingList = class {
|
|
4174
4201
|
/**
|
|
4175
4202
|
* Creates a new ShoppingList instance
|
|
4176
4203
|
* @param categoryConfigStr - The category configuration to parse.
|
|
@@ -4265,7 +4292,7 @@ var ShoppingList = class _ShoppingList {
|
|
|
4265
4292
|
}
|
|
4266
4293
|
}
|
|
4267
4294
|
if (numericEntries.length > 1) {
|
|
4268
|
-
const ratioMap =
|
|
4295
|
+
const ratioMap = buildEquivalenceRatioMap(
|
|
4269
4296
|
getEquivalentUnitsLists(...numericEntries)
|
|
4270
4297
|
);
|
|
4271
4298
|
if (Object.keys(ratioMap).length > 0) {
|
|
@@ -4322,10 +4349,12 @@ var ShoppingList = class _ShoppingList {
|
|
|
4322
4349
|
const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
|
|
4323
4350
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4324
4351
|
const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
|
|
4352
|
+
const leafDef = normalizeUnit(leaf.unit);
|
|
4353
|
+
const pantryDef = normalizeUnit(pantryExtended.unit?.name);
|
|
4325
4354
|
if (unitMismatch) {
|
|
4326
4355
|
const leafUnit = leaf.unit ?? NO_UNIT;
|
|
4327
4356
|
const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
|
|
4328
|
-
const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
|
|
4357
|
+
const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
|
|
4329
4358
|
if (ratioFromPantry !== void 0) {
|
|
4330
4359
|
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4331
4360
|
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
@@ -4340,7 +4369,7 @@ var ShoppingList = class _ShoppingList {
|
|
|
4340
4369
|
type: "fixed",
|
|
4341
4370
|
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4342
4371
|
};
|
|
4343
|
-
const consumedInPantryUnits =
|
|
4372
|
+
const consumedInPantryUnits = subtracted / ratioFromPantry;
|
|
4344
4373
|
const remainingPantryValue = Math.max(
|
|
4345
4374
|
pantryValue - consumedInPantryUnits,
|
|
4346
4375
|
0
|
|
@@ -4357,9 +4386,10 @@ var ShoppingList = class _ShoppingList {
|
|
|
4357
4386
|
};
|
|
4358
4387
|
continue;
|
|
4359
4388
|
}
|
|
4389
|
+
} else {
|
|
4390
|
+
continue;
|
|
4360
4391
|
}
|
|
4361
|
-
}
|
|
4362
|
-
try {
|
|
4392
|
+
} else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
|
|
4363
4393
|
const remaining = subtractQuantities(
|
|
4364
4394
|
ingredientExtended,
|
|
4365
4395
|
pantryExtended,
|
|
@@ -4374,7 +4404,47 @@ var ShoppingList = class _ShoppingList {
|
|
|
4374
4404
|
const updated = toPlainUnit(remaining);
|
|
4375
4405
|
leaf.quantity = updated.quantity;
|
|
4376
4406
|
leaf.unit = updated.unit;
|
|
4377
|
-
}
|
|
4407
|
+
} else if (ratioMap) {
|
|
4408
|
+
const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
|
|
4409
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
4410
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4411
|
+
if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
|
|
4412
|
+
for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
|
|
4413
|
+
const ratio = ratios[canonicalLeaf];
|
|
4414
|
+
if (ratio === void 0) continue;
|
|
4415
|
+
const equivDef = normalizeUnit(equivUnit);
|
|
4416
|
+
if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
|
|
4417
|
+
continue;
|
|
4418
|
+
const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
|
|
4419
|
+
const pantryInLeafUnits = pantryInEquiv / ratio;
|
|
4420
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
4421
|
+
const remainingLeafValue = Math.max(
|
|
4422
|
+
leafValue - pantryInLeafUnits,
|
|
4423
|
+
0
|
|
4424
|
+
);
|
|
4425
|
+
leaf.quantity = {
|
|
4426
|
+
type: "fixed",
|
|
4427
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4428
|
+
};
|
|
4429
|
+
const consumedInEquiv = subtracted * ratio;
|
|
4430
|
+
const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
|
|
4431
|
+
const remainingPantryValue = Math.max(
|
|
4432
|
+
pantryValue - consumedInPantryUnits,
|
|
4433
|
+
0
|
|
4434
|
+
);
|
|
4435
|
+
pantryExtended = {
|
|
4436
|
+
quantity: {
|
|
4437
|
+
type: "fixed",
|
|
4438
|
+
value: {
|
|
4439
|
+
type: "decimal",
|
|
4440
|
+
decimal: remainingPantryValue
|
|
4441
|
+
}
|
|
4442
|
+
},
|
|
4443
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
4444
|
+
};
|
|
4445
|
+
break;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4378
4448
|
}
|
|
4379
4449
|
}
|
|
4380
4450
|
if ("and" in entry) {
|
|
@@ -4385,8 +4455,8 @@ var ShoppingList = class _ShoppingList {
|
|
|
4385
4455
|
entry.and.push(...nonZero);
|
|
4386
4456
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4387
4457
|
if (entry.equivalents && ratioMap) {
|
|
4388
|
-
const equivUnits = entry.equivalents.map((e2) => e2.unit
|
|
4389
|
-
entry.equivalents =
|
|
4458
|
+
const equivUnits = entry.equivalents.map((e2) => e2.unit);
|
|
4459
|
+
entry.equivalents = recomputeEquivalents(
|
|
4390
4460
|
entry.and,
|
|
4391
4461
|
ratioMap,
|
|
4392
4462
|
equivUnits
|
|
@@ -4397,17 +4467,17 @@ var ShoppingList = class _ShoppingList {
|
|
|
4397
4467
|
ingredient.quantities[i2] = {
|
|
4398
4468
|
quantity: single.quantity,
|
|
4399
4469
|
...single.unit && { unit: single.unit },
|
|
4400
|
-
...entry.equivalents && { equivalents: entry.equivalents }
|
|
4401
|
-
...entry.alternatives && { alternatives: entry.alternatives }
|
|
4470
|
+
...entry.equivalents && { equivalents: entry.equivalents }
|
|
4402
4471
|
};
|
|
4403
4472
|
}
|
|
4404
4473
|
} else if ("equivalents" in entry && entry.equivalents) {
|
|
4405
4474
|
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4406
4475
|
if (ratioMap) {
|
|
4407
4476
|
const equivUnits = entry.equivalents.map(
|
|
4408
|
-
(e2) => e2.unit
|
|
4477
|
+
(e2) => e2.unit
|
|
4478
|
+
// equivalents always have units
|
|
4409
4479
|
);
|
|
4410
|
-
const recomputed =
|
|
4480
|
+
const recomputed = recomputeEquivalents(
|
|
4411
4481
|
[entry],
|
|
4412
4482
|
ratioMap,
|
|
4413
4483
|
equivUnits
|
|
@@ -4430,57 +4500,6 @@ var ShoppingList = class _ShoppingList {
|
|
|
4430
4500
|
}
|
|
4431
4501
|
this.resultingPantry = clonedPantry;
|
|
4432
4502
|
}
|
|
4433
|
-
/**
|
|
4434
|
-
* Builds a ratio map from equivalence lists.
|
|
4435
|
-
* For each equivalence list, stores ratio = equiv_value / primary_value
|
|
4436
|
-
* for every pair of units, so equivalents can be recomputed after
|
|
4437
|
-
* pantry subtraction modifies primary quantities.
|
|
4438
|
-
*/
|
|
4439
|
-
static buildEquivalenceRatioMap(unitsLists) {
|
|
4440
|
-
const ratioMap = {};
|
|
4441
|
-
for (const list of unitsLists) {
|
|
4442
|
-
for (const equiv of list) {
|
|
4443
|
-
const equivValue = getAverageValue(equiv.quantity);
|
|
4444
|
-
for (const primary of list) {
|
|
4445
|
-
if (primary === equiv) continue;
|
|
4446
|
-
const primaryValue = getAverageValue(primary.quantity);
|
|
4447
|
-
const equivUnit = equiv.unit.name;
|
|
4448
|
-
const primaryUnit = primary.unit.name;
|
|
4449
|
-
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
4450
|
-
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
4451
|
-
}
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
return ratioMap;
|
|
4455
|
-
}
|
|
4456
|
-
/**
|
|
4457
|
-
* Recomputes equivalent quantities from current primary values and stored ratios.
|
|
4458
|
-
* For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
|
|
4459
|
-
* Returns undefined if all equivalents compute to zero.
|
|
4460
|
-
*/
|
|
4461
|
-
static recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
4462
|
-
const equivalents = [];
|
|
4463
|
-
for (const equivUnit of equivUnits) {
|
|
4464
|
-
const ratios = ratioMap[equivUnit];
|
|
4465
|
-
let total = 0;
|
|
4466
|
-
for (const primary of primaries) {
|
|
4467
|
-
const pUnit = primary.unit ?? NO_UNIT;
|
|
4468
|
-
const ratio = ratios[pUnit];
|
|
4469
|
-
const pValue = getAverageValue(primary.quantity);
|
|
4470
|
-
total += pValue * ratio;
|
|
4471
|
-
}
|
|
4472
|
-
if (total > 0) {
|
|
4473
|
-
equivalents.push({
|
|
4474
|
-
quantity: {
|
|
4475
|
-
type: "fixed",
|
|
4476
|
-
value: { type: "decimal", decimal: total }
|
|
4477
|
-
},
|
|
4478
|
-
...equivUnit !== "" && { unit: equivUnit }
|
|
4479
|
-
});
|
|
4480
|
-
}
|
|
4481
|
-
}
|
|
4482
|
-
return equivalents.length > 0 ? equivalents : void 0;
|
|
4483
|
-
}
|
|
4484
4503
|
/**
|
|
4485
4504
|
* Adds a recipe to the shopping list, then automatically
|
|
4486
4505
|
* recalculates the quantities and recategorize the ingredients.
|
|
@@ -5095,6 +5114,9 @@ function getEffectiveChoices(recipe, variant) {
|
|
|
5095
5114
|
// v8 ignore if -- @preserve: defensive type guard
|
|
5096
5115
|
/* v8 ignore if -- @preserve */
|
|
5097
5116
|
// v8 ignore next -- @preserve
|
|
5117
|
+
// v8 ignore else -- @preserve: text quantities never reach the equivalence path
|
|
5098
5118
|
// v8 ignore else --@preserve: defensive type guard
|
|
5099
5119
|
// v8 ignore else -- @preserve: detection if
|
|
5120
|
+
/* v8 ignore else -- @preserve: only act when there are matches */
|
|
5121
|
+
/* v8 ignore else -- @preserve: initialization pattern */
|
|
5100
5122
|
//# sourceMappingURL=index.cjs.map
|