@tmlmt/cooklang-parser 2.1.7 → 3.0.0-alpha.4
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/README.md +1 -1
- package/dist/index.cjs +1922 -378
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +664 -68
- package/dist/index.d.ts +664 -68
- package/dist/index.js +1918 -378
- package/dist/index.js.map +1 -1
- package/package.json +11 -7
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
33
33
|
var index_exports = {};
|
|
34
34
|
__export(index_exports, {
|
|
35
35
|
CategoryConfig: () => CategoryConfig,
|
|
36
|
+
NoProductCatalogForCartError: () => NoProductCatalogForCartError,
|
|
37
|
+
NoShoppingListForCartError: () => NoShoppingListForCartError,
|
|
38
|
+
ProductCatalog: () => ProductCatalog,
|
|
36
39
|
Recipe: () => Recipe,
|
|
37
40
|
Section: () => Section,
|
|
41
|
+
ShoppingCart: () => ShoppingCart,
|
|
38
42
|
ShoppingList: () => ShoppingList
|
|
39
43
|
});
|
|
40
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -100,33 +104,10 @@ var CategoryConfig = class {
|
|
|
100
104
|
}
|
|
101
105
|
};
|
|
102
106
|
|
|
103
|
-
// src/classes/
|
|
104
|
-
var
|
|
105
|
-
/**
|
|
106
|
-
* Creates an instance of Section.
|
|
107
|
-
* @param name - The name of the section. Defaults to an empty string.
|
|
108
|
-
*/
|
|
109
|
-
constructor(name = "") {
|
|
110
|
-
/**
|
|
111
|
-
* The name of the section. Can be an empty string for the default (first) section.
|
|
112
|
-
* @defaultValue `""`
|
|
113
|
-
*/
|
|
114
|
-
__publicField(this, "name");
|
|
115
|
-
/** An array of steps and notes that make up the content of the section. */
|
|
116
|
-
__publicField(this, "content", []);
|
|
117
|
-
this.name = name;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Checks if the section is blank (has no name and no content).
|
|
121
|
-
* Used during recipe parsing
|
|
122
|
-
* @returns `true` if the section is blank, otherwise `false`.
|
|
123
|
-
*/
|
|
124
|
-
isBlank() {
|
|
125
|
-
return this.name === "" && this.content.length === 0;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
107
|
+
// src/classes/product_catalog.ts
|
|
108
|
+
var import_smol_toml = __toESM(require("smol-toml"), 1);
|
|
128
109
|
|
|
129
|
-
// node_modules/.pnpm/human-regex@2.
|
|
110
|
+
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
130
111
|
var t = /* @__PURE__ */ new Map();
|
|
131
112
|
var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
|
|
132
113
|
var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
|
|
@@ -187,7 +168,7 @@ var a = class {
|
|
|
187
168
|
return this.add(".");
|
|
188
169
|
}
|
|
189
170
|
newline() {
|
|
190
|
-
return this.add("(
|
|
171
|
+
return this.add("(\\r\\n|\\r|\\n)");
|
|
191
172
|
}
|
|
192
173
|
negativeLookahead(t2) {
|
|
193
174
|
return this.add(`(?!${t2})`);
|
|
@@ -328,19 +309,20 @@ var i = (() => {
|
|
|
328
309
|
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
329
310
|
var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
|
|
330
311
|
var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
331
|
-
var
|
|
332
|
-
var
|
|
312
|
+
var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
|
|
313
|
+
var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
|
|
314
|
+
var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
|
|
315
|
+
var quantityAlternativeRegex = d().startNamedGroup("ingredientQuantityValue").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("ingredientUnit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("ingredientAltQuantity").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
|
|
316
|
+
var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
|
|
333
317
|
var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
|
|
334
|
-
var
|
|
335
|
-
var
|
|
336
|
-
var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
|
|
318
|
+
var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
|
|
319
|
+
var timerRegex = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
|
|
337
320
|
var tokensRegex = new RegExp(
|
|
338
321
|
[
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
timer
|
|
322
|
+
ingredientWithGroupKeyRegex,
|
|
323
|
+
ingredientWithAlternativeRegex,
|
|
324
|
+
cookwareRegex,
|
|
325
|
+
timerRegex
|
|
344
326
|
].map((r2) => r2.source).join("|"),
|
|
345
327
|
"gu"
|
|
346
328
|
);
|
|
@@ -351,8 +333,7 @@ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/")
|
|
|
351
333
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
352
334
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
353
335
|
|
|
354
|
-
// src/units.ts
|
|
355
|
-
var import_big = __toESM(require("big.js"), 1);
|
|
336
|
+
// src/units/definitions.ts
|
|
356
337
|
var units = [
|
|
357
338
|
// Mass (Metric)
|
|
358
339
|
{
|
|
@@ -482,20 +463,19 @@ for (const unit of units) {
|
|
|
482
463
|
function normalizeUnit(unit = "") {
|
|
483
464
|
return unitMap.get(unit.toLowerCase().trim());
|
|
484
465
|
}
|
|
485
|
-
var
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
};
|
|
466
|
+
var NO_UNIT = "__no-unit__";
|
|
467
|
+
function resolveUnit(name = NO_UNIT, integerProtected = false) {
|
|
468
|
+
const normalizedUnit = normalizeUnit(name);
|
|
469
|
+
const resolvedUnit = normalizedUnit ? { ...normalizedUnit, name } : { name, type: "other", system: "none" };
|
|
470
|
+
return integerProtected ? { ...resolvedUnit, integerProtected: true } : resolvedUnit;
|
|
471
|
+
}
|
|
472
|
+
function isNoUnit(unit) {
|
|
473
|
+
if (!unit) return true;
|
|
474
|
+
return resolveUnit(unit.name).name === NO_UNIT;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/quantities/numeric.ts
|
|
478
|
+
var import_big = __toESM(require("big.js"), 1);
|
|
499
479
|
function gcd(a2, b) {
|
|
500
480
|
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
501
481
|
}
|
|
@@ -511,14 +491,23 @@ function simplifyFraction(num, den) {
|
|
|
511
491
|
simplifiedDen = -simplifiedDen;
|
|
512
492
|
}
|
|
513
493
|
if (simplifiedDen === 1) {
|
|
514
|
-
return { type: "decimal",
|
|
494
|
+
return { type: "decimal", decimal: simplifiedNum };
|
|
515
495
|
} else {
|
|
516
496
|
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
517
497
|
}
|
|
518
498
|
}
|
|
499
|
+
function getNumericValue(v) {
|
|
500
|
+
if (v.type === "decimal") {
|
|
501
|
+
return v.decimal;
|
|
502
|
+
}
|
|
503
|
+
return v.num / v.den;
|
|
504
|
+
}
|
|
519
505
|
function multiplyNumericValue(v, factor) {
|
|
520
506
|
if (v.type === "decimal") {
|
|
521
|
-
return {
|
|
507
|
+
return {
|
|
508
|
+
type: "decimal",
|
|
509
|
+
decimal: (0, import_big.default)(v.decimal).times(factor).toNumber()
|
|
510
|
+
};
|
|
522
511
|
}
|
|
523
512
|
return simplifyFraction((0, import_big.default)(v.num).times(factor).toNumber(), v.den);
|
|
524
513
|
}
|
|
@@ -528,36 +517,36 @@ function addNumericValues(val1, val2) {
|
|
|
528
517
|
let num2;
|
|
529
518
|
let den2;
|
|
530
519
|
if (val1.type === "decimal") {
|
|
531
|
-
num1 = val1.
|
|
520
|
+
num1 = val1.decimal;
|
|
532
521
|
den1 = 1;
|
|
533
522
|
} else {
|
|
534
523
|
num1 = val1.num;
|
|
535
524
|
den1 = val1.den;
|
|
536
525
|
}
|
|
537
526
|
if (val2.type === "decimal") {
|
|
538
|
-
num2 = val2.
|
|
527
|
+
num2 = val2.decimal;
|
|
539
528
|
den2 = 1;
|
|
540
529
|
} else {
|
|
541
530
|
num2 = val2.num;
|
|
542
531
|
den2 = val2.den;
|
|
543
532
|
}
|
|
544
533
|
if (num1 === 0 && num2 === 0) {
|
|
545
|
-
return { type: "decimal",
|
|
534
|
+
return { type: "decimal", decimal: 0 };
|
|
546
535
|
}
|
|
547
|
-
if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.
|
|
536
|
+
if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.decimal === 0 || val2.type === "fraction" && val1.type === "decimal" && val1.decimal === 0) {
|
|
548
537
|
const commonDen = den1 * den2;
|
|
549
538
|
const sumNum = num1 * den2 + num2 * den1;
|
|
550
539
|
return simplifyFraction(sumNum, commonDen);
|
|
551
540
|
} else {
|
|
552
541
|
return {
|
|
553
542
|
type: "decimal",
|
|
554
|
-
|
|
543
|
+
decimal: (0, import_big.default)(num1).div(den1).add((0, import_big.default)(num2).div(den2)).toNumber()
|
|
555
544
|
};
|
|
556
545
|
}
|
|
557
546
|
}
|
|
558
547
|
var toRoundedDecimal = (v) => {
|
|
559
|
-
const value = v.type === "decimal" ? v.
|
|
560
|
-
return { type: "decimal",
|
|
548
|
+
const value = v.type === "decimal" ? v.decimal : v.num / v.den;
|
|
549
|
+
return { type: "decimal", decimal: Math.round(value * 1e3) / 1e3 };
|
|
561
550
|
};
|
|
562
551
|
function multiplyQuantityValue(value, factor) {
|
|
563
552
|
if (value.type === "fixed") {
|
|
@@ -583,13 +572,139 @@ function multiplyQuantityValue(value, factor) {
|
|
|
583
572
|
max: multiplyNumericValue(value.max, factor)
|
|
584
573
|
};
|
|
585
574
|
}
|
|
575
|
+
function getAverageValue(q) {
|
|
576
|
+
if (q.type === "fixed") {
|
|
577
|
+
return q.value.type === "text" ? q.value.text : getNumericValue(q.value);
|
|
578
|
+
} else {
|
|
579
|
+
return (getNumericValue(q.min) + getNumericValue(q.max)) / 2;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/errors.ts
|
|
584
|
+
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
585
|
+
constructor(item_type, item_name, new_modifier) {
|
|
586
|
+
super(
|
|
587
|
+
`The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
|
|
588
|
+
You can either remove the reference to create a new ${item_type} defined as ${new_modifier} or add the ${new_modifier} flag to the original definition of the ${item_type}`
|
|
589
|
+
);
|
|
590
|
+
this.name = "ReferencedItemCannotBeRedefinedError";
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var NoProductCatalogForCartError = class extends Error {
|
|
594
|
+
constructor() {
|
|
595
|
+
super(
|
|
596
|
+
`Cannot build a cart without a product catalog. Please set one using setProductCatalog()`
|
|
597
|
+
);
|
|
598
|
+
this.name = "NoProductCatalogForCartError";
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var NoShoppingListForCartError = class extends Error {
|
|
602
|
+
constructor() {
|
|
603
|
+
super(
|
|
604
|
+
`Cannot build a cart without a shopping list. Please set one using setShoppingList()`
|
|
605
|
+
);
|
|
606
|
+
this.name = "NoShoppingListForCartError";
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
var NoProductMatchError = class extends Error {
|
|
610
|
+
constructor(item_name, code) {
|
|
611
|
+
const messageMap = {
|
|
612
|
+
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
613
|
+
noProduct: "No product was found linked to ingredient name ${item_name} in the shopping list",
|
|
614
|
+
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
615
|
+
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
616
|
+
textValue_incompatibleUnits: `Multiple alternative quantities were provided for ingredient ${item_name} in the shopping list but they were either text values or no product in catalog were found to have compatible units`
|
|
617
|
+
};
|
|
618
|
+
super(messageMap[code]);
|
|
619
|
+
__publicField(this, "code");
|
|
620
|
+
this.code = code;
|
|
621
|
+
this.name = "NoProductMatchError";
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
var InvalidProductCatalogFormat = class extends Error {
|
|
625
|
+
constructor() {
|
|
626
|
+
super("Invalid product catalog format.");
|
|
627
|
+
this.name = "InvalidProductCatalogFormat";
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
var CannotAddTextValueError = class extends Error {
|
|
631
|
+
constructor() {
|
|
632
|
+
super("Cannot add a quantity with a text value.");
|
|
633
|
+
this.name = "CannotAddTextValueError";
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
var IncompatibleUnitsError = class extends Error {
|
|
637
|
+
constructor(unit1, unit2) {
|
|
638
|
+
super(
|
|
639
|
+
`Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
|
|
640
|
+
);
|
|
641
|
+
this.name = "IncompatibleUnitsError";
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
var InvalidQuantityFormat = class extends Error {
|
|
645
|
+
constructor(value) {
|
|
646
|
+
super(`Invalid quantity format found in: ${value}`);
|
|
647
|
+
this.name = "InvalidQuantityFormat";
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// src/utils/type_guards.ts
|
|
652
|
+
function isGroup(x) {
|
|
653
|
+
return x && "type" in x;
|
|
654
|
+
}
|
|
655
|
+
function isOrGroup(x) {
|
|
656
|
+
return isGroup(x) && x.type === "or";
|
|
657
|
+
}
|
|
658
|
+
function isAndGroup(x) {
|
|
659
|
+
return isGroup(x) && x.type === "and";
|
|
660
|
+
}
|
|
661
|
+
function isQuantity(x) {
|
|
662
|
+
return x && typeof x === "object" && "quantity" in x;
|
|
663
|
+
}
|
|
664
|
+
function isNumericValueIntegerLike(v) {
|
|
665
|
+
if (v.type === "decimal") return Number.isInteger(v.decimal);
|
|
666
|
+
return v.num % v.den === 0;
|
|
667
|
+
}
|
|
668
|
+
function isValueIntegerLike(q) {
|
|
669
|
+
if (q.type === "fixed") {
|
|
670
|
+
if (q.value.type === "text") return false;
|
|
671
|
+
return isNumericValueIntegerLike(q.value);
|
|
672
|
+
}
|
|
673
|
+
return isNumericValueIntegerLike(q.min) && isNumericValueIntegerLike(q.max);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/quantities/mutations.ts
|
|
677
|
+
function extendAllUnits(q) {
|
|
678
|
+
if (isGroup(q)) {
|
|
679
|
+
return { ...q, entries: q.entries.map(extendAllUnits) };
|
|
680
|
+
} else {
|
|
681
|
+
const newQ = {
|
|
682
|
+
quantity: q.quantity
|
|
683
|
+
};
|
|
684
|
+
if (q.unit) {
|
|
685
|
+
newQ.unit = { name: q.unit };
|
|
686
|
+
}
|
|
687
|
+
return newQ;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function normalizeAllUnits(q) {
|
|
691
|
+
if (isGroup(q)) {
|
|
692
|
+
return { ...q, entries: q.entries.map(normalizeAllUnits) };
|
|
693
|
+
} else {
|
|
694
|
+
const newQ = {
|
|
695
|
+
quantity: q.quantity,
|
|
696
|
+
unit: resolveUnit(q.unit)
|
|
697
|
+
};
|
|
698
|
+
return newQ;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
586
701
|
var convertQuantityValue = (value, def, targetDef) => {
|
|
587
702
|
if (def.name === targetDef.name) return value;
|
|
588
703
|
const factor = def.toBase / targetDef.toBase;
|
|
589
704
|
return multiplyQuantityValue(value, factor);
|
|
590
705
|
};
|
|
591
706
|
function getDefaultQuantityValue() {
|
|
592
|
-
return { type: "fixed", value: { type: "decimal",
|
|
707
|
+
return { type: "fixed", value: { type: "decimal", decimal: 0 } };
|
|
593
708
|
}
|
|
594
709
|
function addQuantityValues(v1, v2) {
|
|
595
710
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
@@ -615,28 +730,31 @@ function addQuantityValues(v1, v2) {
|
|
|
615
730
|
return { type: "range", min: newMin, max: newMax };
|
|
616
731
|
}
|
|
617
732
|
function addQuantities(q1, q2) {
|
|
618
|
-
const v1 = q1.
|
|
619
|
-
const v2 = q2.
|
|
733
|
+
const v1 = q1.quantity;
|
|
734
|
+
const v2 = q2.quantity;
|
|
620
735
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
621
736
|
throw new CannotAddTextValueError();
|
|
622
737
|
}
|
|
623
|
-
const unit1Def = normalizeUnit(q1.unit);
|
|
624
|
-
const unit2Def = normalizeUnit(q2.unit);
|
|
625
|
-
const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({
|
|
626
|
-
|
|
738
|
+
const unit1Def = normalizeUnit(q1.unit?.name);
|
|
739
|
+
const unit2Def = normalizeUnit(q2.unit?.name);
|
|
740
|
+
const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({
|
|
741
|
+
quantity: addQuantityValues(val1, val2),
|
|
742
|
+
unit
|
|
743
|
+
});
|
|
744
|
+
if ((q1.unit?.name === "" || q1.unit === void 0) && q2.unit !== void 0) {
|
|
627
745
|
return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
|
|
628
746
|
}
|
|
629
|
-
if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
747
|
+
if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
630
748
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
631
749
|
}
|
|
632
|
-
if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
|
|
750
|
+
if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.name.toLowerCase() === q2.unit.name.toLowerCase()) {
|
|
633
751
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
634
752
|
}
|
|
635
753
|
if (unit1Def && unit2Def) {
|
|
636
754
|
if (unit1Def.type !== unit2Def.type) {
|
|
637
755
|
throw new IncompatibleUnitsError(
|
|
638
|
-
`${unit1Def.type} (${q1.unit})`,
|
|
639
|
-
`${unit2Def.type} (${q2.unit})`
|
|
756
|
+
`${unit1Def.type} (${q1.unit?.name})`,
|
|
757
|
+
`${unit2Def.type} (${q2.unit?.name})`
|
|
640
758
|
);
|
|
641
759
|
}
|
|
642
760
|
let targetUnitDef;
|
|
@@ -650,27 +768,131 @@ function addQuantities(q1, q2) {
|
|
|
650
768
|
}
|
|
651
769
|
const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
|
|
652
770
|
const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
convertedV2,
|
|
656
|
-
targetUnitDef.name
|
|
657
|
-
);
|
|
771
|
+
const targetUnit = { name: targetUnitDef.name };
|
|
772
|
+
return addQuantityValuesAndSetUnit(convertedV1, convertedV2, targetUnit);
|
|
658
773
|
}
|
|
659
|
-
throw new IncompatibleUnitsError(
|
|
774
|
+
throw new IncompatibleUnitsError(
|
|
775
|
+
q1.unit?.name,
|
|
776
|
+
q2.unit?.name
|
|
777
|
+
);
|
|
660
778
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
779
|
+
function toPlainUnit(quantity) {
|
|
780
|
+
if (isQuantity(quantity))
|
|
781
|
+
return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
|
|
782
|
+
else {
|
|
783
|
+
return {
|
|
784
|
+
...quantity,
|
|
785
|
+
entries: quantity.entries.map(toPlainUnit)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function toExtendedUnit(q) {
|
|
790
|
+
if (isQuantity(q)) {
|
|
791
|
+
return q.unit ? { ...q, unit: { name: q.unit } } : q;
|
|
792
|
+
} else {
|
|
793
|
+
return {
|
|
794
|
+
...q,
|
|
795
|
+
entries: q.entries.map(
|
|
796
|
+
(entry) => isQuantity(entry) ? toExtendedUnit(entry) : toExtendedUnit(entry)
|
|
797
|
+
)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function deNormalizeQuantity(q) {
|
|
802
|
+
const result = {
|
|
803
|
+
quantity: q.quantity
|
|
804
|
+
};
|
|
805
|
+
if (!isNoUnit(q.unit)) {
|
|
806
|
+
result.unit = { name: q.unit.name };
|
|
807
|
+
}
|
|
808
|
+
return result;
|
|
809
|
+
}
|
|
810
|
+
var flattenPlainUnitGroup = (summed) => {
|
|
811
|
+
if (isOrGroup(summed)) {
|
|
812
|
+
const entries = summed.entries;
|
|
813
|
+
const andGroupEntry = entries.find(
|
|
814
|
+
(e2) => isGroup(e2) && e2.type === "and"
|
|
668
815
|
);
|
|
669
|
-
|
|
816
|
+
if (andGroupEntry) {
|
|
817
|
+
const andEntries = [];
|
|
818
|
+
for (const entry of andGroupEntry.entries) {
|
|
819
|
+
if (isQuantity(entry)) {
|
|
820
|
+
andEntries.push({
|
|
821
|
+
quantity: entry.quantity,
|
|
822
|
+
unit: entry.unit
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const equivalentsList = entries.filter((e2) => isQuantity(e2)).map((e2) => ({ quantity: e2.quantity, unit: e2.unit }));
|
|
827
|
+
if (equivalentsList.length > 0) {
|
|
828
|
+
return [
|
|
829
|
+
{
|
|
830
|
+
type: "and",
|
|
831
|
+
entries: andEntries,
|
|
832
|
+
equivalents: equivalentsList
|
|
833
|
+
}
|
|
834
|
+
];
|
|
835
|
+
} else {
|
|
836
|
+
return andEntries.map((entry) => ({ groupQuantity: entry }));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const simpleEntries = entries.filter(
|
|
840
|
+
(e2) => isQuantity(e2)
|
|
841
|
+
);
|
|
842
|
+
if (simpleEntries.length > 0) {
|
|
843
|
+
const result = {
|
|
844
|
+
quantity: simpleEntries[0].quantity,
|
|
845
|
+
unit: simpleEntries[0].unit
|
|
846
|
+
};
|
|
847
|
+
if (simpleEntries.length > 1) {
|
|
848
|
+
result.equivalents = simpleEntries.slice(1);
|
|
849
|
+
}
|
|
850
|
+
return [{ groupQuantity: result }];
|
|
851
|
+
} else {
|
|
852
|
+
const first = entries[0];
|
|
853
|
+
return [
|
|
854
|
+
{ groupQuantity: { quantity: first.quantity, unit: first.unit } }
|
|
855
|
+
];
|
|
856
|
+
}
|
|
857
|
+
} else if (isGroup(summed)) {
|
|
858
|
+
const andEntries = [];
|
|
859
|
+
const equivalentsList = [];
|
|
860
|
+
for (const entry of summed.entries) {
|
|
861
|
+
if (isOrGroup(entry)) {
|
|
862
|
+
const orEntries = entry.entries.filter(
|
|
863
|
+
(e2) => isQuantity(e2)
|
|
864
|
+
);
|
|
865
|
+
if (orEntries.length > 0) {
|
|
866
|
+
andEntries.push({
|
|
867
|
+
quantity: orEntries[0].quantity,
|
|
868
|
+
unit: orEntries[0].unit
|
|
869
|
+
});
|
|
870
|
+
equivalentsList.push(...orEntries.slice(1));
|
|
871
|
+
}
|
|
872
|
+
} else if (isQuantity(entry)) {
|
|
873
|
+
andEntries.push({
|
|
874
|
+
quantity: entry.quantity,
|
|
875
|
+
unit: entry.unit
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (equivalentsList.length === 0) {
|
|
880
|
+
return andEntries.map((entry) => ({ groupQuantity: entry }));
|
|
881
|
+
}
|
|
882
|
+
const result = {
|
|
883
|
+
type: "and",
|
|
884
|
+
entries: andEntries,
|
|
885
|
+
equivalents: equivalentsList
|
|
886
|
+
};
|
|
887
|
+
return [result];
|
|
888
|
+
} else {
|
|
889
|
+
return [
|
|
890
|
+
{ groupQuantity: { quantity: summed.quantity, unit: summed.unit } }
|
|
891
|
+
];
|
|
670
892
|
}
|
|
671
893
|
};
|
|
672
894
|
|
|
673
|
-
// src/parser_helpers.ts
|
|
895
|
+
// src/utils/parser_helpers.ts
|
|
674
896
|
function flushPendingNote(section, note) {
|
|
675
897
|
if (note.length > 0) {
|
|
676
898
|
section.content.push({ type: "note", note });
|
|
@@ -687,7 +909,7 @@ function flushPendingItems(section, items) {
|
|
|
687
909
|
return false;
|
|
688
910
|
}
|
|
689
911
|
function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
|
|
690
|
-
const { name
|
|
912
|
+
const { name } = newIngredient;
|
|
691
913
|
if (isReference) {
|
|
692
914
|
const indexFind = ingredients.findIndex(
|
|
693
915
|
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
@@ -698,52 +920,28 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
|
|
|
698
920
|
);
|
|
699
921
|
}
|
|
700
922
|
const existingIngredient = ingredients[indexFind];
|
|
701
|
-
|
|
702
|
-
if (
|
|
923
|
+
if (!newIngredient.flags) {
|
|
924
|
+
if (Array.isArray(existingIngredient.flags) && existingIngredient.flags.length > 0) {
|
|
703
925
|
throw new ReferencedItemCannotBeRedefinedError(
|
|
704
926
|
"ingredient",
|
|
705
927
|
existingIngredient.name,
|
|
706
|
-
|
|
928
|
+
existingIngredient.flags[0]
|
|
707
929
|
);
|
|
708
930
|
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const newQuantity = { value: quantity, unit: unit ?? "" };
|
|
717
|
-
try {
|
|
718
|
-
const total = addQuantities(currentQuantity, newQuantity);
|
|
719
|
-
existingIngredient.quantity = total.value;
|
|
720
|
-
existingIngredient.unit = total.unit || void 0;
|
|
721
|
-
if (existingIngredient.quantityParts) {
|
|
722
|
-
existingIngredient.quantityParts.push(
|
|
723
|
-
...newIngredient.quantityParts
|
|
931
|
+
} else {
|
|
932
|
+
for (const flag of newIngredient.flags) {
|
|
933
|
+
if (existingIngredient.flags === void 0 || !existingIngredient.flags.includes(flag)) {
|
|
934
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
935
|
+
"ingredient",
|
|
936
|
+
existingIngredient.name,
|
|
937
|
+
flag
|
|
724
938
|
);
|
|
725
|
-
} else {
|
|
726
|
-
existingIngredient.quantityParts = newIngredient.quantityParts;
|
|
727
|
-
}
|
|
728
|
-
quantityPartIndex = existingIngredient.quantityParts.length - 1;
|
|
729
|
-
} catch (e2) {
|
|
730
|
-
if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
|
|
731
|
-
return {
|
|
732
|
-
ingredientIndex: ingredients.push(newIngredient) - 1,
|
|
733
|
-
quantityPartIndex: 0
|
|
734
|
-
};
|
|
735
939
|
}
|
|
736
940
|
}
|
|
737
941
|
}
|
|
738
|
-
return
|
|
739
|
-
ingredientIndex: indexFind,
|
|
740
|
-
quantityPartIndex
|
|
741
|
-
};
|
|
942
|
+
return indexFind;
|
|
742
943
|
}
|
|
743
|
-
return
|
|
744
|
-
ingredientIndex: ingredients.push(newIngredient) - 1,
|
|
745
|
-
quantityPartIndex: newIngredient.quantity ? 0 : void 0
|
|
746
|
-
};
|
|
944
|
+
return ingredients.push(newIngredient) - 1;
|
|
747
945
|
}
|
|
748
946
|
function findAndUpsertCookware(cookware, newCookware, isReference) {
|
|
749
947
|
const { name, quantity } = newCookware;
|
|
@@ -757,58 +955,48 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
|
|
|
757
955
|
);
|
|
758
956
|
}
|
|
759
957
|
const existingCookware = cookware[index];
|
|
760
|
-
|
|
761
|
-
if (
|
|
958
|
+
if (!newCookware.flags) {
|
|
959
|
+
if (Array.isArray(existingCookware.flags) && existingCookware.flags.length > 0) {
|
|
762
960
|
throw new ReferencedItemCannotBeRedefinedError(
|
|
763
961
|
"cookware",
|
|
764
962
|
existingCookware.name,
|
|
765
|
-
|
|
963
|
+
existingCookware.flags[0]
|
|
766
964
|
);
|
|
767
965
|
}
|
|
966
|
+
} else {
|
|
967
|
+
for (const flag of newCookware.flags) {
|
|
968
|
+
if (existingCookware.flags === void 0 || !existingCookware.flags.includes(flag)) {
|
|
969
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
970
|
+
"cookware",
|
|
971
|
+
existingCookware.name,
|
|
972
|
+
flag
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
768
976
|
}
|
|
769
|
-
let quantityPartIndex = void 0;
|
|
770
977
|
if (quantity !== void 0) {
|
|
771
978
|
if (!existingCookware.quantity) {
|
|
772
979
|
existingCookware.quantity = quantity;
|
|
773
|
-
existingCookware.quantityParts = newCookware.quantityParts;
|
|
774
|
-
quantityPartIndex = 0;
|
|
775
980
|
} else {
|
|
776
981
|
try {
|
|
777
982
|
existingCookware.quantity = addQuantityValues(
|
|
778
983
|
existingCookware.quantity,
|
|
779
984
|
quantity
|
|
780
985
|
);
|
|
781
|
-
if (!existingCookware.quantityParts) {
|
|
782
|
-
existingCookware.quantityParts = newCookware.quantityParts;
|
|
783
|
-
quantityPartIndex = 0;
|
|
784
|
-
} else {
|
|
785
|
-
quantityPartIndex = existingCookware.quantityParts.push(
|
|
786
|
-
...newCookware.quantityParts
|
|
787
|
-
) - 1;
|
|
788
|
-
}
|
|
789
986
|
} catch (e2) {
|
|
790
987
|
if (e2 instanceof CannotAddTextValueError) {
|
|
791
|
-
return
|
|
792
|
-
cookwareIndex: cookware.push(newCookware) - 1,
|
|
793
|
-
quantityPartIndex: 0
|
|
794
|
-
};
|
|
988
|
+
return cookware.push(newCookware) - 1;
|
|
795
989
|
}
|
|
796
990
|
}
|
|
797
991
|
}
|
|
798
992
|
}
|
|
799
|
-
return
|
|
800
|
-
cookwareIndex: index,
|
|
801
|
-
quantityPartIndex
|
|
802
|
-
};
|
|
993
|
+
return index;
|
|
803
994
|
}
|
|
804
|
-
return
|
|
805
|
-
cookwareIndex: cookware.push(newCookware) - 1,
|
|
806
|
-
quantityPartIndex: quantity ? 0 : void 0
|
|
807
|
-
};
|
|
995
|
+
return cookware.push(newCookware) - 1;
|
|
808
996
|
}
|
|
809
997
|
var parseFixedValue = (input_str) => {
|
|
810
998
|
if (!numberLikeRegex.test(input_str)) {
|
|
811
|
-
return { type: "text",
|
|
999
|
+
return { type: "text", text: input_str };
|
|
812
1000
|
}
|
|
813
1001
|
const s = input_str.trim().replace(",", ".");
|
|
814
1002
|
if (s.includes("/")) {
|
|
@@ -817,8 +1005,22 @@ var parseFixedValue = (input_str) => {
|
|
|
817
1005
|
const den = Number(parts[1]);
|
|
818
1006
|
return { type: "fraction", num, den };
|
|
819
1007
|
}
|
|
820
|
-
return { type: "decimal",
|
|
1008
|
+
return { type: "decimal", decimal: Number(s) };
|
|
821
1009
|
};
|
|
1010
|
+
function stringifyQuantityValue(quantity) {
|
|
1011
|
+
if (quantity.type === "fixed") {
|
|
1012
|
+
return stringifyFixedValue(quantity);
|
|
1013
|
+
} else {
|
|
1014
|
+
return `${stringifyFixedValue({ type: "fixed", value: quantity.min })}-${stringifyFixedValue({ type: "fixed", value: quantity.max })}`;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function stringifyFixedValue(quantity) {
|
|
1018
|
+
if (quantity.value.type === "fraction")
|
|
1019
|
+
return `${quantity.value.num}/${quantity.value.den}`;
|
|
1020
|
+
else if (quantity.value.type === "decimal")
|
|
1021
|
+
return String(quantity.value.decimal);
|
|
1022
|
+
else return quantity.value.text;
|
|
1023
|
+
}
|
|
822
1024
|
function parseQuantityInput(input_str) {
|
|
823
1025
|
const clean_str = String(input_str).trim();
|
|
824
1026
|
if (rangeRegex.test(clean_str)) {
|
|
@@ -860,7 +1062,7 @@ function parseListMetaVar(content, varName) {
|
|
|
860
1062
|
function extractMetadata(content) {
|
|
861
1063
|
const metadata = {};
|
|
862
1064
|
let servings = void 0;
|
|
863
|
-
const metadataContent = content.match(metadataRegex)?.[
|
|
1065
|
+
const metadataContent = content.match(metadataRegex)?.[2];
|
|
864
1066
|
if (!metadataContent) {
|
|
865
1067
|
return { metadata };
|
|
866
1068
|
}
|
|
@@ -905,74 +1107,1168 @@ function extractMetadata(content) {
|
|
|
905
1107
|
}
|
|
906
1108
|
return { metadata, servings };
|
|
907
1109
|
}
|
|
1110
|
+
function isPositiveIntegerString(str) {
|
|
1111
|
+
return /^\d+$/.test(str);
|
|
1112
|
+
}
|
|
1113
|
+
function unionOfSets(s1, s2) {
|
|
1114
|
+
const result = new Set(s1);
|
|
1115
|
+
for (const item of s2) {
|
|
1116
|
+
result.add(item);
|
|
1117
|
+
}
|
|
1118
|
+
return result;
|
|
1119
|
+
}
|
|
1120
|
+
function getAlternativeSignature(alternatives) {
|
|
1121
|
+
if (!alternatives || alternatives.length === 0) return null;
|
|
1122
|
+
return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
1123
|
+
}
|
|
908
1124
|
|
|
909
|
-
// src/classes/
|
|
910
|
-
var
|
|
911
|
-
|
|
1125
|
+
// src/classes/product_catalog.ts
|
|
1126
|
+
var ProductCatalog = class {
|
|
1127
|
+
constructor(tomlContent) {
|
|
1128
|
+
__publicField(this, "products", []);
|
|
1129
|
+
if (tomlContent) this.parse(tomlContent);
|
|
1130
|
+
}
|
|
912
1131
|
/**
|
|
913
|
-
*
|
|
914
|
-
* @param
|
|
1132
|
+
* Parses a TOML string into a list of product options.
|
|
1133
|
+
* @param tomlContent - The TOML string to parse.
|
|
1134
|
+
* @returns A parsed list of `ProductOption`.
|
|
915
1135
|
*/
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
/**
|
|
922
|
-
* The parsed recipe ingredients.
|
|
923
|
-
*/
|
|
924
|
-
__publicField(this, "ingredients", []);
|
|
925
|
-
/**
|
|
926
|
-
* The parsed recipe sections.
|
|
927
|
-
*/
|
|
928
|
-
__publicField(this, "sections", []);
|
|
929
|
-
/**
|
|
930
|
-
* The parsed recipe cookware.
|
|
931
|
-
*/
|
|
932
|
-
__publicField(this, "cookware", []);
|
|
933
|
-
/**
|
|
934
|
-
* The parsed recipe timers.
|
|
935
|
-
*/
|
|
936
|
-
__publicField(this, "timers", []);
|
|
937
|
-
/**
|
|
938
|
-
* The parsed recipe servings. Used for scaling. Parsed from one of
|
|
939
|
-
* {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
|
|
940
|
-
* metadata fields.
|
|
941
|
-
*
|
|
942
|
-
* @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
|
|
943
|
-
*/
|
|
944
|
-
__publicField(this, "servings");
|
|
945
|
-
if (content) {
|
|
946
|
-
this.parse(content);
|
|
1136
|
+
parse(tomlContent) {
|
|
1137
|
+
const catalogRaw = import_smol_toml.default.parse(tomlContent);
|
|
1138
|
+
this.products = [];
|
|
1139
|
+
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1140
|
+
throw new InvalidProductCatalogFormat();
|
|
947
1141
|
}
|
|
1142
|
+
for (const [ingredientName, ingredientData] of Object.entries(catalogRaw)) {
|
|
1143
|
+
const ingredientTable = ingredientData;
|
|
1144
|
+
const aliases = ingredientTable.aliases;
|
|
1145
|
+
for (const [key, productData] of Object.entries(ingredientTable)) {
|
|
1146
|
+
if (key === "aliases") {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const productId = key;
|
|
1150
|
+
const { name, size, price, ...rest } = productData;
|
|
1151
|
+
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
1152
|
+
const sizes = sizeStrings.map((sizeStr) => {
|
|
1153
|
+
const sizeAndUnitRaw = sizeStr.split("%");
|
|
1154
|
+
const sizeParsed = parseQuantityInput(
|
|
1155
|
+
sizeAndUnitRaw[0]
|
|
1156
|
+
);
|
|
1157
|
+
const productSize = { size: sizeParsed };
|
|
1158
|
+
if (sizeAndUnitRaw.length > 1) {
|
|
1159
|
+
productSize.unit = sizeAndUnitRaw[1];
|
|
1160
|
+
}
|
|
1161
|
+
return productSize;
|
|
1162
|
+
});
|
|
1163
|
+
const productOption = {
|
|
1164
|
+
id: productId,
|
|
1165
|
+
productName: name,
|
|
1166
|
+
ingredientName,
|
|
1167
|
+
price,
|
|
1168
|
+
sizes,
|
|
1169
|
+
...rest
|
|
1170
|
+
};
|
|
1171
|
+
if (aliases) {
|
|
1172
|
+
productOption.ingredientAliases = aliases;
|
|
1173
|
+
}
|
|
1174
|
+
this.products.push(productOption);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return this.products;
|
|
948
1178
|
}
|
|
949
1179
|
/**
|
|
950
|
-
*
|
|
951
|
-
* @
|
|
1180
|
+
* Stringifies the catalog to a TOML string.
|
|
1181
|
+
* @returns The TOML string representation of the catalog.
|
|
952
1182
|
*/
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (
|
|
965
|
-
|
|
966
|
-
note = flushPendingNote(section, note);
|
|
967
|
-
blankLineBefore = true;
|
|
968
|
-
inNote = false;
|
|
969
|
-
continue;
|
|
1183
|
+
stringify() {
|
|
1184
|
+
const grouped = {};
|
|
1185
|
+
for (const product of this.products) {
|
|
1186
|
+
const {
|
|
1187
|
+
id,
|
|
1188
|
+
ingredientName,
|
|
1189
|
+
ingredientAliases,
|
|
1190
|
+
sizes,
|
|
1191
|
+
productName,
|
|
1192
|
+
...rest
|
|
1193
|
+
} = product;
|
|
1194
|
+
if (!grouped[ingredientName]) {
|
|
1195
|
+
grouped[ingredientName] = {};
|
|
970
1196
|
}
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1197
|
+
if (ingredientAliases && !grouped[ingredientName].aliases) {
|
|
1198
|
+
grouped[ingredientName].aliases = ingredientAliases;
|
|
1199
|
+
}
|
|
1200
|
+
const sizeStrings = sizes.map(
|
|
1201
|
+
(s) => s.unit ? `${stringifyQuantityValue(s.size)}%${s.unit}` : stringifyQuantityValue(s.size)
|
|
1202
|
+
);
|
|
1203
|
+
grouped[ingredientName][id] = {
|
|
1204
|
+
...rest,
|
|
1205
|
+
name: productName,
|
|
1206
|
+
// Use array if multiple sizes, otherwise single string
|
|
1207
|
+
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
return import_smol_toml.default.stringify(grouped);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Adds a product to the catalog.
|
|
1214
|
+
* @param productOption - The product to add.
|
|
1215
|
+
*/
|
|
1216
|
+
add(productOption) {
|
|
1217
|
+
this.products.push(productOption);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Removes a product from the catalog by its ID.
|
|
1221
|
+
* @param productId - The ID of the product to remove.
|
|
1222
|
+
*/
|
|
1223
|
+
remove(productId) {
|
|
1224
|
+
this.products = this.products.filter((product) => product.id !== productId);
|
|
1225
|
+
}
|
|
1226
|
+
isValidTomlContent(catalog) {
|
|
1227
|
+
for (const productsRaw of Object.values(catalog)) {
|
|
1228
|
+
if (typeof productsRaw !== "object" || productsRaw === null) {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
for (const [id, obj] of Object.entries(productsRaw)) {
|
|
1232
|
+
if (id === "aliases") {
|
|
1233
|
+
if (!Array.isArray(obj)) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
if (!isPositiveIntegerString(id)) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
const record = obj;
|
|
1244
|
+
const keys = Object.keys(record);
|
|
1245
|
+
const mandatoryKeys = ["name", "size", "price"];
|
|
1246
|
+
if (mandatoryKeys.some((key) => !keys.includes(key))) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
const hasProductName = typeof record.name === "string";
|
|
1250
|
+
const hasSize = typeof record.size === "string" || Array.isArray(record.size) && record.size.every((s) => typeof s === "string");
|
|
1251
|
+
const hasPrice = typeof record.price === "number";
|
|
1252
|
+
if (!(hasProductName && hasSize && hasPrice)) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// src/classes/section.ts
|
|
1263
|
+
var Section = class {
|
|
1264
|
+
/**
|
|
1265
|
+
* Creates an instance of Section.
|
|
1266
|
+
* @param name - The name of the section. Defaults to an empty string.
|
|
1267
|
+
*/
|
|
1268
|
+
constructor(name = "") {
|
|
1269
|
+
/**
|
|
1270
|
+
* The name of the section. Can be an empty string for the default (first) section.
|
|
1271
|
+
* @defaultValue `""`
|
|
1272
|
+
*/
|
|
1273
|
+
__publicField(this, "name");
|
|
1274
|
+
/** An array of steps and notes that make up the content of the section. */
|
|
1275
|
+
__publicField(this, "content", []);
|
|
1276
|
+
this.name = name;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Checks if the section is blank (has no name and no content).
|
|
1280
|
+
* Used during recipe parsing
|
|
1281
|
+
* @returns `true` if the section is blank, otherwise `false`.
|
|
1282
|
+
*/
|
|
1283
|
+
isBlank() {
|
|
1284
|
+
return this.name === "" && this.content.length === 0;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/quantities/alternatives.ts
|
|
1289
|
+
var import_big3 = __toESM(require("big.js"), 1);
|
|
1290
|
+
|
|
1291
|
+
// src/units/conversion.ts
|
|
1292
|
+
var import_big2 = __toESM(require("big.js"), 1);
|
|
1293
|
+
function getUnitRatio(q1, q2) {
|
|
1294
|
+
const q1Value = getAverageValue(q1.quantity);
|
|
1295
|
+
const q2Value = getAverageValue(q2.quantity);
|
|
1296
|
+
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
1297
|
+
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
1298
|
+
throw Error(
|
|
1299
|
+
"One of both values is not a number, so a ratio cannot be computed"
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
1303
|
+
}
|
|
1304
|
+
function getBaseUnitRatio(q, qRef) {
|
|
1305
|
+
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
1306
|
+
return q.unit.toBase / qRef.unit.toBase;
|
|
1307
|
+
} else {
|
|
1308
|
+
return 1;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/units/lookup.ts
|
|
1313
|
+
function areUnitsCompatible(u1, u2) {
|
|
1314
|
+
if (u1.name === u2.name) {
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
if (u1.type !== "other" && u1.type === u2.type && u1.system === u2.system) {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
function findListWithCompatibleQuantity(list, quantity) {
|
|
1323
|
+
const quantityWithUnitDef = {
|
|
1324
|
+
...quantity,
|
|
1325
|
+
unit: resolveUnit(quantity.unit?.name)
|
|
1326
|
+
};
|
|
1327
|
+
return list.find(
|
|
1328
|
+
(l) => l.some((lq) => areUnitsCompatible(lq.unit, quantityWithUnitDef.unit))
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
function findCompatibleQuantityWithinList(list, quantity) {
|
|
1332
|
+
const quantityWithUnitDef = {
|
|
1333
|
+
...quantity,
|
|
1334
|
+
unit: resolveUnit(quantity.unit?.name)
|
|
1335
|
+
};
|
|
1336
|
+
return list.find(
|
|
1337
|
+
(q) => q.unit.name === quantityWithUnitDef.unit.name || q.unit.type === quantityWithUnitDef.unit.type && q.unit.type !== "other"
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/utils/general.ts
|
|
1342
|
+
var legacyDeepClone = (v) => {
|
|
1343
|
+
if (v === null || typeof v !== "object") {
|
|
1344
|
+
return v;
|
|
1345
|
+
}
|
|
1346
|
+
if (v instanceof Map) {
|
|
1347
|
+
return new Map(
|
|
1348
|
+
Array.from(v.entries()).map(([k, val]) => [
|
|
1349
|
+
legacyDeepClone(k),
|
|
1350
|
+
legacyDeepClone(val)
|
|
1351
|
+
])
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (v instanceof Set) {
|
|
1355
|
+
return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
|
|
1356
|
+
}
|
|
1357
|
+
if (v instanceof Date) {
|
|
1358
|
+
return new Date(v.getTime());
|
|
1359
|
+
}
|
|
1360
|
+
if (Array.isArray(v)) {
|
|
1361
|
+
return v.map((item) => legacyDeepClone(item));
|
|
1362
|
+
}
|
|
1363
|
+
const cloned = {};
|
|
1364
|
+
for (const key of Object.keys(v)) {
|
|
1365
|
+
cloned[key] = legacyDeepClone(v[key]);
|
|
1366
|
+
}
|
|
1367
|
+
return cloned;
|
|
1368
|
+
};
|
|
1369
|
+
var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
|
|
1370
|
+
|
|
1371
|
+
// src/quantities/alternatives.ts
|
|
1372
|
+
function getEquivalentUnitsLists(...quantities) {
|
|
1373
|
+
const quantitiesCopy = deepClone(quantities);
|
|
1374
|
+
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.entries.length > 1);
|
|
1375
|
+
const unitLists = [];
|
|
1376
|
+
const normalizeOrGroup = (og) => ({
|
|
1377
|
+
...og,
|
|
1378
|
+
entries: og.entries.map((q) => ({
|
|
1379
|
+
...q,
|
|
1380
|
+
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
1381
|
+
}))
|
|
1382
|
+
});
|
|
1383
|
+
function findLinkIndexForUnits(lists, unitsToCheck) {
|
|
1384
|
+
return lists.findIndex((l) => {
|
|
1385
|
+
const listItem = l.map((q) => resolveUnit(q.unit?.name));
|
|
1386
|
+
return unitsToCheck.some(
|
|
1387
|
+
(u) => listItem.some(
|
|
1388
|
+
(lu) => lu.name === u?.name || lu.system === u?.system && lu.type === u?.type && lu.type !== "other"
|
|
1389
|
+
)
|
|
1390
|
+
);
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
function mergeOrGroupIntoList(lists, idx, og) {
|
|
1394
|
+
let unitRatio;
|
|
1395
|
+
const commonUnitList = lists[idx].reduce((acc, v) => {
|
|
1396
|
+
const normalizedV = {
|
|
1397
|
+
...v,
|
|
1398
|
+
unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
|
|
1399
|
+
};
|
|
1400
|
+
const commonQuantity = og.entries.find(
|
|
1401
|
+
(q) => isQuantity(q) && areUnitsCompatible(q.unit, normalizedV.unit)
|
|
1402
|
+
);
|
|
1403
|
+
if (commonQuantity) {
|
|
1404
|
+
acc.push(normalizedV);
|
|
1405
|
+
unitRatio = getUnitRatio(normalizedV, commonQuantity);
|
|
1406
|
+
}
|
|
1407
|
+
return acc;
|
|
1408
|
+
}, []);
|
|
1409
|
+
for (const newQ of og.entries) {
|
|
1410
|
+
if (commonUnitList.some((q) => areUnitsCompatible(q.unit, newQ.unit))) {
|
|
1411
|
+
continue;
|
|
1412
|
+
} else {
|
|
1413
|
+
const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
|
|
1414
|
+
lists[idx].push({ ...newQ, quantity: scaledQuantity });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
for (const orGroup of OrGroups) {
|
|
1419
|
+
const orGroupModified = normalizeOrGroup(orGroup);
|
|
1420
|
+
const units2 = orGroupModified.entries.map((q) => q.unit);
|
|
1421
|
+
const linkIndex = findLinkIndexForUnits(unitLists, units2);
|
|
1422
|
+
if (linkIndex === -1) {
|
|
1423
|
+
unitLists.push(orGroupModified.entries);
|
|
1424
|
+
} else {
|
|
1425
|
+
mergeOrGroupIntoList(unitLists, linkIndex, orGroupModified);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return unitLists;
|
|
1429
|
+
}
|
|
1430
|
+
function sortUnitList(list) {
|
|
1431
|
+
if (!list || list.length <= 1) return list;
|
|
1432
|
+
const priorityList = [];
|
|
1433
|
+
const nonPriorityList = [];
|
|
1434
|
+
for (const q of list) {
|
|
1435
|
+
if (q.unit.integerProtected || q.unit.system === "none") {
|
|
1436
|
+
priorityList.push(q);
|
|
1437
|
+
} else {
|
|
1438
|
+
nonPriorityList.push(q);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return priorityList.sort((a2, b) => {
|
|
1442
|
+
const prefixA = a2.unit.integerProtected ? "___" : "";
|
|
1443
|
+
const prefixB = b.unit.integerProtected ? "___" : "";
|
|
1444
|
+
return (prefixA + a2.unit.name).localeCompare(prefixB + b.unit.name, "en");
|
|
1445
|
+
}).concat(nonPriorityList);
|
|
1446
|
+
}
|
|
1447
|
+
function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
1448
|
+
function reduceToQuantity(firstQuantity) {
|
|
1449
|
+
const equivalentList = sortUnitList(
|
|
1450
|
+
findListWithCompatibleQuantity(unitList, firstQuantity)
|
|
1451
|
+
);
|
|
1452
|
+
if (!equivalentList) return firstQuantity;
|
|
1453
|
+
const firstQuantityInList = findCompatibleQuantityWithinList(
|
|
1454
|
+
equivalentList,
|
|
1455
|
+
firstQuantity
|
|
1456
|
+
);
|
|
1457
|
+
const normalizedFirstQuantity = {
|
|
1458
|
+
...firstQuantity,
|
|
1459
|
+
unit: resolveUnit(firstQuantity.unit?.name)
|
|
1460
|
+
};
|
|
1461
|
+
if (firstQuantityInList.unit.integerProtected) {
|
|
1462
|
+
const resultQuantity = {
|
|
1463
|
+
quantity: firstQuantity.quantity
|
|
1464
|
+
};
|
|
1465
|
+
if (!isNoUnit(normalizedFirstQuantity.unit)) {
|
|
1466
|
+
resultQuantity.unit = { name: normalizedFirstQuantity.unit.name };
|
|
1467
|
+
}
|
|
1468
|
+
return resultQuantity;
|
|
1469
|
+
} else {
|
|
1470
|
+
let nextProtected;
|
|
1471
|
+
const equivalentListTemp = [...equivalentList];
|
|
1472
|
+
while (nextProtected !== -1) {
|
|
1473
|
+
nextProtected = equivalentListTemp.findIndex(
|
|
1474
|
+
(eq) => eq.unit?.integerProtected
|
|
1475
|
+
);
|
|
1476
|
+
if (nextProtected !== -1) {
|
|
1477
|
+
const unitRatio2 = getUnitRatio(
|
|
1478
|
+
equivalentListTemp[nextProtected],
|
|
1479
|
+
firstQuantityInList
|
|
1480
|
+
);
|
|
1481
|
+
const nextProtectedQuantityValue = multiplyQuantityValue(
|
|
1482
|
+
firstQuantity.quantity,
|
|
1483
|
+
unitRatio2
|
|
1484
|
+
);
|
|
1485
|
+
if (isValueIntegerLike(nextProtectedQuantityValue)) {
|
|
1486
|
+
const nextProtectedQuantity = {
|
|
1487
|
+
quantity: nextProtectedQuantityValue
|
|
1488
|
+
};
|
|
1489
|
+
if (!isNoUnit(equivalentListTemp[nextProtected].unit)) {
|
|
1490
|
+
nextProtectedQuantity.unit = {
|
|
1491
|
+
name: equivalentListTemp[nextProtected].unit.name
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
return nextProtectedQuantity;
|
|
1495
|
+
} else {
|
|
1496
|
+
equivalentListTemp.splice(nextProtected, 1);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const firstNonIntegerProtected = equivalentListTemp.filter(
|
|
1501
|
+
(q) => !q.unit.integerProtected
|
|
1502
|
+
)[0];
|
|
1503
|
+
const unitRatio = getUnitRatio(
|
|
1504
|
+
firstNonIntegerProtected,
|
|
1505
|
+
firstQuantityInList
|
|
1506
|
+
).times(getBaseUnitRatio(normalizedFirstQuantity, firstQuantityInList));
|
|
1507
|
+
const firstEqQuantity = {
|
|
1508
|
+
quantity: firstNonIntegerProtected.unit.name === firstQuantity.unit.name ? firstQuantity.quantity : multiplyQuantityValue(firstQuantity.quantity, unitRatio)
|
|
1509
|
+
};
|
|
1510
|
+
if (!isNoUnit(firstNonIntegerProtected.unit)) {
|
|
1511
|
+
firstEqQuantity.unit = { name: firstNonIntegerProtected.unit.name };
|
|
1512
|
+
}
|
|
1513
|
+
return firstEqQuantity;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return quantities.map((q) => {
|
|
1517
|
+
if (isQuantity(q)) return reduceToQuantity(q);
|
|
1518
|
+
const qListModified = sortUnitList(
|
|
1519
|
+
q.entries.map((qq) => ({
|
|
1520
|
+
...qq,
|
|
1521
|
+
unit: resolveUnit(qq.unit?.name, qq.unit?.integerProtected)
|
|
1522
|
+
}))
|
|
1523
|
+
);
|
|
1524
|
+
return reduceToQuantity(qListModified[0]);
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function addQuantitiesOrGroups(...quantities) {
|
|
1528
|
+
if (quantities.length === 0)
|
|
1529
|
+
return {
|
|
1530
|
+
sum: {
|
|
1531
|
+
quantity: getDefaultQuantityValue(),
|
|
1532
|
+
unit: resolveUnit()
|
|
1533
|
+
},
|
|
1534
|
+
unitsLists: []
|
|
1535
|
+
};
|
|
1536
|
+
if (quantities.length === 1) {
|
|
1537
|
+
if (isQuantity(quantities[0]))
|
|
1538
|
+
return {
|
|
1539
|
+
sum: {
|
|
1540
|
+
...quantities[0],
|
|
1541
|
+
unit: resolveUnit(quantities[0].unit?.name)
|
|
1542
|
+
},
|
|
1543
|
+
unitsLists: []
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
const unitsLists = getEquivalentUnitsLists(...quantities);
|
|
1547
|
+
const reducedQuantities = reduceOrsToFirstEquivalent(unitsLists, quantities);
|
|
1548
|
+
const sum = [];
|
|
1549
|
+
for (const nextQ of reducedQuantities) {
|
|
1550
|
+
const existingQ = findCompatibleQuantityWithinList(sum, nextQ);
|
|
1551
|
+
if (existingQ === void 0) {
|
|
1552
|
+
sum.push({
|
|
1553
|
+
...nextQ,
|
|
1554
|
+
unit: resolveUnit(nextQ.unit?.name)
|
|
1555
|
+
});
|
|
1556
|
+
} else {
|
|
1557
|
+
const sumQ = addQuantities(existingQ, nextQ);
|
|
1558
|
+
existingQ.quantity = sumQ.quantity;
|
|
1559
|
+
existingQ.unit = resolveUnit(sumQ.unit?.name);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (sum.length === 1) {
|
|
1563
|
+
return { sum: sum[0], unitsLists };
|
|
1564
|
+
}
|
|
1565
|
+
return { sum: { type: "and", entries: sum }, unitsLists };
|
|
1566
|
+
}
|
|
1567
|
+
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
1568
|
+
const sumQuantities = isGroup(sum) ? sum.entries : [sum];
|
|
1569
|
+
const result = [];
|
|
1570
|
+
const processedQuantities = /* @__PURE__ */ new Set();
|
|
1571
|
+
for (const list of unitsLists) {
|
|
1572
|
+
const listCopy = deepClone(list);
|
|
1573
|
+
const main = [];
|
|
1574
|
+
const mainCandidates = sumQuantities.filter(
|
|
1575
|
+
(q) => !processedQuantities.has(q)
|
|
1576
|
+
);
|
|
1577
|
+
if (mainCandidates.length === 0) continue;
|
|
1578
|
+
mainCandidates.forEach((q) => {
|
|
1579
|
+
const mainInList = findCompatibleQuantityWithinList(listCopy, q);
|
|
1580
|
+
if (mainInList !== void 0) {
|
|
1581
|
+
processedQuantities.add(q);
|
|
1582
|
+
main.push(q);
|
|
1583
|
+
listCopy.splice(listCopy.indexOf(mainInList), 1);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
const equivalents = sortUnitList(listCopy).map((equiv) => {
|
|
1587
|
+
const initialValue = {
|
|
1588
|
+
quantity: getDefaultQuantityValue()
|
|
1589
|
+
};
|
|
1590
|
+
if (equiv.unit) {
|
|
1591
|
+
initialValue.unit = { name: equiv.unit.name };
|
|
1592
|
+
}
|
|
1593
|
+
return main.reduce((acc, v) => {
|
|
1594
|
+
const mainInList = findCompatibleQuantityWithinList(list, v);
|
|
1595
|
+
const newValue = {
|
|
1596
|
+
quantity: multiplyQuantityValue(
|
|
1597
|
+
v.quantity,
|
|
1598
|
+
(0, import_big3.default)(getAverageValue(equiv.quantity)).div(
|
|
1599
|
+
getAverageValue(mainInList.quantity)
|
|
1600
|
+
)
|
|
1601
|
+
)
|
|
1602
|
+
};
|
|
1603
|
+
if (equiv.unit && !isNoUnit(equiv.unit)) {
|
|
1604
|
+
newValue.unit = { name: equiv.unit.name };
|
|
1605
|
+
}
|
|
1606
|
+
return addQuantities(acc, newValue);
|
|
1607
|
+
}, initialValue);
|
|
1608
|
+
});
|
|
1609
|
+
if (main.length + equivalents.length > 1) {
|
|
1610
|
+
const resultMain = main.length > 1 ? {
|
|
1611
|
+
type: "and",
|
|
1612
|
+
entries: main.map(deNormalizeQuantity)
|
|
1613
|
+
} : deNormalizeQuantity(main[0]);
|
|
1614
|
+
result.push({
|
|
1615
|
+
type: "or",
|
|
1616
|
+
entries: [resultMain, ...equivalents]
|
|
1617
|
+
});
|
|
1618
|
+
} else {
|
|
1619
|
+
result.push(deNormalizeQuantity(main[0]));
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
|
|
1623
|
+
return result;
|
|
1624
|
+
}
|
|
1625
|
+
function addEquivalentsAndSimplify(...quantities) {
|
|
1626
|
+
if (quantities.length === 1) {
|
|
1627
|
+
return toPlainUnit(quantities[0]);
|
|
1628
|
+
}
|
|
1629
|
+
const { sum, unitsLists } = addQuantitiesOrGroups(...quantities);
|
|
1630
|
+
const regrouped = regroupQuantitiesAndExpandEquivalents(sum, unitsLists);
|
|
1631
|
+
if (regrouped.length === 1) {
|
|
1632
|
+
return toPlainUnit(regrouped[0]);
|
|
1633
|
+
} else {
|
|
1634
|
+
return { type: "and", entries: regrouped.map(toPlainUnit) };
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/classes/recipe.ts
|
|
1639
|
+
var import_big4 = __toESM(require("big.js"), 1);
|
|
1640
|
+
var _Recipe = class _Recipe {
|
|
1641
|
+
/**
|
|
1642
|
+
* Creates a new Recipe instance.
|
|
1643
|
+
* @param content - The recipe content to parse.
|
|
1644
|
+
*/
|
|
1645
|
+
constructor(content) {
|
|
1646
|
+
/**
|
|
1647
|
+
* The parsed recipe metadata.
|
|
1648
|
+
*/
|
|
1649
|
+
__publicField(this, "metadata", {});
|
|
1650
|
+
/**
|
|
1651
|
+
* The default or manual choice of alternative ingredients.
|
|
1652
|
+
* Contains the full context including alternatives list and active selection index.
|
|
1653
|
+
*/
|
|
1654
|
+
__publicField(this, "choices", {
|
|
1655
|
+
ingredientItems: /* @__PURE__ */ new Map(),
|
|
1656
|
+
ingredientGroups: /* @__PURE__ */ new Map()
|
|
1657
|
+
});
|
|
1658
|
+
/**
|
|
1659
|
+
* The parsed recipe ingredients.
|
|
1660
|
+
*/
|
|
1661
|
+
__publicField(this, "ingredients", []);
|
|
1662
|
+
/**
|
|
1663
|
+
* The parsed recipe sections.
|
|
1664
|
+
*/
|
|
1665
|
+
__publicField(this, "sections", []);
|
|
1666
|
+
/**
|
|
1667
|
+
* The parsed recipe cookware.
|
|
1668
|
+
*/
|
|
1669
|
+
__publicField(this, "cookware", []);
|
|
1670
|
+
/**
|
|
1671
|
+
* The parsed recipe timers.
|
|
1672
|
+
*/
|
|
1673
|
+
__publicField(this, "timers", []);
|
|
1674
|
+
/**
|
|
1675
|
+
* The parsed recipe servings. Used for scaling. Parsed from one of
|
|
1676
|
+
* {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
|
|
1677
|
+
* metadata fields.
|
|
1678
|
+
*
|
|
1679
|
+
* @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
|
|
1680
|
+
*/
|
|
1681
|
+
__publicField(this, "servings");
|
|
1682
|
+
_Recipe.itemCounts.set(this, 0);
|
|
1683
|
+
if (content) {
|
|
1684
|
+
this.parse(content);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Gets the current item count for this recipe.
|
|
1689
|
+
*/
|
|
1690
|
+
getItemCount() {
|
|
1691
|
+
return _Recipe.itemCounts.get(this);
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Gets the current item count and increments it.
|
|
1695
|
+
*/
|
|
1696
|
+
getAndIncrementItemCount() {
|
|
1697
|
+
const current = this.getItemCount();
|
|
1698
|
+
_Recipe.itemCounts.set(this, current + 1);
|
|
1699
|
+
return current;
|
|
1700
|
+
}
|
|
1701
|
+
_parseQuantityRecursive(quantityRaw) {
|
|
1702
|
+
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
1703
|
+
const quantities = [];
|
|
1704
|
+
while (quantityMatch?.groups) {
|
|
1705
|
+
const value = quantityMatch.groups.ingredientQuantityValue ? parseQuantityInput(quantityMatch.groups.ingredientQuantityValue) : void 0;
|
|
1706
|
+
const unit = quantityMatch.groups.ingredientUnit;
|
|
1707
|
+
if (value) {
|
|
1708
|
+
const newQuantity = { quantity: value };
|
|
1709
|
+
if (unit) {
|
|
1710
|
+
if (unit.startsWith("=")) {
|
|
1711
|
+
newQuantity.unit = {
|
|
1712
|
+
name: unit.substring(1),
|
|
1713
|
+
integerProtected: true
|
|
1714
|
+
};
|
|
1715
|
+
} else {
|
|
1716
|
+
newQuantity.unit = { name: unit };
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
quantities.push(newQuantity);
|
|
1720
|
+
} else {
|
|
1721
|
+
throw new InvalidQuantityFormat(quantityRaw);
|
|
1722
|
+
}
|
|
1723
|
+
quantityMatch = quantityMatch.groups.ingredientAltQuantity ? quantityMatch.groups.ingredientAltQuantity.match(
|
|
1724
|
+
quantityAlternativeRegex
|
|
1725
|
+
) : null;
|
|
1726
|
+
}
|
|
1727
|
+
return quantities;
|
|
1728
|
+
}
|
|
1729
|
+
_parseIngredientWithAlternativeRecursive(ingredientMatchString, items) {
|
|
1730
|
+
const alternatives = [];
|
|
1731
|
+
let testString = ingredientMatchString;
|
|
1732
|
+
while (true) {
|
|
1733
|
+
const match = testString.match(
|
|
1734
|
+
alternatives.length > 0 ? inlineIngredientAlternativesRegex : ingredientWithAlternativeRegex
|
|
1735
|
+
);
|
|
1736
|
+
if (!match?.groups) break;
|
|
1737
|
+
const groups = match.groups;
|
|
1738
|
+
let name = groups.mIngredientName || groups.sIngredientName;
|
|
1739
|
+
const preparation = groups.ingredientPreparation;
|
|
1740
|
+
const modifiers = groups.ingredientModifiers;
|
|
1741
|
+
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1742
|
+
const flags = [];
|
|
1743
|
+
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
1744
|
+
flags.push("optional");
|
|
1745
|
+
}
|
|
1746
|
+
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1747
|
+
flags.push("hidden");
|
|
1748
|
+
}
|
|
1749
|
+
if (modifiers !== void 0 && modifiers.includes("@") || groups.ingredientRecipeAnchor) {
|
|
1750
|
+
flags.push("recipe");
|
|
1751
|
+
}
|
|
1752
|
+
let extras = void 0;
|
|
1753
|
+
if (flags.includes("recipe")) {
|
|
1754
|
+
extras = { path: `${name}.cook` };
|
|
1755
|
+
name = name.substring(name.lastIndexOf("/") + 1);
|
|
1756
|
+
}
|
|
1757
|
+
const aliasMatch = name.match(ingredientAliasRegex);
|
|
1758
|
+
let listName, displayName;
|
|
1759
|
+
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
1760
|
+
listName = aliasMatch.groups.ingredientListName.trim();
|
|
1761
|
+
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
1762
|
+
} else {
|
|
1763
|
+
listName = name;
|
|
1764
|
+
displayName = name;
|
|
1765
|
+
}
|
|
1766
|
+
const newIngredient = {
|
|
1767
|
+
name: listName
|
|
1768
|
+
};
|
|
1769
|
+
if (preparation) {
|
|
1770
|
+
newIngredient.preparation = preparation;
|
|
1771
|
+
}
|
|
1772
|
+
if (flags.length > 0) {
|
|
1773
|
+
newIngredient.flags = flags;
|
|
1774
|
+
}
|
|
1775
|
+
if (extras) {
|
|
1776
|
+
newIngredient.extras = extras;
|
|
1777
|
+
}
|
|
1778
|
+
const idxInList = findAndUpsertIngredient(
|
|
1779
|
+
this.ingredients,
|
|
1780
|
+
newIngredient,
|
|
1781
|
+
reference
|
|
1782
|
+
);
|
|
1783
|
+
let itemQuantity = void 0;
|
|
1784
|
+
if (groups.ingredientQuantity) {
|
|
1785
|
+
const parsedQuantities = this._parseQuantityRecursive(
|
|
1786
|
+
groups.ingredientQuantity
|
|
1787
|
+
);
|
|
1788
|
+
const [primary, ...rest] = parsedQuantities;
|
|
1789
|
+
if (primary) {
|
|
1790
|
+
itemQuantity = {
|
|
1791
|
+
...primary,
|
|
1792
|
+
scalable: groups.ingredientQuantityModifier !== "="
|
|
1793
|
+
};
|
|
1794
|
+
if (rest.length > 0) {
|
|
1795
|
+
itemQuantity.equivalents = rest;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const alternative = {
|
|
1800
|
+
index: idxInList,
|
|
1801
|
+
displayName
|
|
1802
|
+
};
|
|
1803
|
+
const note = groups.ingredientNote?.trim();
|
|
1804
|
+
if (note) {
|
|
1805
|
+
alternative.note = note;
|
|
1806
|
+
}
|
|
1807
|
+
if (itemQuantity) {
|
|
1808
|
+
alternative.itemQuantity = itemQuantity;
|
|
1809
|
+
}
|
|
1810
|
+
alternatives.push(alternative);
|
|
1811
|
+
testString = groups.ingredientAlternative || "";
|
|
1812
|
+
}
|
|
1813
|
+
if (alternatives.length > 1) {
|
|
1814
|
+
const alternativesIndexes = alternatives.map((alt) => alt.index);
|
|
1815
|
+
for (const ingredientIndex of alternativesIndexes) {
|
|
1816
|
+
const ingredient = this.ingredients[ingredientIndex];
|
|
1817
|
+
if (ingredient) {
|
|
1818
|
+
if (!ingredient.alternatives) {
|
|
1819
|
+
ingredient.alternatives = new Set(
|
|
1820
|
+
alternativesIndexes.filter((index) => index !== ingredientIndex)
|
|
1821
|
+
);
|
|
1822
|
+
} else {
|
|
1823
|
+
ingredient.alternatives = unionOfSets(
|
|
1824
|
+
ingredient.alternatives,
|
|
1825
|
+
new Set(
|
|
1826
|
+
alternativesIndexes.filter(
|
|
1827
|
+
(index) => index !== ingredientIndex
|
|
1828
|
+
)
|
|
1829
|
+
)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
|
|
1836
|
+
const newItem = {
|
|
1837
|
+
type: "ingredient",
|
|
1838
|
+
id,
|
|
1839
|
+
alternatives
|
|
1840
|
+
};
|
|
1841
|
+
items.push(newItem);
|
|
1842
|
+
if (alternatives.length > 1) {
|
|
1843
|
+
this.choices.ingredientItems.set(id, alternatives);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
_parseIngredientWithGroupKey(ingredientMatchString, items) {
|
|
1847
|
+
const match = ingredientMatchString.match(ingredientWithGroupKeyRegex);
|
|
1848
|
+
if (!match?.groups) return;
|
|
1849
|
+
const groups = match.groups;
|
|
1850
|
+
const groupKey = groups.gIngredientGroupKey;
|
|
1851
|
+
let name = groups.gmIngredientName || groups.gsIngredientName;
|
|
1852
|
+
const preparation = groups.gIngredientPreparation;
|
|
1853
|
+
const modifiers = groups.gIngredientModifiers;
|
|
1854
|
+
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1855
|
+
const flags = [];
|
|
1856
|
+
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
1857
|
+
flags.push("optional");
|
|
1858
|
+
}
|
|
1859
|
+
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1860
|
+
flags.push("hidden");
|
|
1861
|
+
}
|
|
1862
|
+
if (modifiers !== void 0 && modifiers.includes("@") || groups.gIngredientRecipeAnchor) {
|
|
1863
|
+
flags.push("recipe");
|
|
1864
|
+
}
|
|
1865
|
+
let extras = void 0;
|
|
1866
|
+
if (flags.includes("recipe")) {
|
|
1867
|
+
extras = { path: `${name}.cook` };
|
|
1868
|
+
name = name.substring(name.lastIndexOf("/") + 1);
|
|
1869
|
+
}
|
|
1870
|
+
const aliasMatch = name.match(ingredientAliasRegex);
|
|
1871
|
+
let listName, displayName;
|
|
1872
|
+
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
1873
|
+
listName = aliasMatch.groups.ingredientListName.trim();
|
|
1874
|
+
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
1875
|
+
} else {
|
|
1876
|
+
listName = name;
|
|
1877
|
+
displayName = name;
|
|
1878
|
+
}
|
|
1879
|
+
const newIngredient = {
|
|
1880
|
+
name: listName
|
|
1881
|
+
};
|
|
1882
|
+
if (preparation) {
|
|
1883
|
+
newIngredient.preparation = preparation;
|
|
1884
|
+
}
|
|
1885
|
+
if (flags.length > 0) {
|
|
1886
|
+
newIngredient.flags = flags;
|
|
1887
|
+
}
|
|
1888
|
+
if (extras) {
|
|
1889
|
+
newIngredient.extras = extras;
|
|
1890
|
+
}
|
|
1891
|
+
const idxInList = findAndUpsertIngredient(
|
|
1892
|
+
this.ingredients,
|
|
1893
|
+
newIngredient,
|
|
1894
|
+
reference
|
|
1895
|
+
);
|
|
1896
|
+
let itemQuantity = void 0;
|
|
1897
|
+
if (groups.gIngredientQuantity) {
|
|
1898
|
+
const parsedQuantities = this._parseQuantityRecursive(
|
|
1899
|
+
groups.gIngredientQuantity
|
|
1900
|
+
);
|
|
1901
|
+
const [primary, ...rest] = parsedQuantities;
|
|
1902
|
+
itemQuantity = {
|
|
1903
|
+
...primary,
|
|
1904
|
+
// there's necessarily a primary quantity as the match group was detected
|
|
1905
|
+
scalable: groups.gIngredientQuantityModifier !== "="
|
|
1906
|
+
};
|
|
1907
|
+
if (rest.length > 0) {
|
|
1908
|
+
itemQuantity.equivalents = rest;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const alternative = {
|
|
1912
|
+
index: idxInList,
|
|
1913
|
+
displayName
|
|
1914
|
+
};
|
|
1915
|
+
if (itemQuantity) {
|
|
1916
|
+
alternative.itemQuantity = itemQuantity;
|
|
1917
|
+
}
|
|
1918
|
+
const existingAlternatives = this.choices.ingredientGroups.get(groupKey);
|
|
1919
|
+
function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
|
|
1920
|
+
const ingredient = ingredients[ingredientIdx];
|
|
1921
|
+
if (ingredient) {
|
|
1922
|
+
if (ingredient.alternatives === void 0) {
|
|
1923
|
+
ingredient.alternatives = /* @__PURE__ */ new Set([newAlternativeIdx]);
|
|
1924
|
+
} else {
|
|
1925
|
+
ingredient.alternatives.add(newAlternativeIdx);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (existingAlternatives) {
|
|
1930
|
+
for (const alt of existingAlternatives) {
|
|
1931
|
+
upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
|
|
1932
|
+
upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
|
|
1936
|
+
const newItem = {
|
|
1937
|
+
type: "ingredient",
|
|
1938
|
+
id,
|
|
1939
|
+
group: groupKey,
|
|
1940
|
+
alternatives: [alternative]
|
|
1941
|
+
};
|
|
1942
|
+
items.push(newItem);
|
|
1943
|
+
const choiceAlternative = deepClone(alternative);
|
|
1944
|
+
choiceAlternative.itemId = id;
|
|
1945
|
+
const existingChoice = this.choices.ingredientGroups.get(groupKey);
|
|
1946
|
+
if (!existingChoice) {
|
|
1947
|
+
this.choices.ingredientGroups.set(groupKey, [choiceAlternative]);
|
|
1948
|
+
} else {
|
|
1949
|
+
existingChoice.push(choiceAlternative);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Populates the `quantities` property for each ingredient based on
|
|
1954
|
+
* how they appear in the recipe preparation. Only primary ingredients
|
|
1955
|
+
* get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
|
|
1956
|
+
*
|
|
1957
|
+
* For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
|
|
1958
|
+
* For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
|
|
1959
|
+
*
|
|
1960
|
+
* Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
|
|
1961
|
+
* @internal
|
|
1962
|
+
*/
|
|
1963
|
+
_populate_ingredient_quantities() {
|
|
1964
|
+
this.ingredients = this.ingredients.map((ing) => {
|
|
1965
|
+
if (ing.quantities) {
|
|
1966
|
+
delete ing.quantities;
|
|
1967
|
+
}
|
|
1968
|
+
if (ing.usedAsPrimary) {
|
|
1969
|
+
delete ing.usedAsPrimary;
|
|
1970
|
+
}
|
|
1971
|
+
return ing;
|
|
1972
|
+
});
|
|
1973
|
+
const seenGroups = /* @__PURE__ */ new Set();
|
|
1974
|
+
const ingredientGroups = /* @__PURE__ */ new Map();
|
|
1975
|
+
for (const section of this.sections) {
|
|
1976
|
+
for (const step of section.content.filter(
|
|
1977
|
+
(item) => item.type === "step"
|
|
1978
|
+
)) {
|
|
1979
|
+
for (const item of step.items.filter(
|
|
1980
|
+
(item2) => item2.type === "ingredient"
|
|
1981
|
+
)) {
|
|
1982
|
+
const isGroupedItem = "group" in item && item.group !== void 0;
|
|
1983
|
+
const isFirstInGroup = isGroupedItem && !seenGroups.has(item.group);
|
|
1984
|
+
if (isGroupedItem) {
|
|
1985
|
+
seenGroups.add(item.group);
|
|
1986
|
+
}
|
|
1987
|
+
const isPrimary = !isGroupedItem || isFirstInGroup;
|
|
1988
|
+
const alternative = item.alternatives[0];
|
|
1989
|
+
if (isPrimary) {
|
|
1990
|
+
const primaryIngredient = this.ingredients[alternative.index];
|
|
1991
|
+
if (primaryIngredient) {
|
|
1992
|
+
primaryIngredient.usedAsPrimary = true;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (!isPrimary || !alternative.itemQuantity) continue;
|
|
1996
|
+
const allQuantities = [
|
|
1997
|
+
{
|
|
1998
|
+
quantity: alternative.itemQuantity.quantity,
|
|
1999
|
+
unit: alternative.itemQuantity.unit
|
|
2000
|
+
}
|
|
2001
|
+
];
|
|
2002
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2003
|
+
allQuantities.push(...alternative.itemQuantity.equivalents);
|
|
2004
|
+
}
|
|
2005
|
+
const quantityEntry = allQuantities.length === 1 ? allQuantities[0] : { type: "or", entries: allQuantities };
|
|
2006
|
+
const hasInlineAlternatives = item.alternatives.length > 1;
|
|
2007
|
+
const hasGroupedAlternatives = isGroupedItem && this.choices.ingredientGroups.has(item.group);
|
|
2008
|
+
let alternativeRefs;
|
|
2009
|
+
if (hasInlineAlternatives) {
|
|
2010
|
+
alternativeRefs = [];
|
|
2011
|
+
for (let j = 1; j < item.alternatives.length; j++) {
|
|
2012
|
+
const otherAlt = item.alternatives[j];
|
|
2013
|
+
const newRef = {
|
|
2014
|
+
index: otherAlt.index
|
|
2015
|
+
};
|
|
2016
|
+
if (otherAlt.itemQuantity) {
|
|
2017
|
+
const altQty = {
|
|
2018
|
+
quantity: otherAlt.itemQuantity.quantity
|
|
2019
|
+
};
|
|
2020
|
+
if (otherAlt.itemQuantity.unit) {
|
|
2021
|
+
altQty.unit = otherAlt.itemQuantity.unit.name;
|
|
2022
|
+
}
|
|
2023
|
+
if (otherAlt.itemQuantity.equivalents) {
|
|
2024
|
+
altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
|
|
2025
|
+
(eq) => toPlainUnit(eq)
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
newRef.alternativeQuantities = [altQty];
|
|
2029
|
+
}
|
|
2030
|
+
alternativeRefs.push(newRef);
|
|
2031
|
+
}
|
|
2032
|
+
} else if (hasGroupedAlternatives) {
|
|
2033
|
+
const groupAlternatives = this.choices.ingredientGroups.get(
|
|
2034
|
+
item.group
|
|
2035
|
+
);
|
|
2036
|
+
alternativeRefs = [];
|
|
2037
|
+
for (let j = 1; j < groupAlternatives.length; j++) {
|
|
2038
|
+
const otherAlt = groupAlternatives[j];
|
|
2039
|
+
if (otherAlt.itemQuantity) {
|
|
2040
|
+
const altQty = {
|
|
2041
|
+
quantity: otherAlt.itemQuantity.quantity
|
|
2042
|
+
};
|
|
2043
|
+
if (otherAlt.itemQuantity.unit) {
|
|
2044
|
+
altQty.unit = otherAlt.itemQuantity.unit.name;
|
|
2045
|
+
}
|
|
2046
|
+
if (otherAlt.itemQuantity.equivalents) {
|
|
2047
|
+
altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
|
|
2048
|
+
(eq) => toPlainUnit(eq)
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
alternativeRefs.push({
|
|
2052
|
+
index: otherAlt.index,
|
|
2053
|
+
alternativeQuantities: [altQty]
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (alternativeRefs.length === 0) {
|
|
2058
|
+
alternativeRefs = void 0;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (!ingredientGroups.has(alternative.index)) {
|
|
2062
|
+
ingredientGroups.set(alternative.index, /* @__PURE__ */ new Map());
|
|
2063
|
+
}
|
|
2064
|
+
const groupsForIngredient = ingredientGroups.get(alternative.index);
|
|
2065
|
+
const baseSignature = getAlternativeSignature(alternativeRefs);
|
|
2066
|
+
const signature = isGroupedItem ? `group:${item.group}|${baseSignature ?? ""}` : baseSignature;
|
|
2067
|
+
if (!groupsForIngredient.has(signature)) {
|
|
2068
|
+
groupsForIngredient.set(signature, {
|
|
2069
|
+
alternativeQuantities: /* @__PURE__ */ new Map(),
|
|
2070
|
+
quantities: []
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
const group = groupsForIngredient.get(signature);
|
|
2074
|
+
group.quantities.push(quantityEntry);
|
|
2075
|
+
if (alternativeRefs) {
|
|
2076
|
+
for (const ref of alternativeRefs) {
|
|
2077
|
+
if (!group.alternativeQuantities.has(ref.index)) {
|
|
2078
|
+
group.alternativeQuantities.set(ref.index, []);
|
|
2079
|
+
}
|
|
2080
|
+
if (ref.alternativeQuantities && ref.alternativeQuantities.length > 0) {
|
|
2081
|
+
for (const altQty of ref.alternativeQuantities) {
|
|
2082
|
+
if (altQty.equivalents && altQty.equivalents.length > 0) {
|
|
2083
|
+
const entries = [
|
|
2084
|
+
toExtendedUnit({
|
|
2085
|
+
quantity: altQty.quantity,
|
|
2086
|
+
unit: altQty.unit
|
|
2087
|
+
}),
|
|
2088
|
+
...altQty.equivalents.map((eq) => toExtendedUnit(eq))
|
|
2089
|
+
];
|
|
2090
|
+
group.alternativeQuantities.get(ref.index).push({ type: "or", entries });
|
|
2091
|
+
} else {
|
|
2092
|
+
group.alternativeQuantities.get(ref.index).push(
|
|
2093
|
+
toExtendedUnit({
|
|
2094
|
+
quantity: altQty.quantity,
|
|
2095
|
+
unit: altQty.unit
|
|
2096
|
+
})
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
for (const [index, groupsForIngredient] of ingredientGroups) {
|
|
2107
|
+
const ingredient = this.ingredients[index];
|
|
2108
|
+
const quantityGroups = [];
|
|
2109
|
+
for (const [, group] of groupsForIngredient) {
|
|
2110
|
+
const summedGroupQuantity = addEquivalentsAndSimplify(
|
|
2111
|
+
...group.quantities
|
|
2112
|
+
);
|
|
2113
|
+
const groupQuantities = flattenPlainUnitGroup(summedGroupQuantity);
|
|
2114
|
+
let alternatives;
|
|
2115
|
+
if (group.alternativeQuantities.size > 0) {
|
|
2116
|
+
alternatives = [];
|
|
2117
|
+
for (const [altIndex, altQuantities] of group.alternativeQuantities) {
|
|
2118
|
+
const ref = { index: altIndex };
|
|
2119
|
+
if (altQuantities.length > 0) {
|
|
2120
|
+
const summedAltQuantity = addEquivalentsAndSimplify(
|
|
2121
|
+
...altQuantities
|
|
2122
|
+
);
|
|
2123
|
+
const flattenedAlt = flattenPlainUnitGroup(summedAltQuantity);
|
|
2124
|
+
ref.alternativeQuantities = flattenedAlt.flatMap((item) => {
|
|
2125
|
+
if ("groupQuantity" in item) {
|
|
2126
|
+
return [item.groupQuantity];
|
|
2127
|
+
} else {
|
|
2128
|
+
return item.entries;
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
alternatives.push(ref);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
for (const gq of groupQuantities) {
|
|
2136
|
+
if ("type" in gq && gq.type === "and") {
|
|
2137
|
+
const andGroup = {
|
|
2138
|
+
type: "and",
|
|
2139
|
+
entries: gq.entries
|
|
2140
|
+
};
|
|
2141
|
+
if (gq.equivalents && gq.equivalents.length > 0) {
|
|
2142
|
+
andGroup.equivalents = gq.equivalents;
|
|
2143
|
+
}
|
|
2144
|
+
if (alternatives && alternatives.length > 0) {
|
|
2145
|
+
andGroup.alternatives = alternatives;
|
|
2146
|
+
}
|
|
2147
|
+
quantityGroups.push(andGroup);
|
|
2148
|
+
} else {
|
|
2149
|
+
const quantityGroup = gq;
|
|
2150
|
+
if (alternatives && alternatives.length > 0) {
|
|
2151
|
+
quantityGroup.alternatives = alternatives;
|
|
2152
|
+
}
|
|
2153
|
+
quantityGroups.push(quantityGroup);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (quantityGroups.length > 0) {
|
|
2158
|
+
ingredient.quantities = quantityGroups;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Calculates ingredient quantities based on the provided choices.
|
|
2164
|
+
* Returns a list of computed ingredients with their total quantities.
|
|
2165
|
+
*
|
|
2166
|
+
* @param choices - The recipe choices to apply when computing quantities.
|
|
2167
|
+
* If not provided, uses the default choices (first alternative for each item).
|
|
2168
|
+
* @returns An array of ComputedIngredient with quantityTotal calculated based on choices.
|
|
2169
|
+
*/
|
|
2170
|
+
calc_ingredient_quantities(choices) {
|
|
2171
|
+
const effectiveChoices = choices || {
|
|
2172
|
+
ingredientItems: new Map(
|
|
2173
|
+
Array.from(this.choices.ingredientItems.keys()).map((k) => [k, 0])
|
|
2174
|
+
),
|
|
2175
|
+
ingredientGroups: new Map(
|
|
2176
|
+
Array.from(this.choices.ingredientGroups.keys()).map((k) => [k, 0])
|
|
2177
|
+
)
|
|
2178
|
+
};
|
|
2179
|
+
const ingredientQuantities = /* @__PURE__ */ new Map();
|
|
2180
|
+
const selectedIngredientIndices = /* @__PURE__ */ new Set();
|
|
2181
|
+
for (const section of this.sections) {
|
|
2182
|
+
for (const step of section.content.filter(
|
|
2183
|
+
(item) => item.type === "step"
|
|
2184
|
+
)) {
|
|
2185
|
+
for (const item of step.items.filter(
|
|
2186
|
+
(item2) => item2.type === "ingredient"
|
|
2187
|
+
)) {
|
|
2188
|
+
for (let i2 = 0; i2 < item.alternatives.length; i2++) {
|
|
2189
|
+
const alternative = item.alternatives[i2];
|
|
2190
|
+
const isAlternativeChoiceItem = effectiveChoices.ingredientItems?.get(item.id) === i2;
|
|
2191
|
+
const alternativeChoiceGroupIdx = item.group ? effectiveChoices.ingredientGroups?.get(item.group) : void 0;
|
|
2192
|
+
const alternativeChoiceGroup = item.group ? this.choices.ingredientGroups.get(item.group) : void 0;
|
|
2193
|
+
const isAlternativeChoiceGroup = alternativeChoiceGroup && alternativeChoiceGroupIdx !== void 0 ? alternativeChoiceGroup[alternativeChoiceGroupIdx]?.itemId === item.id : false;
|
|
2194
|
+
const isSelected = !("group" in item) && (item.alternatives.length === 1 || isAlternativeChoiceItem) || isAlternativeChoiceGroup;
|
|
2195
|
+
if (isSelected) {
|
|
2196
|
+
selectedIngredientIndices.add(alternative.index);
|
|
2197
|
+
if (alternative.itemQuantity) {
|
|
2198
|
+
const allQuantities = [
|
|
2199
|
+
{
|
|
2200
|
+
quantity: alternative.itemQuantity.quantity,
|
|
2201
|
+
unit: alternative.itemQuantity.unit
|
|
2202
|
+
}
|
|
2203
|
+
];
|
|
2204
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2205
|
+
allQuantities.push(...alternative.itemQuantity.equivalents);
|
|
2206
|
+
}
|
|
2207
|
+
const equivalents = allQuantities.length === 1 ? allQuantities[0] : {
|
|
2208
|
+
type: "or",
|
|
2209
|
+
entries: allQuantities
|
|
2210
|
+
};
|
|
2211
|
+
ingredientQuantities.set(alternative.index, [
|
|
2212
|
+
...ingredientQuantities.get(alternative.index) || [],
|
|
2213
|
+
equivalents
|
|
2214
|
+
]);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const computedIngredients = [];
|
|
2222
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
2223
|
+
if (!selectedIngredientIndices.has(index)) continue;
|
|
2224
|
+
const ing = this.ingredients[index];
|
|
2225
|
+
const computed = {
|
|
2226
|
+
name: ing.name
|
|
2227
|
+
};
|
|
2228
|
+
if (ing.preparation) {
|
|
2229
|
+
computed.preparation = ing.preparation;
|
|
2230
|
+
}
|
|
2231
|
+
if (ing.flags) {
|
|
2232
|
+
computed.flags = ing.flags;
|
|
2233
|
+
}
|
|
2234
|
+
if (ing.extras) {
|
|
2235
|
+
computed.extras = ing.extras;
|
|
2236
|
+
}
|
|
2237
|
+
const quantities = ingredientQuantities.get(index);
|
|
2238
|
+
if (quantities && quantities.length > 0) {
|
|
2239
|
+
computed.quantityTotal = addEquivalentsAndSimplify(...quantities);
|
|
2240
|
+
}
|
|
2241
|
+
computedIngredients.push(computed);
|
|
2242
|
+
}
|
|
2243
|
+
return computedIngredients;
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Parses a recipe from a string.
|
|
2247
|
+
* @param content - The recipe content to parse.
|
|
2248
|
+
*/
|
|
2249
|
+
parse(content) {
|
|
2250
|
+
const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
|
|
2251
|
+
const { metadata, servings } = extractMetadata(content);
|
|
2252
|
+
this.metadata = metadata;
|
|
2253
|
+
this.servings = servings;
|
|
2254
|
+
let blankLineBefore = true;
|
|
2255
|
+
let section = new Section();
|
|
2256
|
+
const items = [];
|
|
2257
|
+
let note = "";
|
|
2258
|
+
let inNote = false;
|
|
2259
|
+
for (const line of cleanContent) {
|
|
2260
|
+
if (line.trim().length === 0) {
|
|
2261
|
+
flushPendingItems(section, items);
|
|
2262
|
+
note = flushPendingNote(section, note);
|
|
2263
|
+
blankLineBefore = true;
|
|
2264
|
+
inNote = false;
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
if (line.startsWith("=")) {
|
|
2268
|
+
flushPendingItems(section, items);
|
|
2269
|
+
note = flushPendingNote(section, note);
|
|
2270
|
+
if (this.sections.length === 0 && section.isBlank()) {
|
|
2271
|
+
section.name = line.replace(/^=+|=+$/g, "").trim();
|
|
976
2272
|
} else {
|
|
977
2273
|
if (!section.isBlank()) {
|
|
978
2274
|
this.sections.push(section);
|
|
@@ -1009,12 +2305,13 @@ var Recipe = class _Recipe {
|
|
|
1009
2305
|
}
|
|
1010
2306
|
const groups = match.groups;
|
|
1011
2307
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
const modifiers = groups.
|
|
2308
|
+
this._parseIngredientWithAlternativeRecursive(match[0], items);
|
|
2309
|
+
} else if (groups.gmIngredientName || groups.gsIngredientName) {
|
|
2310
|
+
this._parseIngredientWithGroupKey(match[0], items);
|
|
2311
|
+
} else if (groups.mCookwareName || groups.sCookwareName) {
|
|
2312
|
+
const name = groups.mCookwareName || groups.sCookwareName;
|
|
2313
|
+
const modifiers = groups.cookwareModifiers;
|
|
2314
|
+
const quantityRaw = groups.cookwareQuantity;
|
|
1018
2315
|
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1019
2316
|
const flags = [];
|
|
1020
2317
|
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
@@ -1023,83 +2320,29 @@ var Recipe = class _Recipe {
|
|
|
1023
2320
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1024
2321
|
flags.push("hidden");
|
|
1025
2322
|
}
|
|
1026
|
-
if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
|
|
1027
|
-
flags.push("recipe");
|
|
1028
|
-
}
|
|
1029
|
-
let extras = void 0;
|
|
1030
|
-
if (flags.includes("recipe")) {
|
|
1031
|
-
extras = { path: `${name}.cook` };
|
|
1032
|
-
name = name.substring(name.lastIndexOf("/") + 1);
|
|
1033
|
-
}
|
|
1034
2323
|
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
1038
|
-
listName = aliasMatch.groups.ingredientListName.trim();
|
|
1039
|
-
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
1040
|
-
} else {
|
|
1041
|
-
listName = name;
|
|
1042
|
-
displayName = name;
|
|
1043
|
-
}
|
|
1044
|
-
const newIngredient = {
|
|
1045
|
-
name: listName,
|
|
1046
|
-
quantity,
|
|
1047
|
-
quantityParts: quantity ? [
|
|
1048
|
-
{
|
|
1049
|
-
value: quantity,
|
|
1050
|
-
unit,
|
|
1051
|
-
scalable: scalableQuantity
|
|
1052
|
-
}
|
|
1053
|
-
] : void 0,
|
|
1054
|
-
unit,
|
|
1055
|
-
preparation,
|
|
1056
|
-
flags
|
|
2324
|
+
const newCookware = {
|
|
2325
|
+
name
|
|
1057
2326
|
};
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
2327
|
+
if (quantity) {
|
|
2328
|
+
newCookware.quantity = quantity;
|
|
2329
|
+
}
|
|
2330
|
+
if (flags.length > 0) {
|
|
2331
|
+
newCookware.flags = flags;
|
|
1060
2332
|
}
|
|
1061
|
-
const
|
|
1062
|
-
this.
|
|
1063
|
-
|
|
2333
|
+
const idxInList = findAndUpsertCookware(
|
|
2334
|
+
this.cookware,
|
|
2335
|
+
newCookware,
|
|
1064
2336
|
reference
|
|
1065
2337
|
);
|
|
1066
2338
|
const newItem = {
|
|
1067
|
-
type: "
|
|
1068
|
-
index:
|
|
1069
|
-
displayName
|
|
2339
|
+
type: "cookware",
|
|
2340
|
+
index: idxInList
|
|
1070
2341
|
};
|
|
1071
|
-
if (
|
|
1072
|
-
newItem.
|
|
2342
|
+
if (quantity) {
|
|
2343
|
+
newItem.quantity = quantity;
|
|
1073
2344
|
}
|
|
1074
2345
|
items.push(newItem);
|
|
1075
|
-
} else if (groups.mCookwareName || groups.sCookwareName) {
|
|
1076
|
-
const name = groups.mCookwareName || groups.sCookwareName;
|
|
1077
|
-
const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
|
|
1078
|
-
const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
|
|
1079
|
-
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1080
|
-
const flags = [];
|
|
1081
|
-
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
1082
|
-
flags.push("optional");
|
|
1083
|
-
}
|
|
1084
|
-
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1085
|
-
flags.push("hidden");
|
|
1086
|
-
}
|
|
1087
|
-
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
1088
|
-
const idxsInList = findAndUpsertCookware(
|
|
1089
|
-
this.cookware,
|
|
1090
|
-
{
|
|
1091
|
-
name,
|
|
1092
|
-
quantity,
|
|
1093
|
-
quantityParts: quantity ? [quantity] : void 0,
|
|
1094
|
-
flags
|
|
1095
|
-
},
|
|
1096
|
-
reference
|
|
1097
|
-
);
|
|
1098
|
-
items.push({
|
|
1099
|
-
type: "cookware",
|
|
1100
|
-
index: idxsInList.cookwareIndex,
|
|
1101
|
-
quantityPartIndex: idxsInList.quantityPartIndex
|
|
1102
|
-
});
|
|
1103
2346
|
} else {
|
|
1104
2347
|
const durationStr = groups.timerQuantity.trim();
|
|
1105
2348
|
const unit = (groups.timerUnit || "").trim();
|
|
@@ -1127,6 +2370,7 @@ var Recipe = class _Recipe {
|
|
|
1127
2370
|
if (!section.isBlank()) {
|
|
1128
2371
|
this.sections.push(section);
|
|
1129
2372
|
}
|
|
2373
|
+
this._populate_ingredient_quantities();
|
|
1130
2374
|
}
|
|
1131
2375
|
/**
|
|
1132
2376
|
* Scales the recipe to a new number of servings. In practice, it calls
|
|
@@ -1141,7 +2385,7 @@ var Recipe = class _Recipe {
|
|
|
1141
2385
|
if (originalServings === void 0 || originalServings === 0) {
|
|
1142
2386
|
throw new Error("Error scaling recipe: no initial servings value set");
|
|
1143
2387
|
}
|
|
1144
|
-
const factor = (0,
|
|
2388
|
+
const factor = (0, import_big4.default)(newServings).div(originalServings);
|
|
1145
2389
|
return this.scaleBy(factor);
|
|
1146
2390
|
}
|
|
1147
2391
|
/**
|
|
@@ -1156,44 +2400,62 @@ var Recipe = class _Recipe {
|
|
|
1156
2400
|
if (originalServings === void 0 || originalServings === 0) {
|
|
1157
2401
|
throw new Error("Error scaling recipe: no initial servings value set");
|
|
1158
2402
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
)
|
|
1172
|
-
|
|
2403
|
+
function scaleAlternativesBy(alternatives, factor2) {
|
|
2404
|
+
for (const alternative of alternatives) {
|
|
2405
|
+
if (alternative.itemQuantity) {
|
|
2406
|
+
const scaleFactor = alternative.itemQuantity.scalable ? (0, import_big4.default)(factor2) : 1;
|
|
2407
|
+
if (alternative.itemQuantity.quantity.type !== "fixed" || alternative.itemQuantity.quantity.value.type !== "text") {
|
|
2408
|
+
alternative.itemQuantity.quantity = multiplyQuantityValue(
|
|
2409
|
+
alternative.itemQuantity.quantity,
|
|
2410
|
+
scaleFactor
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2414
|
+
alternative.itemQuantity.equivalents = alternative.itemQuantity.equivalents.map(
|
|
2415
|
+
(altQuantity) => {
|
|
2416
|
+
if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
|
|
2417
|
+
return altQuantity;
|
|
2418
|
+
} else {
|
|
2419
|
+
return {
|
|
2420
|
+
...altQuantity,
|
|
2421
|
+
quantity: multiplyQuantityValue(
|
|
2422
|
+
altQuantity.quantity,
|
|
2423
|
+
scaleFactor
|
|
2424
|
+
)
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
);
|
|
1173
2429
|
}
|
|
1174
|
-
);
|
|
1175
|
-
if (ingredient.quantityParts.length === 1) {
|
|
1176
|
-
ingredient.quantity = ingredient.quantityParts[0].value;
|
|
1177
|
-
ingredient.unit = ingredient.quantityParts[0].unit;
|
|
1178
|
-
} else {
|
|
1179
|
-
const totalQuantity = ingredient.quantityParts.reduce(
|
|
1180
|
-
(acc, val) => addQuantities(acc, { value: val.value, unit: val.unit }),
|
|
1181
|
-
{ value: getDefaultQuantityValue() }
|
|
1182
|
-
);
|
|
1183
|
-
ingredient.quantity = totalQuantity.value;
|
|
1184
|
-
ingredient.unit = totalQuantity.unit;
|
|
1185
2430
|
}
|
|
1186
2431
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
2432
|
+
}
|
|
2433
|
+
for (const section of newRecipe.sections) {
|
|
2434
|
+
for (const step of section.content.filter(
|
|
2435
|
+
(item) => item.type === "step"
|
|
2436
|
+
)) {
|
|
2437
|
+
for (const item of step.items.filter(
|
|
2438
|
+
(item2) => item2.type === "ingredient"
|
|
2439
|
+
)) {
|
|
2440
|
+
scaleAlternativesBy(item.alternatives, factor);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
|
|
2445
|
+
scaleAlternativesBy(alternatives, factor);
|
|
2446
|
+
}
|
|
2447
|
+
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
2448
|
+
scaleAlternativesBy(alternatives, factor);
|
|
2449
|
+
}
|
|
2450
|
+
newRecipe._populate_ingredient_quantities();
|
|
2451
|
+
newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
|
|
1190
2452
|
if (newRecipe.metadata.servings && this.metadata.servings) {
|
|
1191
2453
|
if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
|
|
1192
2454
|
const servingsValue = parseFloat(
|
|
1193
2455
|
String(this.metadata.servings).replace(",", ".")
|
|
1194
2456
|
);
|
|
1195
2457
|
newRecipe.metadata.servings = String(
|
|
1196
|
-
(0,
|
|
2458
|
+
(0, import_big4.default)(servingsValue).times(factor).toNumber()
|
|
1197
2459
|
);
|
|
1198
2460
|
}
|
|
1199
2461
|
}
|
|
@@ -1203,7 +2465,7 @@ var Recipe = class _Recipe {
|
|
|
1203
2465
|
String(this.metadata.yield).replace(",", ".")
|
|
1204
2466
|
);
|
|
1205
2467
|
newRecipe.metadata.yield = String(
|
|
1206
|
-
(0,
|
|
2468
|
+
(0, import_big4.default)(yieldValue).times(factor).toNumber()
|
|
1207
2469
|
);
|
|
1208
2470
|
}
|
|
1209
2471
|
}
|
|
@@ -1213,7 +2475,7 @@ var Recipe = class _Recipe {
|
|
|
1213
2475
|
String(this.metadata.serves).replace(",", ".")
|
|
1214
2476
|
);
|
|
1215
2477
|
newRecipe.metadata.serves = String(
|
|
1216
|
-
(0,
|
|
2478
|
+
(0, import_big4.default)(servesValue).times(factor).toNumber()
|
|
1217
2479
|
);
|
|
1218
2480
|
}
|
|
1219
2481
|
}
|
|
@@ -1236,19 +2498,27 @@ var Recipe = class _Recipe {
|
|
|
1236
2498
|
*/
|
|
1237
2499
|
clone() {
|
|
1238
2500
|
const newRecipe = new _Recipe();
|
|
1239
|
-
newRecipe.
|
|
1240
|
-
newRecipe
|
|
1241
|
-
|
|
1242
|
-
);
|
|
1243
|
-
newRecipe.sections =
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
2501
|
+
newRecipe.choices = deepClone(this.choices);
|
|
2502
|
+
_Recipe.itemCounts.set(newRecipe, this.getItemCount());
|
|
2503
|
+
newRecipe.metadata = deepClone(this.metadata);
|
|
2504
|
+
newRecipe.ingredients = deepClone(this.ingredients);
|
|
2505
|
+
newRecipe.sections = this.sections.map((section) => {
|
|
2506
|
+
const newSection = new Section(section.name);
|
|
2507
|
+
newSection.content = deepClone(section.content);
|
|
2508
|
+
return newSection;
|
|
2509
|
+
});
|
|
2510
|
+
newRecipe.cookware = deepClone(this.cookware);
|
|
2511
|
+
newRecipe.timers = deepClone(this.timers);
|
|
1248
2512
|
newRecipe.servings = this.servings;
|
|
1249
2513
|
return newRecipe;
|
|
1250
2514
|
}
|
|
1251
2515
|
};
|
|
2516
|
+
/**
|
|
2517
|
+
* External storage for item count (not a property on instances).
|
|
2518
|
+
* Used for giving ID numbers to items during parsing.
|
|
2519
|
+
*/
|
|
2520
|
+
__publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
|
|
2521
|
+
var Recipe = _Recipe;
|
|
1252
2522
|
|
|
1253
2523
|
// src/classes/shopping_list.ts
|
|
1254
2524
|
var ShoppingList = class {
|
|
@@ -1257,6 +2527,7 @@ var ShoppingList = class {
|
|
|
1257
2527
|
* @param category_config_str - The category configuration to parse.
|
|
1258
2528
|
*/
|
|
1259
2529
|
constructor(category_config_str) {
|
|
2530
|
+
// TODO: backport type change
|
|
1260
2531
|
/**
|
|
1261
2532
|
* The ingredients in the shopping list.
|
|
1262
2533
|
*/
|
|
@@ -1279,6 +2550,33 @@ var ShoppingList = class {
|
|
|
1279
2550
|
}
|
|
1280
2551
|
calculate_ingredients() {
|
|
1281
2552
|
this.ingredients = [];
|
|
2553
|
+
const addIngredientQuantity = (name, quantityTotal) => {
|
|
2554
|
+
const quantityTotalExtended = extendAllUnits(quantityTotal);
|
|
2555
|
+
const newQuantities = isAndGroup(quantityTotalExtended) ? quantityTotalExtended.entries : [quantityTotalExtended];
|
|
2556
|
+
const existing = this.ingredients.find((i2) => i2.name === name);
|
|
2557
|
+
if (existing) {
|
|
2558
|
+
if (!existing.quantityTotal) {
|
|
2559
|
+
existing.quantityTotal = quantityTotal;
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
try {
|
|
2563
|
+
const existingQuantityTotalExtended = extendAllUnits(
|
|
2564
|
+
existing.quantityTotal
|
|
2565
|
+
);
|
|
2566
|
+
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.entries : [existingQuantityTotalExtended];
|
|
2567
|
+
existing.quantityTotal = addEquivalentsAndSimplify(
|
|
2568
|
+
...existingQuantities,
|
|
2569
|
+
...newQuantities
|
|
2570
|
+
);
|
|
2571
|
+
return;
|
|
2572
|
+
} catch {
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
this.ingredients.push({
|
|
2576
|
+
name,
|
|
2577
|
+
quantityTotal
|
|
2578
|
+
});
|
|
2579
|
+
};
|
|
1282
2580
|
for (const addedRecipe of this.recipes) {
|
|
1283
2581
|
let scaledRecipe;
|
|
1284
2582
|
if ("factor" in addedRecipe) {
|
|
@@ -1287,62 +2585,47 @@ var ShoppingList = class {
|
|
|
1287
2585
|
} else {
|
|
1288
2586
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
1289
2587
|
}
|
|
1290
|
-
|
|
2588
|
+
const computedIngredients = scaledRecipe.calc_ingredient_quantities(
|
|
2589
|
+
addedRecipe.choices
|
|
2590
|
+
);
|
|
2591
|
+
for (const ingredient of computedIngredients) {
|
|
1291
2592
|
if (ingredient.flags && ingredient.flags.includes("hidden")) {
|
|
1292
2593
|
continue;
|
|
1293
2594
|
}
|
|
1294
|
-
|
|
1295
|
-
(
|
|
1296
|
-
)
|
|
1297
|
-
|
|
1298
|
-
try {
|
|
1299
|
-
if (existingIngredient && ingredient.quantity) {
|
|
1300
|
-
if (existingIngredient.quantity) {
|
|
1301
|
-
const newQuantity = addQuantities(
|
|
1302
|
-
{
|
|
1303
|
-
value: existingIngredient.quantity,
|
|
1304
|
-
unit: existingIngredient.unit ?? ""
|
|
1305
|
-
},
|
|
1306
|
-
{
|
|
1307
|
-
value: ingredient.quantity,
|
|
1308
|
-
unit: ingredient.unit ?? ""
|
|
1309
|
-
}
|
|
1310
|
-
);
|
|
1311
|
-
existingIngredient.quantity = newQuantity.value;
|
|
1312
|
-
if (newQuantity.unit) {
|
|
1313
|
-
existingIngredient.unit = newQuantity.unit;
|
|
1314
|
-
}
|
|
1315
|
-
} else {
|
|
1316
|
-
existingIngredient.quantity = ingredient.quantity;
|
|
1317
|
-
if (ingredient.unit) {
|
|
1318
|
-
existingIngredient.unit = ingredient.unit;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
} catch {
|
|
1323
|
-
addSeparate = true;
|
|
1324
|
-
}
|
|
1325
|
-
if (!existingIngredient || addSeparate) {
|
|
1326
|
-
const newIngredient = { name: ingredient.name };
|
|
1327
|
-
if (ingredient.quantity) {
|
|
1328
|
-
newIngredient.quantity = ingredient.quantity;
|
|
1329
|
-
}
|
|
1330
|
-
if (ingredient.unit) {
|
|
1331
|
-
newIngredient.unit = ingredient.unit;
|
|
1332
|
-
}
|
|
1333
|
-
this.ingredients.push(newIngredient);
|
|
2595
|
+
if (ingredient.quantityTotal) {
|
|
2596
|
+
addIngredientQuantity(ingredient.name, ingredient.quantityTotal);
|
|
2597
|
+
} else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
|
|
2598
|
+
this.ingredients.push({ name: ingredient.name });
|
|
1334
2599
|
}
|
|
1335
2600
|
}
|
|
1336
2601
|
}
|
|
1337
2602
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
2603
|
+
/**
|
|
2604
|
+
* Adds a recipe to the shopping list, then automatically
|
|
2605
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
2606
|
+
* @param recipe - The recipe to add.
|
|
2607
|
+
* @param options - Options for adding the recipe.
|
|
2608
|
+
*/
|
|
2609
|
+
add_recipe(recipe, options = {}) {
|
|
2610
|
+
if (!options.scaling) {
|
|
2611
|
+
this.recipes.push({
|
|
2612
|
+
recipe,
|
|
2613
|
+
factor: options.scaling ?? 1,
|
|
2614
|
+
choices: options.choices
|
|
2615
|
+
});
|
|
1341
2616
|
} else {
|
|
1342
|
-
if ("factor" in scaling) {
|
|
1343
|
-
this.recipes.push({
|
|
2617
|
+
if ("factor" in options.scaling) {
|
|
2618
|
+
this.recipes.push({
|
|
2619
|
+
recipe,
|
|
2620
|
+
factor: options.scaling.factor,
|
|
2621
|
+
choices: options.choices
|
|
2622
|
+
});
|
|
1344
2623
|
} else {
|
|
1345
|
-
this.recipes.push({
|
|
2624
|
+
this.recipes.push({
|
|
2625
|
+
recipe,
|
|
2626
|
+
servings: options.scaling.servings,
|
|
2627
|
+
choices: options.choices
|
|
2628
|
+
});
|
|
1346
2629
|
}
|
|
1347
2630
|
}
|
|
1348
2631
|
this.calculate_ingredients();
|
|
@@ -1407,15 +2690,276 @@ var ShoppingList = class {
|
|
|
1407
2690
|
this.categories = categories;
|
|
1408
2691
|
}
|
|
1409
2692
|
};
|
|
2693
|
+
|
|
2694
|
+
// src/classes/shopping_cart.ts
|
|
2695
|
+
var import_yalps = require("yalps");
|
|
2696
|
+
var ShoppingCart = class {
|
|
2697
|
+
/**
|
|
2698
|
+
* Creates a new ShoppingCart instance
|
|
2699
|
+
* @param options - {@link ShoppingCartOptions | Options} for the constructor
|
|
2700
|
+
*/
|
|
2701
|
+
constructor(options) {
|
|
2702
|
+
/**
|
|
2703
|
+
* The product catalog to use for matching products
|
|
2704
|
+
*/
|
|
2705
|
+
__publicField(this, "productCatalog");
|
|
2706
|
+
/**
|
|
2707
|
+
* The shopping list to build the cart from
|
|
2708
|
+
*/
|
|
2709
|
+
__publicField(this, "shoppingList");
|
|
2710
|
+
/**
|
|
2711
|
+
* The content of the cart
|
|
2712
|
+
*/
|
|
2713
|
+
__publicField(this, "cart", []);
|
|
2714
|
+
/**
|
|
2715
|
+
* The ingredients that were successfully matched with products
|
|
2716
|
+
*/
|
|
2717
|
+
__publicField(this, "match", []);
|
|
2718
|
+
/**
|
|
2719
|
+
* The ingredients that could not be matched with products
|
|
2720
|
+
*/
|
|
2721
|
+
__publicField(this, "misMatch", []);
|
|
2722
|
+
/**
|
|
2723
|
+
* Key information about the shopping cart
|
|
2724
|
+
*/
|
|
2725
|
+
__publicField(this, "summary");
|
|
2726
|
+
if (options?.catalog) this.productCatalog = options.catalog;
|
|
2727
|
+
if (options?.list) this.shoppingList = options.list;
|
|
2728
|
+
this.summary = { totalPrice: 0, totalItems: 0 };
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Sets the product catalog to use for matching products
|
|
2732
|
+
* To use if a catalog was not provided at the creation of the instance
|
|
2733
|
+
* @param catalog - The {@link ProductCatalog} to set
|
|
2734
|
+
*/
|
|
2735
|
+
setProductCatalog(catalog) {
|
|
2736
|
+
this.productCatalog = catalog;
|
|
2737
|
+
}
|
|
2738
|
+
// TODO: harmonize recipe name to use underscores
|
|
2739
|
+
/**
|
|
2740
|
+
* Sets the shopping list to build the cart from.
|
|
2741
|
+
* To use if a shopping list was not provided at the creation of the instance
|
|
2742
|
+
* @param list - The {@link ShoppingList} to set
|
|
2743
|
+
*/
|
|
2744
|
+
setShoppingList(list) {
|
|
2745
|
+
this.shoppingList = list;
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Builds the cart from the shopping list and product catalog
|
|
2749
|
+
* @remarks
|
|
2750
|
+
* - If a combination of product(s) is successfully found for a given ingredient, the latter will be listed in the {@link ShoppingCart.match | match} array
|
|
2751
|
+
* in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
|
|
2752
|
+
* - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
|
|
2753
|
+
* - No product is listed in the catalog for that ingredient
|
|
2754
|
+
* - The ingredient has no quantity, a text quantity
|
|
2755
|
+
* - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
|
|
2756
|
+
* @throws {@link NoProductCatalogForCartError} if no product catalog is set
|
|
2757
|
+
* @throws {@link NoShoppingListForCartError} if no shopping list is set
|
|
2758
|
+
* @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
|
|
2759
|
+
*/
|
|
2760
|
+
buildCart() {
|
|
2761
|
+
this.resetCart();
|
|
2762
|
+
if (this.productCatalog === void 0) {
|
|
2763
|
+
throw new NoProductCatalogForCartError();
|
|
2764
|
+
} else if (this.shoppingList === void 0) {
|
|
2765
|
+
throw new NoShoppingListForCartError();
|
|
2766
|
+
}
|
|
2767
|
+
for (const ingredient of this.shoppingList.ingredients) {
|
|
2768
|
+
const productOptions = this.getProductOptions(ingredient);
|
|
2769
|
+
try {
|
|
2770
|
+
const optimumMatch = this.getOptimumMatch(ingredient, productOptions);
|
|
2771
|
+
this.cart.push(...optimumMatch);
|
|
2772
|
+
this.match.push({ ingredient, selection: optimumMatch });
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
if (error instanceof NoProductMatchError) {
|
|
2775
|
+
this.misMatch.push({ ingredient, reason: error.code });
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
this.summarize();
|
|
2780
|
+
return this.misMatch.length > 0;
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Gets the product options for a given ingredient
|
|
2784
|
+
* @param ingredient - The ingredient to get the product options for
|
|
2785
|
+
* @returns An array of {@link ProductOption}
|
|
2786
|
+
*/
|
|
2787
|
+
getProductOptions(ingredient) {
|
|
2788
|
+
return this.productCatalog.products.filter(
|
|
2789
|
+
(product) => product.ingredientName === ingredient.name || product.ingredientAliases?.includes(ingredient.name)
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Gets the optimum match for a given ingredient and product option
|
|
2794
|
+
* @param ingredient - The ingredient to match
|
|
2795
|
+
* @param options - The product options to choose from
|
|
2796
|
+
* @returns An array of {@link ProductSelection}
|
|
2797
|
+
* @throws {@link NoProductMatchError} if no match can be found
|
|
2798
|
+
*/
|
|
2799
|
+
getOptimumMatch(ingredient, options) {
|
|
2800
|
+
if (options.length === 0)
|
|
2801
|
+
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
2802
|
+
if (!ingredient.quantityTotal)
|
|
2803
|
+
throw new NoProductMatchError(ingredient.name, "noQuantity");
|
|
2804
|
+
const normalizedOptions = options.map(
|
|
2805
|
+
(option) => ({
|
|
2806
|
+
...option,
|
|
2807
|
+
sizes: option.sizes.map((s) => {
|
|
2808
|
+
const resolvedUnit = resolveUnit(s.unit);
|
|
2809
|
+
return {
|
|
2810
|
+
size: resolvedUnit && "toBase" in resolvedUnit ? multiplyQuantityValue(
|
|
2811
|
+
s.size,
|
|
2812
|
+
resolvedUnit.toBase
|
|
2813
|
+
) : s.size,
|
|
2814
|
+
unit: resolvedUnit
|
|
2815
|
+
};
|
|
2816
|
+
})
|
|
2817
|
+
})
|
|
2818
|
+
);
|
|
2819
|
+
const normalizedQuantityTotal = normalizeAllUnits(ingredient.quantityTotal);
|
|
2820
|
+
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
2821
|
+
if (isAndGroup(normalizedQuantities)) {
|
|
2822
|
+
for (const q of normalizedQuantities.entries) {
|
|
2823
|
+
const result = getOptimumMatchForQuantityParts(
|
|
2824
|
+
q,
|
|
2825
|
+
normalizedOptions2,
|
|
2826
|
+
selection
|
|
2827
|
+
);
|
|
2828
|
+
selection.push(...result);
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.entries : [normalizedQuantities];
|
|
2832
|
+
const solutions = [];
|
|
2833
|
+
const errors = /* @__PURE__ */ new Set();
|
|
2834
|
+
for (const alternative of alternativeUnitsOfQuantity) {
|
|
2835
|
+
if (alternative.quantity.type === "fixed" && alternative.quantity.value.type === "text") {
|
|
2836
|
+
errors.add("textValue");
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
const scaledQuantity = multiplyQuantityValue(
|
|
2840
|
+
alternative.quantity,
|
|
2841
|
+
"toBase" in alternative.unit ? alternative.unit.toBase : 1
|
|
2842
|
+
);
|
|
2843
|
+
alternative.quantity = scaledQuantity;
|
|
2844
|
+
const matchOptions = normalizedOptions2.filter(
|
|
2845
|
+
(option) => option.sizes.some(
|
|
2846
|
+
(s) => areUnitsCompatible(alternative.unit, s.unit)
|
|
2847
|
+
)
|
|
2848
|
+
);
|
|
2849
|
+
if (matchOptions.length > 0) {
|
|
2850
|
+
const findCompatibleSize = (option) => option.sizes.find(
|
|
2851
|
+
(s) => areUnitsCompatible(alternative.unit, s.unit)
|
|
2852
|
+
);
|
|
2853
|
+
if (matchOptions.length == 1) {
|
|
2854
|
+
const matchedOption = matchOptions[0];
|
|
2855
|
+
const compatibleSize = findCompatibleSize(matchedOption);
|
|
2856
|
+
const product = options.find(
|
|
2857
|
+
(opt) => opt.id === matchedOption.id
|
|
2858
|
+
);
|
|
2859
|
+
const targetQuantity = scaledQuantity.type === "fixed" ? scaledQuantity.value : scaledQuantity.min;
|
|
2860
|
+
const resQuantity = Math.ceil(
|
|
2861
|
+
getNumericValue(targetQuantity) / getNumericValue(compatibleSize.size.value)
|
|
2862
|
+
);
|
|
2863
|
+
solutions.push([
|
|
2864
|
+
{
|
|
2865
|
+
product,
|
|
2866
|
+
quantity: resQuantity,
|
|
2867
|
+
totalPrice: resQuantity * matchedOption.price
|
|
2868
|
+
}
|
|
2869
|
+
]);
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
const model = {
|
|
2873
|
+
direction: "minimize",
|
|
2874
|
+
objective: "price",
|
|
2875
|
+
integers: true,
|
|
2876
|
+
constraints: {
|
|
2877
|
+
size: {
|
|
2878
|
+
min: scaledQuantity.type === "fixed" ? getNumericValue(scaledQuantity.value) : getNumericValue(scaledQuantity.min)
|
|
2879
|
+
}
|
|
2880
|
+
},
|
|
2881
|
+
variables: matchOptions.reduce(
|
|
2882
|
+
(acc, option) => {
|
|
2883
|
+
const compatibleSize = findCompatibleSize(option);
|
|
2884
|
+
acc[option.id] = {
|
|
2885
|
+
price: option.price,
|
|
2886
|
+
size: getNumericValue(compatibleSize.size.value)
|
|
2887
|
+
};
|
|
2888
|
+
return acc;
|
|
2889
|
+
},
|
|
2890
|
+
{}
|
|
2891
|
+
)
|
|
2892
|
+
};
|
|
2893
|
+
const solution = (0, import_yalps.solve)(model);
|
|
2894
|
+
solutions.push(
|
|
2895
|
+
solution.variables.map((variable) => {
|
|
2896
|
+
const resProductSelection = {
|
|
2897
|
+
product: options.find((option) => option.id === variable[0]),
|
|
2898
|
+
quantity: variable[1]
|
|
2899
|
+
};
|
|
2900
|
+
return {
|
|
2901
|
+
...resProductSelection,
|
|
2902
|
+
totalPrice: resProductSelection.quantity * resProductSelection.product.price
|
|
2903
|
+
};
|
|
2904
|
+
})
|
|
2905
|
+
);
|
|
2906
|
+
} else {
|
|
2907
|
+
errors.add("incompatibleUnits");
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (solutions.length === 0) {
|
|
2911
|
+
throw new NoProductMatchError(
|
|
2912
|
+
ingredient.name,
|
|
2913
|
+
errors.size === 1 ? errors.values().next().value : "textValue_incompatibleUnits"
|
|
2914
|
+
);
|
|
2915
|
+
} else {
|
|
2916
|
+
return solutions.sort(
|
|
2917
|
+
(a2, b) => a2.reduce((acc, item) => acc + item.totalPrice, 0) - b.reduce((acc, item) => acc + item.totalPrice, 0)
|
|
2918
|
+
)[0];
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
return selection;
|
|
2922
|
+
}
|
|
2923
|
+
return getOptimumMatchForQuantityParts(
|
|
2924
|
+
normalizedQuantityTotal,
|
|
2925
|
+
normalizedOptions
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Reset the cart's properties
|
|
2930
|
+
*/
|
|
2931
|
+
resetCart() {
|
|
2932
|
+
this.cart = [];
|
|
2933
|
+
this.match = [];
|
|
2934
|
+
this.misMatch = [];
|
|
2935
|
+
this.summary = { totalPrice: 0, totalItems: 0 };
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
|
|
2939
|
+
* This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
|
|
2940
|
+
* @returns the total price and number of items in the cart
|
|
2941
|
+
*/
|
|
2942
|
+
summarize() {
|
|
2943
|
+
this.summary.totalPrice = this.cart.reduce(
|
|
2944
|
+
(acc, item) => acc + item.totalPrice,
|
|
2945
|
+
0
|
|
2946
|
+
);
|
|
2947
|
+
this.summary.totalItems = this.cart.length;
|
|
2948
|
+
return this.summary;
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
1410
2951
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1411
2952
|
0 && (module.exports = {
|
|
1412
2953
|
CategoryConfig,
|
|
2954
|
+
NoProductCatalogForCartError,
|
|
2955
|
+
NoShoppingListForCartError,
|
|
2956
|
+
ProductCatalog,
|
|
1413
2957
|
Recipe,
|
|
1414
2958
|
Section,
|
|
2959
|
+
ShoppingCart,
|
|
1415
2960
|
ShoppingList
|
|
1416
2961
|
});
|
|
1417
2962
|
/* v8 ignore else -- @preserve */
|
|
1418
|
-
/* v8 ignore else -- expliciting error types -- @preserve */
|
|
1419
2963
|
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
1420
|
-
/* v8 ignore
|
|
2964
|
+
/* v8 ignore if -- @preserve */
|
|
1421
2965
|
//# sourceMappingURL=index.cjs.map
|