@tmlmt/cooklang-parser 3.0.0-alpha.13 → 3.0.0-alpha.15
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 +902 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +269 -5
- package/dist/index.d.ts +269 -5
- package/dist/index.js +899 -121
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -32,9 +32,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
32
32
|
// src/index.ts
|
|
33
33
|
var index_exports = {};
|
|
34
34
|
__export(index_exports, {
|
|
35
|
+
BadIndentationError: () => BadIndentationError,
|
|
35
36
|
CategoryConfig: () => CategoryConfig,
|
|
36
37
|
NoProductCatalogForCartError: () => NoProductCatalogForCartError,
|
|
37
38
|
NoShoppingListForCartError: () => NoShoppingListForCartError,
|
|
39
|
+
NoTabAsIndentError: () => NoTabAsIndentError,
|
|
40
|
+
Pantry: () => Pantry,
|
|
38
41
|
ProductCatalog: () => ProductCatalog,
|
|
39
42
|
Recipe: () => Recipe,
|
|
40
43
|
Section: () => Section,
|
|
@@ -118,7 +121,7 @@ var CategoryConfig = class {
|
|
|
118
121
|
}
|
|
119
122
|
};
|
|
120
123
|
|
|
121
|
-
// src/classes/
|
|
124
|
+
// src/classes/pantry.ts
|
|
122
125
|
var import_smol_toml = __toESM(require("smol-toml"), 1);
|
|
123
126
|
|
|
124
127
|
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
@@ -354,6 +357,37 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
|
|
|
354
357
|
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();
|
|
355
358
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
356
359
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
360
|
+
var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
|
|
361
|
+
var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
|
|
362
|
+
var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
|
|
363
|
+
var mdTripleAsterisk = d().literal("***").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("***");
|
|
364
|
+
var mdTripleUnderscore = d().literal("___").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("___");
|
|
365
|
+
var mdBoldAstItalicUnd = d().literal("**_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_**");
|
|
366
|
+
var mdBoldUndItalicAst = d().literal("__*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*__");
|
|
367
|
+
var mdItalicAstBoldUnd = d().literal("*__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__*");
|
|
368
|
+
var mdItalicUndBoldAst = d().literal("_**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**_");
|
|
369
|
+
var mdBoldAsterisk = d().literal("**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**");
|
|
370
|
+
var mdBoldUnderscore = d().wordBoundary().literal("__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__").wordBoundary();
|
|
371
|
+
var mdItalicAsterisk = d().literal("*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*");
|
|
372
|
+
var mdItalicUnderscore = d().wordBoundary().literal("_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_").wordBoundary();
|
|
373
|
+
var markdownRegex = new RegExp(
|
|
374
|
+
[
|
|
375
|
+
mdEscaped,
|
|
376
|
+
mdInlineCode,
|
|
377
|
+
mdLink,
|
|
378
|
+
mdTripleAsterisk,
|
|
379
|
+
mdTripleUnderscore,
|
|
380
|
+
mdBoldAstItalicUnd,
|
|
381
|
+
mdBoldUndItalicAst,
|
|
382
|
+
mdItalicAstBoldUnd,
|
|
383
|
+
mdItalicUndBoldAst,
|
|
384
|
+
mdBoldAsterisk,
|
|
385
|
+
mdBoldUnderscore,
|
|
386
|
+
mdItalicAsterisk,
|
|
387
|
+
mdItalicUnderscore
|
|
388
|
+
].map((r2) => r2.toRegExp().source).join("|"),
|
|
389
|
+
"g"
|
|
390
|
+
);
|
|
357
391
|
|
|
358
392
|
// src/units/definitions.ts
|
|
359
393
|
var units = [
|
|
@@ -984,21 +1018,6 @@ function hasAlternatives(entry) {
|
|
|
984
1018
|
}
|
|
985
1019
|
|
|
986
1020
|
// src/quantities/mutations.ts
|
|
987
|
-
function extendAllUnits(q) {
|
|
988
|
-
if (isAndGroup(q)) {
|
|
989
|
-
return { and: q.and.map(extendAllUnits) };
|
|
990
|
-
} else if (isOrGroup(q)) {
|
|
991
|
-
return { or: q.or.map(extendAllUnits) };
|
|
992
|
-
} else {
|
|
993
|
-
const newQ = {
|
|
994
|
-
quantity: q.quantity
|
|
995
|
-
};
|
|
996
|
-
if (q.unit) {
|
|
997
|
-
newQ.unit = { name: q.unit };
|
|
998
|
-
}
|
|
999
|
-
return newQ;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
1021
|
function normalizeAllUnits(q) {
|
|
1003
1022
|
if (isAndGroup(q)) {
|
|
1004
1023
|
return { and: q.and.map(normalizeAllUnits) };
|
|
@@ -1263,30 +1282,51 @@ var flattenPlainUnitGroup = (summed) => {
|
|
|
1263
1282
|
}
|
|
1264
1283
|
} else if (isAndGroup(summed)) {
|
|
1265
1284
|
const andEntries = [];
|
|
1285
|
+
const standaloneEntries = [];
|
|
1266
1286
|
const equivalentsList = [];
|
|
1267
1287
|
for (const entry of summed.and) {
|
|
1268
1288
|
if (isOrGroup(entry)) {
|
|
1269
1289
|
const orEntries = entry.or;
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1290
|
+
const firstEntry = orEntries[0];
|
|
1291
|
+
if (isAndGroup(firstEntry)) {
|
|
1292
|
+
for (const nestedEntry of firstEntry.and) {
|
|
1293
|
+
andEntries.push({
|
|
1294
|
+
quantity: nestedEntry.quantity,
|
|
1295
|
+
...nestedEntry.unit && { unit: nestedEntry.unit }
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
const primary = firstEntry;
|
|
1300
|
+
andEntries.push({
|
|
1301
|
+
quantity: primary.quantity,
|
|
1302
|
+
...primary.unit && { unit: primary.unit }
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
const equivEntries = orEntries.slice(1).filter((e2) => isQuantity(e2));
|
|
1306
|
+
equivalentsList.push(
|
|
1307
|
+
...equivEntries.map((e2) => ({
|
|
1308
|
+
quantity: e2.quantity,
|
|
1309
|
+
...e2.unit && { unit: e2.unit }
|
|
1310
|
+
}))
|
|
1311
|
+
);
|
|
1312
|
+
} else {
|
|
1313
|
+
const simpleQuantityEntry = entry;
|
|
1314
|
+
standaloneEntries.push({
|
|
1315
|
+
quantity: simpleQuantityEntry.quantity,
|
|
1316
|
+
...simpleQuantityEntry.unit && { unit: simpleQuantityEntry.unit }
|
|
1279
1317
|
});
|
|
1280
1318
|
}
|
|
1281
1319
|
}
|
|
1282
1320
|
if (equivalentsList.length === 0) {
|
|
1283
|
-
return andEntries;
|
|
1321
|
+
return [...andEntries, ...standaloneEntries];
|
|
1284
1322
|
}
|
|
1285
|
-
const result =
|
|
1323
|
+
const result = [];
|
|
1324
|
+
result.push({
|
|
1286
1325
|
and: andEntries,
|
|
1287
1326
|
equivalents: equivalentsList
|
|
1288
|
-
};
|
|
1289
|
-
|
|
1327
|
+
});
|
|
1328
|
+
result.push(...standaloneEntries);
|
|
1329
|
+
return result;
|
|
1290
1330
|
} else {
|
|
1291
1331
|
return [
|
|
1292
1332
|
{ quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
|
|
@@ -1340,6 +1380,24 @@ function applyBestUnit(q, system) {
|
|
|
1340
1380
|
unit: { name: bestUnit.name }
|
|
1341
1381
|
};
|
|
1342
1382
|
}
|
|
1383
|
+
function subtractQuantities(q1, q2, options = {}) {
|
|
1384
|
+
const { clampToZero = true, system } = options;
|
|
1385
|
+
const negatedQ2 = {
|
|
1386
|
+
...q2,
|
|
1387
|
+
quantity: multiplyQuantityValue(q2.quantity, -1)
|
|
1388
|
+
};
|
|
1389
|
+
const result = addQuantities(q1, negatedQ2, system);
|
|
1390
|
+
if (clampToZero) {
|
|
1391
|
+
const avg = getAverageValue(result.quantity);
|
|
1392
|
+
if (typeof avg === "number" && avg < 0) {
|
|
1393
|
+
return {
|
|
1394
|
+
quantity: { type: "fixed", value: { type: "decimal", decimal: 0 } },
|
|
1395
|
+
unit: result.unit
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1343
1401
|
|
|
1344
1402
|
// src/utils/parser_helpers.ts
|
|
1345
1403
|
function flushPendingNote(section, noteItems) {
|
|
@@ -1480,12 +1538,197 @@ function parseQuantityInput(input_str) {
|
|
|
1480
1538
|
}
|
|
1481
1539
|
return { type: "fixed", value: parseFixedValue(clean_str) };
|
|
1482
1540
|
}
|
|
1541
|
+
function parseQuantityWithUnit(input) {
|
|
1542
|
+
const trimmed = input.trim();
|
|
1543
|
+
const separatorIndex = trimmed.indexOf("%");
|
|
1544
|
+
if (separatorIndex === -1) {
|
|
1545
|
+
return { value: parseQuantityInput(trimmed) };
|
|
1546
|
+
}
|
|
1547
|
+
const valuePart = trimmed.slice(0, separatorIndex).trim();
|
|
1548
|
+
const unitPart = trimmed.slice(separatorIndex + 1).trim();
|
|
1549
|
+
return {
|
|
1550
|
+
value: parseQuantityInput(valuePart),
|
|
1551
|
+
unit: unitPart || void 0
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
function parseDateFromFormat(input, format) {
|
|
1555
|
+
const delimiterMatch = format.match(/[^A-Za-z]/);
|
|
1556
|
+
if (!delimiterMatch) {
|
|
1557
|
+
throw new Error(`Invalid date format: ${format}. No delimiter found.`);
|
|
1558
|
+
}
|
|
1559
|
+
const delimiter = delimiterMatch[0];
|
|
1560
|
+
const formatParts = format.split(delimiter);
|
|
1561
|
+
const inputParts = input.trim().split(delimiter);
|
|
1562
|
+
if (formatParts.length !== 3 || inputParts.length !== 3) {
|
|
1563
|
+
throw new Error(
|
|
1564
|
+
`Invalid date input "${input}" for format "${format}". Expected 3 parts.`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
let day = 0, month = 0, year = 0;
|
|
1568
|
+
for (let i2 = 0; i2 < 3; i2++) {
|
|
1569
|
+
const token = formatParts[i2].toUpperCase();
|
|
1570
|
+
const value = parseInt(inputParts[i2], 10);
|
|
1571
|
+
if (isNaN(value)) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
`Invalid date input "${input}": non-numeric part "${inputParts[i2]}".`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
if (token === "DD") day = value;
|
|
1577
|
+
else if (token === "MM") month = value;
|
|
1578
|
+
else if (token === "YYYY") year = value;
|
|
1579
|
+
else
|
|
1580
|
+
throw new Error(
|
|
1581
|
+
`Unknown token "${formatParts[i2]}" in format "${format}"`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
const date = new Date(year, month - 1, day);
|
|
1585
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1586
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1587
|
+
}
|
|
1588
|
+
return date;
|
|
1589
|
+
}
|
|
1590
|
+
function disambiguateDayMonth(first, second, year) {
|
|
1591
|
+
if (second > 12 && first <= 12) {
|
|
1592
|
+
return [second, first, year];
|
|
1593
|
+
}
|
|
1594
|
+
return [first, second, year];
|
|
1595
|
+
}
|
|
1596
|
+
function parseFuzzyDate(input) {
|
|
1597
|
+
const trimmed = input.trim();
|
|
1598
|
+
const delimiterMatch = trimmed.match(/[./-]/);
|
|
1599
|
+
if (!delimiterMatch) {
|
|
1600
|
+
throw new Error(`Cannot parse date "${input}": no delimiter found.`);
|
|
1601
|
+
}
|
|
1602
|
+
const delimiter = delimiterMatch[0];
|
|
1603
|
+
const parts = trimmed.split(delimiter);
|
|
1604
|
+
if (parts.length !== 3) {
|
|
1605
|
+
throw new Error(
|
|
1606
|
+
`Cannot parse date "${input}": expected 3 parts, got ${parts.length}.`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
const nums = parts.map((p) => parseInt(p, 10));
|
|
1610
|
+
if (nums.some((n2) => isNaN(n2))) {
|
|
1611
|
+
throw new Error(`Cannot parse date "${input}": non-numeric parts found.`);
|
|
1612
|
+
}
|
|
1613
|
+
let day, month, year;
|
|
1614
|
+
if (nums[0] >= 1e3) {
|
|
1615
|
+
year = nums[0];
|
|
1616
|
+
month = nums[1];
|
|
1617
|
+
day = nums[2];
|
|
1618
|
+
} else if (nums[2] >= 1e3) {
|
|
1619
|
+
[day, month, year] = disambiguateDayMonth(nums[0], nums[1], nums[2]);
|
|
1620
|
+
} else {
|
|
1621
|
+
if (nums[2] >= 100)
|
|
1622
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1623
|
+
[day, month] = disambiguateDayMonth(nums[0], nums[1], 0);
|
|
1624
|
+
year = 2e3 + nums[2];
|
|
1625
|
+
}
|
|
1626
|
+
const date = new Date(year, month - 1, day);
|
|
1627
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1628
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1629
|
+
}
|
|
1630
|
+
return date;
|
|
1631
|
+
}
|
|
1632
|
+
function parseMarkdownSegments(text) {
|
|
1633
|
+
const items = [];
|
|
1634
|
+
let cursor = 0;
|
|
1635
|
+
for (const match of text.matchAll(markdownRegex)) {
|
|
1636
|
+
const idx = match.index;
|
|
1637
|
+
if (idx > cursor) {
|
|
1638
|
+
items.push({ type: "text", value: text.slice(cursor, idx) });
|
|
1639
|
+
}
|
|
1640
|
+
const [
|
|
1641
|
+
,
|
|
1642
|
+
escaped,
|
|
1643
|
+
// group 1: escaped character
|
|
1644
|
+
code,
|
|
1645
|
+
// group 2: inline code
|
|
1646
|
+
linkText,
|
|
1647
|
+
// group 3: link text
|
|
1648
|
+
linkUrl,
|
|
1649
|
+
// group 4: link url
|
|
1650
|
+
tripleAst,
|
|
1651
|
+
// group 5: ***bold+italic***
|
|
1652
|
+
tripleUnd,
|
|
1653
|
+
// group 6: ___bold+italic___
|
|
1654
|
+
astUnd,
|
|
1655
|
+
// group 7: **_bold+italic_**
|
|
1656
|
+
undAst,
|
|
1657
|
+
// group 8: __*bold+italic*__
|
|
1658
|
+
astUndUnd,
|
|
1659
|
+
// group 9: *__bold+italic__*
|
|
1660
|
+
undAstAst,
|
|
1661
|
+
// group 10: _**bold+italic**_
|
|
1662
|
+
boldAst,
|
|
1663
|
+
// group 11: **bold**
|
|
1664
|
+
boldUnd,
|
|
1665
|
+
// group 12: __bold__
|
|
1666
|
+
italicAst,
|
|
1667
|
+
// group 13: *italic*
|
|
1668
|
+
italicUnd
|
|
1669
|
+
// group 14: _italic_
|
|
1670
|
+
] = match;
|
|
1671
|
+
let value;
|
|
1672
|
+
let attribute;
|
|
1673
|
+
let href;
|
|
1674
|
+
if (escaped !== void 0) {
|
|
1675
|
+
items.push({ type: "text", value: escaped });
|
|
1676
|
+
cursor = idx + match[0].length;
|
|
1677
|
+
continue;
|
|
1678
|
+
} else if (code !== void 0) {
|
|
1679
|
+
value = code;
|
|
1680
|
+
attribute = "code";
|
|
1681
|
+
} else if (linkText !== void 0) {
|
|
1682
|
+
value = linkText;
|
|
1683
|
+
attribute = "link";
|
|
1684
|
+
href = linkUrl;
|
|
1685
|
+
} else if (tripleAst !== void 0 || tripleUnd !== void 0 || astUnd !== void 0 || undAst !== void 0 || astUndUnd !== void 0 || undAstAst !== void 0) {
|
|
1686
|
+
value = tripleAst ?? tripleUnd ?? astUnd ?? undAst ?? astUndUnd ?? undAstAst;
|
|
1687
|
+
attribute = "bold+italic";
|
|
1688
|
+
} else if (boldAst !== void 0 || boldUnd !== void 0) {
|
|
1689
|
+
value = boldAst ?? boldUnd;
|
|
1690
|
+
attribute = "bold";
|
|
1691
|
+
} else {
|
|
1692
|
+
value = italicAst ?? italicUnd;
|
|
1693
|
+
attribute = "italic";
|
|
1694
|
+
}
|
|
1695
|
+
const item = { type: "text", value };
|
|
1696
|
+
if (attribute) item.attribute = attribute;
|
|
1697
|
+
if (href) item.href = href;
|
|
1698
|
+
items.push(item);
|
|
1699
|
+
cursor = idx + match[0].length;
|
|
1700
|
+
}
|
|
1701
|
+
if (cursor < text.length) {
|
|
1702
|
+
items.push({ type: "text", value: text.slice(cursor) });
|
|
1703
|
+
}
|
|
1704
|
+
return items;
|
|
1705
|
+
}
|
|
1483
1706
|
function parseSimpleMetaVar(content, varName) {
|
|
1484
1707
|
const varMatch = content.match(
|
|
1485
1708
|
new RegExp(`^${varName}:\\s*(.*(?:\\r?\\n\\s+.*)*)+`, "m")
|
|
1486
1709
|
);
|
|
1487
1710
|
return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
|
|
1488
1711
|
}
|
|
1712
|
+
function parseBlockScalarMetaVar(content, varName) {
|
|
1713
|
+
const match = content.match(
|
|
1714
|
+
new RegExp(
|
|
1715
|
+
`^${varName}:\\s*([|>])\\s*\\r?\\n((?:(?:[ ]+.*|\\s*)(?:\\r?\\n|$))+)`,
|
|
1716
|
+
"m"
|
|
1717
|
+
)
|
|
1718
|
+
);
|
|
1719
|
+
if (!match) return void 0;
|
|
1720
|
+
const style = match[1];
|
|
1721
|
+
const rawBlock = match[2];
|
|
1722
|
+
const lines = rawBlock.split(/\r?\n/);
|
|
1723
|
+
const firstNonEmpty = lines.find((l) => l.trim() !== "");
|
|
1724
|
+
if (!firstNonEmpty) return void 0;
|
|
1725
|
+
const baseIndent = firstNonEmpty.match(/^([ ]*)/)[1].length;
|
|
1726
|
+
const stripped = lines.map((line) => line.trim() === "" ? "" : line.slice(baseIndent)).join("\n").replace(/\n+$/, "");
|
|
1727
|
+
if (style === "|") {
|
|
1728
|
+
return stripped;
|
|
1729
|
+
}
|
|
1730
|
+
return stripped.replace(/\n\n/g, "\0").replace(/\n/g, " ").replace(/\0/g, "\n");
|
|
1731
|
+
}
|
|
1489
1732
|
function parseScalingMetaVar(content, varName) {
|
|
1490
1733
|
const varMatch = content.match(scalingMetaValueRegex(varName));
|
|
1491
1734
|
if (!varMatch) return void 0;
|
|
@@ -1668,6 +1911,13 @@ function extractMetadata(content) {
|
|
|
1668
1911
|
"cuisine",
|
|
1669
1912
|
"difficulty"
|
|
1670
1913
|
]) {
|
|
1914
|
+
if (metaVar === "description" || metaVar === "introduction") {
|
|
1915
|
+
const blockValue = parseBlockScalarMetaVar(metadataContent, metaVar);
|
|
1916
|
+
if (blockValue) {
|
|
1917
|
+
metadata[metaVar] = blockValue;
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1671
1921
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1672
1922
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1673
1923
|
}
|
|
@@ -1759,7 +2009,227 @@ function getAlternativeSignature(alternatives) {
|
|
|
1759
2009
|
return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
1760
2010
|
}
|
|
1761
2011
|
|
|
2012
|
+
// src/classes/pantry.ts
|
|
2013
|
+
var Pantry = class {
|
|
2014
|
+
/**
|
|
2015
|
+
* Creates a new Pantry instance.
|
|
2016
|
+
* @param tomlContent - Optional TOML content to parse.
|
|
2017
|
+
* @param options - Optional configuration options.
|
|
2018
|
+
*/
|
|
2019
|
+
constructor(tomlContent, options = {}) {
|
|
2020
|
+
/**
|
|
2021
|
+
* The parsed pantry items.
|
|
2022
|
+
*/
|
|
2023
|
+
__publicField(this, "items", []);
|
|
2024
|
+
/**
|
|
2025
|
+
* Options for date parsing and other configuration.
|
|
2026
|
+
*/
|
|
2027
|
+
__publicField(this, "options");
|
|
2028
|
+
/**
|
|
2029
|
+
* Optional category configuration for alias-based lookups.
|
|
2030
|
+
*/
|
|
2031
|
+
__publicField(this, "categoryConfig");
|
|
2032
|
+
this.options = options;
|
|
2033
|
+
if (tomlContent) {
|
|
2034
|
+
this.parse(tomlContent);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Parses a TOML string into pantry items.
|
|
2039
|
+
* @param tomlContent - The TOML string to parse.
|
|
2040
|
+
* @returns The parsed list of pantry items.
|
|
2041
|
+
*/
|
|
2042
|
+
parse(tomlContent) {
|
|
2043
|
+
const raw = import_smol_toml.default.parse(tomlContent);
|
|
2044
|
+
this.items = [];
|
|
2045
|
+
for (const [location, locationData] of Object.entries(raw)) {
|
|
2046
|
+
const locationTable = locationData;
|
|
2047
|
+
for (const [itemName, itemData] of Object.entries(locationTable)) {
|
|
2048
|
+
const item = this.parseItem(
|
|
2049
|
+
itemName,
|
|
2050
|
+
location,
|
|
2051
|
+
itemData
|
|
2052
|
+
);
|
|
2053
|
+
this.items.push(item);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return this.items;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Parses a single pantry item from its TOML representation.
|
|
2060
|
+
*/
|
|
2061
|
+
parseItem(name, location, data) {
|
|
2062
|
+
const item = { name, location };
|
|
2063
|
+
if (typeof data === "string") {
|
|
2064
|
+
const parsed = parseQuantityWithUnit(data);
|
|
2065
|
+
item.quantity = parsed.value;
|
|
2066
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2067
|
+
} else {
|
|
2068
|
+
if (data.quantity) {
|
|
2069
|
+
const parsed = parseQuantityWithUnit(data.quantity);
|
|
2070
|
+
item.quantity = parsed.value;
|
|
2071
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2072
|
+
}
|
|
2073
|
+
if (data.low) {
|
|
2074
|
+
const parsed = parseQuantityWithUnit(data.low);
|
|
2075
|
+
item.low = parsed.value;
|
|
2076
|
+
if (parsed.unit) item.lowUnit = parsed.unit;
|
|
2077
|
+
}
|
|
2078
|
+
if (data.bought) {
|
|
2079
|
+
item.bought = this.parseDate(data.bought);
|
|
2080
|
+
}
|
|
2081
|
+
if (data.expire) {
|
|
2082
|
+
item.expire = this.parseDate(data.expire);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
return item;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Parses a date string using the configured format or fuzzy detection.
|
|
2089
|
+
*/
|
|
2090
|
+
parseDate(input) {
|
|
2091
|
+
if (this.options.dateFormat) {
|
|
2092
|
+
return parseDateFromFormat(input, this.options.dateFormat);
|
|
2093
|
+
}
|
|
2094
|
+
return parseFuzzyDate(input);
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Sets a category configuration for alias-based item lookups.
|
|
2098
|
+
* @param config - The category configuration to use.
|
|
2099
|
+
*/
|
|
2100
|
+
setCategoryConfig(config) {
|
|
2101
|
+
this.categoryConfig = config;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Finds a pantry item by name, using exact match first, then alias lookup
|
|
2105
|
+
* via the stored CategoryConfig.
|
|
2106
|
+
* @param name - The name to search for.
|
|
2107
|
+
* @returns The matching pantry item, or undefined if not found.
|
|
2108
|
+
*/
|
|
2109
|
+
findItem(name) {
|
|
2110
|
+
const lowerName = name.toLowerCase();
|
|
2111
|
+
const exact = this.items.find(
|
|
2112
|
+
(item) => item.name.toLowerCase() === lowerName
|
|
2113
|
+
);
|
|
2114
|
+
if (exact) return exact;
|
|
2115
|
+
if (this.categoryConfig) {
|
|
2116
|
+
for (const category of this.categoryConfig.categories) {
|
|
2117
|
+
for (const catIngredient of category.ingredients) {
|
|
2118
|
+
if (catIngredient.aliases.some(
|
|
2119
|
+
(alias) => alias.toLowerCase() === lowerName
|
|
2120
|
+
)) {
|
|
2121
|
+
const canonicalName = catIngredient.name.toLowerCase();
|
|
2122
|
+
const byCanonical = this.items.find(
|
|
2123
|
+
(item) => item.name.toLowerCase() === canonicalName
|
|
2124
|
+
);
|
|
2125
|
+
if (byCanonical) return byCanonical;
|
|
2126
|
+
for (const alias of catIngredient.aliases) {
|
|
2127
|
+
const byAlias = this.items.find(
|
|
2128
|
+
(item) => item.name.toLowerCase() === alias.toLowerCase()
|
|
2129
|
+
);
|
|
2130
|
+
if (byAlias) return byAlias;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return void 0;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Gets the numeric value of a pantry item's quantity, optionally converted to base units.
|
|
2140
|
+
* Returns undefined if the quantity has a text value or is not set.
|
|
2141
|
+
*/
|
|
2142
|
+
getItemNumericValue(quantity, unit) {
|
|
2143
|
+
if (!quantity) return void 0;
|
|
2144
|
+
let numericValue;
|
|
2145
|
+
if (quantity.type === "fixed") {
|
|
2146
|
+
if (quantity.value.type === "text") return void 0;
|
|
2147
|
+
numericValue = getNumericValue(quantity.value);
|
|
2148
|
+
} else {
|
|
2149
|
+
numericValue = (getNumericValue(quantity.min) + getNumericValue(quantity.max)) / 2;
|
|
2150
|
+
}
|
|
2151
|
+
if (unit) {
|
|
2152
|
+
const unitDef = normalizeUnit(unit);
|
|
2153
|
+
if (unitDef) {
|
|
2154
|
+
const toBase = getToBase(unitDef);
|
|
2155
|
+
numericValue *= toBase;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return numericValue;
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Returns all items that are depleted (quantity = 0) or below their low threshold.
|
|
2162
|
+
* @returns An array of depleted pantry items.
|
|
2163
|
+
*/
|
|
2164
|
+
getDepletedItems() {
|
|
2165
|
+
return this.items.filter((item) => this.isItemLow(item));
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Returns all items whose expiration date is within `nbDays` days from today
|
|
2169
|
+
* (or already passed).
|
|
2170
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0 (already expired).
|
|
2171
|
+
* @returns An array of expired pantry items.
|
|
2172
|
+
*/
|
|
2173
|
+
getExpiredItems(nbDays = 0) {
|
|
2174
|
+
return this.items.filter((item) => this.isItemExpired(item, nbDays));
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Checks if a specific item is low (quantity = 0 or below `low` threshold).
|
|
2178
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2179
|
+
* @returns true if the item is low, false otherwise. Returns false if item not found.
|
|
2180
|
+
*/
|
|
2181
|
+
isLow(itemName) {
|
|
2182
|
+
const item = this.findItem(itemName);
|
|
2183
|
+
if (!item) return false;
|
|
2184
|
+
return this.isItemLow(item);
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Checks if a specific item is expired or expires within `nbDays` days.
|
|
2188
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2189
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0.
|
|
2190
|
+
* @returns true if the item is expired, false otherwise. Returns false if item not found.
|
|
2191
|
+
*/
|
|
2192
|
+
isExpired(itemName, nbDays = 0) {
|
|
2193
|
+
const item = this.findItem(itemName);
|
|
2194
|
+
if (!item) return false;
|
|
2195
|
+
return this.isItemExpired(item, nbDays);
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Internal: checks if a pantry item is low.
|
|
2199
|
+
*/
|
|
2200
|
+
isItemLow(item) {
|
|
2201
|
+
if (!item.quantity) return false;
|
|
2202
|
+
const qtyValue = this.getItemNumericValue(item.quantity, item.unit);
|
|
2203
|
+
if (qtyValue === void 0) return false;
|
|
2204
|
+
if (qtyValue === 0) return true;
|
|
2205
|
+
if (item.low) {
|
|
2206
|
+
const lowValue = this.getItemNumericValue(item.low, item.lowUnit);
|
|
2207
|
+
if (lowValue !== void 0 && qtyValue <= lowValue) return true;
|
|
2208
|
+
}
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Internal: checks if a pantry item is expired.
|
|
2213
|
+
*/
|
|
2214
|
+
isItemExpired(item, nbDays) {
|
|
2215
|
+
if (!item.expire) return false;
|
|
2216
|
+
const now = /* @__PURE__ */ new Date();
|
|
2217
|
+
const cutoff = new Date(
|
|
2218
|
+
now.getFullYear(),
|
|
2219
|
+
now.getMonth(),
|
|
2220
|
+
now.getDate() + nbDays
|
|
2221
|
+
);
|
|
2222
|
+
const expireDay = new Date(
|
|
2223
|
+
item.expire.getFullYear(),
|
|
2224
|
+
item.expire.getMonth(),
|
|
2225
|
+
item.expire.getDate()
|
|
2226
|
+
);
|
|
2227
|
+
return expireDay <= cutoff;
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
|
|
1762
2231
|
// src/classes/product_catalog.ts
|
|
2232
|
+
var import_smol_toml2 = __toESM(require("smol-toml"), 1);
|
|
1763
2233
|
var ProductCatalog = class {
|
|
1764
2234
|
constructor(tomlContent) {
|
|
1765
2235
|
__publicField(this, "products", []);
|
|
@@ -1771,7 +2241,7 @@ var ProductCatalog = class {
|
|
|
1771
2241
|
* @returns A parsed list of `ProductOption`.
|
|
1772
2242
|
*/
|
|
1773
2243
|
parse(tomlContent) {
|
|
1774
|
-
const catalogRaw =
|
|
2244
|
+
const catalogRaw = import_smol_toml2.default.parse(tomlContent);
|
|
1775
2245
|
this.products = [];
|
|
1776
2246
|
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1777
2247
|
throw new InvalidProductCatalogFormat();
|
|
@@ -1844,7 +2314,7 @@ var ProductCatalog = class {
|
|
|
1844
2314
|
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1845
2315
|
};
|
|
1846
2316
|
}
|
|
1847
|
-
return
|
|
2317
|
+
return import_smol_toml2.default.stringify(grouped);
|
|
1848
2318
|
}
|
|
1849
2319
|
/**
|
|
1850
2320
|
* Adds a product to the catalog.
|
|
@@ -1981,7 +2451,6 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1981
2451
|
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
|
|
1982
2452
|
const unitLists = [];
|
|
1983
2453
|
const normalizeOrGroup = (og) => ({
|
|
1984
|
-
...og,
|
|
1985
2454
|
or: og.or.map((q) => ({
|
|
1986
2455
|
...q,
|
|
1987
2456
|
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
@@ -2371,13 +2840,13 @@ var _Recipe = class _Recipe {
|
|
|
2371
2840
|
for (const match of text.matchAll(globalRegex)) {
|
|
2372
2841
|
const idx = match.index;
|
|
2373
2842
|
if (idx > cursor) {
|
|
2374
|
-
noteItems.push(
|
|
2843
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor, idx)));
|
|
2375
2844
|
}
|
|
2376
2845
|
this._parseArbitraryScalable(match.groups, noteItems);
|
|
2377
2846
|
cursor = idx + match[0].length;
|
|
2378
2847
|
}
|
|
2379
2848
|
if (cursor < text.length) {
|
|
2380
|
-
noteItems.push(
|
|
2849
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor)));
|
|
2381
2850
|
}
|
|
2382
2851
|
return noteItems;
|
|
2383
2852
|
}
|
|
@@ -2662,34 +3131,10 @@ var _Recipe = class _Recipe {
|
|
|
2662
3131
|
}
|
|
2663
3132
|
}
|
|
2664
3133
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
* When no options are provided, returns all recipe ingredients with quantities
|
|
2670
|
-
* calculated using primary alternatives (same as after parsing).
|
|
2671
|
-
*
|
|
2672
|
-
* @param options - Options for filtering and choice selection:
|
|
2673
|
-
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
2674
|
-
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
2675
|
-
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
2676
|
-
* @returns Array of Ingredient objects with quantities populated
|
|
2677
|
-
*
|
|
2678
|
-
* @example
|
|
2679
|
-
* ```typescript
|
|
2680
|
-
* // Get all ingredients with primary alternatives
|
|
2681
|
-
* const ingredients = recipe.getIngredientQuantities();
|
|
2682
|
-
*
|
|
2683
|
-
* // Get ingredients for a specific section
|
|
2684
|
-
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
2685
|
-
*
|
|
2686
|
-
* // Get ingredients with specific choices applied
|
|
2687
|
-
* const withChoices = recipe.getIngredientQuantities({
|
|
2688
|
-
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
2689
|
-
* });
|
|
2690
|
-
* ```
|
|
2691
|
-
*/
|
|
2692
|
-
getIngredientQuantities(options) {
|
|
3134
|
+
// Type for accumulated quantities (used internally by collectQuantityGroups)
|
|
3135
|
+
// Defined as a static type alias for the private method's return type
|
|
3136
|
+
/** @internal */
|
|
3137
|
+
collectQuantityGroups(options) {
|
|
2693
3138
|
const { section, step, choices } = options || {};
|
|
2694
3139
|
const sectionsToProcess = section !== void 0 ? (() => {
|
|
2695
3140
|
const idx = typeof section === "number" ? section : this.sections.indexOf(section);
|
|
@@ -2807,6 +3252,78 @@ var _Recipe = class _Recipe {
|
|
|
2807
3252
|
}
|
|
2808
3253
|
}
|
|
2809
3254
|
}
|
|
3255
|
+
return { ingredientGroups, selectedIndices, referencedIndices };
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* Gets the raw (unprocessed) quantity groups for each ingredient, before
|
|
3259
|
+
* any summation or equivalents simplification. This is useful for cross-recipe
|
|
3260
|
+
* aggregation (e.g., in {@link ShoppingList}), where quantities from multiple
|
|
3261
|
+
* recipes should be combined before processing.
|
|
3262
|
+
*
|
|
3263
|
+
* @param options - Options for filtering and choice selection (same as {@link getIngredientQuantities}).
|
|
3264
|
+
* @returns Array of {@link RawQuantityGroup} objects, one per ingredient with quantities.
|
|
3265
|
+
*
|
|
3266
|
+
* @example
|
|
3267
|
+
* ```typescript
|
|
3268
|
+
* const rawGroups = recipe.getRawQuantityGroups();
|
|
3269
|
+
* // Each group has: name, usedAsPrimary, flags, quantities[]
|
|
3270
|
+
* // quantities are the raw QuantityWithExtendedUnit or FlatOrGroup entries
|
|
3271
|
+
* ```
|
|
3272
|
+
*/
|
|
3273
|
+
getRawQuantityGroups(options) {
|
|
3274
|
+
const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
|
|
3275
|
+
const result = [];
|
|
3276
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
3277
|
+
if (!referencedIndices.has(index)) continue;
|
|
3278
|
+
const orig = this.ingredients[index];
|
|
3279
|
+
const usedAsPrimary = selectedIndices.has(index);
|
|
3280
|
+
const quantities = [];
|
|
3281
|
+
if (usedAsPrimary) {
|
|
3282
|
+
const groupsForIng = ingredientGroups.get(index);
|
|
3283
|
+
if (groupsForIng) {
|
|
3284
|
+
for (const [, group] of groupsForIng) {
|
|
3285
|
+
quantities.push(...group.quantities);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
result.push({
|
|
3290
|
+
name: orig.name,
|
|
3291
|
+
...usedAsPrimary && { usedAsPrimary: true },
|
|
3292
|
+
...orig.flags && { flags: orig.flags },
|
|
3293
|
+
quantities
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
return result;
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Gets ingredients with their quantities populated, optionally filtered by section/step
|
|
3300
|
+
* and respecting user choices for alternatives.
|
|
3301
|
+
*
|
|
3302
|
+
* When no options are provided, returns all recipe ingredients with quantities
|
|
3303
|
+
* calculated using primary alternatives (same as after parsing).
|
|
3304
|
+
*
|
|
3305
|
+
* @param options - Options for filtering and choice selection:
|
|
3306
|
+
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
3307
|
+
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
3308
|
+
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
3309
|
+
* @returns Array of Ingredient objects with quantities populated
|
|
3310
|
+
*
|
|
3311
|
+
* @example
|
|
3312
|
+
* ```typescript
|
|
3313
|
+
* // Get all ingredients with primary alternatives
|
|
3314
|
+
* const ingredients = recipe.getIngredientQuantities();
|
|
3315
|
+
*
|
|
3316
|
+
* // Get ingredients for a specific section
|
|
3317
|
+
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
3318
|
+
*
|
|
3319
|
+
* // Get ingredients with specific choices applied
|
|
3320
|
+
* const withChoices = recipe.getIngredientQuantities({
|
|
3321
|
+
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
3322
|
+
* });
|
|
3323
|
+
* ```
|
|
3324
|
+
*/
|
|
3325
|
+
getIngredientQuantities(options) {
|
|
3326
|
+
const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
|
|
2810
3327
|
const result = [];
|
|
2811
3328
|
for (let index = 0; index < this.ingredients.length; index++) {
|
|
2812
3329
|
if (!referencedIndices.has(index)) continue;
|
|
@@ -2931,7 +3448,7 @@ var _Recipe = class _Recipe {
|
|
|
2931
3448
|
for (const match of line.matchAll(tokensRegex)) {
|
|
2932
3449
|
const idx = match.index;
|
|
2933
3450
|
if (idx > cursor) {
|
|
2934
|
-
items.push(
|
|
3451
|
+
items.push(...parseMarkdownSegments(line.slice(cursor, idx)));
|
|
2935
3452
|
}
|
|
2936
3453
|
const groups = match.groups;
|
|
2937
3454
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
@@ -2993,7 +3510,7 @@ var _Recipe = class _Recipe {
|
|
|
2993
3510
|
cursor = idx + match[0].length;
|
|
2994
3511
|
}
|
|
2995
3512
|
if (cursor < line.length) {
|
|
2996
|
-
items.push(
|
|
3513
|
+
items.push(...parseMarkdownSegments(line.slice(cursor)));
|
|
2997
3514
|
}
|
|
2998
3515
|
blankLineBefore = false;
|
|
2999
3516
|
}
|
|
@@ -3335,13 +3852,12 @@ __publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
|
|
|
3335
3852
|
var Recipe = _Recipe;
|
|
3336
3853
|
|
|
3337
3854
|
// src/classes/shopping_list.ts
|
|
3338
|
-
var ShoppingList = class {
|
|
3855
|
+
var ShoppingList = class _ShoppingList {
|
|
3339
3856
|
/**
|
|
3340
3857
|
* Creates a new ShoppingList instance
|
|
3341
3858
|
* @param categoryConfigStr - The category configuration to parse.
|
|
3342
3859
|
*/
|
|
3343
3860
|
constructor(categoryConfigStr) {
|
|
3344
|
-
// TODO: backport type change
|
|
3345
3861
|
/**
|
|
3346
3862
|
* The ingredients in the shopping list.
|
|
3347
3863
|
*/
|
|
@@ -3358,38 +3874,38 @@ var ShoppingList = class {
|
|
|
3358
3874
|
* The categorized ingredients in the shopping list.
|
|
3359
3875
|
*/
|
|
3360
3876
|
__publicField(this, "categories");
|
|
3877
|
+
/**
|
|
3878
|
+
* The unit system to use for quantity simplification.
|
|
3879
|
+
* When set, overrides per-recipe unit systems.
|
|
3880
|
+
*/
|
|
3881
|
+
__publicField(this, "unitSystem");
|
|
3882
|
+
/**
|
|
3883
|
+
* Per-ingredient equivalence ratio maps for recomputing equivalents
|
|
3884
|
+
* after pantry subtraction. Keyed by ingredient name.
|
|
3885
|
+
* @internal
|
|
3886
|
+
*/
|
|
3887
|
+
__publicField(this, "equivalenceRatios", /* @__PURE__ */ new Map());
|
|
3888
|
+
/**
|
|
3889
|
+
* The original pantry (never mutated by recipe calculations).
|
|
3890
|
+
*/
|
|
3891
|
+
__publicField(this, "pantry");
|
|
3892
|
+
/**
|
|
3893
|
+
* The pantry with quantities updated after subtracting recipe needs.
|
|
3894
|
+
* Recomputed on every {@link ShoppingList.calculateIngredients | calculateIngredients()} call.
|
|
3895
|
+
*/
|
|
3896
|
+
__publicField(this, "resultingPantry");
|
|
3361
3897
|
if (categoryConfigStr) {
|
|
3362
3898
|
this.setCategoryConfig(categoryConfigStr);
|
|
3363
3899
|
}
|
|
3364
3900
|
}
|
|
3365
3901
|
calculateIngredients() {
|
|
3366
3902
|
this.ingredients = [];
|
|
3367
|
-
const
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
if (!existing.quantityTotal) {
|
|
3373
|
-
existing.quantityTotal = quantityTotal;
|
|
3374
|
-
return;
|
|
3375
|
-
}
|
|
3376
|
-
try {
|
|
3377
|
-
const existingQuantityTotalExtended = extendAllUnits(
|
|
3378
|
-
existing.quantityTotal
|
|
3379
|
-
);
|
|
3380
|
-
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
|
|
3381
|
-
existing.quantityTotal = addEquivalentsAndSimplify([
|
|
3382
|
-
...existingQuantities,
|
|
3383
|
-
...newQuantities
|
|
3384
|
-
]);
|
|
3385
|
-
return;
|
|
3386
|
-
} catch {
|
|
3387
|
-
}
|
|
3903
|
+
const rawQuantitiesMap = /* @__PURE__ */ new Map();
|
|
3904
|
+
const nameOrder = [];
|
|
3905
|
+
const trackName = (name) => {
|
|
3906
|
+
if (!nameOrder.includes(name)) {
|
|
3907
|
+
nameOrder.push(name);
|
|
3388
3908
|
}
|
|
3389
|
-
this.ingredients.push({
|
|
3390
|
-
name,
|
|
3391
|
-
quantityTotal
|
|
3392
|
-
});
|
|
3393
3909
|
};
|
|
3394
3910
|
for (const addedRecipe of this.recipes) {
|
|
3395
3911
|
let scaledRecipe;
|
|
@@ -3399,48 +3915,253 @@ var ShoppingList = class {
|
|
|
3399
3915
|
} else {
|
|
3400
3916
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
3401
3917
|
}
|
|
3402
|
-
const
|
|
3918
|
+
const rawGroups = scaledRecipe.getRawQuantityGroups({
|
|
3403
3919
|
choices: addedRecipe.choices
|
|
3404
3920
|
});
|
|
3405
|
-
for (const
|
|
3406
|
-
if (
|
|
3921
|
+
for (const group of rawGroups) {
|
|
3922
|
+
if (group.flags?.includes("hidden") || !group.usedAsPrimary) {
|
|
3407
3923
|
continue;
|
|
3408
3924
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3925
|
+
trackName(group.name);
|
|
3926
|
+
if (group.quantities.length > 0) {
|
|
3927
|
+
const existing = rawQuantitiesMap.get(group.name) ?? [];
|
|
3928
|
+
existing.push(...group.quantities);
|
|
3929
|
+
rawQuantitiesMap.set(group.name, existing);
|
|
3411
3930
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
this.equivalenceRatios.clear();
|
|
3934
|
+
for (const name of nameOrder) {
|
|
3935
|
+
const rawQuantities = rawQuantitiesMap.get(name);
|
|
3936
|
+
if (!rawQuantities || rawQuantities.length === 0) {
|
|
3937
|
+
this.ingredients.push({ name });
|
|
3938
|
+
continue;
|
|
3939
|
+
}
|
|
3940
|
+
const textEntries = [];
|
|
3941
|
+
const numericEntries = [];
|
|
3942
|
+
for (const q of rawQuantities) {
|
|
3943
|
+
if ("quantity" in q && q.quantity.type === "fixed" && q.quantity.value.type === "text") {
|
|
3944
|
+
textEntries.push(q);
|
|
3945
|
+
} else {
|
|
3946
|
+
numericEntries.push(q);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
if (numericEntries.length > 1) {
|
|
3950
|
+
const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
|
|
3951
|
+
getEquivalentUnitsLists(...numericEntries)
|
|
3952
|
+
);
|
|
3953
|
+
if (Object.keys(ratioMap).length > 0) {
|
|
3954
|
+
this.equivalenceRatios.set(name, ratioMap);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
const resultQuantities = [];
|
|
3958
|
+
for (const t2 of textEntries) {
|
|
3959
|
+
resultQuantities.push(toPlainUnit(t2));
|
|
3960
|
+
}
|
|
3961
|
+
if (numericEntries.length > 0) {
|
|
3962
|
+
resultQuantities.push(
|
|
3963
|
+
...flattenPlainUnitGroup(
|
|
3964
|
+
addEquivalentsAndSimplify(numericEntries, this.unitSystem)
|
|
3965
|
+
)
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
this.ingredients.push({
|
|
3969
|
+
name,
|
|
3970
|
+
quantities: resultQuantities
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
this.applyPantrySubtraction();
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Subtracts pantry item quantities from calculated ingredient quantities
|
|
3977
|
+
* and updates the resultingPantry to reflect consumed stock.
|
|
3978
|
+
*/
|
|
3979
|
+
applyPantrySubtraction() {
|
|
3980
|
+
if (!this.pantry) {
|
|
3981
|
+
this.resultingPantry = void 0;
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const clonedPantry = new Pantry();
|
|
3985
|
+
clonedPantry.items = deepClone(this.pantry.items);
|
|
3986
|
+
if (this.categoryConfig) {
|
|
3987
|
+
clonedPantry.setCategoryConfig(this.categoryConfig);
|
|
3988
|
+
}
|
|
3989
|
+
for (const ingredient of this.ingredients) {
|
|
3990
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
3991
|
+
continue;
|
|
3992
|
+
const pantryItem = clonedPantry.findItem(ingredient.name);
|
|
3993
|
+
if (!pantryItem || !pantryItem.quantity) continue;
|
|
3994
|
+
let pantryExtended = {
|
|
3995
|
+
quantity: pantryItem.quantity,
|
|
3996
|
+
...pantryItem.unit && { unit: { name: pantryItem.unit } }
|
|
3997
|
+
};
|
|
3998
|
+
for (let i2 = 0; i2 < ingredient.quantities.length; i2++) {
|
|
3999
|
+
const entry = ingredient.quantities[i2];
|
|
4000
|
+
const leaves = "and" in entry ? entry.and : [entry];
|
|
4001
|
+
for (const leaf of leaves) {
|
|
4002
|
+
const ingredientExtended = toExtendedUnit(leaf);
|
|
4003
|
+
const leafHasUnit = leaf.unit !== void 0 && leaf.unit !== "";
|
|
4004
|
+
const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
|
|
4005
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4006
|
+
const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
|
|
4007
|
+
if (unitMismatch) {
|
|
4008
|
+
const leafUnit = leaf.unit ?? NO_UNIT;
|
|
4009
|
+
const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
|
|
4010
|
+
const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
|
|
4011
|
+
if (ratioFromPantry !== void 0) {
|
|
4012
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4013
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
4014
|
+
if (typeof pantryValue === "number" && typeof leafValue === "number") {
|
|
4015
|
+
const pantryInLeafUnits = pantryValue * ratioFromPantry;
|
|
4016
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
4017
|
+
const remainingLeafValue = Math.max(
|
|
4018
|
+
leafValue - pantryInLeafUnits,
|
|
4019
|
+
0
|
|
4020
|
+
);
|
|
4021
|
+
leaf.quantity = {
|
|
4022
|
+
type: "fixed",
|
|
4023
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4024
|
+
};
|
|
4025
|
+
const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
|
|
4026
|
+
const remainingPantryValue = Math.max(
|
|
4027
|
+
pantryValue - consumedInPantryUnits,
|
|
4028
|
+
0
|
|
4029
|
+
);
|
|
4030
|
+
pantryExtended = {
|
|
4031
|
+
quantity: {
|
|
4032
|
+
type: "fixed",
|
|
4033
|
+
value: {
|
|
4034
|
+
type: "decimal",
|
|
4035
|
+
decimal: remainingPantryValue
|
|
4036
|
+
}
|
|
4037
|
+
},
|
|
4038
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
4039
|
+
};
|
|
4040
|
+
continue;
|
|
3418
4041
|
}
|
|
3419
|
-
} else {
|
|
3420
|
-
const plainQty = {
|
|
3421
|
-
quantity: qGroup.quantity
|
|
3422
|
-
};
|
|
3423
|
-
if (qGroup.unit) plainQty.unit = qGroup.unit;
|
|
3424
|
-
if (qGroup.equivalents) plainQty.equivalents = qGroup.equivalents;
|
|
3425
|
-
allQuantities.push(plainQty);
|
|
3426
4042
|
}
|
|
3427
4043
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
4044
|
+
try {
|
|
4045
|
+
const remaining = subtractQuantities(
|
|
4046
|
+
ingredientExtended,
|
|
4047
|
+
pantryExtended,
|
|
4048
|
+
{ clampToZero: true }
|
|
3433
4049
|
);
|
|
3434
|
-
const
|
|
3435
|
-
|
|
4050
|
+
const consumed = subtractQuantities(
|
|
4051
|
+
pantryExtended,
|
|
4052
|
+
ingredientExtended,
|
|
4053
|
+
{ clampToZero: true }
|
|
3436
4054
|
);
|
|
3437
|
-
|
|
4055
|
+
pantryExtended = consumed;
|
|
4056
|
+
const updated = toPlainUnit(remaining);
|
|
4057
|
+
leaf.quantity = updated.quantity;
|
|
4058
|
+
leaf.unit = updated.unit;
|
|
4059
|
+
} catch {
|
|
3438
4060
|
}
|
|
3439
|
-
} else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
|
|
3440
|
-
this.ingredients.push({ name: ingredient.name });
|
|
3441
4061
|
}
|
|
4062
|
+
if ("and" in entry) {
|
|
4063
|
+
const nonZero = entry.and.filter(
|
|
4064
|
+
(leaf) => leaf.quantity.type !== "fixed" || leaf.quantity.value.type !== "decimal" || leaf.quantity.value.decimal !== 0
|
|
4065
|
+
);
|
|
4066
|
+
entry.and.length = 0;
|
|
4067
|
+
entry.and.push(...nonZero);
|
|
4068
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4069
|
+
if (entry.equivalents && ratioMap) {
|
|
4070
|
+
const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
|
|
4071
|
+
entry.equivalents = _ShoppingList.recomputeEquivalents(
|
|
4072
|
+
entry.and,
|
|
4073
|
+
ratioMap,
|
|
4074
|
+
equivUnits
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
if (entry.and.length === 1) {
|
|
4078
|
+
const single = entry.and[0];
|
|
4079
|
+
ingredient.quantities[i2] = {
|
|
4080
|
+
quantity: single.quantity,
|
|
4081
|
+
...single.unit && { unit: single.unit },
|
|
4082
|
+
...entry.equivalents && { equivalents: entry.equivalents },
|
|
4083
|
+
...entry.alternatives && { alternatives: entry.alternatives }
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
} else if ("equivalents" in entry && entry.equivalents) {
|
|
4087
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4088
|
+
if (ratioMap) {
|
|
4089
|
+
const equivUnits = entry.equivalents.map(
|
|
4090
|
+
(e2) => e2.unit ?? NO_UNIT
|
|
4091
|
+
);
|
|
4092
|
+
const recomputed = _ShoppingList.recomputeEquivalents(
|
|
4093
|
+
[entry],
|
|
4094
|
+
ratioMap,
|
|
4095
|
+
equivUnits
|
|
4096
|
+
);
|
|
4097
|
+
entry.equivalents = recomputed;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
ingredient.quantities = ingredient.quantities.filter((entry) => {
|
|
4102
|
+
if ("and" in entry) return entry.and.length > 0;
|
|
4103
|
+
return !(entry.quantity.type === "fixed" && entry.quantity.value.type === "decimal" && entry.quantity.value.decimal === 0);
|
|
4104
|
+
});
|
|
4105
|
+
if (ingredient.quantities.length === 0) {
|
|
4106
|
+
ingredient.quantities = void 0;
|
|
4107
|
+
}
|
|
4108
|
+
pantryItem.quantity = pantryExtended.quantity;
|
|
4109
|
+
if (pantryExtended.unit) {
|
|
4110
|
+
pantryItem.unit = pantryExtended.unit.name;
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
this.resultingPantry = clonedPantry;
|
|
4114
|
+
}
|
|
4115
|
+
/**
|
|
4116
|
+
* Builds a ratio map from equivalence lists.
|
|
4117
|
+
* For each equivalence list, stores ratio = equiv_value / primary_value
|
|
4118
|
+
* for every pair of units, so equivalents can be recomputed after
|
|
4119
|
+
* pantry subtraction modifies primary quantities.
|
|
4120
|
+
*/
|
|
4121
|
+
static buildEquivalenceRatioMap(unitsLists) {
|
|
4122
|
+
const ratioMap = {};
|
|
4123
|
+
for (const list of unitsLists) {
|
|
4124
|
+
for (const equiv of list) {
|
|
4125
|
+
const equivValue = getAverageValue(equiv.quantity);
|
|
4126
|
+
for (const primary of list) {
|
|
4127
|
+
if (primary === equiv) continue;
|
|
4128
|
+
const primaryValue = getAverageValue(primary.quantity);
|
|
4129
|
+
const equivUnit = equiv.unit.name;
|
|
4130
|
+
const primaryUnit = primary.unit.name;
|
|
4131
|
+
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
4132
|
+
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
return ratioMap;
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Recomputes equivalent quantities from current primary values and stored ratios.
|
|
4140
|
+
* For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
|
|
4141
|
+
* Returns undefined if all equivalents compute to zero.
|
|
4142
|
+
*/
|
|
4143
|
+
static recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
4144
|
+
const equivalents = [];
|
|
4145
|
+
for (const equivUnit of equivUnits) {
|
|
4146
|
+
const ratios = ratioMap[equivUnit];
|
|
4147
|
+
let total = 0;
|
|
4148
|
+
for (const primary of primaries) {
|
|
4149
|
+
const pUnit = primary.unit ?? NO_UNIT;
|
|
4150
|
+
const ratio = ratios[pUnit];
|
|
4151
|
+
const pValue = getAverageValue(primary.quantity);
|
|
4152
|
+
total += pValue * ratio;
|
|
4153
|
+
}
|
|
4154
|
+
if (total > 0) {
|
|
4155
|
+
equivalents.push({
|
|
4156
|
+
quantity: {
|
|
4157
|
+
type: "fixed",
|
|
4158
|
+
value: { type: "decimal", decimal: total }
|
|
4159
|
+
},
|
|
4160
|
+
...equivUnit !== "" && { unit: equivUnit }
|
|
4161
|
+
});
|
|
3442
4162
|
}
|
|
3443
4163
|
}
|
|
4164
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
3444
4165
|
}
|
|
3445
4166
|
/**
|
|
3446
4167
|
* Adds a recipe to the shopping list, then automatically
|
|
@@ -3529,9 +4250,41 @@ var ShoppingList = class {
|
|
|
3529
4250
|
this.calculateIngredients();
|
|
3530
4251
|
this.categorize();
|
|
3531
4252
|
}
|
|
4253
|
+
/**
|
|
4254
|
+
* Adds a pantry to the shopping list. On-hand pantry quantities will be
|
|
4255
|
+
* subtracted from recipe ingredient needs on each recalculation.
|
|
4256
|
+
* @param pantry - A Pantry instance or a TOML string to parse.
|
|
4257
|
+
* @param options - Options for pantry parsing (only used when providing a TOML string).
|
|
4258
|
+
*/
|
|
4259
|
+
addPantry(pantry, options) {
|
|
4260
|
+
if (typeof pantry === "string") {
|
|
4261
|
+
this.pantry = new Pantry(pantry, options);
|
|
4262
|
+
} else if (pantry instanceof Pantry) {
|
|
4263
|
+
this.pantry = pantry;
|
|
4264
|
+
} else {
|
|
4265
|
+
throw new Error(
|
|
4266
|
+
"Invalid pantry: expected a Pantry instance or TOML string"
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
if (this.categoryConfig) {
|
|
4270
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4271
|
+
}
|
|
4272
|
+
this.calculateIngredients();
|
|
4273
|
+
this.categorize();
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Returns the resulting pantry with quantities updated to reflect
|
|
4277
|
+
* what was consumed by the shopping list's recipes.
|
|
4278
|
+
* Returns undefined if no pantry was added.
|
|
4279
|
+
* @returns The resulting Pantry, or undefined.
|
|
4280
|
+
*/
|
|
4281
|
+
getPantry() {
|
|
4282
|
+
return this.resultingPantry;
|
|
4283
|
+
}
|
|
3532
4284
|
/**
|
|
3533
4285
|
* Sets the category configuration for the shopping list
|
|
3534
4286
|
* and automatically categorize current ingredients from the list.
|
|
4287
|
+
* Also propagates the configuration to the pantry if one is set.
|
|
3535
4288
|
* @param config - The category configuration to parse.
|
|
3536
4289
|
*/
|
|
3537
4290
|
setCategoryConfig(config) {
|
|
@@ -3539,6 +4292,9 @@ var ShoppingList = class {
|
|
|
3539
4292
|
this.categoryConfig = new CategoryConfig(config);
|
|
3540
4293
|
else if (config instanceof CategoryConfig) this.categoryConfig = config;
|
|
3541
4294
|
else throw new Error("Invalid category configuration");
|
|
4295
|
+
if (this.pantry) {
|
|
4296
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4297
|
+
}
|
|
3542
4298
|
this.categorize();
|
|
3543
4299
|
}
|
|
3544
4300
|
/**
|
|
@@ -3683,8 +4439,27 @@ var ShoppingCart = class {
|
|
|
3683
4439
|
getOptimumMatch(ingredient, options) {
|
|
3684
4440
|
if (options.length === 0)
|
|
3685
4441
|
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
3686
|
-
if (!ingredient.
|
|
4442
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
3687
4443
|
throw new NoProductMatchError(ingredient.name, "noQuantity");
|
|
4444
|
+
const allPlainEntries = [];
|
|
4445
|
+
for (const q of ingredient.quantities) {
|
|
4446
|
+
if ("and" in q) {
|
|
4447
|
+
allPlainEntries.push({ and: q.and });
|
|
4448
|
+
} else {
|
|
4449
|
+
const entry = {
|
|
4450
|
+
quantity: q.quantity,
|
|
4451
|
+
...q.unit && { unit: q.unit },
|
|
4452
|
+
...q.equivalents && { equivalents: q.equivalents }
|
|
4453
|
+
};
|
|
4454
|
+
allPlainEntries.push(entry);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
let quantityTotal;
|
|
4458
|
+
if (allPlainEntries.length === 1) {
|
|
4459
|
+
quantityTotal = allPlainEntries[0];
|
|
4460
|
+
} else {
|
|
4461
|
+
quantityTotal = { and: allPlainEntries };
|
|
4462
|
+
}
|
|
3688
4463
|
const normalizedOptions = options.map(
|
|
3689
4464
|
(option) => ({
|
|
3690
4465
|
...option,
|
|
@@ -3700,7 +4475,7 @@ var ShoppingCart = class {
|
|
|
3700
4475
|
})
|
|
3701
4476
|
})
|
|
3702
4477
|
);
|
|
3703
|
-
const normalizedQuantityTotal = normalizeAllUnits(
|
|
4478
|
+
const normalizedQuantityTotal = normalizeAllUnits(quantityTotal);
|
|
3704
4479
|
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
3705
4480
|
if (isAndGroup(normalizedQuantities)) {
|
|
3706
4481
|
for (const q of normalizedQuantities.and) {
|
|
@@ -3923,9 +4698,12 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
|
3923
4698
|
}
|
|
3924
4699
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3925
4700
|
0 && (module.exports = {
|
|
4701
|
+
BadIndentationError,
|
|
3926
4702
|
CategoryConfig,
|
|
3927
4703
|
NoProductCatalogForCartError,
|
|
3928
4704
|
NoShoppingListForCartError,
|
|
4705
|
+
NoTabAsIndentError,
|
|
4706
|
+
Pantry,
|
|
3929
4707
|
ProductCatalog,
|
|
3930
4708
|
Recipe,
|
|
3931
4709
|
Section,
|
|
@@ -3952,6 +4730,9 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
|
3952
4730
|
// v8 ignore else -- @preserve
|
|
3953
4731
|
// v8 ignore if -- @preserve
|
|
3954
4732
|
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
4733
|
+
// v8 ignore if -- @preserve: defensive type guard
|
|
3955
4734
|
/* v8 ignore if -- @preserve */
|
|
3956
4735
|
// v8 ignore next -- @preserve
|
|
4736
|
+
// v8 ignore else --@preserve: defensive type guard
|
|
4737
|
+
// v8 ignore else -- @preserve: detection if
|
|
3957
4738
|
//# sourceMappingURL=index.cjs.map
|