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