@tmlmt/cooklang-parser 3.0.0-alpha.11 → 3.0.0-alpha.13
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 +294 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -71
- package/dist/index.d.ts +83 -71
- package/dist/index.js +294 -81
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -265,6 +265,12 @@ var i = (() => {
|
|
|
265
265
|
})();
|
|
266
266
|
|
|
267
267
|
// src/regex.ts
|
|
268
|
+
var metadataKeyRegex = /^([^:\n]+?):/gm;
|
|
269
|
+
var numericValueRegex = /^-?\d+(\.\d+)?$/;
|
|
270
|
+
var nestedMetaVarRegex = (varName) => new RegExp(
|
|
271
|
+
`^${varName}:\\s*\\r?\\n((?:[ ]+.+(?:\\r?\\n|$))+)`,
|
|
272
|
+
"m"
|
|
273
|
+
);
|
|
268
274
|
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
269
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();
|
|
270
276
|
var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
@@ -876,6 +882,20 @@ var InvalidQuantityFormat = class extends Error {
|
|
|
876
882
|
this.name = "InvalidQuantityFormat";
|
|
877
883
|
}
|
|
878
884
|
};
|
|
885
|
+
var NoTabAsIndentError = class extends Error {
|
|
886
|
+
constructor() {
|
|
887
|
+
super(
|
|
888
|
+
`Tabs are not allowed for indentation in metadata blocks. Please use spaces only.`
|
|
889
|
+
);
|
|
890
|
+
this.name = "NoTabAsIndentError";
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var BadIndentationError = class extends Error {
|
|
894
|
+
constructor() {
|
|
895
|
+
super(`Bad identation of a nested block. Please use spaces only.`);
|
|
896
|
+
this.name = "BadIndentationError";
|
|
897
|
+
}
|
|
898
|
+
};
|
|
879
899
|
|
|
880
900
|
// src/utils/type_guards.ts
|
|
881
901
|
function isGroup(x) {
|
|
@@ -1433,6 +1453,108 @@ function parseListMetaVar(content, varName) {
|
|
|
1433
1453
|
return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
|
|
1434
1454
|
}
|
|
1435
1455
|
}
|
|
1456
|
+
function extractAllMetadataKeys(content) {
|
|
1457
|
+
const keys = [];
|
|
1458
|
+
for (const match of content.matchAll(metadataKeyRegex)) {
|
|
1459
|
+
keys.push(match[1].trim());
|
|
1460
|
+
}
|
|
1461
|
+
return [...new Set(keys)];
|
|
1462
|
+
}
|
|
1463
|
+
function parseNestedMetaVar(content, varName) {
|
|
1464
|
+
const match = content.match(nestedMetaVarRegex(varName));
|
|
1465
|
+
if (!match) return void 0;
|
|
1466
|
+
const nestedContent = match[1];
|
|
1467
|
+
return parseNestedBlock(nestedContent);
|
|
1468
|
+
}
|
|
1469
|
+
function parseNestedBlock(content) {
|
|
1470
|
+
const lines = content.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
1471
|
+
if (lines.length === 0) return void 0;
|
|
1472
|
+
const baseIndentMatch = lines[0].match(/^(\s*)/);
|
|
1473
|
+
if (baseIndentMatch?.[0]?.includes(" ")) {
|
|
1474
|
+
throw new NoTabAsIndentError();
|
|
1475
|
+
}
|
|
1476
|
+
const baseIndent = baseIndentMatch?.[1]?.length;
|
|
1477
|
+
if (lines[0].trim().startsWith("- ")) return void 0;
|
|
1478
|
+
const result = {};
|
|
1479
|
+
let i2 = 0;
|
|
1480
|
+
while (i2 < lines.length) {
|
|
1481
|
+
const line = lines[i2];
|
|
1482
|
+
const leadingWhitespace = line.match(/^(\s*)/)?.[1];
|
|
1483
|
+
if (leadingWhitespace && leadingWhitespace.includes(" ")) {
|
|
1484
|
+
throw new NoTabAsIndentError();
|
|
1485
|
+
}
|
|
1486
|
+
const currentIndent = leadingWhitespace.length;
|
|
1487
|
+
if (currentIndent < baseIndent) {
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
if (currentIndent !== baseIndent) {
|
|
1491
|
+
throw new BadIndentationError();
|
|
1492
|
+
}
|
|
1493
|
+
const keyValueMatch = line.match(/^[ ]*([^:\n]+?):\s*(.*)$/);
|
|
1494
|
+
if (!keyValueMatch) {
|
|
1495
|
+
i2++;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const key = keyValueMatch[1].trim();
|
|
1499
|
+
const rawValue = keyValueMatch[2].trim();
|
|
1500
|
+
if (rawValue === "") {
|
|
1501
|
+
const childLines = [];
|
|
1502
|
+
let j = i2 + 1;
|
|
1503
|
+
while (j < lines.length) {
|
|
1504
|
+
const childLine = lines[j];
|
|
1505
|
+
const childIndent = childLine.match(/^([ ]*)/)?.[1]?.length;
|
|
1506
|
+
if (childIndent && childIndent > baseIndent) {
|
|
1507
|
+
childLines.push(childLine);
|
|
1508
|
+
j++;
|
|
1509
|
+
} else {
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (childLines.length > 0) {
|
|
1514
|
+
const firstChildTrimmed = childLines[0].trim();
|
|
1515
|
+
if (firstChildTrimmed.startsWith("- ")) {
|
|
1516
|
+
const reconstructedContent = `${key}:
|
|
1517
|
+
${childLines.join("\n")}`;
|
|
1518
|
+
const listResult = parseListMetaVar(reconstructedContent, key);
|
|
1519
|
+
if (listResult) {
|
|
1520
|
+
result[key] = listResult.map(
|
|
1521
|
+
(item) => parseMetadataValue(item)
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
} else {
|
|
1525
|
+
const childContent = childLines.join("\n");
|
|
1526
|
+
const nested = parseNestedBlock(childContent);
|
|
1527
|
+
if (nested) {
|
|
1528
|
+
result[key] = nested;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
i2 = j;
|
|
1533
|
+
} else {
|
|
1534
|
+
result[key] = parseMetadataValue(rawValue);
|
|
1535
|
+
i2++;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
function parseMetadataValue(rawValue) {
|
|
1541
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
1542
|
+
return rawValue.slice(1, -1).split(",").map((item) => item.trim());
|
|
1543
|
+
}
|
|
1544
|
+
if (numericValueRegex.test(rawValue)) {
|
|
1545
|
+
return Number(rawValue);
|
|
1546
|
+
}
|
|
1547
|
+
return rawValue;
|
|
1548
|
+
}
|
|
1549
|
+
function parseAnyMetaVar(content, varName) {
|
|
1550
|
+
const nested = parseNestedMetaVar(content, varName);
|
|
1551
|
+
if (nested) return nested;
|
|
1552
|
+
const list = parseListMetaVar(content, varName);
|
|
1553
|
+
if (list) return list;
|
|
1554
|
+
const simple = parseSimpleMetaVar(content, varName);
|
|
1555
|
+
if (simple) return parseMetadataValue(simple);
|
|
1556
|
+
return void 0;
|
|
1557
|
+
}
|
|
1436
1558
|
function extractMetadata(content) {
|
|
1437
1559
|
const metadata = {};
|
|
1438
1560
|
let servings = void 0;
|
|
@@ -1440,13 +1562,24 @@ function extractMetadata(content) {
|
|
|
1440
1562
|
if (!metadataContent) {
|
|
1441
1563
|
return { metadata };
|
|
1442
1564
|
}
|
|
1443
|
-
|
|
1565
|
+
const handledKeys = /* @__PURE__ */ new Set([
|
|
1566
|
+
// Simple string fields
|
|
1444
1567
|
"title",
|
|
1568
|
+
"author",
|
|
1569
|
+
"locale",
|
|
1570
|
+
"introduction",
|
|
1571
|
+
"description",
|
|
1572
|
+
"course",
|
|
1573
|
+
"category",
|
|
1574
|
+
"diet",
|
|
1575
|
+
"cuisine",
|
|
1576
|
+
"difficulty",
|
|
1577
|
+
// Source fields
|
|
1445
1578
|
"source",
|
|
1446
1579
|
"source.name",
|
|
1447
1580
|
"source.url",
|
|
1448
|
-
"author",
|
|
1449
1581
|
"source.author",
|
|
1582
|
+
// Time fields
|
|
1450
1583
|
"prep time",
|
|
1451
1584
|
"time.prep",
|
|
1452
1585
|
"cook time",
|
|
@@ -1454,6 +1587,23 @@ function extractMetadata(content) {
|
|
|
1454
1587
|
"time required",
|
|
1455
1588
|
"time",
|
|
1456
1589
|
"duration",
|
|
1590
|
+
// Image fields
|
|
1591
|
+
"image",
|
|
1592
|
+
"picture",
|
|
1593
|
+
"images",
|
|
1594
|
+
"pictures",
|
|
1595
|
+
// Unit system
|
|
1596
|
+
"unit system",
|
|
1597
|
+
// Scaling fields
|
|
1598
|
+
"servings",
|
|
1599
|
+
"yield",
|
|
1600
|
+
"serves",
|
|
1601
|
+
// List fields
|
|
1602
|
+
"tags"
|
|
1603
|
+
]);
|
|
1604
|
+
for (const metaVar of [
|
|
1605
|
+
"title",
|
|
1606
|
+
"author",
|
|
1457
1607
|
"locale",
|
|
1458
1608
|
"introduction",
|
|
1459
1609
|
"description",
|
|
@@ -1461,17 +1611,57 @@ function extractMetadata(content) {
|
|
|
1461
1611
|
"category",
|
|
1462
1612
|
"diet",
|
|
1463
1613
|
"cuisine",
|
|
1464
|
-
"difficulty"
|
|
1465
|
-
"image",
|
|
1466
|
-
"picture"
|
|
1614
|
+
"difficulty"
|
|
1467
1615
|
]) {
|
|
1468
1616
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1469
1617
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1470
1618
|
}
|
|
1619
|
+
const sourceNested = parseNestedMetaVar(metadataContent, "source");
|
|
1620
|
+
const sourceTxt = parseSimpleMetaVar(metadataContent, "source");
|
|
1621
|
+
const sourceName = parseSimpleMetaVar(metadataContent, "source.name");
|
|
1622
|
+
const sourceUrl = parseSimpleMetaVar(metadataContent, "source.url");
|
|
1623
|
+
const sourceAuthor = parseSimpleMetaVar(metadataContent, "source.author");
|
|
1624
|
+
if (sourceNested) {
|
|
1625
|
+
const source = {};
|
|
1626
|
+
if (typeof sourceNested.name === "string") source.name = sourceNested.name;
|
|
1627
|
+
if (typeof sourceNested.url === "string") source.url = sourceNested.url;
|
|
1628
|
+
if (typeof sourceNested.author === "string")
|
|
1629
|
+
source.author = sourceNested.author;
|
|
1630
|
+
if (Object.keys(source).length > 0) metadata.source = source;
|
|
1631
|
+
} else if (sourceName || sourceAuthor || sourceUrl) {
|
|
1632
|
+
const source = {};
|
|
1633
|
+
if (sourceName) source.name = sourceName;
|
|
1634
|
+
if (sourceUrl) source.url = sourceUrl;
|
|
1635
|
+
if (sourceAuthor) source.author = sourceAuthor;
|
|
1636
|
+
metadata.source = source;
|
|
1637
|
+
} else if (sourceTxt) {
|
|
1638
|
+
metadata.source = sourceTxt;
|
|
1639
|
+
}
|
|
1640
|
+
const timeNested = parseNestedMetaVar(metadataContent, "time");
|
|
1641
|
+
const prepTime = parseSimpleMetaVar(metadataContent, "prep time") ?? parseSimpleMetaVar(metadataContent, "time.prep");
|
|
1642
|
+
const cookTime = parseSimpleMetaVar(metadataContent, "cook time") ?? parseSimpleMetaVar(metadataContent, "time.cook");
|
|
1643
|
+
const totalTime = parseSimpleMetaVar(metadataContent, "time required") ?? parseSimpleMetaVar(metadataContent, "time") ?? parseSimpleMetaVar(metadataContent, "duration");
|
|
1644
|
+
if (timeNested) {
|
|
1645
|
+
const time = {};
|
|
1646
|
+
if (typeof timeNested.prep === "string") time.prep = timeNested.prep;
|
|
1647
|
+
if (typeof timeNested.cook === "string") time.cook = timeNested.cook;
|
|
1648
|
+
if (typeof timeNested.total === "string") time.total = timeNested.total;
|
|
1649
|
+
if (Object.keys(time).length > 0) metadata.time = time;
|
|
1650
|
+
} else if (prepTime || cookTime || totalTime) {
|
|
1651
|
+
const time = {};
|
|
1652
|
+
if (prepTime) time.prep = prepTime;
|
|
1653
|
+
if (cookTime) time.cook = cookTime;
|
|
1654
|
+
if (totalTime) time.total = totalTime;
|
|
1655
|
+
metadata.time = time;
|
|
1656
|
+
}
|
|
1657
|
+
const image = parseSimpleMetaVar(metadataContent, "image") ?? parseSimpleMetaVar(metadataContent, "picture");
|
|
1658
|
+
if (image) metadata.image = image;
|
|
1659
|
+
const images = parseListMetaVar(metadataContent, "images") ?? parseListMetaVar(metadataContent, "pictures");
|
|
1660
|
+
if (images) metadata.images = images;
|
|
1471
1661
|
let unitSystem;
|
|
1472
1662
|
const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
|
|
1473
1663
|
if (unitSystemRaw) {
|
|
1474
|
-
metadata
|
|
1664
|
+
metadata.unitSystem = unitSystemRaw;
|
|
1475
1665
|
const unitSystemMap = {
|
|
1476
1666
|
metric: "metric",
|
|
1477
1667
|
us: "US",
|
|
@@ -1480,16 +1670,22 @@ function extractMetadata(content) {
|
|
|
1480
1670
|
};
|
|
1481
1671
|
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
1482
1672
|
}
|
|
1483
|
-
for (const metaVar of ["
|
|
1673
|
+
for (const metaVar of ["servings", "yield", "serves"]) {
|
|
1484
1674
|
const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
|
|
1485
1675
|
if (scalingMetaValue && scalingMetaValue[1]) {
|
|
1486
1676
|
metadata[metaVar] = scalingMetaValue[1];
|
|
1487
1677
|
servings = scalingMetaValue[0];
|
|
1488
1678
|
}
|
|
1489
1679
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1680
|
+
const tags = parseListMetaVar(metadataContent, "tags");
|
|
1681
|
+
if (tags) metadata.tags = tags;
|
|
1682
|
+
const allKeys = extractAllMetadataKeys(metadataContent);
|
|
1683
|
+
for (const key of allKeys) {
|
|
1684
|
+
if (handledKeys.has(key)) continue;
|
|
1685
|
+
const value = parseAnyMetaVar(metadataContent, key);
|
|
1686
|
+
if (value !== void 0) {
|
|
1687
|
+
metadata[key] = value;
|
|
1688
|
+
}
|
|
1493
1689
|
}
|
|
1494
1690
|
return { metadata, servings, unitSystem };
|
|
1495
1691
|
}
|
|
@@ -2235,7 +2431,7 @@ var _Recipe = class _Recipe {
|
|
|
2235
2431
|
alternative.note = note;
|
|
2236
2432
|
}
|
|
2237
2433
|
if (itemQuantity) {
|
|
2238
|
-
alternative
|
|
2434
|
+
Object.assign(alternative, itemQuantity);
|
|
2239
2435
|
}
|
|
2240
2436
|
alternatives.push(alternative);
|
|
2241
2437
|
testString = groups.ingredientAlternative || "";
|
|
@@ -2343,7 +2539,7 @@ var _Recipe = class _Recipe {
|
|
|
2343
2539
|
displayName
|
|
2344
2540
|
};
|
|
2345
2541
|
if (itemQuantity) {
|
|
2346
|
-
alternative
|
|
2542
|
+
Object.assign(alternative, itemQuantity);
|
|
2347
2543
|
}
|
|
2348
2544
|
const existingAlternatives = this.choices.ingredientGroups.get(groupKey);
|
|
2349
2545
|
function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
|
|
@@ -2390,7 +2586,7 @@ var _Recipe = class _Recipe {
|
|
|
2390
2586
|
* Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
|
|
2391
2587
|
* @internal
|
|
2392
2588
|
*/
|
|
2393
|
-
|
|
2589
|
+
_populateIngredientQuantities() {
|
|
2394
2590
|
for (const ing of this.ingredients) {
|
|
2395
2591
|
delete ing.quantities;
|
|
2396
2592
|
delete ing.usedAsPrimary;
|
|
@@ -2479,28 +2675,28 @@ var _Recipe = class _Recipe {
|
|
|
2479
2675
|
for (const alt of allAlts) {
|
|
2480
2676
|
referencedIndices.add(alt.index);
|
|
2481
2677
|
}
|
|
2482
|
-
if (!alternative.
|
|
2678
|
+
if (!alternative.quantity) continue;
|
|
2483
2679
|
const baseQty = {
|
|
2484
|
-
quantity: alternative.
|
|
2485
|
-
...alternative.
|
|
2486
|
-
unit: alternative.
|
|
2680
|
+
quantity: alternative.quantity,
|
|
2681
|
+
...alternative.unit && {
|
|
2682
|
+
unit: alternative.unit
|
|
2487
2683
|
}
|
|
2488
2684
|
};
|
|
2489
|
-
const quantityEntry = alternative.
|
|
2685
|
+
const quantityEntry = alternative.equivalents?.length ? { or: [baseQty, ...alternative.equivalents] } : baseQty;
|
|
2490
2686
|
let alternativeRefs;
|
|
2491
2687
|
if (!hasExplicitChoice && allAlts.length > 1) {
|
|
2492
2688
|
alternativeRefs = allAlts.filter(
|
|
2493
2689
|
(alt) => isGrouped ? alt.itemId !== item.id : alt.index !== alternative.index
|
|
2494
2690
|
).map((otherAlt) => {
|
|
2495
2691
|
const ref = { index: otherAlt.index };
|
|
2496
|
-
if (otherAlt.
|
|
2692
|
+
if (otherAlt.quantity) {
|
|
2497
2693
|
const altQty = {
|
|
2498
|
-
quantity: otherAlt.
|
|
2499
|
-
...otherAlt.
|
|
2500
|
-
unit: otherAlt.
|
|
2694
|
+
quantity: otherAlt.quantity,
|
|
2695
|
+
...otherAlt.unit && {
|
|
2696
|
+
unit: otherAlt.unit.name
|
|
2501
2697
|
},
|
|
2502
|
-
...otherAlt.
|
|
2503
|
-
equivalents: otherAlt.
|
|
2698
|
+
...otherAlt.equivalents && {
|
|
2699
|
+
equivalents: otherAlt.equivalents.map(
|
|
2504
2700
|
(eq) => toPlainUnit(eq)
|
|
2505
2701
|
)
|
|
2506
2702
|
}
|
|
@@ -2513,14 +2709,10 @@ var _Recipe = class _Recipe {
|
|
|
2513
2709
|
const altIndices = getAlternativeSignature(alternativeRefs) ?? "";
|
|
2514
2710
|
let signature;
|
|
2515
2711
|
if (isGrouped) {
|
|
2516
|
-
const resolvedUnit = resolveUnit(
|
|
2517
|
-
alternative.itemQuantity.unit?.name
|
|
2518
|
-
);
|
|
2712
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
2519
2713
|
signature = `group:${item.group}|${altIndices}|${resolvedUnit.type}`;
|
|
2520
2714
|
} else if (altIndices) {
|
|
2521
|
-
const resolvedUnit = resolveUnit(
|
|
2522
|
-
alternative.itemQuantity.unit?.name
|
|
2523
|
-
);
|
|
2715
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
2524
2716
|
signature = `${altIndices}|${resolvedUnit.type}}`;
|
|
2525
2717
|
} else {
|
|
2526
2718
|
signature = null;
|
|
@@ -2755,7 +2947,7 @@ var _Recipe = class _Recipe {
|
|
|
2755
2947
|
if (!section.isBlank()) {
|
|
2756
2948
|
this.sections.push(section);
|
|
2757
2949
|
}
|
|
2758
|
-
this.
|
|
2950
|
+
this._populateIngredientQuantities();
|
|
2759
2951
|
}
|
|
2760
2952
|
/**
|
|
2761
2953
|
* Scales the recipe to a new number of servings. In practice, it calls
|
|
@@ -2788,16 +2980,16 @@ var _Recipe = class _Recipe {
|
|
|
2788
2980
|
const unitSystem = this.unitSystem;
|
|
2789
2981
|
function scaleAlternativesBy(alternatives, factor2) {
|
|
2790
2982
|
for (const alternative of alternatives) {
|
|
2791
|
-
if (alternative.
|
|
2792
|
-
const scaleFactor = alternative.
|
|
2793
|
-
if (alternative.
|
|
2794
|
-
alternative.
|
|
2795
|
-
alternative.
|
|
2983
|
+
if (alternative.quantity) {
|
|
2984
|
+
const scaleFactor = alternative.scalable ? Big4(factor2) : 1;
|
|
2985
|
+
if (alternative.quantity.type !== "fixed" || alternative.quantity.value.type !== "text") {
|
|
2986
|
+
alternative.quantity = multiplyQuantityValue(
|
|
2987
|
+
alternative.quantity,
|
|
2796
2988
|
scaleFactor
|
|
2797
2989
|
);
|
|
2798
2990
|
}
|
|
2799
|
-
if (alternative.
|
|
2800
|
-
alternative.
|
|
2991
|
+
if (alternative.equivalents) {
|
|
2992
|
+
alternative.equivalents = alternative.equivalents.map(
|
|
2801
2993
|
(altQuantity) => {
|
|
2802
2994
|
if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
|
|
2803
2995
|
return altQuantity;
|
|
@@ -2815,15 +3007,15 @@ var _Recipe = class _Recipe {
|
|
|
2815
3007
|
}
|
|
2816
3008
|
const optimizedPrimary = applyBestUnit(
|
|
2817
3009
|
{
|
|
2818
|
-
quantity: alternative.
|
|
2819
|
-
unit: alternative.
|
|
3010
|
+
quantity: alternative.quantity,
|
|
3011
|
+
unit: alternative.unit
|
|
2820
3012
|
},
|
|
2821
3013
|
unitSystem
|
|
2822
3014
|
);
|
|
2823
|
-
alternative.
|
|
2824
|
-
alternative.
|
|
2825
|
-
if (alternative.
|
|
2826
|
-
alternative.
|
|
3015
|
+
alternative.quantity = optimizedPrimary.quantity;
|
|
3016
|
+
alternative.unit = optimizedPrimary.unit;
|
|
3017
|
+
if (alternative.equivalents) {
|
|
3018
|
+
alternative.equivalents = alternative.equivalents.map(
|
|
2827
3019
|
(eq) => applyBestUnit(eq, unitSystem)
|
|
2828
3020
|
);
|
|
2829
3021
|
}
|
|
@@ -2853,7 +3045,7 @@ var _Recipe = class _Recipe {
|
|
|
2853
3045
|
factor
|
|
2854
3046
|
);
|
|
2855
3047
|
}
|
|
2856
|
-
newRecipe.
|
|
3048
|
+
newRecipe._populateIngredientQuantities();
|
|
2857
3049
|
newRecipe.servings = Big4(originalServings).times(factor).toNumber();
|
|
2858
3050
|
if (newRecipe.metadata.servings && this.metadata.servings) {
|
|
2859
3051
|
if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
|
|
@@ -2918,27 +3110,36 @@ var _Recipe = class _Recipe {
|
|
|
2918
3110
|
if (method === "remove") {
|
|
2919
3111
|
return newPrimary;
|
|
2920
3112
|
} else if (method === "replace") {
|
|
3113
|
+
if (source === "converted") remainingEquivalents.push(oldPrimary);
|
|
2921
3114
|
if (remainingEquivalents.length > 0) {
|
|
2922
3115
|
newPrimary.equivalents = remainingEquivalents;
|
|
2923
|
-
if (source === "converted") newPrimary.equivalents.push(oldPrimary);
|
|
2924
3116
|
}
|
|
2925
3117
|
} else {
|
|
2926
3118
|
newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
|
|
2927
3119
|
}
|
|
2928
3120
|
return newPrimary;
|
|
2929
3121
|
}
|
|
2930
|
-
function
|
|
2931
|
-
const primaryUnit = resolveUnit(
|
|
2932
|
-
const equivalents =
|
|
3122
|
+
function convertAlternativeQuantity(alternative) {
|
|
3123
|
+
const primaryUnit = resolveUnit(alternative.unit?.name);
|
|
3124
|
+
const equivalents = alternative.equivalents ?? [];
|
|
2933
3125
|
const oldPrimary = {
|
|
2934
|
-
quantity:
|
|
2935
|
-
unit:
|
|
3126
|
+
quantity: alternative.quantity,
|
|
3127
|
+
unit: alternative.unit
|
|
2936
3128
|
};
|
|
2937
3129
|
if (primaryUnit.type !== "other" && isUnitCompatibleWithSystem(primaryUnit, system)) {
|
|
2938
3130
|
if (method === "remove") {
|
|
2939
|
-
return {
|
|
3131
|
+
return {
|
|
3132
|
+
quantity: alternative.quantity,
|
|
3133
|
+
unit: alternative.unit,
|
|
3134
|
+
scalable: alternative.scalable
|
|
3135
|
+
};
|
|
2940
3136
|
}
|
|
2941
|
-
return
|
|
3137
|
+
return {
|
|
3138
|
+
quantity: alternative.quantity,
|
|
3139
|
+
unit: alternative.unit,
|
|
3140
|
+
scalable: alternative.scalable,
|
|
3141
|
+
equivalents
|
|
3142
|
+
};
|
|
2942
3143
|
}
|
|
2943
3144
|
const targetEquivIndex = equivalents.findIndex((eq) => {
|
|
2944
3145
|
const eqUnit = resolveUnit(eq.unit?.name);
|
|
@@ -2953,7 +3154,7 @@ var _Recipe = class _Recipe {
|
|
|
2953
3154
|
targetEquiv,
|
|
2954
3155
|
oldPrimary,
|
|
2955
3156
|
remainingEquivalents,
|
|
2956
|
-
|
|
3157
|
+
alternative.scalable,
|
|
2957
3158
|
targetEquiv.unit?.integerProtected,
|
|
2958
3159
|
"swapped"
|
|
2959
3160
|
);
|
|
@@ -2964,8 +3165,8 @@ var _Recipe = class _Recipe {
|
|
|
2964
3165
|
converted,
|
|
2965
3166
|
oldPrimary,
|
|
2966
3167
|
equivalents,
|
|
2967
|
-
|
|
2968
|
-
|
|
3168
|
+
alternative.scalable,
|
|
3169
|
+
alternative.unit?.integerProtected,
|
|
2969
3170
|
"swapped"
|
|
2970
3171
|
);
|
|
2971
3172
|
}
|
|
@@ -2978,24 +3179,37 @@ var _Recipe = class _Recipe {
|
|
|
2978
3179
|
convertedEquiv,
|
|
2979
3180
|
oldPrimary,
|
|
2980
3181
|
remainingEquivalents,
|
|
2981
|
-
|
|
3182
|
+
alternative.scalable,
|
|
2982
3183
|
equiv.unit?.integerProtected,
|
|
2983
3184
|
"converted"
|
|
2984
3185
|
);
|
|
2985
3186
|
}
|
|
2986
3187
|
}
|
|
2987
3188
|
if (method === "remove") {
|
|
2988
|
-
return {
|
|
3189
|
+
return {
|
|
3190
|
+
quantity: alternative.quantity,
|
|
3191
|
+
unit: alternative.unit,
|
|
3192
|
+
scalable: alternative.scalable
|
|
3193
|
+
};
|
|
2989
3194
|
} else {
|
|
2990
|
-
return
|
|
3195
|
+
return {
|
|
3196
|
+
quantity: alternative.quantity,
|
|
3197
|
+
unit: alternative.unit,
|
|
3198
|
+
scalable: alternative.scalable,
|
|
3199
|
+
equivalents
|
|
3200
|
+
};
|
|
2991
3201
|
}
|
|
2992
3202
|
}
|
|
2993
3203
|
function convertAlternatives(alternatives) {
|
|
2994
3204
|
for (const alternative of alternatives) {
|
|
2995
|
-
if (alternative.
|
|
2996
|
-
|
|
2997
|
-
alternative
|
|
3205
|
+
if (alternative.quantity) {
|
|
3206
|
+
const converted = convertAlternativeQuantity(
|
|
3207
|
+
alternative
|
|
2998
3208
|
);
|
|
3209
|
+
alternative.quantity = converted.quantity;
|
|
3210
|
+
alternative.unit = converted.unit;
|
|
3211
|
+
alternative.scalable = converted.scalable;
|
|
3212
|
+
alternative.equivalents = converted.equivalents;
|
|
2999
3213
|
}
|
|
3000
3214
|
}
|
|
3001
3215
|
}
|
|
@@ -3016,7 +3230,7 @@ var _Recipe = class _Recipe {
|
|
|
3016
3230
|
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
3017
3231
|
convertAlternatives(alternatives);
|
|
3018
3232
|
}
|
|
3019
|
-
newRecipe.
|
|
3233
|
+
newRecipe._populateIngredientQuantities();
|
|
3020
3234
|
if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
|
|
3021
3235
|
return newRecipe;
|
|
3022
3236
|
}
|
|
@@ -3069,9 +3283,9 @@ var Recipe = _Recipe;
|
|
|
3069
3283
|
var ShoppingList = class {
|
|
3070
3284
|
/**
|
|
3071
3285
|
* Creates a new ShoppingList instance
|
|
3072
|
-
* @param
|
|
3286
|
+
* @param categoryConfigStr - The category configuration to parse.
|
|
3073
3287
|
*/
|
|
3074
|
-
constructor(
|
|
3288
|
+
constructor(categoryConfigStr) {
|
|
3075
3289
|
// TODO: backport type change
|
|
3076
3290
|
/**
|
|
3077
3291
|
* The ingredients in the shopping list.
|
|
@@ -3084,16 +3298,16 @@ var ShoppingList = class {
|
|
|
3084
3298
|
/**
|
|
3085
3299
|
* The category configuration for the shopping list.
|
|
3086
3300
|
*/
|
|
3087
|
-
__publicField(this, "
|
|
3301
|
+
__publicField(this, "categoryConfig");
|
|
3088
3302
|
/**
|
|
3089
3303
|
* The categorized ingredients in the shopping list.
|
|
3090
3304
|
*/
|
|
3091
3305
|
__publicField(this, "categories");
|
|
3092
|
-
if (
|
|
3093
|
-
this.
|
|
3306
|
+
if (categoryConfigStr) {
|
|
3307
|
+
this.setCategoryConfig(categoryConfigStr);
|
|
3094
3308
|
}
|
|
3095
3309
|
}
|
|
3096
|
-
|
|
3310
|
+
calculateIngredients() {
|
|
3097
3311
|
this.ingredients = [];
|
|
3098
3312
|
const addIngredientQuantity = (name, quantityTotal) => {
|
|
3099
3313
|
const quantityTotalExtended = extendAllUnits(quantityTotal);
|
|
@@ -3180,7 +3394,7 @@ var ShoppingList = class {
|
|
|
3180
3394
|
* @param options - Options for adding the recipe.
|
|
3181
3395
|
* @throws Error if the recipe has alternatives without corresponding choices.
|
|
3182
3396
|
*/
|
|
3183
|
-
|
|
3397
|
+
addRecipe(recipe, options = {}) {
|
|
3184
3398
|
const errorMessage = this.getUnresolvedAlternativesError(
|
|
3185
3399
|
recipe,
|
|
3186
3400
|
options.choices
|
|
@@ -3209,7 +3423,7 @@ var ShoppingList = class {
|
|
|
3209
3423
|
});
|
|
3210
3424
|
}
|
|
3211
3425
|
}
|
|
3212
|
-
this.
|
|
3426
|
+
this.calculateIngredients();
|
|
3213
3427
|
this.categorize();
|
|
3214
3428
|
}
|
|
3215
3429
|
/**
|
|
@@ -3249,15 +3463,15 @@ var ShoppingList = class {
|
|
|
3249
3463
|
}
|
|
3250
3464
|
/**
|
|
3251
3465
|
* Removes a recipe from the shopping list, then automatically
|
|
3252
|
-
* recalculates the quantities and recategorize the ingredients.
|
|
3466
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
3253
3467
|
* @param index - The index of the recipe to remove.
|
|
3254
3468
|
*/
|
|
3255
|
-
|
|
3469
|
+
removeRecipe(index) {
|
|
3256
3470
|
if (index < 0 || index >= this.recipes.length) {
|
|
3257
3471
|
throw new Error("Index out of bounds");
|
|
3258
3472
|
}
|
|
3259
3473
|
this.recipes.splice(index, 1);
|
|
3260
|
-
this.
|
|
3474
|
+
this.calculateIngredients();
|
|
3261
3475
|
this.categorize();
|
|
3262
3476
|
}
|
|
3263
3477
|
/**
|
|
@@ -3265,10 +3479,10 @@ var ShoppingList = class {
|
|
|
3265
3479
|
* and automatically categorize current ingredients from the list.
|
|
3266
3480
|
* @param config - The category configuration to parse.
|
|
3267
3481
|
*/
|
|
3268
|
-
|
|
3482
|
+
setCategoryConfig(config) {
|
|
3269
3483
|
if (typeof config === "string")
|
|
3270
|
-
this.
|
|
3271
|
-
else if (config instanceof CategoryConfig) this.
|
|
3484
|
+
this.categoryConfig = new CategoryConfig(config);
|
|
3485
|
+
else if (config instanceof CategoryConfig) this.categoryConfig = config;
|
|
3272
3486
|
else throw new Error("Invalid category configuration");
|
|
3273
3487
|
this.categorize();
|
|
3274
3488
|
}
|
|
@@ -3277,17 +3491,17 @@ var ShoppingList = class {
|
|
|
3277
3491
|
* Will use the category config if any, otherwise all ingredients will be placed in the "other" category
|
|
3278
3492
|
*/
|
|
3279
3493
|
categorize() {
|
|
3280
|
-
if (!this.
|
|
3494
|
+
if (!this.categoryConfig) {
|
|
3281
3495
|
this.categories = { other: this.ingredients };
|
|
3282
3496
|
return;
|
|
3283
3497
|
}
|
|
3284
3498
|
const categories = { other: [] };
|
|
3285
|
-
for (const category of this.
|
|
3499
|
+
for (const category of this.categoryConfig.categories) {
|
|
3286
3500
|
categories[category.name] = [];
|
|
3287
3501
|
}
|
|
3288
3502
|
for (const ingredient of this.ingredients) {
|
|
3289
3503
|
let found = false;
|
|
3290
|
-
for (const category of this.
|
|
3504
|
+
for (const category of this.categoryConfig.categories) {
|
|
3291
3505
|
for (const categoryIngredient of category.ingredients) {
|
|
3292
3506
|
if (categoryIngredient.aliases.includes(ingredient.name)) {
|
|
3293
3507
|
categories[category.name].push(ingredient);
|
|
@@ -3351,7 +3565,6 @@ var ShoppingCart = class {
|
|
|
3351
3565
|
setProductCatalog(catalog) {
|
|
3352
3566
|
this.productCatalog = catalog;
|
|
3353
3567
|
}
|
|
3354
|
-
// TODO: harmonize recipe name to use underscores
|
|
3355
3568
|
/**
|
|
3356
3569
|
* Sets the shopping list to build the cart from.
|
|
3357
3570
|
* To use if a shopping list was not provided at the creation of the instance
|