@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.js
CHANGED
|
@@ -63,7 +63,7 @@ var CategoryConfig = class {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// src/classes/
|
|
66
|
+
// src/classes/pantry.ts
|
|
67
67
|
import TOML from "smol-toml";
|
|
68
68
|
|
|
69
69
|
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
@@ -299,6 +299,37 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
|
|
|
299
299
|
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();
|
|
300
300
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
301
301
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
302
|
+
var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
|
|
303
|
+
var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
|
|
304
|
+
var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
|
|
305
|
+
var mdTripleAsterisk = d().literal("***").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("***");
|
|
306
|
+
var mdTripleUnderscore = d().literal("___").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("___");
|
|
307
|
+
var mdBoldAstItalicUnd = d().literal("**_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_**");
|
|
308
|
+
var mdBoldUndItalicAst = d().literal("__*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*__");
|
|
309
|
+
var mdItalicAstBoldUnd = d().literal("*__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__*");
|
|
310
|
+
var mdItalicUndBoldAst = d().literal("_**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**_");
|
|
311
|
+
var mdBoldAsterisk = d().literal("**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**");
|
|
312
|
+
var mdBoldUnderscore = d().wordBoundary().literal("__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__").wordBoundary();
|
|
313
|
+
var mdItalicAsterisk = d().literal("*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*");
|
|
314
|
+
var mdItalicUnderscore = d().wordBoundary().literal("_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_").wordBoundary();
|
|
315
|
+
var markdownRegex = new RegExp(
|
|
316
|
+
[
|
|
317
|
+
mdEscaped,
|
|
318
|
+
mdInlineCode,
|
|
319
|
+
mdLink,
|
|
320
|
+
mdTripleAsterisk,
|
|
321
|
+
mdTripleUnderscore,
|
|
322
|
+
mdBoldAstItalicUnd,
|
|
323
|
+
mdBoldUndItalicAst,
|
|
324
|
+
mdItalicAstBoldUnd,
|
|
325
|
+
mdItalicUndBoldAst,
|
|
326
|
+
mdBoldAsterisk,
|
|
327
|
+
mdBoldUnderscore,
|
|
328
|
+
mdItalicAsterisk,
|
|
329
|
+
mdItalicUnderscore
|
|
330
|
+
].map((r2) => r2.toRegExp().source).join("|"),
|
|
331
|
+
"g"
|
|
332
|
+
);
|
|
302
333
|
|
|
303
334
|
// src/units/definitions.ts
|
|
304
335
|
var units = [
|
|
@@ -929,21 +960,6 @@ function hasAlternatives(entry) {
|
|
|
929
960
|
}
|
|
930
961
|
|
|
931
962
|
// src/quantities/mutations.ts
|
|
932
|
-
function extendAllUnits(q) {
|
|
933
|
-
if (isAndGroup(q)) {
|
|
934
|
-
return { and: q.and.map(extendAllUnits) };
|
|
935
|
-
} else if (isOrGroup(q)) {
|
|
936
|
-
return { or: q.or.map(extendAllUnits) };
|
|
937
|
-
} else {
|
|
938
|
-
const newQ = {
|
|
939
|
-
quantity: q.quantity
|
|
940
|
-
};
|
|
941
|
-
if (q.unit) {
|
|
942
|
-
newQ.unit = { name: q.unit };
|
|
943
|
-
}
|
|
944
|
-
return newQ;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
963
|
function normalizeAllUnits(q) {
|
|
948
964
|
if (isAndGroup(q)) {
|
|
949
965
|
return { and: q.and.map(normalizeAllUnits) };
|
|
@@ -1208,30 +1224,51 @@ var flattenPlainUnitGroup = (summed) => {
|
|
|
1208
1224
|
}
|
|
1209
1225
|
} else if (isAndGroup(summed)) {
|
|
1210
1226
|
const andEntries = [];
|
|
1227
|
+
const standaloneEntries = [];
|
|
1211
1228
|
const equivalentsList = [];
|
|
1212
1229
|
for (const entry of summed.and) {
|
|
1213
1230
|
if (isOrGroup(entry)) {
|
|
1214
1231
|
const orEntries = entry.or;
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1232
|
+
const firstEntry = orEntries[0];
|
|
1233
|
+
if (isAndGroup(firstEntry)) {
|
|
1234
|
+
for (const nestedEntry of firstEntry.and) {
|
|
1235
|
+
andEntries.push({
|
|
1236
|
+
quantity: nestedEntry.quantity,
|
|
1237
|
+
...nestedEntry.unit && { unit: nestedEntry.unit }
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
} else {
|
|
1241
|
+
const primary = firstEntry;
|
|
1242
|
+
andEntries.push({
|
|
1243
|
+
quantity: primary.quantity,
|
|
1244
|
+
...primary.unit && { unit: primary.unit }
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const equivEntries = orEntries.slice(1).filter((e2) => isQuantity(e2));
|
|
1248
|
+
equivalentsList.push(
|
|
1249
|
+
...equivEntries.map((e2) => ({
|
|
1250
|
+
quantity: e2.quantity,
|
|
1251
|
+
...e2.unit && { unit: e2.unit }
|
|
1252
|
+
}))
|
|
1253
|
+
);
|
|
1254
|
+
} else {
|
|
1255
|
+
const simpleQuantityEntry = entry;
|
|
1256
|
+
standaloneEntries.push({
|
|
1257
|
+
quantity: simpleQuantityEntry.quantity,
|
|
1258
|
+
...simpleQuantityEntry.unit && { unit: simpleQuantityEntry.unit }
|
|
1224
1259
|
});
|
|
1225
1260
|
}
|
|
1226
1261
|
}
|
|
1227
1262
|
if (equivalentsList.length === 0) {
|
|
1228
|
-
return andEntries;
|
|
1263
|
+
return [...andEntries, ...standaloneEntries];
|
|
1229
1264
|
}
|
|
1230
|
-
const result =
|
|
1265
|
+
const result = [];
|
|
1266
|
+
result.push({
|
|
1231
1267
|
and: andEntries,
|
|
1232
1268
|
equivalents: equivalentsList
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1269
|
+
});
|
|
1270
|
+
result.push(...standaloneEntries);
|
|
1271
|
+
return result;
|
|
1235
1272
|
} else {
|
|
1236
1273
|
return [
|
|
1237
1274
|
{ quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
|
|
@@ -1285,6 +1322,24 @@ function applyBestUnit(q, system) {
|
|
|
1285
1322
|
unit: { name: bestUnit.name }
|
|
1286
1323
|
};
|
|
1287
1324
|
}
|
|
1325
|
+
function subtractQuantities(q1, q2, options = {}) {
|
|
1326
|
+
const { clampToZero = true, system } = options;
|
|
1327
|
+
const negatedQ2 = {
|
|
1328
|
+
...q2,
|
|
1329
|
+
quantity: multiplyQuantityValue(q2.quantity, -1)
|
|
1330
|
+
};
|
|
1331
|
+
const result = addQuantities(q1, negatedQ2, system);
|
|
1332
|
+
if (clampToZero) {
|
|
1333
|
+
const avg = getAverageValue(result.quantity);
|
|
1334
|
+
if (typeof avg === "number" && avg < 0) {
|
|
1335
|
+
return {
|
|
1336
|
+
quantity: { type: "fixed", value: { type: "decimal", decimal: 0 } },
|
|
1337
|
+
unit: result.unit
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
1288
1343
|
|
|
1289
1344
|
// src/utils/parser_helpers.ts
|
|
1290
1345
|
function flushPendingNote(section, noteItems) {
|
|
@@ -1425,12 +1480,197 @@ function parseQuantityInput(input_str) {
|
|
|
1425
1480
|
}
|
|
1426
1481
|
return { type: "fixed", value: parseFixedValue(clean_str) };
|
|
1427
1482
|
}
|
|
1483
|
+
function parseQuantityWithUnit(input) {
|
|
1484
|
+
const trimmed = input.trim();
|
|
1485
|
+
const separatorIndex = trimmed.indexOf("%");
|
|
1486
|
+
if (separatorIndex === -1) {
|
|
1487
|
+
return { value: parseQuantityInput(trimmed) };
|
|
1488
|
+
}
|
|
1489
|
+
const valuePart = trimmed.slice(0, separatorIndex).trim();
|
|
1490
|
+
const unitPart = trimmed.slice(separatorIndex + 1).trim();
|
|
1491
|
+
return {
|
|
1492
|
+
value: parseQuantityInput(valuePart),
|
|
1493
|
+
unit: unitPart || void 0
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function parseDateFromFormat(input, format) {
|
|
1497
|
+
const delimiterMatch = format.match(/[^A-Za-z]/);
|
|
1498
|
+
if (!delimiterMatch) {
|
|
1499
|
+
throw new Error(`Invalid date format: ${format}. No delimiter found.`);
|
|
1500
|
+
}
|
|
1501
|
+
const delimiter = delimiterMatch[0];
|
|
1502
|
+
const formatParts = format.split(delimiter);
|
|
1503
|
+
const inputParts = input.trim().split(delimiter);
|
|
1504
|
+
if (formatParts.length !== 3 || inputParts.length !== 3) {
|
|
1505
|
+
throw new Error(
|
|
1506
|
+
`Invalid date input "${input}" for format "${format}". Expected 3 parts.`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
let day = 0, month = 0, year = 0;
|
|
1510
|
+
for (let i2 = 0; i2 < 3; i2++) {
|
|
1511
|
+
const token = formatParts[i2].toUpperCase();
|
|
1512
|
+
const value = parseInt(inputParts[i2], 10);
|
|
1513
|
+
if (isNaN(value)) {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`Invalid date input "${input}": non-numeric part "${inputParts[i2]}".`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (token === "DD") day = value;
|
|
1519
|
+
else if (token === "MM") month = value;
|
|
1520
|
+
else if (token === "YYYY") year = value;
|
|
1521
|
+
else
|
|
1522
|
+
throw new Error(
|
|
1523
|
+
`Unknown token "${formatParts[i2]}" in format "${format}"`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const date = new Date(year, month - 1, day);
|
|
1527
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1528
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1529
|
+
}
|
|
1530
|
+
return date;
|
|
1531
|
+
}
|
|
1532
|
+
function disambiguateDayMonth(first, second, year) {
|
|
1533
|
+
if (second > 12 && first <= 12) {
|
|
1534
|
+
return [second, first, year];
|
|
1535
|
+
}
|
|
1536
|
+
return [first, second, year];
|
|
1537
|
+
}
|
|
1538
|
+
function parseFuzzyDate(input) {
|
|
1539
|
+
const trimmed = input.trim();
|
|
1540
|
+
const delimiterMatch = trimmed.match(/[./-]/);
|
|
1541
|
+
if (!delimiterMatch) {
|
|
1542
|
+
throw new Error(`Cannot parse date "${input}": no delimiter found.`);
|
|
1543
|
+
}
|
|
1544
|
+
const delimiter = delimiterMatch[0];
|
|
1545
|
+
const parts = trimmed.split(delimiter);
|
|
1546
|
+
if (parts.length !== 3) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
`Cannot parse date "${input}": expected 3 parts, got ${parts.length}.`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
const nums = parts.map((p) => parseInt(p, 10));
|
|
1552
|
+
if (nums.some((n2) => isNaN(n2))) {
|
|
1553
|
+
throw new Error(`Cannot parse date "${input}": non-numeric parts found.`);
|
|
1554
|
+
}
|
|
1555
|
+
let day, month, year;
|
|
1556
|
+
if (nums[0] >= 1e3) {
|
|
1557
|
+
year = nums[0];
|
|
1558
|
+
month = nums[1];
|
|
1559
|
+
day = nums[2];
|
|
1560
|
+
} else if (nums[2] >= 1e3) {
|
|
1561
|
+
[day, month, year] = disambiguateDayMonth(nums[0], nums[1], nums[2]);
|
|
1562
|
+
} else {
|
|
1563
|
+
if (nums[2] >= 100)
|
|
1564
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1565
|
+
[day, month] = disambiguateDayMonth(nums[0], nums[1], 0);
|
|
1566
|
+
year = 2e3 + nums[2];
|
|
1567
|
+
}
|
|
1568
|
+
const date = new Date(year, month - 1, day);
|
|
1569
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1570
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1571
|
+
}
|
|
1572
|
+
return date;
|
|
1573
|
+
}
|
|
1574
|
+
function parseMarkdownSegments(text) {
|
|
1575
|
+
const items = [];
|
|
1576
|
+
let cursor = 0;
|
|
1577
|
+
for (const match of text.matchAll(markdownRegex)) {
|
|
1578
|
+
const idx = match.index;
|
|
1579
|
+
if (idx > cursor) {
|
|
1580
|
+
items.push({ type: "text", value: text.slice(cursor, idx) });
|
|
1581
|
+
}
|
|
1582
|
+
const [
|
|
1583
|
+
,
|
|
1584
|
+
escaped,
|
|
1585
|
+
// group 1: escaped character
|
|
1586
|
+
code,
|
|
1587
|
+
// group 2: inline code
|
|
1588
|
+
linkText,
|
|
1589
|
+
// group 3: link text
|
|
1590
|
+
linkUrl,
|
|
1591
|
+
// group 4: link url
|
|
1592
|
+
tripleAst,
|
|
1593
|
+
// group 5: ***bold+italic***
|
|
1594
|
+
tripleUnd,
|
|
1595
|
+
// group 6: ___bold+italic___
|
|
1596
|
+
astUnd,
|
|
1597
|
+
// group 7: **_bold+italic_**
|
|
1598
|
+
undAst,
|
|
1599
|
+
// group 8: __*bold+italic*__
|
|
1600
|
+
astUndUnd,
|
|
1601
|
+
// group 9: *__bold+italic__*
|
|
1602
|
+
undAstAst,
|
|
1603
|
+
// group 10: _**bold+italic**_
|
|
1604
|
+
boldAst,
|
|
1605
|
+
// group 11: **bold**
|
|
1606
|
+
boldUnd,
|
|
1607
|
+
// group 12: __bold__
|
|
1608
|
+
italicAst,
|
|
1609
|
+
// group 13: *italic*
|
|
1610
|
+
italicUnd
|
|
1611
|
+
// group 14: _italic_
|
|
1612
|
+
] = match;
|
|
1613
|
+
let value;
|
|
1614
|
+
let attribute;
|
|
1615
|
+
let href;
|
|
1616
|
+
if (escaped !== void 0) {
|
|
1617
|
+
items.push({ type: "text", value: escaped });
|
|
1618
|
+
cursor = idx + match[0].length;
|
|
1619
|
+
continue;
|
|
1620
|
+
} else if (code !== void 0) {
|
|
1621
|
+
value = code;
|
|
1622
|
+
attribute = "code";
|
|
1623
|
+
} else if (linkText !== void 0) {
|
|
1624
|
+
value = linkText;
|
|
1625
|
+
attribute = "link";
|
|
1626
|
+
href = linkUrl;
|
|
1627
|
+
} else if (tripleAst !== void 0 || tripleUnd !== void 0 || astUnd !== void 0 || undAst !== void 0 || astUndUnd !== void 0 || undAstAst !== void 0) {
|
|
1628
|
+
value = tripleAst ?? tripleUnd ?? astUnd ?? undAst ?? astUndUnd ?? undAstAst;
|
|
1629
|
+
attribute = "bold+italic";
|
|
1630
|
+
} else if (boldAst !== void 0 || boldUnd !== void 0) {
|
|
1631
|
+
value = boldAst ?? boldUnd;
|
|
1632
|
+
attribute = "bold";
|
|
1633
|
+
} else {
|
|
1634
|
+
value = italicAst ?? italicUnd;
|
|
1635
|
+
attribute = "italic";
|
|
1636
|
+
}
|
|
1637
|
+
const item = { type: "text", value };
|
|
1638
|
+
if (attribute) item.attribute = attribute;
|
|
1639
|
+
if (href) item.href = href;
|
|
1640
|
+
items.push(item);
|
|
1641
|
+
cursor = idx + match[0].length;
|
|
1642
|
+
}
|
|
1643
|
+
if (cursor < text.length) {
|
|
1644
|
+
items.push({ type: "text", value: text.slice(cursor) });
|
|
1645
|
+
}
|
|
1646
|
+
return items;
|
|
1647
|
+
}
|
|
1428
1648
|
function parseSimpleMetaVar(content, varName) {
|
|
1429
1649
|
const varMatch = content.match(
|
|
1430
1650
|
new RegExp(`^${varName}:\\s*(.*(?:\\r?\\n\\s+.*)*)+`, "m")
|
|
1431
1651
|
);
|
|
1432
1652
|
return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
|
|
1433
1653
|
}
|
|
1654
|
+
function parseBlockScalarMetaVar(content, varName) {
|
|
1655
|
+
const match = content.match(
|
|
1656
|
+
new RegExp(
|
|
1657
|
+
`^${varName}:\\s*([|>])\\s*\\r?\\n((?:(?:[ ]+.*|\\s*)(?:\\r?\\n|$))+)`,
|
|
1658
|
+
"m"
|
|
1659
|
+
)
|
|
1660
|
+
);
|
|
1661
|
+
if (!match) return void 0;
|
|
1662
|
+
const style = match[1];
|
|
1663
|
+
const rawBlock = match[2];
|
|
1664
|
+
const lines = rawBlock.split(/\r?\n/);
|
|
1665
|
+
const firstNonEmpty = lines.find((l) => l.trim() !== "");
|
|
1666
|
+
if (!firstNonEmpty) return void 0;
|
|
1667
|
+
const baseIndent = firstNonEmpty.match(/^([ ]*)/)[1].length;
|
|
1668
|
+
const stripped = lines.map((line) => line.trim() === "" ? "" : line.slice(baseIndent)).join("\n").replace(/\n+$/, "");
|
|
1669
|
+
if (style === "|") {
|
|
1670
|
+
return stripped;
|
|
1671
|
+
}
|
|
1672
|
+
return stripped.replace(/\n\n/g, "\0").replace(/\n/g, " ").replace(/\0/g, "\n");
|
|
1673
|
+
}
|
|
1434
1674
|
function parseScalingMetaVar(content, varName) {
|
|
1435
1675
|
const varMatch = content.match(scalingMetaValueRegex(varName));
|
|
1436
1676
|
if (!varMatch) return void 0;
|
|
@@ -1613,6 +1853,13 @@ function extractMetadata(content) {
|
|
|
1613
1853
|
"cuisine",
|
|
1614
1854
|
"difficulty"
|
|
1615
1855
|
]) {
|
|
1856
|
+
if (metaVar === "description" || metaVar === "introduction") {
|
|
1857
|
+
const blockValue = parseBlockScalarMetaVar(metadataContent, metaVar);
|
|
1858
|
+
if (blockValue) {
|
|
1859
|
+
metadata[metaVar] = blockValue;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1616
1863
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1617
1864
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1618
1865
|
}
|
|
@@ -1704,7 +1951,227 @@ function getAlternativeSignature(alternatives) {
|
|
|
1704
1951
|
return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
1705
1952
|
}
|
|
1706
1953
|
|
|
1954
|
+
// src/classes/pantry.ts
|
|
1955
|
+
var Pantry = class {
|
|
1956
|
+
/**
|
|
1957
|
+
* Creates a new Pantry instance.
|
|
1958
|
+
* @param tomlContent - Optional TOML content to parse.
|
|
1959
|
+
* @param options - Optional configuration options.
|
|
1960
|
+
*/
|
|
1961
|
+
constructor(tomlContent, options = {}) {
|
|
1962
|
+
/**
|
|
1963
|
+
* The parsed pantry items.
|
|
1964
|
+
*/
|
|
1965
|
+
__publicField(this, "items", []);
|
|
1966
|
+
/**
|
|
1967
|
+
* Options for date parsing and other configuration.
|
|
1968
|
+
*/
|
|
1969
|
+
__publicField(this, "options");
|
|
1970
|
+
/**
|
|
1971
|
+
* Optional category configuration for alias-based lookups.
|
|
1972
|
+
*/
|
|
1973
|
+
__publicField(this, "categoryConfig");
|
|
1974
|
+
this.options = options;
|
|
1975
|
+
if (tomlContent) {
|
|
1976
|
+
this.parse(tomlContent);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Parses a TOML string into pantry items.
|
|
1981
|
+
* @param tomlContent - The TOML string to parse.
|
|
1982
|
+
* @returns The parsed list of pantry items.
|
|
1983
|
+
*/
|
|
1984
|
+
parse(tomlContent) {
|
|
1985
|
+
const raw = TOML.parse(tomlContent);
|
|
1986
|
+
this.items = [];
|
|
1987
|
+
for (const [location, locationData] of Object.entries(raw)) {
|
|
1988
|
+
const locationTable = locationData;
|
|
1989
|
+
for (const [itemName, itemData] of Object.entries(locationTable)) {
|
|
1990
|
+
const item = this.parseItem(
|
|
1991
|
+
itemName,
|
|
1992
|
+
location,
|
|
1993
|
+
itemData
|
|
1994
|
+
);
|
|
1995
|
+
this.items.push(item);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
return this.items;
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Parses a single pantry item from its TOML representation.
|
|
2002
|
+
*/
|
|
2003
|
+
parseItem(name, location, data) {
|
|
2004
|
+
const item = { name, location };
|
|
2005
|
+
if (typeof data === "string") {
|
|
2006
|
+
const parsed = parseQuantityWithUnit(data);
|
|
2007
|
+
item.quantity = parsed.value;
|
|
2008
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2009
|
+
} else {
|
|
2010
|
+
if (data.quantity) {
|
|
2011
|
+
const parsed = parseQuantityWithUnit(data.quantity);
|
|
2012
|
+
item.quantity = parsed.value;
|
|
2013
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2014
|
+
}
|
|
2015
|
+
if (data.low) {
|
|
2016
|
+
const parsed = parseQuantityWithUnit(data.low);
|
|
2017
|
+
item.low = parsed.value;
|
|
2018
|
+
if (parsed.unit) item.lowUnit = parsed.unit;
|
|
2019
|
+
}
|
|
2020
|
+
if (data.bought) {
|
|
2021
|
+
item.bought = this.parseDate(data.bought);
|
|
2022
|
+
}
|
|
2023
|
+
if (data.expire) {
|
|
2024
|
+
item.expire = this.parseDate(data.expire);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return item;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Parses a date string using the configured format or fuzzy detection.
|
|
2031
|
+
*/
|
|
2032
|
+
parseDate(input) {
|
|
2033
|
+
if (this.options.dateFormat) {
|
|
2034
|
+
return parseDateFromFormat(input, this.options.dateFormat);
|
|
2035
|
+
}
|
|
2036
|
+
return parseFuzzyDate(input);
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Sets a category configuration for alias-based item lookups.
|
|
2040
|
+
* @param config - The category configuration to use.
|
|
2041
|
+
*/
|
|
2042
|
+
setCategoryConfig(config) {
|
|
2043
|
+
this.categoryConfig = config;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Finds a pantry item by name, using exact match first, then alias lookup
|
|
2047
|
+
* via the stored CategoryConfig.
|
|
2048
|
+
* @param name - The name to search for.
|
|
2049
|
+
* @returns The matching pantry item, or undefined if not found.
|
|
2050
|
+
*/
|
|
2051
|
+
findItem(name) {
|
|
2052
|
+
const lowerName = name.toLowerCase();
|
|
2053
|
+
const exact = this.items.find(
|
|
2054
|
+
(item) => item.name.toLowerCase() === lowerName
|
|
2055
|
+
);
|
|
2056
|
+
if (exact) return exact;
|
|
2057
|
+
if (this.categoryConfig) {
|
|
2058
|
+
for (const category of this.categoryConfig.categories) {
|
|
2059
|
+
for (const catIngredient of category.ingredients) {
|
|
2060
|
+
if (catIngredient.aliases.some(
|
|
2061
|
+
(alias) => alias.toLowerCase() === lowerName
|
|
2062
|
+
)) {
|
|
2063
|
+
const canonicalName = catIngredient.name.toLowerCase();
|
|
2064
|
+
const byCanonical = this.items.find(
|
|
2065
|
+
(item) => item.name.toLowerCase() === canonicalName
|
|
2066
|
+
);
|
|
2067
|
+
if (byCanonical) return byCanonical;
|
|
2068
|
+
for (const alias of catIngredient.aliases) {
|
|
2069
|
+
const byAlias = this.items.find(
|
|
2070
|
+
(item) => item.name.toLowerCase() === alias.toLowerCase()
|
|
2071
|
+
);
|
|
2072
|
+
if (byAlias) return byAlias;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return void 0;
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Gets the numeric value of a pantry item's quantity, optionally converted to base units.
|
|
2082
|
+
* Returns undefined if the quantity has a text value or is not set.
|
|
2083
|
+
*/
|
|
2084
|
+
getItemNumericValue(quantity, unit) {
|
|
2085
|
+
if (!quantity) return void 0;
|
|
2086
|
+
let numericValue;
|
|
2087
|
+
if (quantity.type === "fixed") {
|
|
2088
|
+
if (quantity.value.type === "text") return void 0;
|
|
2089
|
+
numericValue = getNumericValue(quantity.value);
|
|
2090
|
+
} else {
|
|
2091
|
+
numericValue = (getNumericValue(quantity.min) + getNumericValue(quantity.max)) / 2;
|
|
2092
|
+
}
|
|
2093
|
+
if (unit) {
|
|
2094
|
+
const unitDef = normalizeUnit(unit);
|
|
2095
|
+
if (unitDef) {
|
|
2096
|
+
const toBase = getToBase(unitDef);
|
|
2097
|
+
numericValue *= toBase;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return numericValue;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Returns all items that are depleted (quantity = 0) or below their low threshold.
|
|
2104
|
+
* @returns An array of depleted pantry items.
|
|
2105
|
+
*/
|
|
2106
|
+
getDepletedItems() {
|
|
2107
|
+
return this.items.filter((item) => this.isItemLow(item));
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Returns all items whose expiration date is within `nbDays` days from today
|
|
2111
|
+
* (or already passed).
|
|
2112
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0 (already expired).
|
|
2113
|
+
* @returns An array of expired pantry items.
|
|
2114
|
+
*/
|
|
2115
|
+
getExpiredItems(nbDays = 0) {
|
|
2116
|
+
return this.items.filter((item) => this.isItemExpired(item, nbDays));
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Checks if a specific item is low (quantity = 0 or below `low` threshold).
|
|
2120
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2121
|
+
* @returns true if the item is low, false otherwise. Returns false if item not found.
|
|
2122
|
+
*/
|
|
2123
|
+
isLow(itemName) {
|
|
2124
|
+
const item = this.findItem(itemName);
|
|
2125
|
+
if (!item) return false;
|
|
2126
|
+
return this.isItemLow(item);
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Checks if a specific item is expired or expires within `nbDays` days.
|
|
2130
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2131
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0.
|
|
2132
|
+
* @returns true if the item is expired, false otherwise. Returns false if item not found.
|
|
2133
|
+
*/
|
|
2134
|
+
isExpired(itemName, nbDays = 0) {
|
|
2135
|
+
const item = this.findItem(itemName);
|
|
2136
|
+
if (!item) return false;
|
|
2137
|
+
return this.isItemExpired(item, nbDays);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Internal: checks if a pantry item is low.
|
|
2141
|
+
*/
|
|
2142
|
+
isItemLow(item) {
|
|
2143
|
+
if (!item.quantity) return false;
|
|
2144
|
+
const qtyValue = this.getItemNumericValue(item.quantity, item.unit);
|
|
2145
|
+
if (qtyValue === void 0) return false;
|
|
2146
|
+
if (qtyValue === 0) return true;
|
|
2147
|
+
if (item.low) {
|
|
2148
|
+
const lowValue = this.getItemNumericValue(item.low, item.lowUnit);
|
|
2149
|
+
if (lowValue !== void 0 && qtyValue <= lowValue) return true;
|
|
2150
|
+
}
|
|
2151
|
+
return false;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Internal: checks if a pantry item is expired.
|
|
2155
|
+
*/
|
|
2156
|
+
isItemExpired(item, nbDays) {
|
|
2157
|
+
if (!item.expire) return false;
|
|
2158
|
+
const now = /* @__PURE__ */ new Date();
|
|
2159
|
+
const cutoff = new Date(
|
|
2160
|
+
now.getFullYear(),
|
|
2161
|
+
now.getMonth(),
|
|
2162
|
+
now.getDate() + nbDays
|
|
2163
|
+
);
|
|
2164
|
+
const expireDay = new Date(
|
|
2165
|
+
item.expire.getFullYear(),
|
|
2166
|
+
item.expire.getMonth(),
|
|
2167
|
+
item.expire.getDate()
|
|
2168
|
+
);
|
|
2169
|
+
return expireDay <= cutoff;
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
|
|
1707
2173
|
// src/classes/product_catalog.ts
|
|
2174
|
+
import TOML2 from "smol-toml";
|
|
1708
2175
|
var ProductCatalog = class {
|
|
1709
2176
|
constructor(tomlContent) {
|
|
1710
2177
|
__publicField(this, "products", []);
|
|
@@ -1716,7 +2183,7 @@ var ProductCatalog = class {
|
|
|
1716
2183
|
* @returns A parsed list of `ProductOption`.
|
|
1717
2184
|
*/
|
|
1718
2185
|
parse(tomlContent) {
|
|
1719
|
-
const catalogRaw =
|
|
2186
|
+
const catalogRaw = TOML2.parse(tomlContent);
|
|
1720
2187
|
this.products = [];
|
|
1721
2188
|
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1722
2189
|
throw new InvalidProductCatalogFormat();
|
|
@@ -1789,7 +2256,7 @@ var ProductCatalog = class {
|
|
|
1789
2256
|
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1790
2257
|
};
|
|
1791
2258
|
}
|
|
1792
|
-
return
|
|
2259
|
+
return TOML2.stringify(grouped);
|
|
1793
2260
|
}
|
|
1794
2261
|
/**
|
|
1795
2262
|
* Adds a product to the catalog.
|
|
@@ -1926,7 +2393,6 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1926
2393
|
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
|
|
1927
2394
|
const unitLists = [];
|
|
1928
2395
|
const normalizeOrGroup = (og) => ({
|
|
1929
|
-
...og,
|
|
1930
2396
|
or: og.or.map((q) => ({
|
|
1931
2397
|
...q,
|
|
1932
2398
|
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
@@ -2316,13 +2782,13 @@ var _Recipe = class _Recipe {
|
|
|
2316
2782
|
for (const match of text.matchAll(globalRegex)) {
|
|
2317
2783
|
const idx = match.index;
|
|
2318
2784
|
if (idx > cursor) {
|
|
2319
|
-
noteItems.push(
|
|
2785
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor, idx)));
|
|
2320
2786
|
}
|
|
2321
2787
|
this._parseArbitraryScalable(match.groups, noteItems);
|
|
2322
2788
|
cursor = idx + match[0].length;
|
|
2323
2789
|
}
|
|
2324
2790
|
if (cursor < text.length) {
|
|
2325
|
-
noteItems.push(
|
|
2791
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor)));
|
|
2326
2792
|
}
|
|
2327
2793
|
return noteItems;
|
|
2328
2794
|
}
|
|
@@ -2607,34 +3073,10 @@ var _Recipe = class _Recipe {
|
|
|
2607
3073
|
}
|
|
2608
3074
|
}
|
|
2609
3075
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
* When no options are provided, returns all recipe ingredients with quantities
|
|
2615
|
-
* calculated using primary alternatives (same as after parsing).
|
|
2616
|
-
*
|
|
2617
|
-
* @param options - Options for filtering and choice selection:
|
|
2618
|
-
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
2619
|
-
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
2620
|
-
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
2621
|
-
* @returns Array of Ingredient objects with quantities populated
|
|
2622
|
-
*
|
|
2623
|
-
* @example
|
|
2624
|
-
* ```typescript
|
|
2625
|
-
* // Get all ingredients with primary alternatives
|
|
2626
|
-
* const ingredients = recipe.getIngredientQuantities();
|
|
2627
|
-
*
|
|
2628
|
-
* // Get ingredients for a specific section
|
|
2629
|
-
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
2630
|
-
*
|
|
2631
|
-
* // Get ingredients with specific choices applied
|
|
2632
|
-
* const withChoices = recipe.getIngredientQuantities({
|
|
2633
|
-
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
2634
|
-
* });
|
|
2635
|
-
* ```
|
|
2636
|
-
*/
|
|
2637
|
-
getIngredientQuantities(options) {
|
|
3076
|
+
// Type for accumulated quantities (used internally by collectQuantityGroups)
|
|
3077
|
+
// Defined as a static type alias for the private method's return type
|
|
3078
|
+
/** @internal */
|
|
3079
|
+
collectQuantityGroups(options) {
|
|
2638
3080
|
const { section, step, choices } = options || {};
|
|
2639
3081
|
const sectionsToProcess = section !== void 0 ? (() => {
|
|
2640
3082
|
const idx = typeof section === "number" ? section : this.sections.indexOf(section);
|
|
@@ -2752,6 +3194,78 @@ var _Recipe = class _Recipe {
|
|
|
2752
3194
|
}
|
|
2753
3195
|
}
|
|
2754
3196
|
}
|
|
3197
|
+
return { ingredientGroups, selectedIndices, referencedIndices };
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Gets the raw (unprocessed) quantity groups for each ingredient, before
|
|
3201
|
+
* any summation or equivalents simplification. This is useful for cross-recipe
|
|
3202
|
+
* aggregation (e.g., in {@link ShoppingList}), where quantities from multiple
|
|
3203
|
+
* recipes should be combined before processing.
|
|
3204
|
+
*
|
|
3205
|
+
* @param options - Options for filtering and choice selection (same as {@link getIngredientQuantities}).
|
|
3206
|
+
* @returns Array of {@link RawQuantityGroup} objects, one per ingredient with quantities.
|
|
3207
|
+
*
|
|
3208
|
+
* @example
|
|
3209
|
+
* ```typescript
|
|
3210
|
+
* const rawGroups = recipe.getRawQuantityGroups();
|
|
3211
|
+
* // Each group has: name, usedAsPrimary, flags, quantities[]
|
|
3212
|
+
* // quantities are the raw QuantityWithExtendedUnit or FlatOrGroup entries
|
|
3213
|
+
* ```
|
|
3214
|
+
*/
|
|
3215
|
+
getRawQuantityGroups(options) {
|
|
3216
|
+
const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
|
|
3217
|
+
const result = [];
|
|
3218
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
3219
|
+
if (!referencedIndices.has(index)) continue;
|
|
3220
|
+
const orig = this.ingredients[index];
|
|
3221
|
+
const usedAsPrimary = selectedIndices.has(index);
|
|
3222
|
+
const quantities = [];
|
|
3223
|
+
if (usedAsPrimary) {
|
|
3224
|
+
const groupsForIng = ingredientGroups.get(index);
|
|
3225
|
+
if (groupsForIng) {
|
|
3226
|
+
for (const [, group] of groupsForIng) {
|
|
3227
|
+
quantities.push(...group.quantities);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
result.push({
|
|
3232
|
+
name: orig.name,
|
|
3233
|
+
...usedAsPrimary && { usedAsPrimary: true },
|
|
3234
|
+
...orig.flags && { flags: orig.flags },
|
|
3235
|
+
quantities
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
return result;
|
|
3239
|
+
}
|
|
3240
|
+
/**
|
|
3241
|
+
* Gets ingredients with their quantities populated, optionally filtered by section/step
|
|
3242
|
+
* and respecting user choices for alternatives.
|
|
3243
|
+
*
|
|
3244
|
+
* When no options are provided, returns all recipe ingredients with quantities
|
|
3245
|
+
* calculated using primary alternatives (same as after parsing).
|
|
3246
|
+
*
|
|
3247
|
+
* @param options - Options for filtering and choice selection:
|
|
3248
|
+
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
3249
|
+
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
3250
|
+
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
3251
|
+
* @returns Array of Ingredient objects with quantities populated
|
|
3252
|
+
*
|
|
3253
|
+
* @example
|
|
3254
|
+
* ```typescript
|
|
3255
|
+
* // Get all ingredients with primary alternatives
|
|
3256
|
+
* const ingredients = recipe.getIngredientQuantities();
|
|
3257
|
+
*
|
|
3258
|
+
* // Get ingredients for a specific section
|
|
3259
|
+
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
3260
|
+
*
|
|
3261
|
+
* // Get ingredients with specific choices applied
|
|
3262
|
+
* const withChoices = recipe.getIngredientQuantities({
|
|
3263
|
+
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
3264
|
+
* });
|
|
3265
|
+
* ```
|
|
3266
|
+
*/
|
|
3267
|
+
getIngredientQuantities(options) {
|
|
3268
|
+
const { ingredientGroups, selectedIndices, referencedIndices } = this.collectQuantityGroups(options);
|
|
2755
3269
|
const result = [];
|
|
2756
3270
|
for (let index = 0; index < this.ingredients.length; index++) {
|
|
2757
3271
|
if (!referencedIndices.has(index)) continue;
|
|
@@ -2876,7 +3390,7 @@ var _Recipe = class _Recipe {
|
|
|
2876
3390
|
for (const match of line.matchAll(tokensRegex)) {
|
|
2877
3391
|
const idx = match.index;
|
|
2878
3392
|
if (idx > cursor) {
|
|
2879
|
-
items.push(
|
|
3393
|
+
items.push(...parseMarkdownSegments(line.slice(cursor, idx)));
|
|
2880
3394
|
}
|
|
2881
3395
|
const groups = match.groups;
|
|
2882
3396
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
@@ -2938,7 +3452,7 @@ var _Recipe = class _Recipe {
|
|
|
2938
3452
|
cursor = idx + match[0].length;
|
|
2939
3453
|
}
|
|
2940
3454
|
if (cursor < line.length) {
|
|
2941
|
-
items.push(
|
|
3455
|
+
items.push(...parseMarkdownSegments(line.slice(cursor)));
|
|
2942
3456
|
}
|
|
2943
3457
|
blankLineBefore = false;
|
|
2944
3458
|
}
|
|
@@ -3280,13 +3794,12 @@ __publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
|
|
|
3280
3794
|
var Recipe = _Recipe;
|
|
3281
3795
|
|
|
3282
3796
|
// src/classes/shopping_list.ts
|
|
3283
|
-
var ShoppingList = class {
|
|
3797
|
+
var ShoppingList = class _ShoppingList {
|
|
3284
3798
|
/**
|
|
3285
3799
|
* Creates a new ShoppingList instance
|
|
3286
3800
|
* @param categoryConfigStr - The category configuration to parse.
|
|
3287
3801
|
*/
|
|
3288
3802
|
constructor(categoryConfigStr) {
|
|
3289
|
-
// TODO: backport type change
|
|
3290
3803
|
/**
|
|
3291
3804
|
* The ingredients in the shopping list.
|
|
3292
3805
|
*/
|
|
@@ -3303,38 +3816,38 @@ var ShoppingList = class {
|
|
|
3303
3816
|
* The categorized ingredients in the shopping list.
|
|
3304
3817
|
*/
|
|
3305
3818
|
__publicField(this, "categories");
|
|
3819
|
+
/**
|
|
3820
|
+
* The unit system to use for quantity simplification.
|
|
3821
|
+
* When set, overrides per-recipe unit systems.
|
|
3822
|
+
*/
|
|
3823
|
+
__publicField(this, "unitSystem");
|
|
3824
|
+
/**
|
|
3825
|
+
* Per-ingredient equivalence ratio maps for recomputing equivalents
|
|
3826
|
+
* after pantry subtraction. Keyed by ingredient name.
|
|
3827
|
+
* @internal
|
|
3828
|
+
*/
|
|
3829
|
+
__publicField(this, "equivalenceRatios", /* @__PURE__ */ new Map());
|
|
3830
|
+
/**
|
|
3831
|
+
* The original pantry (never mutated by recipe calculations).
|
|
3832
|
+
*/
|
|
3833
|
+
__publicField(this, "pantry");
|
|
3834
|
+
/**
|
|
3835
|
+
* The pantry with quantities updated after subtracting recipe needs.
|
|
3836
|
+
* Recomputed on every {@link ShoppingList.calculateIngredients | calculateIngredients()} call.
|
|
3837
|
+
*/
|
|
3838
|
+
__publicField(this, "resultingPantry");
|
|
3306
3839
|
if (categoryConfigStr) {
|
|
3307
3840
|
this.setCategoryConfig(categoryConfigStr);
|
|
3308
3841
|
}
|
|
3309
3842
|
}
|
|
3310
3843
|
calculateIngredients() {
|
|
3311
3844
|
this.ingredients = [];
|
|
3312
|
-
const
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
if (!existing.quantityTotal) {
|
|
3318
|
-
existing.quantityTotal = quantityTotal;
|
|
3319
|
-
return;
|
|
3320
|
-
}
|
|
3321
|
-
try {
|
|
3322
|
-
const existingQuantityTotalExtended = extendAllUnits(
|
|
3323
|
-
existing.quantityTotal
|
|
3324
|
-
);
|
|
3325
|
-
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
|
|
3326
|
-
existing.quantityTotal = addEquivalentsAndSimplify([
|
|
3327
|
-
...existingQuantities,
|
|
3328
|
-
...newQuantities
|
|
3329
|
-
]);
|
|
3330
|
-
return;
|
|
3331
|
-
} catch {
|
|
3332
|
-
}
|
|
3845
|
+
const rawQuantitiesMap = /* @__PURE__ */ new Map();
|
|
3846
|
+
const nameOrder = [];
|
|
3847
|
+
const trackName = (name) => {
|
|
3848
|
+
if (!nameOrder.includes(name)) {
|
|
3849
|
+
nameOrder.push(name);
|
|
3333
3850
|
}
|
|
3334
|
-
this.ingredients.push({
|
|
3335
|
-
name,
|
|
3336
|
-
quantityTotal
|
|
3337
|
-
});
|
|
3338
3851
|
};
|
|
3339
3852
|
for (const addedRecipe of this.recipes) {
|
|
3340
3853
|
let scaledRecipe;
|
|
@@ -3344,48 +3857,253 @@ var ShoppingList = class {
|
|
|
3344
3857
|
} else {
|
|
3345
3858
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
3346
3859
|
}
|
|
3347
|
-
const
|
|
3860
|
+
const rawGroups = scaledRecipe.getRawQuantityGroups({
|
|
3348
3861
|
choices: addedRecipe.choices
|
|
3349
3862
|
});
|
|
3350
|
-
for (const
|
|
3351
|
-
if (
|
|
3863
|
+
for (const group of rawGroups) {
|
|
3864
|
+
if (group.flags?.includes("hidden") || !group.usedAsPrimary) {
|
|
3352
3865
|
continue;
|
|
3353
3866
|
}
|
|
3354
|
-
|
|
3355
|
-
|
|
3867
|
+
trackName(group.name);
|
|
3868
|
+
if (group.quantities.length > 0) {
|
|
3869
|
+
const existing = rawQuantitiesMap.get(group.name) ?? [];
|
|
3870
|
+
existing.push(...group.quantities);
|
|
3871
|
+
rawQuantitiesMap.set(group.name, existing);
|
|
3356
3872
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
this.equivalenceRatios.clear();
|
|
3876
|
+
for (const name of nameOrder) {
|
|
3877
|
+
const rawQuantities = rawQuantitiesMap.get(name);
|
|
3878
|
+
if (!rawQuantities || rawQuantities.length === 0) {
|
|
3879
|
+
this.ingredients.push({ name });
|
|
3880
|
+
continue;
|
|
3881
|
+
}
|
|
3882
|
+
const textEntries = [];
|
|
3883
|
+
const numericEntries = [];
|
|
3884
|
+
for (const q of rawQuantities) {
|
|
3885
|
+
if ("quantity" in q && q.quantity.type === "fixed" && q.quantity.value.type === "text") {
|
|
3886
|
+
textEntries.push(q);
|
|
3887
|
+
} else {
|
|
3888
|
+
numericEntries.push(q);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
if (numericEntries.length > 1) {
|
|
3892
|
+
const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
|
|
3893
|
+
getEquivalentUnitsLists(...numericEntries)
|
|
3894
|
+
);
|
|
3895
|
+
if (Object.keys(ratioMap).length > 0) {
|
|
3896
|
+
this.equivalenceRatios.set(name, ratioMap);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
const resultQuantities = [];
|
|
3900
|
+
for (const t2 of textEntries) {
|
|
3901
|
+
resultQuantities.push(toPlainUnit(t2));
|
|
3902
|
+
}
|
|
3903
|
+
if (numericEntries.length > 0) {
|
|
3904
|
+
resultQuantities.push(
|
|
3905
|
+
...flattenPlainUnitGroup(
|
|
3906
|
+
addEquivalentsAndSimplify(numericEntries, this.unitSystem)
|
|
3907
|
+
)
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
this.ingredients.push({
|
|
3911
|
+
name,
|
|
3912
|
+
quantities: resultQuantities
|
|
3913
|
+
});
|
|
3914
|
+
}
|
|
3915
|
+
this.applyPantrySubtraction();
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Subtracts pantry item quantities from calculated ingredient quantities
|
|
3919
|
+
* and updates the resultingPantry to reflect consumed stock.
|
|
3920
|
+
*/
|
|
3921
|
+
applyPantrySubtraction() {
|
|
3922
|
+
if (!this.pantry) {
|
|
3923
|
+
this.resultingPantry = void 0;
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
const clonedPantry = new Pantry();
|
|
3927
|
+
clonedPantry.items = deepClone(this.pantry.items);
|
|
3928
|
+
if (this.categoryConfig) {
|
|
3929
|
+
clonedPantry.setCategoryConfig(this.categoryConfig);
|
|
3930
|
+
}
|
|
3931
|
+
for (const ingredient of this.ingredients) {
|
|
3932
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
3933
|
+
continue;
|
|
3934
|
+
const pantryItem = clonedPantry.findItem(ingredient.name);
|
|
3935
|
+
if (!pantryItem || !pantryItem.quantity) continue;
|
|
3936
|
+
let pantryExtended = {
|
|
3937
|
+
quantity: pantryItem.quantity,
|
|
3938
|
+
...pantryItem.unit && { unit: { name: pantryItem.unit } }
|
|
3939
|
+
};
|
|
3940
|
+
for (let i2 = 0; i2 < ingredient.quantities.length; i2++) {
|
|
3941
|
+
const entry = ingredient.quantities[i2];
|
|
3942
|
+
const leaves = "and" in entry ? entry.and : [entry];
|
|
3943
|
+
for (const leaf of leaves) {
|
|
3944
|
+
const ingredientExtended = toExtendedUnit(leaf);
|
|
3945
|
+
const leafHasUnit = leaf.unit !== void 0 && leaf.unit !== "";
|
|
3946
|
+
const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
|
|
3947
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
3948
|
+
const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
|
|
3949
|
+
if (unitMismatch) {
|
|
3950
|
+
const leafUnit = leaf.unit ?? NO_UNIT;
|
|
3951
|
+
const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
|
|
3952
|
+
const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
|
|
3953
|
+
if (ratioFromPantry !== void 0) {
|
|
3954
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
3955
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
3956
|
+
if (typeof pantryValue === "number" && typeof leafValue === "number") {
|
|
3957
|
+
const pantryInLeafUnits = pantryValue * ratioFromPantry;
|
|
3958
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
3959
|
+
const remainingLeafValue = Math.max(
|
|
3960
|
+
leafValue - pantryInLeafUnits,
|
|
3961
|
+
0
|
|
3962
|
+
);
|
|
3963
|
+
leaf.quantity = {
|
|
3964
|
+
type: "fixed",
|
|
3965
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
3966
|
+
};
|
|
3967
|
+
const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
|
|
3968
|
+
const remainingPantryValue = Math.max(
|
|
3969
|
+
pantryValue - consumedInPantryUnits,
|
|
3970
|
+
0
|
|
3971
|
+
);
|
|
3972
|
+
pantryExtended = {
|
|
3973
|
+
quantity: {
|
|
3974
|
+
type: "fixed",
|
|
3975
|
+
value: {
|
|
3976
|
+
type: "decimal",
|
|
3977
|
+
decimal: remainingPantryValue
|
|
3978
|
+
}
|
|
3979
|
+
},
|
|
3980
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
3981
|
+
};
|
|
3982
|
+
continue;
|
|
3363
3983
|
}
|
|
3364
|
-
} else {
|
|
3365
|
-
const plainQty = {
|
|
3366
|
-
quantity: qGroup.quantity
|
|
3367
|
-
};
|
|
3368
|
-
if (qGroup.unit) plainQty.unit = qGroup.unit;
|
|
3369
|
-
if (qGroup.equivalents) plainQty.equivalents = qGroup.equivalents;
|
|
3370
|
-
allQuantities.push(plainQty);
|
|
3371
3984
|
}
|
|
3372
3985
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3986
|
+
try {
|
|
3987
|
+
const remaining = subtractQuantities(
|
|
3988
|
+
ingredientExtended,
|
|
3989
|
+
pantryExtended,
|
|
3990
|
+
{ clampToZero: true }
|
|
3378
3991
|
);
|
|
3379
|
-
const
|
|
3380
|
-
|
|
3992
|
+
const consumed = subtractQuantities(
|
|
3993
|
+
pantryExtended,
|
|
3994
|
+
ingredientExtended,
|
|
3995
|
+
{ clampToZero: true }
|
|
3381
3996
|
);
|
|
3382
|
-
|
|
3997
|
+
pantryExtended = consumed;
|
|
3998
|
+
const updated = toPlainUnit(remaining);
|
|
3999
|
+
leaf.quantity = updated.quantity;
|
|
4000
|
+
leaf.unit = updated.unit;
|
|
4001
|
+
} catch {
|
|
3383
4002
|
}
|
|
3384
|
-
} else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
|
|
3385
|
-
this.ingredients.push({ name: ingredient.name });
|
|
3386
4003
|
}
|
|
4004
|
+
if ("and" in entry) {
|
|
4005
|
+
const nonZero = entry.and.filter(
|
|
4006
|
+
(leaf) => leaf.quantity.type !== "fixed" || leaf.quantity.value.type !== "decimal" || leaf.quantity.value.decimal !== 0
|
|
4007
|
+
);
|
|
4008
|
+
entry.and.length = 0;
|
|
4009
|
+
entry.and.push(...nonZero);
|
|
4010
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4011
|
+
if (entry.equivalents && ratioMap) {
|
|
4012
|
+
const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
|
|
4013
|
+
entry.equivalents = _ShoppingList.recomputeEquivalents(
|
|
4014
|
+
entry.and,
|
|
4015
|
+
ratioMap,
|
|
4016
|
+
equivUnits
|
|
4017
|
+
);
|
|
4018
|
+
}
|
|
4019
|
+
if (entry.and.length === 1) {
|
|
4020
|
+
const single = entry.and[0];
|
|
4021
|
+
ingredient.quantities[i2] = {
|
|
4022
|
+
quantity: single.quantity,
|
|
4023
|
+
...single.unit && { unit: single.unit },
|
|
4024
|
+
...entry.equivalents && { equivalents: entry.equivalents },
|
|
4025
|
+
...entry.alternatives && { alternatives: entry.alternatives }
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
} else if ("equivalents" in entry && entry.equivalents) {
|
|
4029
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4030
|
+
if (ratioMap) {
|
|
4031
|
+
const equivUnits = entry.equivalents.map(
|
|
4032
|
+
(e2) => e2.unit ?? NO_UNIT
|
|
4033
|
+
);
|
|
4034
|
+
const recomputed = _ShoppingList.recomputeEquivalents(
|
|
4035
|
+
[entry],
|
|
4036
|
+
ratioMap,
|
|
4037
|
+
equivUnits
|
|
4038
|
+
);
|
|
4039
|
+
entry.equivalents = recomputed;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
ingredient.quantities = ingredient.quantities.filter((entry) => {
|
|
4044
|
+
if ("and" in entry) return entry.and.length > 0;
|
|
4045
|
+
return !(entry.quantity.type === "fixed" && entry.quantity.value.type === "decimal" && entry.quantity.value.decimal === 0);
|
|
4046
|
+
});
|
|
4047
|
+
if (ingredient.quantities.length === 0) {
|
|
4048
|
+
ingredient.quantities = void 0;
|
|
4049
|
+
}
|
|
4050
|
+
pantryItem.quantity = pantryExtended.quantity;
|
|
4051
|
+
if (pantryExtended.unit) {
|
|
4052
|
+
pantryItem.unit = pantryExtended.unit.name;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
this.resultingPantry = clonedPantry;
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Builds a ratio map from equivalence lists.
|
|
4059
|
+
* For each equivalence list, stores ratio = equiv_value / primary_value
|
|
4060
|
+
* for every pair of units, so equivalents can be recomputed after
|
|
4061
|
+
* pantry subtraction modifies primary quantities.
|
|
4062
|
+
*/
|
|
4063
|
+
static buildEquivalenceRatioMap(unitsLists) {
|
|
4064
|
+
const ratioMap = {};
|
|
4065
|
+
for (const list of unitsLists) {
|
|
4066
|
+
for (const equiv of list) {
|
|
4067
|
+
const equivValue = getAverageValue(equiv.quantity);
|
|
4068
|
+
for (const primary of list) {
|
|
4069
|
+
if (primary === equiv) continue;
|
|
4070
|
+
const primaryValue = getAverageValue(primary.quantity);
|
|
4071
|
+
const equivUnit = equiv.unit.name;
|
|
4072
|
+
const primaryUnit = primary.unit.name;
|
|
4073
|
+
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
4074
|
+
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return ratioMap;
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Recomputes equivalent quantities from current primary values and stored ratios.
|
|
4082
|
+
* For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
|
|
4083
|
+
* Returns undefined if all equivalents compute to zero.
|
|
4084
|
+
*/
|
|
4085
|
+
static recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
4086
|
+
const equivalents = [];
|
|
4087
|
+
for (const equivUnit of equivUnits) {
|
|
4088
|
+
const ratios = ratioMap[equivUnit];
|
|
4089
|
+
let total = 0;
|
|
4090
|
+
for (const primary of primaries) {
|
|
4091
|
+
const pUnit = primary.unit ?? NO_UNIT;
|
|
4092
|
+
const ratio = ratios[pUnit];
|
|
4093
|
+
const pValue = getAverageValue(primary.quantity);
|
|
4094
|
+
total += pValue * ratio;
|
|
4095
|
+
}
|
|
4096
|
+
if (total > 0) {
|
|
4097
|
+
equivalents.push({
|
|
4098
|
+
quantity: {
|
|
4099
|
+
type: "fixed",
|
|
4100
|
+
value: { type: "decimal", decimal: total }
|
|
4101
|
+
},
|
|
4102
|
+
...equivUnit !== "" && { unit: equivUnit }
|
|
4103
|
+
});
|
|
3387
4104
|
}
|
|
3388
4105
|
}
|
|
4106
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
3389
4107
|
}
|
|
3390
4108
|
/**
|
|
3391
4109
|
* Adds a recipe to the shopping list, then automatically
|
|
@@ -3474,9 +4192,41 @@ var ShoppingList = class {
|
|
|
3474
4192
|
this.calculateIngredients();
|
|
3475
4193
|
this.categorize();
|
|
3476
4194
|
}
|
|
4195
|
+
/**
|
|
4196
|
+
* Adds a pantry to the shopping list. On-hand pantry quantities will be
|
|
4197
|
+
* subtracted from recipe ingredient needs on each recalculation.
|
|
4198
|
+
* @param pantry - A Pantry instance or a TOML string to parse.
|
|
4199
|
+
* @param options - Options for pantry parsing (only used when providing a TOML string).
|
|
4200
|
+
*/
|
|
4201
|
+
addPantry(pantry, options) {
|
|
4202
|
+
if (typeof pantry === "string") {
|
|
4203
|
+
this.pantry = new Pantry(pantry, options);
|
|
4204
|
+
} else if (pantry instanceof Pantry) {
|
|
4205
|
+
this.pantry = pantry;
|
|
4206
|
+
} else {
|
|
4207
|
+
throw new Error(
|
|
4208
|
+
"Invalid pantry: expected a Pantry instance or TOML string"
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
if (this.categoryConfig) {
|
|
4212
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4213
|
+
}
|
|
4214
|
+
this.calculateIngredients();
|
|
4215
|
+
this.categorize();
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Returns the resulting pantry with quantities updated to reflect
|
|
4219
|
+
* what was consumed by the shopping list's recipes.
|
|
4220
|
+
* Returns undefined if no pantry was added.
|
|
4221
|
+
* @returns The resulting Pantry, or undefined.
|
|
4222
|
+
*/
|
|
4223
|
+
getPantry() {
|
|
4224
|
+
return this.resultingPantry;
|
|
4225
|
+
}
|
|
3477
4226
|
/**
|
|
3478
4227
|
* Sets the category configuration for the shopping list
|
|
3479
4228
|
* and automatically categorize current ingredients from the list.
|
|
4229
|
+
* Also propagates the configuration to the pantry if one is set.
|
|
3480
4230
|
* @param config - The category configuration to parse.
|
|
3481
4231
|
*/
|
|
3482
4232
|
setCategoryConfig(config) {
|
|
@@ -3484,6 +4234,9 @@ var ShoppingList = class {
|
|
|
3484
4234
|
this.categoryConfig = new CategoryConfig(config);
|
|
3485
4235
|
else if (config instanceof CategoryConfig) this.categoryConfig = config;
|
|
3486
4236
|
else throw new Error("Invalid category configuration");
|
|
4237
|
+
if (this.pantry) {
|
|
4238
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4239
|
+
}
|
|
3487
4240
|
this.categorize();
|
|
3488
4241
|
}
|
|
3489
4242
|
/**
|
|
@@ -3628,8 +4381,27 @@ var ShoppingCart = class {
|
|
|
3628
4381
|
getOptimumMatch(ingredient, options) {
|
|
3629
4382
|
if (options.length === 0)
|
|
3630
4383
|
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
3631
|
-
if (!ingredient.
|
|
4384
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
3632
4385
|
throw new NoProductMatchError(ingredient.name, "noQuantity");
|
|
4386
|
+
const allPlainEntries = [];
|
|
4387
|
+
for (const q of ingredient.quantities) {
|
|
4388
|
+
if ("and" in q) {
|
|
4389
|
+
allPlainEntries.push({ and: q.and });
|
|
4390
|
+
} else {
|
|
4391
|
+
const entry = {
|
|
4392
|
+
quantity: q.quantity,
|
|
4393
|
+
...q.unit && { unit: q.unit },
|
|
4394
|
+
...q.equivalents && { equivalents: q.equivalents }
|
|
4395
|
+
};
|
|
4396
|
+
allPlainEntries.push(entry);
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
let quantityTotal;
|
|
4400
|
+
if (allPlainEntries.length === 1) {
|
|
4401
|
+
quantityTotal = allPlainEntries[0];
|
|
4402
|
+
} else {
|
|
4403
|
+
quantityTotal = { and: allPlainEntries };
|
|
4404
|
+
}
|
|
3633
4405
|
const normalizedOptions = options.map(
|
|
3634
4406
|
(option) => ({
|
|
3635
4407
|
...option,
|
|
@@ -3645,7 +4417,7 @@ var ShoppingCart = class {
|
|
|
3645
4417
|
})
|
|
3646
4418
|
})
|
|
3647
4419
|
);
|
|
3648
|
-
const normalizedQuantityTotal = normalizeAllUnits(
|
|
4420
|
+
const normalizedQuantityTotal = normalizeAllUnits(quantityTotal);
|
|
3649
4421
|
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
3650
4422
|
if (isAndGroup(normalizedQuantities)) {
|
|
3651
4423
|
for (const q of normalizedQuantities.and) {
|
|
@@ -3867,9 +4639,12 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
|
3867
4639
|
return alternativeIndex === selectedIndex;
|
|
3868
4640
|
}
|
|
3869
4641
|
export {
|
|
4642
|
+
BadIndentationError,
|
|
3870
4643
|
CategoryConfig,
|
|
3871
4644
|
NoProductCatalogForCartError,
|
|
3872
4645
|
NoShoppingListForCartError,
|
|
4646
|
+
NoTabAsIndentError,
|
|
4647
|
+
Pantry,
|
|
3873
4648
|
ProductCatalog,
|
|
3874
4649
|
Recipe,
|
|
3875
4650
|
Section,
|
|
@@ -3896,6 +4671,9 @@ export {
|
|
|
3896
4671
|
// v8 ignore else -- @preserve
|
|
3897
4672
|
// v8 ignore if -- @preserve
|
|
3898
4673
|
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
4674
|
+
// v8 ignore if -- @preserve: defensive type guard
|
|
3899
4675
|
/* v8 ignore if -- @preserve */
|
|
3900
4676
|
// v8 ignore next -- @preserve
|
|
4677
|
+
// v8 ignore else --@preserve: defensive type guard
|
|
4678
|
+
// v8 ignore else -- @preserve: detection if
|
|
3901
4679
|
//# sourceMappingURL=index.js.map
|