@tmlmt/cooklang-parser 2.1.8 → 3.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2741 -418
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1076 -70
- package/dist/index.d.ts +1076 -70
- package/dist/index.js +2722 -417
- package/dist/index.js.map +1 -1
- package/package.json +23 -20
package/dist/index.cjs
CHANGED
|
@@ -33,9 +33,27 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
33
33
|
var index_exports = {};
|
|
34
34
|
__export(index_exports, {
|
|
35
35
|
CategoryConfig: () => CategoryConfig,
|
|
36
|
+
NoProductCatalogForCartError: () => NoProductCatalogForCartError,
|
|
37
|
+
NoShoppingListForCartError: () => NoShoppingListForCartError,
|
|
38
|
+
ProductCatalog: () => ProductCatalog,
|
|
36
39
|
Recipe: () => Recipe,
|
|
37
40
|
Section: () => Section,
|
|
38
|
-
|
|
41
|
+
ShoppingCart: () => ShoppingCart,
|
|
42
|
+
ShoppingList: () => ShoppingList,
|
|
43
|
+
convertQuantityToSystem: () => convertQuantityToSystem,
|
|
44
|
+
formatExtendedQuantity: () => formatExtendedQuantity,
|
|
45
|
+
formatItemQuantity: () => formatItemQuantity,
|
|
46
|
+
formatNumericValue: () => formatNumericValue,
|
|
47
|
+
formatQuantity: () => formatQuantity,
|
|
48
|
+
formatQuantityWithUnit: () => formatQuantityWithUnit,
|
|
49
|
+
formatSingleValue: () => formatSingleValue,
|
|
50
|
+
formatUnit: () => formatUnit,
|
|
51
|
+
hasAlternatives: () => hasAlternatives,
|
|
52
|
+
isAlternativeSelected: () => isAlternativeSelected,
|
|
53
|
+
isAndGroup: () => isAndGroup,
|
|
54
|
+
isGroupedItem: () => isGroupedItem,
|
|
55
|
+
isSimpleGroup: () => isSimpleGroup,
|
|
56
|
+
renderFractionAsVulgar: () => renderFractionAsVulgar
|
|
39
57
|
});
|
|
40
58
|
module.exports = __toCommonJS(index_exports);
|
|
41
59
|
|
|
@@ -100,33 +118,10 @@ var CategoryConfig = class {
|
|
|
100
118
|
}
|
|
101
119
|
};
|
|
102
120
|
|
|
103
|
-
// src/classes/
|
|
104
|
-
var
|
|
105
|
-
/**
|
|
106
|
-
* Creates an instance of Section.
|
|
107
|
-
* @param name - The name of the section. Defaults to an empty string.
|
|
108
|
-
*/
|
|
109
|
-
constructor(name = "") {
|
|
110
|
-
/**
|
|
111
|
-
* The name of the section. Can be an empty string for the default (first) section.
|
|
112
|
-
* @defaultValue `""`
|
|
113
|
-
*/
|
|
114
|
-
__publicField(this, "name");
|
|
115
|
-
/** An array of steps and notes that make up the content of the section. */
|
|
116
|
-
__publicField(this, "content", []);
|
|
117
|
-
this.name = name;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Checks if the section is blank (has no name and no content).
|
|
121
|
-
* Used during recipe parsing
|
|
122
|
-
* @returns `true` if the section is blank, otherwise `false`.
|
|
123
|
-
*/
|
|
124
|
-
isBlank() {
|
|
125
|
-
return this.name === "" && this.content.length === 0;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
121
|
+
// src/classes/product_catalog.ts
|
|
122
|
+
var import_smol_toml = __toESM(require("smol-toml"), 1);
|
|
128
123
|
|
|
129
|
-
// node_modules/.pnpm/human-regex@2.
|
|
124
|
+
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
130
125
|
var t = /* @__PURE__ */ new Map();
|
|
131
126
|
var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
|
|
132
127
|
var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
|
|
@@ -187,7 +182,7 @@ var a = class {
|
|
|
187
182
|
return this.add(".");
|
|
188
183
|
}
|
|
189
184
|
newline() {
|
|
190
|
-
return this.add("(
|
|
185
|
+
return this.add("(\\r\\n|\\r|\\n)");
|
|
191
186
|
}
|
|
192
187
|
negativeLookahead(t2) {
|
|
193
188
|
return this.add(`(?!${t2})`);
|
|
@@ -328,19 +323,22 @@ var i = (() => {
|
|
|
328
323
|
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
329
324
|
var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
|
|
330
325
|
var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
331
|
-
var
|
|
332
|
-
var
|
|
326
|
+
var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
|
|
327
|
+
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();
|
|
328
|
+
var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
|
|
329
|
+
var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
|
|
330
|
+
var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
|
|
333
331
|
var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
|
|
334
|
-
var
|
|
335
|
-
var
|
|
336
|
-
var
|
|
332
|
+
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();
|
|
333
|
+
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();
|
|
334
|
+
var arbitraryScalableRegex = d().literal("{{").startGroup().startNamedGroup("arbitraryName").notAnyOf("}:%").oneOrMore().endGroup().literal(":").endGroup().optional().startNamedGroup("arbitraryQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}}").toRegExp();
|
|
337
335
|
var tokensRegex = new RegExp(
|
|
338
336
|
[
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
337
|
+
ingredientWithGroupKeyRegex,
|
|
338
|
+
ingredientWithAlternativeRegex,
|
|
339
|
+
cookwareRegex,
|
|
340
|
+
timerRegex,
|
|
341
|
+
arbitraryScalableRegex
|
|
344
342
|
].map((r2) => r2.source).join("|"),
|
|
345
343
|
"gu"
|
|
346
344
|
);
|
|
@@ -351,8 +349,7 @@ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/")
|
|
|
351
349
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
352
350
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
353
351
|
|
|
354
|
-
// src/units.ts
|
|
355
|
-
var import_big = __toESM(require("big.js"), 1);
|
|
352
|
+
// src/units/definitions.ts
|
|
356
353
|
var units = [
|
|
357
354
|
// Mass (Metric)
|
|
358
355
|
{
|
|
@@ -360,7 +357,8 @@ var units = [
|
|
|
360
357
|
type: "mass",
|
|
361
358
|
system: "metric",
|
|
362
359
|
aliases: ["gram", "grams", "grammes"],
|
|
363
|
-
toBase: 1
|
|
360
|
+
toBase: 1,
|
|
361
|
+
maxValue: 999
|
|
364
362
|
},
|
|
365
363
|
{
|
|
366
364
|
name: "kg",
|
|
@@ -369,20 +367,28 @@ var units = [
|
|
|
369
367
|
aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
|
|
370
368
|
toBase: 1e3
|
|
371
369
|
},
|
|
372
|
-
// Mass (
|
|
370
|
+
// Mass (US/UK - identical in both systems)
|
|
373
371
|
{
|
|
374
372
|
name: "oz",
|
|
375
373
|
type: "mass",
|
|
376
|
-
system: "
|
|
374
|
+
system: "ambiguous",
|
|
377
375
|
aliases: ["ounce", "ounces"],
|
|
378
|
-
toBase: 28.3495
|
|
376
|
+
toBase: 28.3495,
|
|
377
|
+
// default: US (same as UK)
|
|
378
|
+
toBaseBySystem: { US: 28.3495, UK: 28.3495 },
|
|
379
|
+
maxValue: 31,
|
|
380
|
+
// 16 oz = 1 lb, allow a bit more
|
|
381
|
+
fractions: { enabled: true, denominators: [2] }
|
|
379
382
|
},
|
|
380
383
|
{
|
|
381
384
|
name: "lb",
|
|
382
385
|
type: "mass",
|
|
383
|
-
system: "
|
|
386
|
+
system: "ambiguous",
|
|
384
387
|
aliases: ["pound", "pounds"],
|
|
385
|
-
toBase: 453.592
|
|
388
|
+
toBase: 453.592,
|
|
389
|
+
// default: US (same as UK)
|
|
390
|
+
toBaseBySystem: { US: 453.592, UK: 453.592 },
|
|
391
|
+
fractions: { enabled: true, denominators: [2, 4] }
|
|
386
392
|
},
|
|
387
393
|
// Volume (Metric)
|
|
388
394
|
{
|
|
@@ -390,21 +396,26 @@ var units = [
|
|
|
390
396
|
type: "volume",
|
|
391
397
|
system: "metric",
|
|
392
398
|
aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
|
|
393
|
-
toBase: 1
|
|
399
|
+
toBase: 1,
|
|
400
|
+
maxValue: 999
|
|
394
401
|
},
|
|
395
402
|
{
|
|
396
403
|
name: "cl",
|
|
397
404
|
type: "volume",
|
|
398
405
|
system: "metric",
|
|
399
406
|
aliases: ["centiliter", "centiliters", "centilitre", "centilitres"],
|
|
400
|
-
toBase: 10
|
|
407
|
+
toBase: 10,
|
|
408
|
+
isBestUnit: false
|
|
409
|
+
// exists but not a "best" candidate
|
|
401
410
|
},
|
|
402
411
|
{
|
|
403
412
|
name: "dl",
|
|
404
413
|
type: "volume",
|
|
405
414
|
system: "metric",
|
|
406
415
|
aliases: ["deciliter", "deciliters", "decilitre", "decilitres"],
|
|
407
|
-
toBase: 100
|
|
416
|
+
toBase: 100,
|
|
417
|
+
isBestUnit: false
|
|
418
|
+
// exists but not a "best" candidate
|
|
408
419
|
},
|
|
409
420
|
{
|
|
410
421
|
name: "l",
|
|
@@ -413,55 +424,102 @@ var units = [
|
|
|
413
424
|
aliases: ["liter", "liters", "litre", "litres"],
|
|
414
425
|
toBase: 1e3
|
|
415
426
|
},
|
|
427
|
+
// Volume (JP)
|
|
428
|
+
{
|
|
429
|
+
name: "go",
|
|
430
|
+
type: "volume",
|
|
431
|
+
system: "JP",
|
|
432
|
+
aliases: ["gou", "goo", "\u5408", "rice cup"],
|
|
433
|
+
toBase: 180,
|
|
434
|
+
maxValue: 10
|
|
435
|
+
},
|
|
436
|
+
// Volume (Ambiguous: metric/US/UK)
|
|
416
437
|
{
|
|
417
438
|
name: "tsp",
|
|
418
439
|
type: "volume",
|
|
419
|
-
system: "
|
|
440
|
+
system: "ambiguous",
|
|
420
441
|
aliases: ["teaspoon", "teaspoons"],
|
|
421
|
-
toBase: 5
|
|
442
|
+
toBase: 5,
|
|
443
|
+
// default: metric
|
|
444
|
+
toBaseBySystem: { metric: 5, US: 4.929, UK: 5.919, JP: 5 },
|
|
445
|
+
maxValue: 5,
|
|
446
|
+
// 3 tsp = 1 tbsp (but allow a bit more)
|
|
447
|
+
fractions: { enabled: true, denominators: [2, 3, 4, 8] }
|
|
422
448
|
},
|
|
423
449
|
{
|
|
424
450
|
name: "tbsp",
|
|
425
451
|
type: "volume",
|
|
426
|
-
system: "
|
|
452
|
+
system: "ambiguous",
|
|
427
453
|
aliases: ["tablespoon", "tablespoons"],
|
|
428
|
-
toBase: 15
|
|
454
|
+
toBase: 15,
|
|
455
|
+
// default: metric
|
|
456
|
+
toBaseBySystem: { metric: 15, US: 14.787, UK: 17.758, JP: 15 },
|
|
457
|
+
maxValue: 4,
|
|
458
|
+
// ~16 tbsp = 1 cup
|
|
459
|
+
fractions: { enabled: true }
|
|
429
460
|
},
|
|
430
|
-
// Volume (
|
|
461
|
+
// Volume (Ambiguous: US/UK only)
|
|
431
462
|
{
|
|
432
463
|
name: "fl-oz",
|
|
433
464
|
type: "volume",
|
|
434
|
-
system: "
|
|
465
|
+
system: "ambiguous",
|
|
435
466
|
aliases: ["fluid ounce", "fluid ounces"],
|
|
436
|
-
toBase: 29.5735
|
|
467
|
+
toBase: 29.5735,
|
|
468
|
+
// default: US
|
|
469
|
+
toBaseBySystem: { US: 29.5735, UK: 28.4131 },
|
|
470
|
+
maxValue: 15,
|
|
471
|
+
// 8 fl-oz ~ 1 cup, allow more
|
|
472
|
+
fractions: { enabled: true, denominators: [2] }
|
|
437
473
|
},
|
|
438
474
|
{
|
|
439
475
|
name: "cup",
|
|
440
476
|
type: "volume",
|
|
441
|
-
system: "
|
|
477
|
+
system: "ambiguous",
|
|
442
478
|
aliases: ["cups"],
|
|
443
|
-
toBase: 236.588
|
|
479
|
+
toBase: 236.588,
|
|
480
|
+
// default: US
|
|
481
|
+
toBaseBySystem: { US: 236.588, UK: 284.131 },
|
|
482
|
+
maxValue: 15,
|
|
483
|
+
// upgrade to gallons above 15 cups
|
|
484
|
+
fractions: { enabled: true }
|
|
444
485
|
},
|
|
445
486
|
{
|
|
446
487
|
name: "pint",
|
|
447
488
|
type: "volume",
|
|
448
|
-
system: "
|
|
489
|
+
system: "ambiguous",
|
|
449
490
|
aliases: ["pints"],
|
|
450
|
-
toBase: 473.176
|
|
491
|
+
toBase: 473.176,
|
|
492
|
+
// default: US
|
|
493
|
+
toBaseBySystem: { US: 473.176, UK: 568.261 },
|
|
494
|
+
maxValue: 3,
|
|
495
|
+
// 2 pints = 1 quart
|
|
496
|
+
fractions: { enabled: true, denominators: [2] },
|
|
497
|
+
isBestUnit: false
|
|
498
|
+
// exists but not a "best" candidate
|
|
451
499
|
},
|
|
452
500
|
{
|
|
453
501
|
name: "quart",
|
|
454
502
|
type: "volume",
|
|
455
|
-
system: "
|
|
503
|
+
system: "ambiguous",
|
|
456
504
|
aliases: ["quarts"],
|
|
457
|
-
toBase: 946.353
|
|
505
|
+
toBase: 946.353,
|
|
506
|
+
// default: US
|
|
507
|
+
toBaseBySystem: { US: 946.353, UK: 1136.52 },
|
|
508
|
+
maxValue: 3,
|
|
509
|
+
// 4 quarts = 1 gallon
|
|
510
|
+
fractions: { enabled: true, denominators: [2] },
|
|
511
|
+
isBestUnit: false
|
|
512
|
+
// exists but not a "best" candidate
|
|
458
513
|
},
|
|
459
514
|
{
|
|
460
515
|
name: "gallon",
|
|
461
516
|
type: "volume",
|
|
462
|
-
system: "
|
|
517
|
+
system: "ambiguous",
|
|
463
518
|
aliases: ["gallons"],
|
|
464
|
-
toBase: 3785.41
|
|
519
|
+
toBase: 3785.41,
|
|
520
|
+
// default: US
|
|
521
|
+
toBaseBySystem: { US: 3785.41, UK: 4546.09 },
|
|
522
|
+
fractions: { enabled: true, denominators: [2] }
|
|
465
523
|
},
|
|
466
524
|
// Count units (no conversion, but recognized as a type)
|
|
467
525
|
{
|
|
@@ -469,7 +527,8 @@ var units = [
|
|
|
469
527
|
type: "count",
|
|
470
528
|
system: "metric",
|
|
471
529
|
aliases: ["pieces", "pc"],
|
|
472
|
-
toBase: 1
|
|
530
|
+
toBase: 1,
|
|
531
|
+
maxValue: 999
|
|
473
532
|
}
|
|
474
533
|
];
|
|
475
534
|
var unitMap = /* @__PURE__ */ new Map();
|
|
@@ -482,20 +541,25 @@ for (const unit of units) {
|
|
|
482
541
|
function normalizeUnit(unit = "") {
|
|
483
542
|
return unitMap.get(unit.toLowerCase().trim());
|
|
484
543
|
}
|
|
485
|
-
var
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
544
|
+
var NO_UNIT = "__no-unit__";
|
|
545
|
+
function resolveUnit(name = NO_UNIT, integerProtected = false) {
|
|
546
|
+
const normalizedUnit = normalizeUnit(name);
|
|
547
|
+
const resolvedUnit = normalizedUnit ? { ...normalizedUnit, name } : { name, type: "other", system: "none" };
|
|
548
|
+
return integerProtected ? { ...resolvedUnit, integerProtected: true } : resolvedUnit;
|
|
549
|
+
}
|
|
550
|
+
function isNoUnit(unit) {
|
|
551
|
+
if (!unit) return true;
|
|
552
|
+
return resolveUnit(unit.name).name === NO_UNIT;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/units/conversion.ts
|
|
556
|
+
var import_big2 = __toESM(require("big.js"), 1);
|
|
557
|
+
|
|
558
|
+
// src/quantities/numeric.ts
|
|
559
|
+
var import_big = __toESM(require("big.js"), 1);
|
|
560
|
+
var DEFAULT_DENOMINATORS = [2, 3, 4];
|
|
561
|
+
var DEFAULT_FRACTION_ACCURACY = 0.05;
|
|
562
|
+
var DEFAULT_MAX_WHOLE = 4;
|
|
499
563
|
function gcd(a2, b) {
|
|
500
564
|
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
501
565
|
}
|
|
@@ -511,14 +575,58 @@ function simplifyFraction(num, den) {
|
|
|
511
575
|
simplifiedDen = -simplifiedDen;
|
|
512
576
|
}
|
|
513
577
|
if (simplifiedDen === 1) {
|
|
514
|
-
return { type: "decimal",
|
|
578
|
+
return { type: "decimal", decimal: simplifiedNum };
|
|
515
579
|
} else {
|
|
516
580
|
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
517
581
|
}
|
|
518
582
|
}
|
|
583
|
+
function approximateFraction(value, denominators = DEFAULT_DENOMINATORS, accuracy = DEFAULT_FRACTION_ACCURACY, maxWhole = DEFAULT_MAX_WHOLE) {
|
|
584
|
+
if (value <= 0 || !Number.isFinite(value)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const wholePart = Math.floor(value);
|
|
588
|
+
if (wholePart > maxWhole) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
const fractionalPart = value - wholePart;
|
|
592
|
+
if (fractionalPart < 1e-4) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
let bestFraction = null;
|
|
596
|
+
for (const den of denominators) {
|
|
597
|
+
const exactNum = value * den;
|
|
598
|
+
const roundedNum = Math.round(exactNum);
|
|
599
|
+
if (roundedNum === 0) continue;
|
|
600
|
+
const approximatedValue = roundedNum / den;
|
|
601
|
+
const relativeError = Math.abs(approximatedValue - value) / value;
|
|
602
|
+
if (relativeError <= accuracy) {
|
|
603
|
+
if (!bestFraction || relativeError < bestFraction.error) {
|
|
604
|
+
bestFraction = { num: roundedNum, den, error: relativeError };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (!bestFraction) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const commonDivisor = gcd(bestFraction.num, bestFraction.den);
|
|
612
|
+
return {
|
|
613
|
+
type: "fraction",
|
|
614
|
+
num: bestFraction.num / commonDivisor,
|
|
615
|
+
den: bestFraction.den / commonDivisor
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function getNumericValue(v) {
|
|
619
|
+
if (v.type === "decimal") {
|
|
620
|
+
return v.decimal;
|
|
621
|
+
}
|
|
622
|
+
return v.num / v.den;
|
|
623
|
+
}
|
|
519
624
|
function multiplyNumericValue(v, factor) {
|
|
520
625
|
if (v.type === "decimal") {
|
|
521
|
-
return {
|
|
626
|
+
return {
|
|
627
|
+
type: "decimal",
|
|
628
|
+
decimal: (0, import_big.default)(v.decimal).times(factor).toNumber()
|
|
629
|
+
};
|
|
522
630
|
}
|
|
523
631
|
return simplifyFraction((0, import_big.default)(v.num).times(factor).toNumber(), v.den);
|
|
524
632
|
}
|
|
@@ -528,36 +636,62 @@ function addNumericValues(val1, val2) {
|
|
|
528
636
|
let num2;
|
|
529
637
|
let den2;
|
|
530
638
|
if (val1.type === "decimal") {
|
|
531
|
-
num1 = val1.
|
|
639
|
+
num1 = val1.decimal;
|
|
532
640
|
den1 = 1;
|
|
533
641
|
} else {
|
|
534
642
|
num1 = val1.num;
|
|
535
643
|
den1 = val1.den;
|
|
536
644
|
}
|
|
537
645
|
if (val2.type === "decimal") {
|
|
538
|
-
num2 = val2.
|
|
646
|
+
num2 = val2.decimal;
|
|
539
647
|
den2 = 1;
|
|
540
648
|
} else {
|
|
541
649
|
num2 = val2.num;
|
|
542
650
|
den2 = val2.den;
|
|
543
651
|
}
|
|
544
652
|
if (num1 === 0 && num2 === 0) {
|
|
545
|
-
return { type: "decimal",
|
|
653
|
+
return { type: "decimal", decimal: 0 };
|
|
546
654
|
}
|
|
547
|
-
if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.
|
|
655
|
+
if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.decimal === 0 || val2.type === "fraction" && val1.type === "decimal" && val1.decimal === 0) {
|
|
548
656
|
const commonDen = den1 * den2;
|
|
549
657
|
const sumNum = num1 * den2 + num2 * den1;
|
|
550
658
|
return simplifyFraction(sumNum, commonDen);
|
|
551
659
|
} else {
|
|
552
660
|
return {
|
|
553
661
|
type: "decimal",
|
|
554
|
-
|
|
662
|
+
decimal: (0, import_big.default)(num1).div(den1).add((0, import_big.default)(num2).div(den2)).toNumber()
|
|
555
663
|
};
|
|
556
664
|
}
|
|
557
665
|
}
|
|
558
|
-
var toRoundedDecimal = (v) => {
|
|
559
|
-
const value = v.type === "decimal" ? v.
|
|
560
|
-
|
|
666
|
+
var toRoundedDecimal = (v, precision = 3) => {
|
|
667
|
+
const value = v.type === "decimal" ? v.decimal : v.num / v.den;
|
|
668
|
+
if (value === 0) {
|
|
669
|
+
return { type: "decimal", decimal: 0 };
|
|
670
|
+
}
|
|
671
|
+
const absValue = Math.abs(value);
|
|
672
|
+
if (absValue >= 1e3) {
|
|
673
|
+
return { type: "decimal", decimal: Math.round(value) };
|
|
674
|
+
}
|
|
675
|
+
const magnitude = Math.floor(Math.log10(absValue));
|
|
676
|
+
const scale = Math.pow(10, precision - 1 - magnitude);
|
|
677
|
+
const rounded = Math.round(value * scale) / scale;
|
|
678
|
+
return { type: "decimal", decimal: rounded };
|
|
679
|
+
};
|
|
680
|
+
var formatOutputValue = (value, unitDef, precision = 3) => {
|
|
681
|
+
if (unitDef.fractions?.enabled) {
|
|
682
|
+
const denominators = unitDef.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
683
|
+
const maxWhole = unitDef.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
684
|
+
const fraction = approximateFraction(
|
|
685
|
+
value,
|
|
686
|
+
denominators,
|
|
687
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
688
|
+
maxWhole
|
|
689
|
+
);
|
|
690
|
+
if (fraction) {
|
|
691
|
+
return fraction;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return toRoundedDecimal({ type: "decimal", decimal: value }, precision);
|
|
561
695
|
};
|
|
562
696
|
function multiplyQuantityValue(value, factor) {
|
|
563
697
|
if (value.type === "fixed") {
|
|
@@ -565,8 +699,8 @@ function multiplyQuantityValue(value, factor) {
|
|
|
565
699
|
value.value,
|
|
566
700
|
(0, import_big.default)(factor)
|
|
567
701
|
);
|
|
568
|
-
if (factor === parseInt(factor.toString()) || // e.g. 2 === int
|
|
569
|
-
(0, import_big.default)(1).div(factor).toNumber() === parseInt((0, import_big.default)(1).div(factor).toString())) {
|
|
702
|
+
if (newValue.type === "fraction" && ((0, import_big.default)(factor).toNumber() === parseInt((0, import_big.default)(factor).toString()) || // e.g. 2 === int
|
|
703
|
+
(0, import_big.default)(1).div(factor).toNumber() === parseInt((0, import_big.default)(1).div(factor).toString()))) {
|
|
570
704
|
return {
|
|
571
705
|
type: "fixed",
|
|
572
706
|
value: newValue
|
|
@@ -583,13 +717,291 @@ function multiplyQuantityValue(value, factor) {
|
|
|
583
717
|
max: multiplyNumericValue(value.max, factor)
|
|
584
718
|
};
|
|
585
719
|
}
|
|
586
|
-
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
720
|
+
function getAverageValue(q) {
|
|
721
|
+
if (q.type === "fixed") {
|
|
722
|
+
return q.value.type === "text" ? q.value.text : getNumericValue(q.value);
|
|
723
|
+
} else {
|
|
724
|
+
return (getNumericValue(q.min) + getNumericValue(q.max)) / 2;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/units/compatibility.ts
|
|
729
|
+
function areUnitsGroupable(u1, u2) {
|
|
730
|
+
if (u1.name === u2.name) {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
if (u1.type === "other" || u2.type === "other") {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
if (u1.type === u2.type && u1.system === u2.system) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
if (u1.type === u2.type) {
|
|
740
|
+
if (u1.system === "ambiguous" && u2.system === "metric" && u1.toBaseBySystem?.metric !== void 0) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
if (u2.system === "ambiguous" && u1.system === "metric" && u2.toBaseBySystem?.metric !== void 0) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
function areUnitsConvertible(u1, u2) {
|
|
750
|
+
if (u1.name === u2.name) return true;
|
|
751
|
+
if (u1.type === "other" || u2.type === "other") return false;
|
|
752
|
+
return u1.type === u2.type;
|
|
753
|
+
}
|
|
754
|
+
function isUnitCompatibleWithSystem(unit, system) {
|
|
755
|
+
if (unit.system === system) return true;
|
|
756
|
+
if (unit.system === "ambiguous") {
|
|
757
|
+
if (unit.toBaseBySystem) {
|
|
758
|
+
return system in unit.toBaseBySystem;
|
|
759
|
+
}
|
|
760
|
+
if (system === "metric") return true;
|
|
761
|
+
}
|
|
762
|
+
if (unit.system === "metric" && system === "JP") {
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/units/conversion.ts
|
|
769
|
+
var EPSILON = 0.01;
|
|
770
|
+
var DEFAULT_MAX_VALUE = 999;
|
|
771
|
+
function isCloseToInteger(value) {
|
|
772
|
+
return Math.abs(value - Math.round(value)) < EPSILON;
|
|
773
|
+
}
|
|
774
|
+
function getMaxValue(unit) {
|
|
775
|
+
return unit.maxValue ?? DEFAULT_MAX_VALUE;
|
|
776
|
+
}
|
|
777
|
+
function isValueInRange(value, unit) {
|
|
778
|
+
const maxValue = getMaxValue(unit);
|
|
779
|
+
if (value >= 1 && value <= maxValue) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
if (value > 0 && value < 1 && unit.fractions?.enabled) {
|
|
783
|
+
const denominators = unit.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
784
|
+
const maxWhole = unit.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
785
|
+
const fraction = approximateFraction(
|
|
786
|
+
value,
|
|
787
|
+
denominators,
|
|
788
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
789
|
+
maxWhole
|
|
790
|
+
);
|
|
791
|
+
return fraction !== null;
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
function findBestUnit(valueInBase, unitType, system, inputUnits) {
|
|
796
|
+
const inputUnitNames = new Set(inputUnits.map((u) => u.name));
|
|
797
|
+
const candidates = units.filter(
|
|
798
|
+
(u) => u.type === unitType && isUnitCompatibleWithSystem(u, system) && (u.isBestUnit !== false || inputUnitNames.has(u.name))
|
|
799
|
+
);
|
|
800
|
+
if (candidates.length === 0) {
|
|
801
|
+
const fallbackUnit = inputUnits[0];
|
|
802
|
+
return {
|
|
803
|
+
unit: fallbackUnit,
|
|
804
|
+
value: valueInBase / getToBase(fallbackUnit, system)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const candidatesWithValues = candidates.map((unit) => ({
|
|
808
|
+
unit,
|
|
809
|
+
value: valueInBase / getToBase(unit, system)
|
|
810
|
+
}));
|
|
811
|
+
const inRange = candidatesWithValues.filter(
|
|
812
|
+
(c) => isValueInRange(c.value, c.unit)
|
|
813
|
+
);
|
|
814
|
+
if (inRange.length > 0) {
|
|
815
|
+
const integersInInputFamily = inRange.filter(
|
|
816
|
+
(c) => isCloseToInteger(c.value) && inputUnitNames.has(c.unit.name)
|
|
817
|
+
);
|
|
818
|
+
if (integersInInputFamily.length > 0) {
|
|
819
|
+
return integersInInputFamily.sort((a2, b) => a2.value - b.value)[0];
|
|
820
|
+
}
|
|
821
|
+
const integersAny = inRange.filter((c) => isCloseToInteger(c.value));
|
|
822
|
+
if (integersAny.length > 0) {
|
|
823
|
+
return integersAny.sort((a2, b) => a2.value - b.value)[0];
|
|
824
|
+
}
|
|
825
|
+
return inRange.sort((a2, b) => {
|
|
826
|
+
const aInFamily = inputUnitNames.has(a2.unit.name) ? 0 : 1;
|
|
827
|
+
const bInFamily = inputUnitNames.has(b.unit.name) ? 0 : 1;
|
|
828
|
+
if (aInFamily !== bInFamily) return aInFamily - bInFamily;
|
|
829
|
+
return a2.value - b.value;
|
|
830
|
+
})[0];
|
|
831
|
+
}
|
|
832
|
+
return candidatesWithValues.sort((a2, b) => {
|
|
833
|
+
const aMaxValue = getMaxValue(a2.unit);
|
|
834
|
+
const bMaxValue = getMaxValue(b.unit);
|
|
835
|
+
const aDistance = a2.value < 1 ? 1 - a2.value : a2.value - aMaxValue;
|
|
836
|
+
const bDistance = b.value < 1 ? 1 - b.value : b.value - bMaxValue;
|
|
837
|
+
return aDistance - bDistance;
|
|
838
|
+
})[0];
|
|
839
|
+
}
|
|
840
|
+
function getUnitRatio(q1, q2) {
|
|
841
|
+
const q1Value = getAverageValue(q1.quantity);
|
|
842
|
+
const q2Value = getAverageValue(q2.quantity);
|
|
843
|
+
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
844
|
+
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
845
|
+
throw Error(
|
|
846
|
+
"One of both values is not a number, so a ratio cannot be computed"
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
850
|
+
}
|
|
851
|
+
function getBaseUnitRatio(q, qRef) {
|
|
852
|
+
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
853
|
+
return q.unit.toBase / qRef.unit.toBase;
|
|
854
|
+
} else {
|
|
855
|
+
return 1;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function getToBase(unit, system) {
|
|
859
|
+
if (unit.system === "ambiguous" && system && unit.toBaseBySystem) {
|
|
860
|
+
return unit.toBaseBySystem[system] ?? unit.toBase;
|
|
861
|
+
}
|
|
862
|
+
return unit.toBase;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/errors.ts
|
|
866
|
+
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
867
|
+
constructor(item_type, item_name, new_modifier) {
|
|
868
|
+
super(
|
|
869
|
+
`The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
|
|
870
|
+
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}`
|
|
871
|
+
);
|
|
872
|
+
this.name = "ReferencedItemCannotBeRedefinedError";
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var NoProductCatalogForCartError = class extends Error {
|
|
876
|
+
constructor() {
|
|
877
|
+
super(
|
|
878
|
+
`Cannot build a cart without a product catalog. Please set one using setProductCatalog()`
|
|
879
|
+
);
|
|
880
|
+
this.name = "NoProductCatalogForCartError";
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
var NoShoppingListForCartError = class extends Error {
|
|
884
|
+
constructor() {
|
|
885
|
+
super(
|
|
886
|
+
`Cannot build a cart without a shopping list. Please set one using setShoppingList()`
|
|
887
|
+
);
|
|
888
|
+
this.name = "NoShoppingListForCartError";
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
var NoProductMatchError = class extends Error {
|
|
892
|
+
constructor(item_name, code) {
|
|
893
|
+
const messageMap = {
|
|
894
|
+
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
895
|
+
noProduct: "No product was found linked to ingredient name ${item_name} in the shopping list",
|
|
896
|
+
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
897
|
+
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
898
|
+
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`
|
|
899
|
+
};
|
|
900
|
+
super(messageMap[code]);
|
|
901
|
+
__publicField(this, "code");
|
|
902
|
+
this.code = code;
|
|
903
|
+
this.name = "NoProductMatchError";
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
var InvalidProductCatalogFormat = class extends Error {
|
|
907
|
+
constructor() {
|
|
908
|
+
super("Invalid product catalog format.");
|
|
909
|
+
this.name = "InvalidProductCatalogFormat";
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
var CannotAddTextValueError = class extends Error {
|
|
913
|
+
constructor() {
|
|
914
|
+
super("Cannot add a quantity with a text value.");
|
|
915
|
+
this.name = "CannotAddTextValueError";
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
var IncompatibleUnitsError = class extends Error {
|
|
919
|
+
constructor(unit1, unit2) {
|
|
920
|
+
super(
|
|
921
|
+
`Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
|
|
922
|
+
);
|
|
923
|
+
this.name = "IncompatibleUnitsError";
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
var InvalidQuantityFormat = class extends Error {
|
|
927
|
+
constructor(value, extra) {
|
|
928
|
+
super(
|
|
929
|
+
`Invalid quantity format found in: ${value}${extra ? ` (${extra})` : ""}`
|
|
930
|
+
);
|
|
931
|
+
this.name = "InvalidQuantityFormat";
|
|
932
|
+
}
|
|
590
933
|
};
|
|
934
|
+
|
|
935
|
+
// src/utils/type_guards.ts
|
|
936
|
+
function isGroup(x) {
|
|
937
|
+
return "and" in x || "or" in x;
|
|
938
|
+
}
|
|
939
|
+
function isOrGroup(x) {
|
|
940
|
+
return isGroup(x) && "or" in x;
|
|
941
|
+
}
|
|
942
|
+
function isAndGroup(x) {
|
|
943
|
+
return "and" in x;
|
|
944
|
+
}
|
|
945
|
+
function isQuantity(x) {
|
|
946
|
+
return x && typeof x === "object" && "quantity" in x;
|
|
947
|
+
}
|
|
948
|
+
function isSimpleGroup(entry) {
|
|
949
|
+
return "quantity" in entry;
|
|
950
|
+
}
|
|
951
|
+
function isNumericValueIntegerLike(v) {
|
|
952
|
+
if (v.type === "decimal") return Number.isInteger(v.decimal);
|
|
953
|
+
return v.num % v.den === 0;
|
|
954
|
+
}
|
|
955
|
+
function isValueIntegerLike(q) {
|
|
956
|
+
if (q.type === "fixed") {
|
|
957
|
+
if (q.value.type === "text") return false;
|
|
958
|
+
return isNumericValueIntegerLike(q.value);
|
|
959
|
+
}
|
|
960
|
+
return isNumericValueIntegerLike(q.min) && isNumericValueIntegerLike(q.max);
|
|
961
|
+
}
|
|
962
|
+
function hasAlternatives(entry) {
|
|
963
|
+
return "alternatives" in entry && Array.isArray(entry.alternatives) && entry.alternatives.length > 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/quantities/mutations.ts
|
|
967
|
+
function extendAllUnits(q) {
|
|
968
|
+
if (isAndGroup(q)) {
|
|
969
|
+
return { and: q.and.map(extendAllUnits) };
|
|
970
|
+
} else if (isOrGroup(q)) {
|
|
971
|
+
return { or: q.or.map(extendAllUnits) };
|
|
972
|
+
} else {
|
|
973
|
+
const newQ = {
|
|
974
|
+
quantity: q.quantity
|
|
975
|
+
};
|
|
976
|
+
if (q.unit) {
|
|
977
|
+
newQ.unit = { name: q.unit };
|
|
978
|
+
}
|
|
979
|
+
return newQ;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function normalizeAllUnits(q) {
|
|
983
|
+
if (isAndGroup(q)) {
|
|
984
|
+
return { and: q.and.map(normalizeAllUnits) };
|
|
985
|
+
} else if (isOrGroup(q)) {
|
|
986
|
+
return { or: q.or.map(normalizeAllUnits) };
|
|
987
|
+
} else {
|
|
988
|
+
const newQ = {
|
|
989
|
+
quantity: q.quantity,
|
|
990
|
+
unit: resolveUnit(q.unit)
|
|
991
|
+
};
|
|
992
|
+
if (q.equivalents && q.equivalents.length > 0) {
|
|
993
|
+
const equivalentsNormalized = q.equivalents.map(
|
|
994
|
+
(eq) => normalizeAllUnits(eq)
|
|
995
|
+
);
|
|
996
|
+
return {
|
|
997
|
+
or: [newQ, ...equivalentsNormalized]
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
return newQ;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
591
1003
|
function getDefaultQuantityValue() {
|
|
592
|
-
return { type: "fixed", value: { type: "decimal",
|
|
1004
|
+
return { type: "fixed", value: { type: "decimal", decimal: 0 } };
|
|
593
1005
|
}
|
|
594
1006
|
function addQuantityValues(v1, v2) {
|
|
595
1007
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
@@ -614,201 +1026,406 @@ function addQuantityValues(v1, v2) {
|
|
|
614
1026
|
);
|
|
615
1027
|
return { type: "range", min: newMin, max: newMax };
|
|
616
1028
|
}
|
|
617
|
-
function addQuantities(q1, q2) {
|
|
618
|
-
const v1 = q1.
|
|
619
|
-
const v2 = q2.
|
|
1029
|
+
function addQuantities(q1, q2, system) {
|
|
1030
|
+
const v1 = q1.quantity;
|
|
1031
|
+
const v2 = q2.quantity;
|
|
620
1032
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
621
1033
|
throw new CannotAddTextValueError();
|
|
622
1034
|
}
|
|
623
|
-
const unit1Def = normalizeUnit(q1.unit);
|
|
624
|
-
const unit2Def = normalizeUnit(q2.unit);
|
|
625
|
-
const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({
|
|
626
|
-
|
|
1035
|
+
const unit1Def = normalizeUnit(q1.unit?.name);
|
|
1036
|
+
const unit2Def = normalizeUnit(q2.unit?.name);
|
|
1037
|
+
const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({
|
|
1038
|
+
quantity: addQuantityValues(val1, val2),
|
|
1039
|
+
unit
|
|
1040
|
+
});
|
|
1041
|
+
if ((q1.unit?.name === "" || q1.unit === void 0) && q2.unit !== void 0) {
|
|
627
1042
|
return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
|
|
628
1043
|
}
|
|
629
|
-
if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
1044
|
+
if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
630
1045
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
631
1046
|
}
|
|
632
|
-
if (!q1.unit && !q2.unit
|
|
1047
|
+
if (!q1.unit && !q2.unit) {
|
|
633
1048
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
634
1049
|
}
|
|
635
|
-
if (
|
|
636
|
-
if (unit1Def
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
);
|
|
1050
|
+
if (q1.unit && q2.unit && q1.unit.name.toLowerCase() === q2.unit.name.toLowerCase()) {
|
|
1051
|
+
if (unit1Def) {
|
|
1052
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unit1Def.system) ? unit1Def.system : "US");
|
|
1053
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit1Def, effectiveSystem, [
|
|
1054
|
+
unit1Def
|
|
1055
|
+
]);
|
|
641
1056
|
}
|
|
642
|
-
|
|
643
|
-
if (unit1Def.system !== unit2Def.system) {
|
|
644
|
-
const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
|
|
645
|
-
targetUnitDef = units.filter((u) => u.type === metricUnitDef.type && u.system === "metric").reduce(
|
|
646
|
-
(prev, current) => prev.toBase > current.toBase ? prev : current
|
|
647
|
-
);
|
|
648
|
-
} else {
|
|
649
|
-
targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
|
|
650
|
-
}
|
|
651
|
-
const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
|
|
652
|
-
const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
|
|
653
|
-
return addQuantityValuesAndSetUnit(
|
|
654
|
-
convertedV1,
|
|
655
|
-
convertedV2,
|
|
656
|
-
targetUnitDef.name
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
throw new IncompatibleUnitsError(q1.unit, q2.unit);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// src/errors.ts
|
|
663
|
-
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
664
|
-
constructor(item_type, item_name, new_modifier) {
|
|
665
|
-
super(
|
|
666
|
-
`The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
|
|
667
|
-
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}`
|
|
668
|
-
);
|
|
669
|
-
this.name = "ReferencedItemCannotBeRedefinedError";
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
// src/parser_helpers.ts
|
|
674
|
-
function flushPendingNote(section, note) {
|
|
675
|
-
if (note.length > 0) {
|
|
676
|
-
section.content.push({ type: "note", note });
|
|
677
|
-
return "";
|
|
678
|
-
}
|
|
679
|
-
return note;
|
|
680
|
-
}
|
|
681
|
-
function flushPendingItems(section, items) {
|
|
682
|
-
if (items.length > 0) {
|
|
683
|
-
section.content.push({ type: "step", items: [...items] });
|
|
684
|
-
items.length = 0;
|
|
685
|
-
return true;
|
|
1057
|
+
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
686
1058
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const indexFind = ingredients.findIndex(
|
|
693
|
-
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
694
|
-
);
|
|
695
|
-
if (indexFind === -1) {
|
|
696
|
-
throw new Error(
|
|
697
|
-
`Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
|
|
1059
|
+
if (unit1Def && unit2Def) {
|
|
1060
|
+
if (!areUnitsConvertible(unit1Def, unit2Def)) {
|
|
1061
|
+
throw new IncompatibleUnitsError(
|
|
1062
|
+
`${unit1Def.type} (${q1.unit?.name})`,
|
|
1063
|
+
`${unit2Def.type} (${q2.unit?.name})`
|
|
698
1064
|
);
|
|
699
1065
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
let quantityPartIndex = void 0;
|
|
711
|
-
if (quantity !== void 0) {
|
|
712
|
-
const currentQuantity = {
|
|
713
|
-
value: existingIngredient.quantity ?? getDefaultQuantityValue(),
|
|
714
|
-
unit: existingIngredient.unit ?? ""
|
|
715
|
-
};
|
|
716
|
-
const newQuantity = { value: quantity, unit: unit ?? "" };
|
|
717
|
-
try {
|
|
718
|
-
const total = addQuantities(currentQuantity, newQuantity);
|
|
719
|
-
existingIngredient.quantity = total.value;
|
|
720
|
-
existingIngredient.unit = total.unit || void 0;
|
|
721
|
-
if (existingIngredient.quantityParts) {
|
|
722
|
-
existingIngredient.quantityParts.push(
|
|
723
|
-
...newIngredient.quantityParts
|
|
724
|
-
);
|
|
1066
|
+
let effectiveSystem = system;
|
|
1067
|
+
if (!effectiveSystem) {
|
|
1068
|
+
if (unit1Def.system === "metric" || unit2Def.system === "metric") {
|
|
1069
|
+
effectiveSystem = "metric";
|
|
1070
|
+
} else {
|
|
1071
|
+
if (unit1Def.system === "JP" && unit2Def.system === "JP") {
|
|
1072
|
+
effectiveSystem = "JP";
|
|
725
1073
|
} else {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
} catch (e2) {
|
|
730
|
-
if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
|
|
731
|
-
return {
|
|
732
|
-
ingredientIndex: ingredients.push(newIngredient) - 1,
|
|
733
|
-
quantityPartIndex: 0
|
|
734
|
-
};
|
|
1074
|
+
const unit1SupportsUS = unit1Def.system === "US" || unit1Def.system === "ambiguous" && unit1Def.toBaseBySystem && "US" in unit1Def.toBaseBySystem;
|
|
1075
|
+
const unit2SupportsUS = unit2Def.system === "US" || unit2Def.system === "ambiguous" && unit2Def.toBaseBySystem && "US" in unit2Def.toBaseBySystem;
|
|
1076
|
+
effectiveSystem = unit1SupportsUS && unit2SupportsUS ? "US" : "metric";
|
|
735
1077
|
}
|
|
736
1078
|
}
|
|
737
1079
|
}
|
|
1080
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit2Def, effectiveSystem, [
|
|
1081
|
+
unit1Def,
|
|
1082
|
+
unit2Def
|
|
1083
|
+
]);
|
|
1084
|
+
}
|
|
1085
|
+
throw new IncompatibleUnitsError(
|
|
1086
|
+
q1.unit?.name,
|
|
1087
|
+
q2.unit?.name
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
function addAndFindBestUnit(v1, v2, unit1Def, unit2Def, system, inputUnits) {
|
|
1091
|
+
const toBase1 = getToBase(unit1Def, system);
|
|
1092
|
+
const toBase2 = getToBase(unit2Def, system);
|
|
1093
|
+
let sumInBase;
|
|
1094
|
+
if (v1.type === "fixed" && v2.type === "fixed") {
|
|
1095
|
+
const val1 = getNumericValue(v1.value);
|
|
1096
|
+
const val2 = getNumericValue(v2.value);
|
|
1097
|
+
sumInBase = val1 * toBase1 + val2 * toBase2;
|
|
1098
|
+
} else {
|
|
1099
|
+
const avg1 = getAverageValue(v1);
|
|
1100
|
+
const avg2 = getAverageValue(v2);
|
|
1101
|
+
sumInBase = avg1 * toBase1 + avg2 * toBase2;
|
|
1102
|
+
}
|
|
1103
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1104
|
+
sumInBase,
|
|
1105
|
+
unit1Def.type,
|
|
1106
|
+
system,
|
|
1107
|
+
inputUnits
|
|
1108
|
+
);
|
|
1109
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1110
|
+
if (v1.type === "range" || v2.type === "range") {
|
|
1111
|
+
const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
|
|
1112
|
+
const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
|
|
1113
|
+
const minInBase = getNumericValue(r1.min) * toBase1 + getNumericValue(r2.min) * toBase2;
|
|
1114
|
+
const maxInBase = getNumericValue(r1.max) * toBase1 + getNumericValue(r2.max) * toBase2;
|
|
1115
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1116
|
+
const minValue = minInBase / bestToBase;
|
|
1117
|
+
const maxValue = maxInBase / bestToBase;
|
|
738
1118
|
return {
|
|
739
|
-
|
|
740
|
-
|
|
1119
|
+
quantity: {
|
|
1120
|
+
type: "range",
|
|
1121
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1122
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1123
|
+
},
|
|
1124
|
+
unit: { name: bestUnit.name }
|
|
741
1125
|
};
|
|
742
1126
|
}
|
|
743
1127
|
return {
|
|
744
|
-
|
|
745
|
-
|
|
1128
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1129
|
+
unit: { name: bestUnit.name }
|
|
746
1130
|
};
|
|
747
1131
|
}
|
|
748
|
-
function
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1132
|
+
function convertQuantityToSystem(quantity, system) {
|
|
1133
|
+
const unitDef = resolveUnit(
|
|
1134
|
+
typeof quantity.unit === "string" ? quantity.unit : quantity.unit?.name
|
|
1135
|
+
);
|
|
1136
|
+
if (unitDef.type === "other" || !("toBase" in unitDef)) {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
const avgValue = getAverageValue(quantity.quantity);
|
|
1140
|
+
if (typeof avgValue !== "number") {
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
const toBase = getToBase(unitDef, system);
|
|
1144
|
+
const valueInBase = avgValue * toBase;
|
|
1145
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1146
|
+
valueInBase,
|
|
1147
|
+
unitDef.type,
|
|
1148
|
+
system,
|
|
1149
|
+
[unitDef]
|
|
1150
|
+
);
|
|
1151
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1152
|
+
if (quantity.quantity.type === "range") {
|
|
1153
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1154
|
+
const minValue = getNumericValue(quantity.quantity.min) * toBase / bestToBase;
|
|
1155
|
+
const maxValue = getNumericValue(quantity.quantity.max) * toBase / bestToBase;
|
|
1156
|
+
return {
|
|
1157
|
+
quantity: {
|
|
1158
|
+
type: "range",
|
|
1159
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1160
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1161
|
+
},
|
|
1162
|
+
unit: { name: bestUnit.name }
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1167
|
+
unit: { name: bestUnit.name }
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function toPlainUnit(quantity) {
|
|
1171
|
+
if (isQuantity(quantity))
|
|
1172
|
+
return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
|
|
1173
|
+
else if (isOrGroup(quantity)) {
|
|
1174
|
+
return {
|
|
1175
|
+
or: quantity.or.map(toPlainUnit)
|
|
1176
|
+
};
|
|
1177
|
+
} else {
|
|
1178
|
+
return {
|
|
1179
|
+
and: quantity.and.map(toPlainUnit)
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function toExtendedUnit(q) {
|
|
1184
|
+
if (isQuantity(q)) {
|
|
1185
|
+
return q.unit ? { ...q, unit: { name: q.unit } } : q;
|
|
1186
|
+
} else if (isOrGroup(q)) {
|
|
1187
|
+
return { or: q.or.map(toExtendedUnit) };
|
|
1188
|
+
} else {
|
|
1189
|
+
return { and: q.and.map(toExtendedUnit) };
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function deNormalizeQuantity(q) {
|
|
1193
|
+
const result = {
|
|
1194
|
+
quantity: q.quantity
|
|
1195
|
+
};
|
|
1196
|
+
if (!isNoUnit(q.unit)) {
|
|
1197
|
+
result.unit = { name: q.unit.name };
|
|
1198
|
+
}
|
|
1199
|
+
return result;
|
|
1200
|
+
}
|
|
1201
|
+
var flattenPlainUnitGroup = (summed) => {
|
|
1202
|
+
if (isOrGroup(summed)) {
|
|
1203
|
+
const entries = summed.or;
|
|
1204
|
+
const andGroupEntry = entries.find(
|
|
1205
|
+
(e2) => isAndGroup(e2)
|
|
1206
|
+
);
|
|
1207
|
+
if (andGroupEntry) {
|
|
1208
|
+
const andEntries = [];
|
|
1209
|
+
const addGroupEntryContent = andGroupEntry.and;
|
|
1210
|
+
for (const entry of addGroupEntryContent) {
|
|
1211
|
+
andEntries.push({
|
|
1212
|
+
quantity: entry.quantity,
|
|
1213
|
+
...entry.unit && { unit: entry.unit }
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
const equivalentsList = entries.filter((e2) => isQuantity(e2)).map((e2) => ({ quantity: e2.quantity, unit: e2.unit }));
|
|
1217
|
+
if (equivalentsList.length > 0) {
|
|
1218
|
+
return [
|
|
1219
|
+
{
|
|
1220
|
+
and: andEntries,
|
|
1221
|
+
equivalents: equivalentsList
|
|
1222
|
+
}
|
|
1223
|
+
];
|
|
1224
|
+
} else {
|
|
1225
|
+
return andEntries;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const simpleEntries = entries.filter(
|
|
1229
|
+
(e2) => isQuantity(e2)
|
|
1230
|
+
);
|
|
1231
|
+
if (simpleEntries.length > 0) {
|
|
1232
|
+
const result = {
|
|
1233
|
+
quantity: simpleEntries[0].quantity,
|
|
1234
|
+
unit: simpleEntries[0].unit
|
|
1235
|
+
};
|
|
1236
|
+
if (simpleEntries.length > 1) {
|
|
1237
|
+
result.equivalents = simpleEntries.slice(1);
|
|
1238
|
+
}
|
|
1239
|
+
return [result];
|
|
1240
|
+
} else {
|
|
1241
|
+
const first = entries[0];
|
|
1242
|
+
return [{ quantity: first.quantity, unit: first.unit }];
|
|
1243
|
+
}
|
|
1244
|
+
} else if (isAndGroup(summed)) {
|
|
1245
|
+
const andEntries = [];
|
|
1246
|
+
const equivalentsList = [];
|
|
1247
|
+
for (const entry of summed.and) {
|
|
1248
|
+
if (isOrGroup(entry)) {
|
|
1249
|
+
const orEntries = entry.or;
|
|
1250
|
+
andEntries.push({
|
|
1251
|
+
quantity: orEntries[0].quantity,
|
|
1252
|
+
...orEntries[0].unit && { unit: orEntries[0].unit }
|
|
1253
|
+
});
|
|
1254
|
+
equivalentsList.push(...orEntries.slice(1));
|
|
1255
|
+
} else if (isQuantity(entry)) {
|
|
1256
|
+
andEntries.push({
|
|
1257
|
+
quantity: entry.quantity,
|
|
1258
|
+
...entry.unit && { unit: entry.unit }
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (equivalentsList.length === 0) {
|
|
1263
|
+
return andEntries;
|
|
1264
|
+
}
|
|
1265
|
+
const result = {
|
|
1266
|
+
and: andEntries,
|
|
1267
|
+
equivalents: equivalentsList
|
|
1268
|
+
};
|
|
1269
|
+
return [result];
|
|
1270
|
+
} else {
|
|
1271
|
+
return [
|
|
1272
|
+
{ quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
|
|
1273
|
+
];
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
function applyBestUnit(q, system) {
|
|
1277
|
+
if (!q.unit?.name) {
|
|
1278
|
+
return q;
|
|
1279
|
+
}
|
|
1280
|
+
const unitDef = resolveUnit(q.unit.name);
|
|
1281
|
+
if (unitDef.type === "other") {
|
|
1282
|
+
return q;
|
|
1283
|
+
}
|
|
1284
|
+
if (q.quantity.type === "fixed" && q.quantity.value.type === "text") {
|
|
1285
|
+
return q;
|
|
1286
|
+
}
|
|
1287
|
+
const avgValue = getAverageValue(q.quantity);
|
|
1288
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unitDef.system) ? unitDef.system : "US");
|
|
1289
|
+
const toBase = getToBase(unitDef, effectiveSystem);
|
|
1290
|
+
const valueInBase = avgValue * toBase;
|
|
1291
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1292
|
+
valueInBase,
|
|
1293
|
+
unitDef.type,
|
|
1294
|
+
effectiveSystem,
|
|
1295
|
+
[unitDef]
|
|
1296
|
+
);
|
|
1297
|
+
const originalCanonicalName = normalizeUnit(q.unit.name)?.name;
|
|
1298
|
+
if (bestUnit.name === originalCanonicalName) {
|
|
1299
|
+
return q;
|
|
1300
|
+
}
|
|
1301
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1302
|
+
if (q.quantity.type === "range") {
|
|
1303
|
+
const bestToBase = getToBase(bestUnit, effectiveSystem);
|
|
1304
|
+
const minValue = getNumericValue(q.quantity.min) * toBase / bestToBase;
|
|
1305
|
+
const maxValue = getNumericValue(q.quantity.max) * toBase / bestToBase;
|
|
1306
|
+
return {
|
|
1307
|
+
quantity: {
|
|
1308
|
+
type: "range",
|
|
1309
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1310
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1311
|
+
},
|
|
1312
|
+
unit: { name: bestUnit.name }
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
quantity: {
|
|
1317
|
+
type: "fixed",
|
|
1318
|
+
value: formattedValue
|
|
1319
|
+
},
|
|
1320
|
+
unit: { name: bestUnit.name }
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/utils/parser_helpers.ts
|
|
1325
|
+
function flushPendingNote(section, noteItems) {
|
|
1326
|
+
if (noteItems.length > 0) {
|
|
1327
|
+
section.content.push({ type: "note", items: [...noteItems] });
|
|
1328
|
+
return [];
|
|
1329
|
+
}
|
|
1330
|
+
return noteItems;
|
|
1331
|
+
}
|
|
1332
|
+
function flushPendingItems(section, items) {
|
|
1333
|
+
if (items.length > 0) {
|
|
1334
|
+
section.content.push({ type: "step", items: [...items] });
|
|
1335
|
+
items.length = 0;
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
|
|
1341
|
+
const { name } = newIngredient;
|
|
1342
|
+
if (isReference) {
|
|
1343
|
+
const indexFind = ingredients.findIndex(
|
|
1344
|
+
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
1345
|
+
);
|
|
1346
|
+
if (indexFind === -1) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
const existingIngredient = ingredients[indexFind];
|
|
1352
|
+
if (!newIngredient.flags) {
|
|
1353
|
+
if (Array.isArray(existingIngredient.flags) && existingIngredient.flags.length > 0) {
|
|
1354
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
1355
|
+
"ingredient",
|
|
1356
|
+
existingIngredient.name,
|
|
1357
|
+
existingIngredient.flags[0]
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
} else {
|
|
1361
|
+
for (const flag of newIngredient.flags) {
|
|
1362
|
+
if (existingIngredient.flags === void 0 || !existingIngredient.flags.includes(flag)) {
|
|
1363
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
1364
|
+
"ingredient",
|
|
1365
|
+
existingIngredient.name,
|
|
1366
|
+
flag
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return indexFind;
|
|
1372
|
+
}
|
|
1373
|
+
return ingredients.push(newIngredient) - 1;
|
|
1374
|
+
}
|
|
1375
|
+
function findAndUpsertCookware(cookware, newCookware, isReference) {
|
|
1376
|
+
const { name, quantity } = newCookware;
|
|
1377
|
+
if (isReference) {
|
|
1378
|
+
const index = cookware.findIndex(
|
|
1379
|
+
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
1380
|
+
);
|
|
1381
|
+
if (index === -1) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
const existingCookware = cookware[index];
|
|
1387
|
+
if (!newCookware.flags) {
|
|
1388
|
+
if (Array.isArray(existingCookware.flags) && existingCookware.flags.length > 0) {
|
|
1389
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
763
1390
|
"cookware",
|
|
764
1391
|
existingCookware.name,
|
|
765
|
-
|
|
1392
|
+
existingCookware.flags[0]
|
|
766
1393
|
);
|
|
767
1394
|
}
|
|
1395
|
+
} else {
|
|
1396
|
+
for (const flag of newCookware.flags) {
|
|
1397
|
+
if (existingCookware.flags === void 0 || !existingCookware.flags.includes(flag)) {
|
|
1398
|
+
throw new ReferencedItemCannotBeRedefinedError(
|
|
1399
|
+
"cookware",
|
|
1400
|
+
existingCookware.name,
|
|
1401
|
+
flag
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
768
1405
|
}
|
|
769
|
-
let quantityPartIndex = void 0;
|
|
770
1406
|
if (quantity !== void 0) {
|
|
771
1407
|
if (!existingCookware.quantity) {
|
|
772
1408
|
existingCookware.quantity = quantity;
|
|
773
|
-
existingCookware.quantityParts = newCookware.quantityParts;
|
|
774
|
-
quantityPartIndex = 0;
|
|
775
1409
|
} else {
|
|
776
1410
|
try {
|
|
777
1411
|
existingCookware.quantity = addQuantityValues(
|
|
778
1412
|
existingCookware.quantity,
|
|
779
1413
|
quantity
|
|
780
1414
|
);
|
|
781
|
-
if (!existingCookware.quantityParts) {
|
|
782
|
-
existingCookware.quantityParts = newCookware.quantityParts;
|
|
783
|
-
quantityPartIndex = 0;
|
|
784
|
-
} else {
|
|
785
|
-
quantityPartIndex = existingCookware.quantityParts.push(
|
|
786
|
-
...newCookware.quantityParts
|
|
787
|
-
) - 1;
|
|
788
|
-
}
|
|
789
1415
|
} catch (e2) {
|
|
790
1416
|
if (e2 instanceof CannotAddTextValueError) {
|
|
791
|
-
return
|
|
792
|
-
cookwareIndex: cookware.push(newCookware) - 1,
|
|
793
|
-
quantityPartIndex: 0
|
|
794
|
-
};
|
|
1417
|
+
return cookware.push(newCookware) - 1;
|
|
795
1418
|
}
|
|
796
1419
|
}
|
|
797
1420
|
}
|
|
798
1421
|
}
|
|
799
|
-
return
|
|
800
|
-
cookwareIndex: index,
|
|
801
|
-
quantityPartIndex
|
|
802
|
-
};
|
|
1422
|
+
return index;
|
|
803
1423
|
}
|
|
804
|
-
return
|
|
805
|
-
cookwareIndex: cookware.push(newCookware) - 1,
|
|
806
|
-
quantityPartIndex: quantity ? 0 : void 0
|
|
807
|
-
};
|
|
1424
|
+
return cookware.push(newCookware) - 1;
|
|
808
1425
|
}
|
|
809
1426
|
var parseFixedValue = (input_str) => {
|
|
810
1427
|
if (!numberLikeRegex.test(input_str)) {
|
|
811
|
-
return { type: "text",
|
|
1428
|
+
return { type: "text", text: input_str };
|
|
812
1429
|
}
|
|
813
1430
|
const s = input_str.trim().replace(",", ".");
|
|
814
1431
|
if (s.includes("/")) {
|
|
@@ -817,8 +1434,22 @@ var parseFixedValue = (input_str) => {
|
|
|
817
1434
|
const den = Number(parts[1]);
|
|
818
1435
|
return { type: "fraction", num, den };
|
|
819
1436
|
}
|
|
820
|
-
return { type: "decimal",
|
|
1437
|
+
return { type: "decimal", decimal: Number(s) };
|
|
821
1438
|
};
|
|
1439
|
+
function stringifyQuantityValue(quantity) {
|
|
1440
|
+
if (quantity.type === "fixed") {
|
|
1441
|
+
return stringifyFixedValue(quantity);
|
|
1442
|
+
} else {
|
|
1443
|
+
return `${stringifyFixedValue({ type: "fixed", value: quantity.min })}-${stringifyFixedValue({ type: "fixed", value: quantity.max })}`;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function stringifyFixedValue(quantity) {
|
|
1447
|
+
if (quantity.value.type === "fraction")
|
|
1448
|
+
return `${quantity.value.num}/${quantity.value.den}`;
|
|
1449
|
+
else if (quantity.value.type === "decimal")
|
|
1450
|
+
return String(quantity.value.decimal);
|
|
1451
|
+
else return quantity.value.text;
|
|
1452
|
+
}
|
|
822
1453
|
function parseQuantityInput(input_str) {
|
|
823
1454
|
const clean_str = String(input_str).trim();
|
|
824
1455
|
if (rangeRegex.test(clean_str)) {
|
|
@@ -860,7 +1491,7 @@ function parseListMetaVar(content, varName) {
|
|
|
860
1491
|
function extractMetadata(content) {
|
|
861
1492
|
const metadata = {};
|
|
862
1493
|
let servings = void 0;
|
|
863
|
-
const metadataContent = content.match(metadataRegex)?.[
|
|
1494
|
+
const metadataContent = content.match(metadataRegex)?.[2];
|
|
864
1495
|
if (!metadataContent) {
|
|
865
1496
|
return { metadata };
|
|
866
1497
|
}
|
|
@@ -892,6 +1523,18 @@ function extractMetadata(content) {
|
|
|
892
1523
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
893
1524
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
894
1525
|
}
|
|
1526
|
+
let unitSystem;
|
|
1527
|
+
const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
|
|
1528
|
+
if (unitSystemRaw) {
|
|
1529
|
+
metadata["unit system"] = unitSystemRaw;
|
|
1530
|
+
const unitSystemMap = {
|
|
1531
|
+
metric: "metric",
|
|
1532
|
+
us: "US",
|
|
1533
|
+
uk: "UK",
|
|
1534
|
+
jp: "JP"
|
|
1535
|
+
};
|
|
1536
|
+
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
1537
|
+
}
|
|
895
1538
|
for (const metaVar of ["serves", "yield", "servings"]) {
|
|
896
1539
|
const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
|
|
897
1540
|
if (scalingMetaValue && scalingMetaValue[1]) {
|
|
@@ -903,12 +1546,521 @@ function extractMetadata(content) {
|
|
|
903
1546
|
const listMetaValue = parseListMetaVar(metadataContent, metaVar);
|
|
904
1547
|
if (listMetaValue) metadata[metaVar] = listMetaValue;
|
|
905
1548
|
}
|
|
906
|
-
return { metadata, servings };
|
|
1549
|
+
return { metadata, servings, unitSystem };
|
|
1550
|
+
}
|
|
1551
|
+
function isPositiveIntegerString(str) {
|
|
1552
|
+
return /^\d+$/.test(str);
|
|
1553
|
+
}
|
|
1554
|
+
function unionOfSets(s1, s2) {
|
|
1555
|
+
const result = new Set(s1);
|
|
1556
|
+
for (const item of s2) {
|
|
1557
|
+
result.add(item);
|
|
1558
|
+
}
|
|
1559
|
+
return result;
|
|
1560
|
+
}
|
|
1561
|
+
function getAlternativeSignature(alternatives) {
|
|
1562
|
+
if (!alternatives || alternatives.length === 0) return null;
|
|
1563
|
+
return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/classes/product_catalog.ts
|
|
1567
|
+
var ProductCatalog = class {
|
|
1568
|
+
constructor(tomlContent) {
|
|
1569
|
+
__publicField(this, "products", []);
|
|
1570
|
+
if (tomlContent) this.parse(tomlContent);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Parses a TOML string into a list of product options.
|
|
1574
|
+
* @param tomlContent - The TOML string to parse.
|
|
1575
|
+
* @returns A parsed list of `ProductOption`.
|
|
1576
|
+
*/
|
|
1577
|
+
parse(tomlContent) {
|
|
1578
|
+
const catalogRaw = import_smol_toml.default.parse(tomlContent);
|
|
1579
|
+
this.products = [];
|
|
1580
|
+
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1581
|
+
throw new InvalidProductCatalogFormat();
|
|
1582
|
+
}
|
|
1583
|
+
for (const [ingredientName, ingredientData] of Object.entries(catalogRaw)) {
|
|
1584
|
+
const ingredientTable = ingredientData;
|
|
1585
|
+
const aliases = ingredientTable.aliases;
|
|
1586
|
+
for (const [key, productData] of Object.entries(ingredientTable)) {
|
|
1587
|
+
if (key === "aliases") {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
const productId = key;
|
|
1591
|
+
const { name, size, price, ...rest } = productData;
|
|
1592
|
+
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
1593
|
+
const sizes = sizeStrings.map((sizeStr) => {
|
|
1594
|
+
const sizeAndUnitRaw = sizeStr.split("%");
|
|
1595
|
+
const sizeParsed = parseQuantityInput(
|
|
1596
|
+
sizeAndUnitRaw[0]
|
|
1597
|
+
);
|
|
1598
|
+
const productSize = { size: sizeParsed };
|
|
1599
|
+
if (sizeAndUnitRaw.length > 1) {
|
|
1600
|
+
productSize.unit = sizeAndUnitRaw[1];
|
|
1601
|
+
}
|
|
1602
|
+
return productSize;
|
|
1603
|
+
});
|
|
1604
|
+
const productOption = {
|
|
1605
|
+
id: productId,
|
|
1606
|
+
productName: name,
|
|
1607
|
+
ingredientName,
|
|
1608
|
+
price,
|
|
1609
|
+
sizes,
|
|
1610
|
+
...rest
|
|
1611
|
+
};
|
|
1612
|
+
if (aliases) {
|
|
1613
|
+
productOption.ingredientAliases = aliases;
|
|
1614
|
+
}
|
|
1615
|
+
this.products.push(productOption);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return this.products;
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Stringifies the catalog to a TOML string.
|
|
1622
|
+
* @returns The TOML string representation of the catalog.
|
|
1623
|
+
*/
|
|
1624
|
+
stringify() {
|
|
1625
|
+
const grouped = {};
|
|
1626
|
+
for (const product of this.products) {
|
|
1627
|
+
const {
|
|
1628
|
+
id,
|
|
1629
|
+
ingredientName,
|
|
1630
|
+
ingredientAliases,
|
|
1631
|
+
sizes,
|
|
1632
|
+
productName,
|
|
1633
|
+
...rest
|
|
1634
|
+
} = product;
|
|
1635
|
+
if (!grouped[ingredientName]) {
|
|
1636
|
+
grouped[ingredientName] = {};
|
|
1637
|
+
}
|
|
1638
|
+
if (ingredientAliases && !grouped[ingredientName].aliases) {
|
|
1639
|
+
grouped[ingredientName].aliases = ingredientAliases;
|
|
1640
|
+
}
|
|
1641
|
+
const sizeStrings = sizes.map(
|
|
1642
|
+
(s) => s.unit ? `${stringifyQuantityValue(s.size)}%${s.unit}` : stringifyQuantityValue(s.size)
|
|
1643
|
+
);
|
|
1644
|
+
grouped[ingredientName][id] = {
|
|
1645
|
+
...rest,
|
|
1646
|
+
name: productName,
|
|
1647
|
+
// Use array if multiple sizes, otherwise single string
|
|
1648
|
+
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
return import_smol_toml.default.stringify(grouped);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Adds a product to the catalog.
|
|
1655
|
+
* @param productOption - The product to add.
|
|
1656
|
+
*/
|
|
1657
|
+
add(productOption) {
|
|
1658
|
+
this.products.push(productOption);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Removes a product from the catalog by its ID.
|
|
1662
|
+
* @param productId - The ID of the product to remove.
|
|
1663
|
+
*/
|
|
1664
|
+
remove(productId) {
|
|
1665
|
+
this.products = this.products.filter((product) => product.id !== productId);
|
|
1666
|
+
}
|
|
1667
|
+
isValidTomlContent(catalog) {
|
|
1668
|
+
for (const productsRaw of Object.values(catalog)) {
|
|
1669
|
+
if (typeof productsRaw !== "object" || productsRaw === null) {
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
for (const [id, obj] of Object.entries(productsRaw)) {
|
|
1673
|
+
if (id === "aliases") {
|
|
1674
|
+
if (!Array.isArray(obj)) {
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
} else {
|
|
1678
|
+
if (!isPositiveIntegerString(id)) {
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
const record = obj;
|
|
1685
|
+
const keys = Object.keys(record);
|
|
1686
|
+
const mandatoryKeys = ["name", "size", "price"];
|
|
1687
|
+
if (mandatoryKeys.some((key) => !keys.includes(key))) {
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
const hasProductName = typeof record.name === "string";
|
|
1691
|
+
const hasSize = typeof record.size === "string" || Array.isArray(record.size) && record.size.every((s) => typeof s === "string");
|
|
1692
|
+
const hasPrice = typeof record.price === "number";
|
|
1693
|
+
if (!(hasProductName && hasSize && hasPrice)) {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// src/classes/section.ts
|
|
1704
|
+
var Section = class {
|
|
1705
|
+
/**
|
|
1706
|
+
* Creates an instance of Section.
|
|
1707
|
+
* @param name - The name of the section. Defaults to an empty string.
|
|
1708
|
+
*/
|
|
1709
|
+
constructor(name = "") {
|
|
1710
|
+
/**
|
|
1711
|
+
* The name of the section. Can be an empty string for the default (first) section.
|
|
1712
|
+
* @defaultValue `""`
|
|
1713
|
+
*/
|
|
1714
|
+
__publicField(this, "name");
|
|
1715
|
+
/** An array of steps and notes that make up the content of the section. */
|
|
1716
|
+
__publicField(this, "content", []);
|
|
1717
|
+
this.name = name;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Checks if the section is blank (has no name and no content).
|
|
1721
|
+
* Used during recipe parsing
|
|
1722
|
+
* @returns `true` if the section is blank, otherwise `false`.
|
|
1723
|
+
*/
|
|
1724
|
+
isBlank() {
|
|
1725
|
+
return this.name === "" && this.content.length === 0;
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
// src/quantities/alternatives.ts
|
|
1730
|
+
var import_big3 = __toESM(require("big.js"), 1);
|
|
1731
|
+
|
|
1732
|
+
// src/units/lookup.ts
|
|
1733
|
+
function findListWithCompatibleQuantity(list, quantity) {
|
|
1734
|
+
const quantityWithUnitDef = {
|
|
1735
|
+
...quantity,
|
|
1736
|
+
unit: resolveUnit(quantity.unit?.name)
|
|
1737
|
+
};
|
|
1738
|
+
return list.find(
|
|
1739
|
+
(l) => l.some((lq) => areUnitsGroupable(lq.unit, quantityWithUnitDef.unit))
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
function findCompatibleQuantityWithinList(list, quantity) {
|
|
1743
|
+
const quantityWithUnitDef = {
|
|
1744
|
+
...quantity,
|
|
1745
|
+
unit: resolveUnit(quantity.unit?.name)
|
|
1746
|
+
};
|
|
1747
|
+
return list.find(
|
|
1748
|
+
(q) => q.unit.name === quantityWithUnitDef.unit.name || q.unit.type === quantityWithUnitDef.unit.type && q.unit.type !== "other"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// src/utils/general.ts
|
|
1753
|
+
var legacyDeepClone = (v) => {
|
|
1754
|
+
if (v === null || typeof v !== "object") {
|
|
1755
|
+
return v;
|
|
1756
|
+
}
|
|
1757
|
+
if (v instanceof Map) {
|
|
1758
|
+
return new Map(
|
|
1759
|
+
Array.from(v.entries()).map(([k, val]) => [
|
|
1760
|
+
legacyDeepClone(k),
|
|
1761
|
+
legacyDeepClone(val)
|
|
1762
|
+
])
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (v instanceof Set) {
|
|
1766
|
+
return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
|
|
1767
|
+
}
|
|
1768
|
+
if (v instanceof Date) {
|
|
1769
|
+
return new Date(v.getTime());
|
|
1770
|
+
}
|
|
1771
|
+
if (Array.isArray(v)) {
|
|
1772
|
+
return v.map((item) => legacyDeepClone(item));
|
|
1773
|
+
}
|
|
1774
|
+
const cloned = {};
|
|
1775
|
+
for (const key of Object.keys(v)) {
|
|
1776
|
+
cloned[key] = legacyDeepClone(v[key]);
|
|
1777
|
+
}
|
|
1778
|
+
return cloned;
|
|
1779
|
+
};
|
|
1780
|
+
var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
|
|
1781
|
+
|
|
1782
|
+
// src/quantities/alternatives.ts
|
|
1783
|
+
function getEquivalentUnitsLists(...quantities) {
|
|
1784
|
+
const quantitiesCopy = deepClone(quantities);
|
|
1785
|
+
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
|
|
1786
|
+
const unitLists = [];
|
|
1787
|
+
const normalizeOrGroup = (og) => ({
|
|
1788
|
+
...og,
|
|
1789
|
+
or: og.or.map((q) => ({
|
|
1790
|
+
...q,
|
|
1791
|
+
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
1792
|
+
}))
|
|
1793
|
+
});
|
|
1794
|
+
function findLinkIndexForUnits(lists, unitsToCheck) {
|
|
1795
|
+
return lists.findIndex((l) => {
|
|
1796
|
+
const listItems = l.map((q) => resolveUnit(q.unit?.name));
|
|
1797
|
+
return unitsToCheck.some(
|
|
1798
|
+
(u) => u && listItems.some((lu) => areUnitsGroupable(lu, u))
|
|
1799
|
+
);
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
function mergeOrGroupIntoList(lists, idx, og) {
|
|
1803
|
+
let unitRatio;
|
|
1804
|
+
const commonUnitList = lists[idx].reduce((acc, v) => {
|
|
1805
|
+
const normalizedV = {
|
|
1806
|
+
...v,
|
|
1807
|
+
unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
|
|
1808
|
+
};
|
|
1809
|
+
const commonQuantity = og.or.find(
|
|
1810
|
+
(q) => isQuantity(q) && areUnitsGroupable(q.unit, normalizedV.unit)
|
|
1811
|
+
);
|
|
1812
|
+
if (commonQuantity) {
|
|
1813
|
+
acc.push(normalizedV);
|
|
1814
|
+
if (!unitRatio) {
|
|
1815
|
+
unitRatio = getUnitRatio(normalizedV, commonQuantity);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return acc;
|
|
1819
|
+
}, []);
|
|
1820
|
+
for (const newQ of og.or) {
|
|
1821
|
+
if (commonUnitList.some((q) => areUnitsGroupable(q.unit, newQ.unit))) {
|
|
1822
|
+
continue;
|
|
1823
|
+
} else {
|
|
1824
|
+
const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
|
|
1825
|
+
lists[idx].push({ ...newQ, quantity: scaledQuantity });
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
for (const orGroup of OrGroups) {
|
|
1830
|
+
const orGroupModified = normalizeOrGroup(orGroup);
|
|
1831
|
+
const units2 = orGroupModified.or.map((q) => q.unit);
|
|
1832
|
+
const linkIndex = findLinkIndexForUnits(unitLists, units2);
|
|
1833
|
+
if (linkIndex === -1) {
|
|
1834
|
+
unitLists.push(orGroupModified.or);
|
|
1835
|
+
} else {
|
|
1836
|
+
mergeOrGroupIntoList(unitLists, linkIndex, orGroupModified);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return unitLists;
|
|
1840
|
+
}
|
|
1841
|
+
function sortUnitList(list) {
|
|
1842
|
+
if (!list || list.length <= 1) return list;
|
|
1843
|
+
const priorityList = [];
|
|
1844
|
+
const nonPriorityList = [];
|
|
1845
|
+
for (const q of list) {
|
|
1846
|
+
if (q.unit.integerProtected || q.unit.system === "none") {
|
|
1847
|
+
priorityList.push(q);
|
|
1848
|
+
} else {
|
|
1849
|
+
nonPriorityList.push(q);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return priorityList.sort((a2, b) => {
|
|
1853
|
+
const prefixA = a2.unit.integerProtected ? "___" : "";
|
|
1854
|
+
const prefixB = b.unit.integerProtected ? "___" : "";
|
|
1855
|
+
return (prefixA + a2.unit.name).localeCompare(prefixB + b.unit.name, "en");
|
|
1856
|
+
}).concat(nonPriorityList);
|
|
1857
|
+
}
|
|
1858
|
+
function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
1859
|
+
function reduceToQuantity(firstQuantity) {
|
|
1860
|
+
const equivalentList = sortUnitList(
|
|
1861
|
+
findListWithCompatibleQuantity(unitList, firstQuantity)
|
|
1862
|
+
);
|
|
1863
|
+
if (!equivalentList) return firstQuantity;
|
|
1864
|
+
const firstQuantityInList = findCompatibleQuantityWithinList(
|
|
1865
|
+
equivalentList,
|
|
1866
|
+
firstQuantity
|
|
1867
|
+
);
|
|
1868
|
+
const normalizedFirstQuantity = {
|
|
1869
|
+
...firstQuantity,
|
|
1870
|
+
unit: resolveUnit(firstQuantity.unit?.name)
|
|
1871
|
+
};
|
|
1872
|
+
if (firstQuantityInList.unit.integerProtected) {
|
|
1873
|
+
const resultQuantity = {
|
|
1874
|
+
quantity: firstQuantity.quantity
|
|
1875
|
+
};
|
|
1876
|
+
if (!isNoUnit(normalizedFirstQuantity.unit)) {
|
|
1877
|
+
resultQuantity.unit = { name: normalizedFirstQuantity.unit.name };
|
|
1878
|
+
}
|
|
1879
|
+
return resultQuantity;
|
|
1880
|
+
} else {
|
|
1881
|
+
let nextProtected;
|
|
1882
|
+
const equivalentListTemp = [...equivalentList];
|
|
1883
|
+
while (nextProtected !== -1) {
|
|
1884
|
+
nextProtected = equivalentListTemp.findIndex(
|
|
1885
|
+
(eq) => eq.unit?.integerProtected
|
|
1886
|
+
);
|
|
1887
|
+
if (nextProtected !== -1) {
|
|
1888
|
+
const unitRatio2 = getUnitRatio(
|
|
1889
|
+
equivalentListTemp[nextProtected],
|
|
1890
|
+
firstQuantityInList
|
|
1891
|
+
);
|
|
1892
|
+
const nextProtectedQuantityValue = multiplyQuantityValue(
|
|
1893
|
+
firstQuantity.quantity,
|
|
1894
|
+
unitRatio2
|
|
1895
|
+
);
|
|
1896
|
+
if (isValueIntegerLike(nextProtectedQuantityValue)) {
|
|
1897
|
+
const nextProtectedQuantity = {
|
|
1898
|
+
quantity: nextProtectedQuantityValue
|
|
1899
|
+
};
|
|
1900
|
+
if (!isNoUnit(equivalentListTemp[nextProtected].unit)) {
|
|
1901
|
+
nextProtectedQuantity.unit = {
|
|
1902
|
+
name: equivalentListTemp[nextProtected].unit.name
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
return nextProtectedQuantity;
|
|
1906
|
+
} else {
|
|
1907
|
+
equivalentListTemp.splice(nextProtected, 1);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const firstNonIntegerProtected = equivalentListTemp.filter(
|
|
1912
|
+
(q) => !q.unit.integerProtected
|
|
1913
|
+
)[0];
|
|
1914
|
+
const unitRatio = getUnitRatio(
|
|
1915
|
+
firstNonIntegerProtected,
|
|
1916
|
+
firstQuantityInList
|
|
1917
|
+
).times(getBaseUnitRatio(normalizedFirstQuantity, firstQuantityInList));
|
|
1918
|
+
const firstEqQuantity = {
|
|
1919
|
+
quantity: firstNonIntegerProtected.unit.name === firstQuantity.unit.name ? firstQuantity.quantity : multiplyQuantityValue(firstQuantity.quantity, unitRatio)
|
|
1920
|
+
};
|
|
1921
|
+
if (!isNoUnit(firstNonIntegerProtected.unit)) {
|
|
1922
|
+
firstEqQuantity.unit = { name: firstNonIntegerProtected.unit.name };
|
|
1923
|
+
}
|
|
1924
|
+
return firstEqQuantity;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return quantities.map((q) => {
|
|
1928
|
+
if (isQuantity(q)) return reduceToQuantity(q);
|
|
1929
|
+
const qListModified = sortUnitList(
|
|
1930
|
+
q.or.map((qq) => ({
|
|
1931
|
+
...qq,
|
|
1932
|
+
unit: resolveUnit(qq.unit?.name, qq.unit?.integerProtected)
|
|
1933
|
+
}))
|
|
1934
|
+
);
|
|
1935
|
+
return reduceToQuantity(qListModified[0]);
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
function addQuantitiesOrGroups(quantities, system) {
|
|
1939
|
+
if (quantities.length === 0)
|
|
1940
|
+
return {
|
|
1941
|
+
sum: {
|
|
1942
|
+
quantity: getDefaultQuantityValue(),
|
|
1943
|
+
unit: resolveUnit()
|
|
1944
|
+
},
|
|
1945
|
+
unitsLists: []
|
|
1946
|
+
};
|
|
1947
|
+
if (quantities.length === 1) {
|
|
1948
|
+
if (isQuantity(quantities[0]))
|
|
1949
|
+
return {
|
|
1950
|
+
sum: {
|
|
1951
|
+
...quantities[0],
|
|
1952
|
+
unit: resolveUnit(quantities[0].unit?.name)
|
|
1953
|
+
},
|
|
1954
|
+
unitsLists: []
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
const unitsLists = getEquivalentUnitsLists(...quantities);
|
|
1958
|
+
const reducedQuantities = reduceOrsToFirstEquivalent(unitsLists, quantities);
|
|
1959
|
+
const sum = [];
|
|
1960
|
+
for (const nextQ of reducedQuantities) {
|
|
1961
|
+
const existingQ = findCompatibleQuantityWithinList(sum, nextQ);
|
|
1962
|
+
if (existingQ === void 0) {
|
|
1963
|
+
sum.push({
|
|
1964
|
+
...nextQ,
|
|
1965
|
+
unit: resolveUnit(nextQ.unit?.name)
|
|
1966
|
+
});
|
|
1967
|
+
} else {
|
|
1968
|
+
const sumQ = addQuantities(existingQ, nextQ, system);
|
|
1969
|
+
existingQ.quantity = sumQ.quantity;
|
|
1970
|
+
existingQ.unit = resolveUnit(sumQ.unit?.name);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (sum.length === 1) {
|
|
1974
|
+
return { sum: sum[0], unitsLists };
|
|
1975
|
+
}
|
|
1976
|
+
return { sum: { and: sum }, unitsLists };
|
|
1977
|
+
}
|
|
1978
|
+
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
|
|
1979
|
+
const sumQuantities = isAndGroup(sum) ? sum.and : [sum];
|
|
1980
|
+
const result = [];
|
|
1981
|
+
const processedQuantities = /* @__PURE__ */ new Set();
|
|
1982
|
+
for (const list of unitsLists) {
|
|
1983
|
+
const listCopy = deepClone(list);
|
|
1984
|
+
const main = [];
|
|
1985
|
+
const mainCandidates = sumQuantities.filter(
|
|
1986
|
+
(q) => !processedQuantities.has(q)
|
|
1987
|
+
);
|
|
1988
|
+
if (mainCandidates.length === 0) continue;
|
|
1989
|
+
mainCandidates.forEach((q) => {
|
|
1990
|
+
const mainInList = findCompatibleQuantityWithinList(listCopy, q);
|
|
1991
|
+
if (mainInList !== void 0) {
|
|
1992
|
+
processedQuantities.add(q);
|
|
1993
|
+
main.push(q);
|
|
1994
|
+
listCopy.splice(listCopy.indexOf(mainInList), 1);
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
const equivalents = sortUnitList(listCopy).map((equiv) => {
|
|
1998
|
+
const initialValue = {
|
|
1999
|
+
quantity: getDefaultQuantityValue()
|
|
2000
|
+
};
|
|
2001
|
+
if (equiv.unit) {
|
|
2002
|
+
initialValue.unit = { name: equiv.unit.name };
|
|
2003
|
+
}
|
|
2004
|
+
return main.reduce((acc, v) => {
|
|
2005
|
+
const mainInList = findCompatibleQuantityWithinList(list, v);
|
|
2006
|
+
const conversionRatio = getBaseUnitRatio(v, mainInList);
|
|
2007
|
+
const valueInOriginalUnit = (0, import_big3.default)(getAverageValue(v.quantity)).times(
|
|
2008
|
+
conversionRatio
|
|
2009
|
+
);
|
|
2010
|
+
const newValue = {
|
|
2011
|
+
quantity: multiplyQuantityValue(
|
|
2012
|
+
{
|
|
2013
|
+
type: "fixed",
|
|
2014
|
+
value: {
|
|
2015
|
+
type: "decimal",
|
|
2016
|
+
decimal: valueInOriginalUnit.toNumber()
|
|
2017
|
+
}
|
|
2018
|
+
},
|
|
2019
|
+
(0, import_big3.default)(getAverageValue(equiv.quantity)).div(
|
|
2020
|
+
getAverageValue(mainInList.quantity)
|
|
2021
|
+
)
|
|
2022
|
+
)
|
|
2023
|
+
};
|
|
2024
|
+
if (equiv.unit && !isNoUnit(equiv.unit)) {
|
|
2025
|
+
newValue.unit = { name: equiv.unit.name };
|
|
2026
|
+
}
|
|
2027
|
+
return addQuantities(acc, newValue, system);
|
|
2028
|
+
}, initialValue);
|
|
2029
|
+
});
|
|
2030
|
+
if (main.length + equivalents.length > 1) {
|
|
2031
|
+
const resultMain = main.length > 1 ? {
|
|
2032
|
+
and: main.map(deNormalizeQuantity)
|
|
2033
|
+
} : deNormalizeQuantity(main[0]);
|
|
2034
|
+
result.push({
|
|
2035
|
+
or: [resultMain, ...equivalents]
|
|
2036
|
+
});
|
|
2037
|
+
} else {
|
|
2038
|
+
result.push(deNormalizeQuantity(main[0]));
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
function addEquivalentsAndSimplify(quantities, system) {
|
|
2045
|
+
if (quantities.length === 1) {
|
|
2046
|
+
return toPlainUnit(quantities[0]);
|
|
2047
|
+
}
|
|
2048
|
+
const { sum, unitsLists } = addQuantitiesOrGroups(quantities, system);
|
|
2049
|
+
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2050
|
+
sum,
|
|
2051
|
+
unitsLists,
|
|
2052
|
+
system
|
|
2053
|
+
);
|
|
2054
|
+
if (regrouped.length === 1) {
|
|
2055
|
+
return toPlainUnit(regrouped[0]);
|
|
2056
|
+
} else {
|
|
2057
|
+
return { and: regrouped.map(toPlainUnit) };
|
|
2058
|
+
}
|
|
907
2059
|
}
|
|
908
2060
|
|
|
909
2061
|
// src/classes/recipe.ts
|
|
910
|
-
var
|
|
911
|
-
var
|
|
2062
|
+
var import_big4 = __toESM(require("big.js"), 1);
|
|
2063
|
+
var _Recipe = class _Recipe {
|
|
912
2064
|
/**
|
|
913
2065
|
* Creates a new Recipe instance.
|
|
914
2066
|
* @param content - The recipe content to parse.
|
|
@@ -918,6 +2070,13 @@ var Recipe = class _Recipe {
|
|
|
918
2070
|
* The parsed recipe metadata.
|
|
919
2071
|
*/
|
|
920
2072
|
__publicField(this, "metadata", {});
|
|
2073
|
+
/**
|
|
2074
|
+
* The possible choices of alternative ingredients for this recipe.
|
|
2075
|
+
*/
|
|
2076
|
+
__publicField(this, "choices", {
|
|
2077
|
+
ingredientItems: /* @__PURE__ */ new Map(),
|
|
2078
|
+
ingredientGroups: /* @__PURE__ */ new Map()
|
|
2079
|
+
});
|
|
921
2080
|
/**
|
|
922
2081
|
* The parsed recipe ingredients.
|
|
923
2082
|
*/
|
|
@@ -934,6 +2093,10 @@ var Recipe = class _Recipe {
|
|
|
934
2093
|
* The parsed recipe timers.
|
|
935
2094
|
*/
|
|
936
2095
|
__publicField(this, "timers", []);
|
|
2096
|
+
/**
|
|
2097
|
+
* The parsed arbitrary quantities.
|
|
2098
|
+
*/
|
|
2099
|
+
__publicField(this, "arbitraries", []);
|
|
937
2100
|
/**
|
|
938
2101
|
* The parsed recipe servings. Used for scaling. Parsed from one of
|
|
939
2102
|
* {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
|
|
@@ -942,35 +2105,608 @@ var Recipe = class _Recipe {
|
|
|
942
2105
|
* @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
|
|
943
2106
|
*/
|
|
944
2107
|
__publicField(this, "servings");
|
|
2108
|
+
_Recipe.itemCounts.set(this, 0);
|
|
945
2109
|
if (content) {
|
|
946
2110
|
this.parse(content);
|
|
947
2111
|
}
|
|
948
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Gets the unit system specified in the recipe metadata.
|
|
2115
|
+
* Used for resolving ambiguous units like tsp, tbsp, cup, etc.
|
|
2116
|
+
*
|
|
2117
|
+
* @returns The unit system if specified, or undefined to use defaults
|
|
2118
|
+
*/
|
|
2119
|
+
get unitSystem() {
|
|
2120
|
+
return _Recipe.unitSystems.get(this);
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Gets the current item count for this recipe.
|
|
2124
|
+
*/
|
|
2125
|
+
getItemCount() {
|
|
2126
|
+
return _Recipe.itemCounts.get(this);
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Gets the current item count and increments it.
|
|
2130
|
+
*/
|
|
2131
|
+
getAndIncrementItemCount() {
|
|
2132
|
+
const current = this.getItemCount();
|
|
2133
|
+
_Recipe.itemCounts.set(this, current + 1);
|
|
2134
|
+
return current;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Parses a matched arbitrary scalable quantity and adds it to the given array.
|
|
2138
|
+
* @private
|
|
2139
|
+
* @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
|
|
2140
|
+
* @param intoArray - The array to push the parsed arbitrary scalable item into.
|
|
2141
|
+
*/
|
|
2142
|
+
_parseArbitraryScalable(regexMatchGroups, intoArray) {
|
|
2143
|
+
if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
|
|
2144
|
+
const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
|
|
2145
|
+
if (quantityMatch?.groups) {
|
|
2146
|
+
const value = parseQuantityInput(quantityMatch.groups.quantity);
|
|
2147
|
+
const unit = quantityMatch.groups.unit;
|
|
2148
|
+
const name = regexMatchGroups.arbitraryName || void 0;
|
|
2149
|
+
if (!value || value.type === "fixed" && value.value.type === "text") {
|
|
2150
|
+
throw new InvalidQuantityFormat(
|
|
2151
|
+
regexMatchGroups.arbitraryQuantity?.trim(),
|
|
2152
|
+
"Arbitrary quantities must have a numerical value"
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
const arbitrary = {
|
|
2156
|
+
quantity: value
|
|
2157
|
+
};
|
|
2158
|
+
if (name) arbitrary.name = name;
|
|
2159
|
+
if (unit) arbitrary.unit = unit;
|
|
2160
|
+
intoArray.push({
|
|
2161
|
+
type: "arbitrary",
|
|
2162
|
+
index: this.arbitraries.push(arbitrary) - 1
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Parses text for arbitrary scalables and returns NoteItem array.
|
|
2168
|
+
* @param text - The text to parse for arbitrary scalables.
|
|
2169
|
+
* @returns Array of NoteItem (text and arbitrary scalable items).
|
|
2170
|
+
*/
|
|
2171
|
+
_parseNoteText(text) {
|
|
2172
|
+
const noteItems = [];
|
|
2173
|
+
let cursor = 0;
|
|
2174
|
+
const globalRegex = new RegExp(arbitraryScalableRegex.source, "g");
|
|
2175
|
+
for (const match of text.matchAll(globalRegex)) {
|
|
2176
|
+
const idx = match.index;
|
|
2177
|
+
if (idx > cursor) {
|
|
2178
|
+
noteItems.push({ type: "text", value: text.slice(cursor, idx) });
|
|
2179
|
+
}
|
|
2180
|
+
this._parseArbitraryScalable(match.groups, noteItems);
|
|
2181
|
+
cursor = idx + match[0].length;
|
|
2182
|
+
}
|
|
2183
|
+
if (cursor < text.length) {
|
|
2184
|
+
noteItems.push({ type: "text", value: text.slice(cursor) });
|
|
2185
|
+
}
|
|
2186
|
+
return noteItems;
|
|
2187
|
+
}
|
|
2188
|
+
_parseQuantityRecursive(quantityRaw) {
|
|
2189
|
+
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
2190
|
+
const quantities = [];
|
|
2191
|
+
while (quantityMatch?.groups) {
|
|
2192
|
+
const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
|
|
2193
|
+
const unit = quantityMatch.groups.unit;
|
|
2194
|
+
if (value) {
|
|
2195
|
+
const newQuantity = { quantity: value };
|
|
2196
|
+
if (unit) {
|
|
2197
|
+
if (unit.startsWith("=")) {
|
|
2198
|
+
newQuantity.unit = {
|
|
2199
|
+
name: unit.substring(1),
|
|
2200
|
+
integerProtected: true
|
|
2201
|
+
};
|
|
2202
|
+
} else {
|
|
2203
|
+
newQuantity.unit = { name: unit };
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
quantities.push(newQuantity);
|
|
2207
|
+
} else {
|
|
2208
|
+
throw new InvalidQuantityFormat(quantityRaw);
|
|
2209
|
+
}
|
|
2210
|
+
quantityMatch = quantityMatch.groups.alternative ? quantityMatch.groups.alternative.match(quantityAlternativeRegex) : null;
|
|
2211
|
+
}
|
|
2212
|
+
return quantities;
|
|
2213
|
+
}
|
|
2214
|
+
_parseIngredientWithAlternativeRecursive(ingredientMatchString, items) {
|
|
2215
|
+
const alternatives = [];
|
|
2216
|
+
let testString = ingredientMatchString;
|
|
2217
|
+
while (true) {
|
|
2218
|
+
const match = testString.match(
|
|
2219
|
+
alternatives.length > 0 ? inlineIngredientAlternativesRegex : ingredientWithAlternativeRegex
|
|
2220
|
+
);
|
|
2221
|
+
if (!match?.groups) break;
|
|
2222
|
+
const groups = match.groups;
|
|
2223
|
+
let name = groups.mIngredientName || groups.sIngredientName;
|
|
2224
|
+
const preparation = groups.ingredientPreparation;
|
|
2225
|
+
const modifiers = groups.ingredientModifiers;
|
|
2226
|
+
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
2227
|
+
const flags = [];
|
|
2228
|
+
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
2229
|
+
flags.push("optional");
|
|
2230
|
+
}
|
|
2231
|
+
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
2232
|
+
flags.push("hidden");
|
|
2233
|
+
}
|
|
2234
|
+
if (modifiers !== void 0 && modifiers.includes("@") || groups.ingredientRecipeAnchor) {
|
|
2235
|
+
flags.push("recipe");
|
|
2236
|
+
}
|
|
2237
|
+
let extras = void 0;
|
|
2238
|
+
if (flags.includes("recipe")) {
|
|
2239
|
+
extras = { path: `${name}.cook` };
|
|
2240
|
+
name = name.substring(name.lastIndexOf("/") + 1);
|
|
2241
|
+
}
|
|
2242
|
+
const aliasMatch = name.match(ingredientAliasRegex);
|
|
2243
|
+
let listName, displayName;
|
|
2244
|
+
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
2245
|
+
listName = aliasMatch.groups.ingredientListName.trim();
|
|
2246
|
+
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
2247
|
+
} else {
|
|
2248
|
+
listName = name;
|
|
2249
|
+
displayName = name;
|
|
2250
|
+
}
|
|
2251
|
+
const newIngredient = {
|
|
2252
|
+
name: listName
|
|
2253
|
+
};
|
|
2254
|
+
if (preparation) {
|
|
2255
|
+
newIngredient.preparation = preparation;
|
|
2256
|
+
}
|
|
2257
|
+
if (flags.length > 0) {
|
|
2258
|
+
newIngredient.flags = flags;
|
|
2259
|
+
}
|
|
2260
|
+
if (extras) {
|
|
2261
|
+
newIngredient.extras = extras;
|
|
2262
|
+
}
|
|
2263
|
+
const idxInList = findAndUpsertIngredient(
|
|
2264
|
+
this.ingredients,
|
|
2265
|
+
newIngredient,
|
|
2266
|
+
reference
|
|
2267
|
+
);
|
|
2268
|
+
let itemQuantity = void 0;
|
|
2269
|
+
if (groups.ingredientQuantity) {
|
|
2270
|
+
const parsedQuantities = this._parseQuantityRecursive(
|
|
2271
|
+
groups.ingredientQuantity
|
|
2272
|
+
);
|
|
2273
|
+
const [primary, ...rest] = parsedQuantities;
|
|
2274
|
+
if (primary) {
|
|
2275
|
+
itemQuantity = {
|
|
2276
|
+
...primary,
|
|
2277
|
+
scalable: groups.ingredientQuantityModifier !== "="
|
|
2278
|
+
};
|
|
2279
|
+
if (rest.length > 0) {
|
|
2280
|
+
itemQuantity.equivalents = rest;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const alternative = {
|
|
2285
|
+
index: idxInList,
|
|
2286
|
+
displayName
|
|
2287
|
+
};
|
|
2288
|
+
const note = groups.ingredientNote?.trim();
|
|
2289
|
+
if (note) {
|
|
2290
|
+
alternative.note = note;
|
|
2291
|
+
}
|
|
2292
|
+
if (itemQuantity) {
|
|
2293
|
+
alternative.itemQuantity = itemQuantity;
|
|
2294
|
+
}
|
|
2295
|
+
alternatives.push(alternative);
|
|
2296
|
+
testString = groups.ingredientAlternative || "";
|
|
2297
|
+
}
|
|
2298
|
+
if (alternatives.length > 1) {
|
|
2299
|
+
const alternativesIndexes = alternatives.map((alt) => alt.index);
|
|
2300
|
+
for (const ingredientIndex of alternativesIndexes) {
|
|
2301
|
+
const ingredient = this.ingredients[ingredientIndex];
|
|
2302
|
+
if (ingredient) {
|
|
2303
|
+
if (!ingredient.alternatives) {
|
|
2304
|
+
ingredient.alternatives = new Set(
|
|
2305
|
+
alternativesIndexes.filter((index) => index !== ingredientIndex)
|
|
2306
|
+
);
|
|
2307
|
+
} else {
|
|
2308
|
+
ingredient.alternatives = unionOfSets(
|
|
2309
|
+
ingredient.alternatives,
|
|
2310
|
+
new Set(
|
|
2311
|
+
alternativesIndexes.filter(
|
|
2312
|
+
(index) => index !== ingredientIndex
|
|
2313
|
+
)
|
|
2314
|
+
)
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
|
|
2321
|
+
const newItem = {
|
|
2322
|
+
type: "ingredient",
|
|
2323
|
+
id,
|
|
2324
|
+
alternatives
|
|
2325
|
+
};
|
|
2326
|
+
items.push(newItem);
|
|
2327
|
+
if (alternatives.length > 1) {
|
|
2328
|
+
this.choices.ingredientItems.set(id, alternatives);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
_parseIngredientWithGroupKey(ingredientMatchString, items) {
|
|
2332
|
+
const match = ingredientMatchString.match(ingredientWithGroupKeyRegex);
|
|
2333
|
+
if (!match?.groups) return;
|
|
2334
|
+
const groups = match.groups;
|
|
2335
|
+
const groupKey = groups.gIngredientGroupKey;
|
|
2336
|
+
let name = groups.gmIngredientName || groups.gsIngredientName;
|
|
2337
|
+
const preparation = groups.gIngredientPreparation;
|
|
2338
|
+
const modifiers = groups.gIngredientModifiers;
|
|
2339
|
+
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
2340
|
+
const flags = [];
|
|
2341
|
+
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
2342
|
+
flags.push("optional");
|
|
2343
|
+
}
|
|
2344
|
+
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
2345
|
+
flags.push("hidden");
|
|
2346
|
+
}
|
|
2347
|
+
if (modifiers !== void 0 && modifiers.includes("@") || groups.gIngredientRecipeAnchor) {
|
|
2348
|
+
flags.push("recipe");
|
|
2349
|
+
}
|
|
2350
|
+
let extras = void 0;
|
|
2351
|
+
if (flags.includes("recipe")) {
|
|
2352
|
+
extras = { path: `${name}.cook` };
|
|
2353
|
+
name = name.substring(name.lastIndexOf("/") + 1);
|
|
2354
|
+
}
|
|
2355
|
+
const aliasMatch = name.match(ingredientAliasRegex);
|
|
2356
|
+
let listName, displayName;
|
|
2357
|
+
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
2358
|
+
listName = aliasMatch.groups.ingredientListName.trim();
|
|
2359
|
+
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
2360
|
+
} else {
|
|
2361
|
+
listName = name;
|
|
2362
|
+
displayName = name;
|
|
2363
|
+
}
|
|
2364
|
+
const newIngredient = {
|
|
2365
|
+
name: listName
|
|
2366
|
+
};
|
|
2367
|
+
if (preparation) {
|
|
2368
|
+
newIngredient.preparation = preparation;
|
|
2369
|
+
}
|
|
2370
|
+
if (flags.length > 0) {
|
|
2371
|
+
newIngredient.flags = flags;
|
|
2372
|
+
}
|
|
2373
|
+
if (extras) {
|
|
2374
|
+
newIngredient.extras = extras;
|
|
2375
|
+
}
|
|
2376
|
+
const idxInList = findAndUpsertIngredient(
|
|
2377
|
+
this.ingredients,
|
|
2378
|
+
newIngredient,
|
|
2379
|
+
reference
|
|
2380
|
+
);
|
|
2381
|
+
let itemQuantity = void 0;
|
|
2382
|
+
if (groups.gIngredientQuantity) {
|
|
2383
|
+
const parsedQuantities = this._parseQuantityRecursive(
|
|
2384
|
+
groups.gIngredientQuantity
|
|
2385
|
+
);
|
|
2386
|
+
const [primary, ...rest] = parsedQuantities;
|
|
2387
|
+
itemQuantity = {
|
|
2388
|
+
...primary,
|
|
2389
|
+
// there's necessarily a primary quantity as the match group was detected
|
|
2390
|
+
scalable: groups.gIngredientQuantityModifier !== "="
|
|
2391
|
+
};
|
|
2392
|
+
if (rest.length > 0) {
|
|
2393
|
+
itemQuantity.equivalents = rest;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
const alternative = {
|
|
2397
|
+
index: idxInList,
|
|
2398
|
+
displayName
|
|
2399
|
+
};
|
|
2400
|
+
if (itemQuantity) {
|
|
2401
|
+
alternative.itemQuantity = itemQuantity;
|
|
2402
|
+
}
|
|
2403
|
+
const existingAlternatives = this.choices.ingredientGroups.get(groupKey);
|
|
2404
|
+
function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
|
|
2405
|
+
const ingredient = ingredients[ingredientIdx];
|
|
2406
|
+
if (ingredient) {
|
|
2407
|
+
if (ingredient.alternatives === void 0) {
|
|
2408
|
+
ingredient.alternatives = /* @__PURE__ */ new Set([newAlternativeIdx]);
|
|
2409
|
+
} else {
|
|
2410
|
+
ingredient.alternatives.add(newAlternativeIdx);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if (existingAlternatives) {
|
|
2415
|
+
for (const alt of existingAlternatives) {
|
|
2416
|
+
upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
|
|
2417
|
+
upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
|
|
2421
|
+
const newItem = {
|
|
2422
|
+
type: "ingredient",
|
|
2423
|
+
id,
|
|
2424
|
+
group: groupKey,
|
|
2425
|
+
alternatives: [alternative]
|
|
2426
|
+
};
|
|
2427
|
+
items.push(newItem);
|
|
2428
|
+
const choiceAlternative = deepClone(alternative);
|
|
2429
|
+
choiceAlternative.itemId = id;
|
|
2430
|
+
const existingChoice = this.choices.ingredientGroups.get(groupKey);
|
|
2431
|
+
if (!existingChoice) {
|
|
2432
|
+
this.choices.ingredientGroups.set(groupKey, [choiceAlternative]);
|
|
2433
|
+
} else {
|
|
2434
|
+
existingChoice.push(choiceAlternative);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Populates the `quantities` property for each ingredient based on
|
|
2439
|
+
* how they appear in the recipe preparation. Only primary ingredients
|
|
2440
|
+
* get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
|
|
2441
|
+
*
|
|
2442
|
+
* For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
|
|
2443
|
+
* For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
|
|
2444
|
+
*
|
|
2445
|
+
* Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
|
|
2446
|
+
* @internal
|
|
2447
|
+
*/
|
|
2448
|
+
_populate_ingredient_quantities() {
|
|
2449
|
+
for (const ing of this.ingredients) {
|
|
2450
|
+
delete ing.quantities;
|
|
2451
|
+
delete ing.usedAsPrimary;
|
|
2452
|
+
}
|
|
2453
|
+
const ingredientsWithQuantities = this.getIngredientQuantities();
|
|
2454
|
+
const matchedIndices = /* @__PURE__ */ new Set();
|
|
2455
|
+
for (const computed of ingredientsWithQuantities) {
|
|
2456
|
+
const idx = this.ingredients.findIndex(
|
|
2457
|
+
(ing2, i2) => ing2.name === computed.name && !matchedIndices.has(i2)
|
|
2458
|
+
);
|
|
2459
|
+
matchedIndices.add(idx);
|
|
2460
|
+
const ing = this.ingredients[idx];
|
|
2461
|
+
if (computed.quantities) {
|
|
2462
|
+
ing.quantities = computed.quantities;
|
|
2463
|
+
}
|
|
2464
|
+
if (computed.usedAsPrimary) {
|
|
2465
|
+
ing.usedAsPrimary = true;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Gets ingredients with their quantities populated, optionally filtered by section/step
|
|
2471
|
+
* and respecting user choices for alternatives.
|
|
2472
|
+
*
|
|
2473
|
+
* When no options are provided, returns all recipe ingredients with quantities
|
|
2474
|
+
* calculated using primary alternatives (same as after parsing).
|
|
2475
|
+
*
|
|
2476
|
+
* @param options - Options for filtering and choice selection:
|
|
2477
|
+
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
2478
|
+
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
2479
|
+
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
2480
|
+
* @returns Array of Ingredient objects with quantities populated
|
|
2481
|
+
*
|
|
2482
|
+
* @example
|
|
2483
|
+
* ```typescript
|
|
2484
|
+
* // Get all ingredients with primary alternatives
|
|
2485
|
+
* const ingredients = recipe.getIngredientQuantities();
|
|
2486
|
+
*
|
|
2487
|
+
* // Get ingredients for a specific section
|
|
2488
|
+
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
2489
|
+
*
|
|
2490
|
+
* // Get ingredients with specific choices applied
|
|
2491
|
+
* const withChoices = recipe.getIngredientQuantities({
|
|
2492
|
+
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
2493
|
+
* });
|
|
2494
|
+
* ```
|
|
2495
|
+
*/
|
|
2496
|
+
getIngredientQuantities(options) {
|
|
2497
|
+
const { section, step, choices } = options || {};
|
|
2498
|
+
const sectionsToProcess = section !== void 0 ? (() => {
|
|
2499
|
+
const idx = typeof section === "number" ? section : this.sections.indexOf(section);
|
|
2500
|
+
return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
|
|
2501
|
+
})() : this.sections;
|
|
2502
|
+
const ingredientGroups = /* @__PURE__ */ new Map();
|
|
2503
|
+
const selectedIndices = /* @__PURE__ */ new Set();
|
|
2504
|
+
const referencedIndices = /* @__PURE__ */ new Set();
|
|
2505
|
+
for (const currentSection of sectionsToProcess) {
|
|
2506
|
+
const allSteps = currentSection.content.filter(
|
|
2507
|
+
(item) => item.type === "step"
|
|
2508
|
+
);
|
|
2509
|
+
const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
|
|
2510
|
+
for (const currentStep of stepsToProcess) {
|
|
2511
|
+
for (const item of currentStep.items.filter(
|
|
2512
|
+
(item2) => item2.type === "ingredient"
|
|
2513
|
+
)) {
|
|
2514
|
+
const isGrouped = "group" in item && item.group !== void 0;
|
|
2515
|
+
const groupAlternatives = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
|
|
2516
|
+
let selectedAltIndex = 0;
|
|
2517
|
+
let isSelected = false;
|
|
2518
|
+
let hasExplicitChoice = false;
|
|
2519
|
+
if (isGrouped) {
|
|
2520
|
+
const groupChoice = choices?.ingredientGroups?.get(item.group);
|
|
2521
|
+
hasExplicitChoice = groupChoice !== void 0;
|
|
2522
|
+
const targetIndex = groupChoice ?? 0;
|
|
2523
|
+
isSelected = groupAlternatives?.[targetIndex]?.itemId === item.id;
|
|
2524
|
+
} else {
|
|
2525
|
+
const itemChoice = choices?.ingredientItems?.get(item.id);
|
|
2526
|
+
hasExplicitChoice = itemChoice !== void 0;
|
|
2527
|
+
selectedAltIndex = itemChoice ?? 0;
|
|
2528
|
+
isSelected = true;
|
|
2529
|
+
}
|
|
2530
|
+
const alternative = item.alternatives[selectedAltIndex];
|
|
2531
|
+
if (!alternative || !isSelected) continue;
|
|
2532
|
+
selectedIndices.add(alternative.index);
|
|
2533
|
+
const allAlts = isGrouped ? groupAlternatives : item.alternatives;
|
|
2534
|
+
for (const alt of allAlts) {
|
|
2535
|
+
referencedIndices.add(alt.index);
|
|
2536
|
+
}
|
|
2537
|
+
if (!alternative.itemQuantity) continue;
|
|
2538
|
+
const baseQty = {
|
|
2539
|
+
quantity: alternative.itemQuantity.quantity,
|
|
2540
|
+
...alternative.itemQuantity.unit && {
|
|
2541
|
+
unit: alternative.itemQuantity.unit
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
const quantityEntry = alternative.itemQuantity.equivalents?.length ? { or: [baseQty, ...alternative.itemQuantity.equivalents] } : baseQty;
|
|
2545
|
+
let alternativeRefs;
|
|
2546
|
+
if (!hasExplicitChoice && allAlts.length > 1) {
|
|
2547
|
+
alternativeRefs = allAlts.filter(
|
|
2548
|
+
(alt) => isGrouped ? alt.itemId !== item.id : alt.index !== alternative.index
|
|
2549
|
+
).map((otherAlt) => {
|
|
2550
|
+
const ref = { index: otherAlt.index };
|
|
2551
|
+
if (otherAlt.itemQuantity) {
|
|
2552
|
+
const altQty = {
|
|
2553
|
+
quantity: otherAlt.itemQuantity.quantity,
|
|
2554
|
+
...otherAlt.itemQuantity.unit && {
|
|
2555
|
+
unit: otherAlt.itemQuantity.unit.name
|
|
2556
|
+
},
|
|
2557
|
+
...otherAlt.itemQuantity.equivalents && {
|
|
2558
|
+
equivalents: otherAlt.itemQuantity.equivalents.map(
|
|
2559
|
+
(eq) => toPlainUnit(eq)
|
|
2560
|
+
)
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
ref.quantities = [altQty];
|
|
2564
|
+
}
|
|
2565
|
+
return ref;
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
const altIndices = getAlternativeSignature(alternativeRefs) ?? "";
|
|
2569
|
+
let signature;
|
|
2570
|
+
if (isGrouped) {
|
|
2571
|
+
const resolvedUnit = resolveUnit(
|
|
2572
|
+
alternative.itemQuantity.unit?.name
|
|
2573
|
+
);
|
|
2574
|
+
signature = `group:${item.group}|${altIndices}|${resolvedUnit.type}`;
|
|
2575
|
+
} else if (altIndices) {
|
|
2576
|
+
const resolvedUnit = resolveUnit(
|
|
2577
|
+
alternative.itemQuantity.unit?.name
|
|
2578
|
+
);
|
|
2579
|
+
signature = `${altIndices}|${resolvedUnit.type}}`;
|
|
2580
|
+
} else {
|
|
2581
|
+
signature = null;
|
|
2582
|
+
}
|
|
2583
|
+
if (!ingredientGroups.has(alternative.index)) {
|
|
2584
|
+
ingredientGroups.set(alternative.index, /* @__PURE__ */ new Map());
|
|
2585
|
+
}
|
|
2586
|
+
const groupsForIng = ingredientGroups.get(alternative.index);
|
|
2587
|
+
if (!groupsForIng.has(signature)) {
|
|
2588
|
+
groupsForIng.set(signature, {
|
|
2589
|
+
quantities: [],
|
|
2590
|
+
alternativeQuantities: /* @__PURE__ */ new Map()
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
const group = groupsForIng.get(signature);
|
|
2594
|
+
group.quantities.push(quantityEntry);
|
|
2595
|
+
for (const ref of alternativeRefs ?? []) {
|
|
2596
|
+
if (!group.alternativeQuantities.has(ref.index)) {
|
|
2597
|
+
group.alternativeQuantities.set(ref.index, []);
|
|
2598
|
+
}
|
|
2599
|
+
for (const altQty of ref.quantities ?? []) {
|
|
2600
|
+
const extended = toExtendedUnit({
|
|
2601
|
+
quantity: altQty.quantity,
|
|
2602
|
+
unit: altQty.unit
|
|
2603
|
+
});
|
|
2604
|
+
if (altQty.equivalents?.length) {
|
|
2605
|
+
const eqEntries = [
|
|
2606
|
+
extended,
|
|
2607
|
+
...altQty.equivalents.map((eq) => toExtendedUnit(eq))
|
|
2608
|
+
];
|
|
2609
|
+
group.alternativeQuantities.get(ref.index).push({ or: eqEntries });
|
|
2610
|
+
} else {
|
|
2611
|
+
group.alternativeQuantities.get(ref.index).push(extended);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
const result = [];
|
|
2619
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
2620
|
+
if (!referencedIndices.has(index)) continue;
|
|
2621
|
+
const orig = this.ingredients[index];
|
|
2622
|
+
const ing = {
|
|
2623
|
+
name: orig.name,
|
|
2624
|
+
...orig.preparation && { preparation: orig.preparation },
|
|
2625
|
+
...orig.flags && { flags: orig.flags },
|
|
2626
|
+
...orig.extras && { extras: orig.extras }
|
|
2627
|
+
};
|
|
2628
|
+
if (selectedIndices.has(index)) {
|
|
2629
|
+
ing.usedAsPrimary = true;
|
|
2630
|
+
const groupsForIng = ingredientGroups.get(index);
|
|
2631
|
+
if (groupsForIng) {
|
|
2632
|
+
const quantityGroups = [];
|
|
2633
|
+
for (const [, group] of groupsForIng) {
|
|
2634
|
+
const summed = addEquivalentsAndSimplify(
|
|
2635
|
+
group.quantities,
|
|
2636
|
+
this.unitSystem
|
|
2637
|
+
);
|
|
2638
|
+
const flattened = flattenPlainUnitGroup(summed);
|
|
2639
|
+
const alternatives = group.alternativeQuantities.size > 0 ? [...group.alternativeQuantities].map(([altIdx, altQtys]) => ({
|
|
2640
|
+
index: altIdx,
|
|
2641
|
+
...altQtys.length > 0 && {
|
|
2642
|
+
quantities: flattenPlainUnitGroup(
|
|
2643
|
+
addEquivalentsAndSimplify(altQtys, this.unitSystem)
|
|
2644
|
+
).flatMap(
|
|
2645
|
+
/* v8 ignore next -- item.and branch requires complex nested AND-with-equivalents structure */
|
|
2646
|
+
(item) => "quantity" in item ? [item] : item.and
|
|
2647
|
+
)
|
|
2648
|
+
}
|
|
2649
|
+
})) : void 0;
|
|
2650
|
+
for (const gq of flattened) {
|
|
2651
|
+
if ("and" in gq) {
|
|
2652
|
+
quantityGroups.push({
|
|
2653
|
+
and: gq.and,
|
|
2654
|
+
...gq.equivalents?.length && {
|
|
2655
|
+
equivalents: gq.equivalents
|
|
2656
|
+
},
|
|
2657
|
+
...alternatives?.length && { alternatives }
|
|
2658
|
+
});
|
|
2659
|
+
} else {
|
|
2660
|
+
quantityGroups.push({
|
|
2661
|
+
...gq,
|
|
2662
|
+
...alternatives?.length && { alternatives }
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (quantityGroups.length > 0) {
|
|
2668
|
+
ing.quantities = quantityGroups;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
result.push(ing);
|
|
2673
|
+
}
|
|
2674
|
+
return result;
|
|
2675
|
+
}
|
|
949
2676
|
/**
|
|
950
2677
|
* Parses a recipe from a string.
|
|
951
2678
|
* @param content - The recipe content to parse.
|
|
952
2679
|
*/
|
|
953
2680
|
parse(content) {
|
|
954
2681
|
const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
|
|
955
|
-
const { metadata, servings } = extractMetadata(content);
|
|
2682
|
+
const { metadata, servings, unitSystem } = extractMetadata(content);
|
|
956
2683
|
this.metadata = metadata;
|
|
957
2684
|
this.servings = servings;
|
|
2685
|
+
if (unitSystem) _Recipe.unitSystems.set(this, unitSystem);
|
|
958
2686
|
let blankLineBefore = true;
|
|
959
2687
|
let section = new Section();
|
|
960
2688
|
const items = [];
|
|
961
|
-
let
|
|
2689
|
+
let noteText = "";
|
|
962
2690
|
let inNote = false;
|
|
963
2691
|
for (const line of cleanContent) {
|
|
964
2692
|
if (line.trim().length === 0) {
|
|
965
2693
|
flushPendingItems(section, items);
|
|
966
|
-
|
|
2694
|
+
flushPendingNote(
|
|
2695
|
+
section,
|
|
2696
|
+
noteText ? this._parseNoteText(noteText) : []
|
|
2697
|
+
);
|
|
2698
|
+
noteText = "";
|
|
967
2699
|
blankLineBefore = true;
|
|
968
2700
|
inNote = false;
|
|
969
2701
|
continue;
|
|
970
2702
|
}
|
|
971
2703
|
if (line.startsWith("=")) {
|
|
972
2704
|
flushPendingItems(section, items);
|
|
973
|
-
|
|
2705
|
+
flushPendingNote(
|
|
2706
|
+
section,
|
|
2707
|
+
noteText ? this._parseNoteText(noteText) : []
|
|
2708
|
+
);
|
|
2709
|
+
noteText = "";
|
|
974
2710
|
if (this.sections.length === 0 && section.isBlank()) {
|
|
975
2711
|
section.name = line.replace(/^=+|=+$/g, "").trim();
|
|
976
2712
|
} else {
|
|
@@ -985,22 +2721,20 @@ var Recipe = class _Recipe {
|
|
|
985
2721
|
}
|
|
986
2722
|
if (blankLineBefore && line.startsWith(">")) {
|
|
987
2723
|
flushPendingItems(section, items);
|
|
988
|
-
|
|
989
|
-
note += line.substring(1).trim();
|
|
2724
|
+
noteText = line.substring(1).trim();
|
|
990
2725
|
inNote = true;
|
|
991
2726
|
blankLineBefore = false;
|
|
992
2727
|
continue;
|
|
993
2728
|
}
|
|
994
2729
|
if (inNote) {
|
|
995
2730
|
if (line.startsWith(">")) {
|
|
996
|
-
|
|
2731
|
+
noteText += " " + line.substring(1).trim();
|
|
997
2732
|
} else {
|
|
998
|
-
|
|
2733
|
+
noteText += " " + line.trim();
|
|
999
2734
|
}
|
|
1000
2735
|
blankLineBefore = false;
|
|
1001
2736
|
continue;
|
|
1002
2737
|
}
|
|
1003
|
-
note = flushPendingNote(section, note);
|
|
1004
2738
|
let cursor = 0;
|
|
1005
2739
|
for (const match of line.matchAll(tokensRegex)) {
|
|
1006
2740
|
const idx = match.index;
|
|
@@ -1009,12 +2743,13 @@ var Recipe = class _Recipe {
|
|
|
1009
2743
|
}
|
|
1010
2744
|
const groups = match.groups;
|
|
1011
2745
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
const modifiers = groups.
|
|
2746
|
+
this._parseIngredientWithAlternativeRecursive(match[0], items);
|
|
2747
|
+
} else if (groups.gmIngredientName || groups.gsIngredientName) {
|
|
2748
|
+
this._parseIngredientWithGroupKey(match[0], items);
|
|
2749
|
+
} else if (groups.mCookwareName || groups.sCookwareName) {
|
|
2750
|
+
const name = groups.mCookwareName || groups.sCookwareName;
|
|
2751
|
+
const modifiers = groups.cookwareModifiers;
|
|
2752
|
+
const quantityRaw = groups.cookwareQuantity;
|
|
1018
2753
|
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1019
2754
|
const flags = [];
|
|
1020
2755
|
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
@@ -1023,83 +2758,31 @@ var Recipe = class _Recipe {
|
|
|
1023
2758
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1024
2759
|
flags.push("hidden");
|
|
1025
2760
|
}
|
|
1026
|
-
if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
|
|
1027
|
-
flags.push("recipe");
|
|
1028
|
-
}
|
|
1029
|
-
let extras = void 0;
|
|
1030
|
-
if (flags.includes("recipe")) {
|
|
1031
|
-
extras = { path: `${name}.cook` };
|
|
1032
|
-
name = name.substring(name.lastIndexOf("/") + 1);
|
|
1033
|
-
}
|
|
1034
2761
|
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
|
|
1038
|
-
listName = aliasMatch.groups.ingredientListName.trim();
|
|
1039
|
-
displayName = aliasMatch.groups.ingredientDisplayName.trim();
|
|
1040
|
-
} else {
|
|
1041
|
-
listName = name;
|
|
1042
|
-
displayName = name;
|
|
1043
|
-
}
|
|
1044
|
-
const newIngredient = {
|
|
1045
|
-
name: listName,
|
|
1046
|
-
quantity,
|
|
1047
|
-
quantityParts: quantity ? [
|
|
1048
|
-
{
|
|
1049
|
-
value: quantity,
|
|
1050
|
-
unit,
|
|
1051
|
-
scalable: scalableQuantity
|
|
1052
|
-
}
|
|
1053
|
-
] : void 0,
|
|
1054
|
-
unit,
|
|
1055
|
-
preparation,
|
|
1056
|
-
flags
|
|
2762
|
+
const newCookware = {
|
|
2763
|
+
name
|
|
1057
2764
|
};
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
2765
|
+
if (quantity) {
|
|
2766
|
+
newCookware.quantity = quantity;
|
|
2767
|
+
}
|
|
2768
|
+
if (flags.length > 0) {
|
|
2769
|
+
newCookware.flags = flags;
|
|
1060
2770
|
}
|
|
1061
|
-
const
|
|
1062
|
-
this.
|
|
1063
|
-
|
|
2771
|
+
const idxInList = findAndUpsertCookware(
|
|
2772
|
+
this.cookware,
|
|
2773
|
+
newCookware,
|
|
1064
2774
|
reference
|
|
1065
2775
|
);
|
|
1066
2776
|
const newItem = {
|
|
1067
|
-
type: "
|
|
1068
|
-
index:
|
|
1069
|
-
displayName
|
|
2777
|
+
type: "cookware",
|
|
2778
|
+
index: idxInList
|
|
1070
2779
|
};
|
|
1071
|
-
if (
|
|
1072
|
-
newItem.
|
|
2780
|
+
if (quantity) {
|
|
2781
|
+
newItem.quantity = quantity;
|
|
1073
2782
|
}
|
|
1074
2783
|
items.push(newItem);
|
|
1075
|
-
} else if (groups.
|
|
1076
|
-
|
|
1077
|
-
const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
|
|
1078
|
-
const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
|
|
1079
|
-
const reference = modifiers !== void 0 && modifiers.includes("&");
|
|
1080
|
-
const flags = [];
|
|
1081
|
-
if (modifiers !== void 0 && modifiers.includes("?")) {
|
|
1082
|
-
flags.push("optional");
|
|
1083
|
-
}
|
|
1084
|
-
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
1085
|
-
flags.push("hidden");
|
|
1086
|
-
}
|
|
1087
|
-
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
1088
|
-
const idxsInList = findAndUpsertCookware(
|
|
1089
|
-
this.cookware,
|
|
1090
|
-
{
|
|
1091
|
-
name,
|
|
1092
|
-
quantity,
|
|
1093
|
-
quantityParts: quantity ? [quantity] : void 0,
|
|
1094
|
-
flags
|
|
1095
|
-
},
|
|
1096
|
-
reference
|
|
1097
|
-
);
|
|
1098
|
-
items.push({
|
|
1099
|
-
type: "cookware",
|
|
1100
|
-
index: idxsInList.cookwareIndex,
|
|
1101
|
-
quantityPartIndex: idxsInList.quantityPartIndex
|
|
1102
|
-
});
|
|
2784
|
+
} else if (groups.arbitraryQuantity) {
|
|
2785
|
+
this._parseArbitraryScalable(groups, items);
|
|
1103
2786
|
} else {
|
|
1104
2787
|
const durationStr = groups.timerQuantity.trim();
|
|
1105
2788
|
const unit = (groups.timerUnit || "").trim();
|
|
@@ -1123,10 +2806,11 @@ var Recipe = class _Recipe {
|
|
|
1123
2806
|
blankLineBefore = false;
|
|
1124
2807
|
}
|
|
1125
2808
|
flushPendingItems(section, items);
|
|
1126
|
-
|
|
2809
|
+
flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
|
|
1127
2810
|
if (!section.isBlank()) {
|
|
1128
2811
|
this.sections.push(section);
|
|
1129
2812
|
}
|
|
2813
|
+
this._populate_ingredient_quantities();
|
|
1130
2814
|
}
|
|
1131
2815
|
/**
|
|
1132
2816
|
* Scales the recipe to a new number of servings. In practice, it calls
|
|
@@ -1141,7 +2825,7 @@ var Recipe = class _Recipe {
|
|
|
1141
2825
|
if (originalServings === void 0 || originalServings === 0) {
|
|
1142
2826
|
originalServings = 1;
|
|
1143
2827
|
}
|
|
1144
|
-
const factor = (0,
|
|
2828
|
+
const factor = (0, import_big4.default)(newServings).div(originalServings);
|
|
1145
2829
|
return this.scaleBy(factor);
|
|
1146
2830
|
}
|
|
1147
2831
|
/**
|
|
@@ -1156,44 +2840,83 @@ var Recipe = class _Recipe {
|
|
|
1156
2840
|
if (originalServings === void 0 || originalServings === 0) {
|
|
1157
2841
|
originalServings = 1;
|
|
1158
2842
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
quantityPart.value,
|
|
1170
|
-
quantityPart.scalable ? (0, import_big2.default)(factor) : 1
|
|
1171
|
-
)
|
|
1172
|
-
};
|
|
2843
|
+
const unitSystem = this.unitSystem;
|
|
2844
|
+
function scaleAlternativesBy(alternatives, factor2) {
|
|
2845
|
+
for (const alternative of alternatives) {
|
|
2846
|
+
if (alternative.itemQuantity) {
|
|
2847
|
+
const scaleFactor = alternative.itemQuantity.scalable ? (0, import_big4.default)(factor2) : 1;
|
|
2848
|
+
if (alternative.itemQuantity.quantity.type !== "fixed" || alternative.itemQuantity.quantity.value.type !== "text") {
|
|
2849
|
+
alternative.itemQuantity.quantity = multiplyQuantityValue(
|
|
2850
|
+
alternative.itemQuantity.quantity,
|
|
2851
|
+
scaleFactor
|
|
2852
|
+
);
|
|
1173
2853
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
2854
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2855
|
+
alternative.itemQuantity.equivalents = alternative.itemQuantity.equivalents.map(
|
|
2856
|
+
(altQuantity) => {
|
|
2857
|
+
if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
|
|
2858
|
+
return altQuantity;
|
|
2859
|
+
} else {
|
|
2860
|
+
return {
|
|
2861
|
+
...altQuantity,
|
|
2862
|
+
quantity: multiplyQuantityValue(
|
|
2863
|
+
altQuantity.quantity,
|
|
2864
|
+
scaleFactor
|
|
2865
|
+
)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
const optimizedPrimary = applyBestUnit(
|
|
2872
|
+
{
|
|
2873
|
+
quantity: alternative.itemQuantity.quantity,
|
|
2874
|
+
unit: alternative.itemQuantity.unit
|
|
2875
|
+
},
|
|
2876
|
+
unitSystem
|
|
1182
2877
|
);
|
|
1183
|
-
|
|
1184
|
-
|
|
2878
|
+
alternative.itemQuantity.quantity = optimizedPrimary.quantity;
|
|
2879
|
+
alternative.itemQuantity.unit = optimizedPrimary.unit;
|
|
2880
|
+
if (alternative.itemQuantity.equivalents) {
|
|
2881
|
+
alternative.itemQuantity.equivalents = alternative.itemQuantity.equivalents.map(
|
|
2882
|
+
(eq) => applyBestUnit(eq, unitSystem)
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
1185
2885
|
}
|
|
1186
2886
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
2887
|
+
}
|
|
2888
|
+
for (const section of newRecipe.sections) {
|
|
2889
|
+
for (const step of section.content.filter(
|
|
2890
|
+
(item) => item.type === "step"
|
|
2891
|
+
)) {
|
|
2892
|
+
for (const item of step.items.filter(
|
|
2893
|
+
(item2) => item2.type === "ingredient"
|
|
2894
|
+
)) {
|
|
2895
|
+
scaleAlternativesBy(item.alternatives, factor);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
|
|
2900
|
+
scaleAlternativesBy(alternatives, factor);
|
|
2901
|
+
}
|
|
2902
|
+
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
2903
|
+
scaleAlternativesBy(alternatives, factor);
|
|
2904
|
+
}
|
|
2905
|
+
for (const arbitrary of newRecipe.arbitraries) {
|
|
2906
|
+
arbitrary.quantity = multiplyQuantityValue(
|
|
2907
|
+
arbitrary.quantity,
|
|
2908
|
+
factor
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
newRecipe._populate_ingredient_quantities();
|
|
2912
|
+
newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
|
|
1190
2913
|
if (newRecipe.metadata.servings && this.metadata.servings) {
|
|
1191
2914
|
if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
|
|
1192
2915
|
const servingsValue = parseFloat(
|
|
1193
2916
|
String(this.metadata.servings).replace(",", ".")
|
|
1194
2917
|
);
|
|
1195
2918
|
newRecipe.metadata.servings = String(
|
|
1196
|
-
(0,
|
|
2919
|
+
(0, import_big4.default)(servingsValue).times(factor).toNumber()
|
|
1197
2920
|
);
|
|
1198
2921
|
}
|
|
1199
2922
|
}
|
|
@@ -1203,7 +2926,7 @@ var Recipe = class _Recipe {
|
|
|
1203
2926
|
String(this.metadata.yield).replace(",", ".")
|
|
1204
2927
|
);
|
|
1205
2928
|
newRecipe.metadata.yield = String(
|
|
1206
|
-
(0,
|
|
2929
|
+
(0, import_big4.default)(yieldValue).times(factor).toNumber()
|
|
1207
2930
|
);
|
|
1208
2931
|
}
|
|
1209
2932
|
}
|
|
@@ -1213,10 +2936,143 @@ var Recipe = class _Recipe {
|
|
|
1213
2936
|
String(this.metadata.serves).replace(",", ".")
|
|
1214
2937
|
);
|
|
1215
2938
|
newRecipe.metadata.serves = String(
|
|
1216
|
-
(0,
|
|
2939
|
+
(0, import_big4.default)(servesValue).times(factor).toNumber()
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
return newRecipe;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Converts all ingredient quantities in the recipe to a target unit system.
|
|
2947
|
+
*
|
|
2948
|
+
* @param system - The target unit system to convert to (metric, US, UK, JP)
|
|
2949
|
+
* @param method - How to handle existing quantities:
|
|
2950
|
+
* - "keep": Keep all existing equivalents (swap if needed, or add converted)
|
|
2951
|
+
* - "replace": Replace primary with target system quantity, discard equivalent used for conversion
|
|
2952
|
+
* - "remove": Only keep target system quantity, delete all equivalents
|
|
2953
|
+
* @returns A new Recipe instance with converted quantities
|
|
2954
|
+
*
|
|
2955
|
+
* @example
|
|
2956
|
+
* ```typescript
|
|
2957
|
+
* // Convert a recipe to metric, keeping original units as equivalents
|
|
2958
|
+
* const metricRecipe = recipe.convertTo("metric", "keep");
|
|
2959
|
+
*
|
|
2960
|
+
* // Convert to US units, removing all other equivalents
|
|
2961
|
+
* const usRecipe = recipe.convertTo("US", "remove");
|
|
2962
|
+
* ```
|
|
2963
|
+
*/
|
|
2964
|
+
convertTo(system, method) {
|
|
2965
|
+
const newRecipe = this.clone();
|
|
2966
|
+
function buildNewPrimary(convertedQty, oldPrimary, remainingEquivalents, scalable, integerProtected, source) {
|
|
2967
|
+
const newUnit = integerProtected && convertedQty.unit ? { name: convertedQty.unit.name, integerProtected: true } : convertedQty.unit;
|
|
2968
|
+
const newPrimary = {
|
|
2969
|
+
quantity: convertedQty.quantity,
|
|
2970
|
+
unit: newUnit,
|
|
2971
|
+
scalable
|
|
2972
|
+
};
|
|
2973
|
+
if (method === "remove") {
|
|
2974
|
+
return newPrimary;
|
|
2975
|
+
} else if (method === "replace") {
|
|
2976
|
+
if (remainingEquivalents.length > 0) {
|
|
2977
|
+
newPrimary.equivalents = remainingEquivalents;
|
|
2978
|
+
if (source === "converted") newPrimary.equivalents.push(oldPrimary);
|
|
2979
|
+
}
|
|
2980
|
+
} else {
|
|
2981
|
+
newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
|
|
2982
|
+
}
|
|
2983
|
+
return newPrimary;
|
|
2984
|
+
}
|
|
2985
|
+
function convertItemQuantity(itemQuantity) {
|
|
2986
|
+
const primaryUnit = resolveUnit(itemQuantity.unit?.name);
|
|
2987
|
+
const equivalents = itemQuantity.equivalents ?? [];
|
|
2988
|
+
const oldPrimary = {
|
|
2989
|
+
quantity: itemQuantity.quantity,
|
|
2990
|
+
unit: itemQuantity.unit
|
|
2991
|
+
};
|
|
2992
|
+
if (primaryUnit.type !== "other" && isUnitCompatibleWithSystem(primaryUnit, system)) {
|
|
2993
|
+
if (method === "remove") {
|
|
2994
|
+
return { ...itemQuantity, equivalents: void 0 };
|
|
2995
|
+
}
|
|
2996
|
+
return itemQuantity;
|
|
2997
|
+
}
|
|
2998
|
+
const targetEquivIndex = equivalents.findIndex((eq) => {
|
|
2999
|
+
const eqUnit = resolveUnit(eq.unit?.name);
|
|
3000
|
+
return eqUnit.type !== "other" && isUnitCompatibleWithSystem(eqUnit, system);
|
|
3001
|
+
});
|
|
3002
|
+
if (targetEquivIndex !== -1) {
|
|
3003
|
+
const targetEquiv = equivalents[targetEquivIndex];
|
|
3004
|
+
const remainingEquivalents = equivalents.filter(
|
|
3005
|
+
(_, i2) => i2 !== targetEquivIndex
|
|
1217
3006
|
);
|
|
3007
|
+
return buildNewPrimary(
|
|
3008
|
+
targetEquiv,
|
|
3009
|
+
oldPrimary,
|
|
3010
|
+
remainingEquivalents,
|
|
3011
|
+
itemQuantity.scalable,
|
|
3012
|
+
targetEquiv.unit?.integerProtected,
|
|
3013
|
+
"swapped"
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
const converted = convertQuantityToSystem(oldPrimary, system);
|
|
3017
|
+
if (converted && converted.unit) {
|
|
3018
|
+
return buildNewPrimary(
|
|
3019
|
+
converted,
|
|
3020
|
+
oldPrimary,
|
|
3021
|
+
equivalents,
|
|
3022
|
+
itemQuantity.scalable,
|
|
3023
|
+
itemQuantity.unit?.integerProtected,
|
|
3024
|
+
"swapped"
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
for (let i2 = 0; i2 < equivalents.length; i2++) {
|
|
3028
|
+
const equiv = equivalents[i2];
|
|
3029
|
+
const convertedEquiv = convertQuantityToSystem(equiv, system);
|
|
3030
|
+
if (convertedEquiv && convertedEquiv.unit) {
|
|
3031
|
+
const remainingEquivalents = method === "keep" ? equivalents : equivalents.filter((_, idx) => idx !== i2);
|
|
3032
|
+
return buildNewPrimary(
|
|
3033
|
+
convertedEquiv,
|
|
3034
|
+
oldPrimary,
|
|
3035
|
+
remainingEquivalents,
|
|
3036
|
+
itemQuantity.scalable,
|
|
3037
|
+
equiv.unit?.integerProtected,
|
|
3038
|
+
"converted"
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (method === "remove") {
|
|
3043
|
+
return { ...itemQuantity, equivalents: void 0 };
|
|
3044
|
+
} else {
|
|
3045
|
+
return itemQuantity;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
function convertAlternatives(alternatives) {
|
|
3049
|
+
for (const alternative of alternatives) {
|
|
3050
|
+
if (alternative.itemQuantity) {
|
|
3051
|
+
alternative.itemQuantity = convertItemQuantity(
|
|
3052
|
+
alternative.itemQuantity
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
1218
3055
|
}
|
|
1219
3056
|
}
|
|
3057
|
+
for (const section of newRecipe.sections) {
|
|
3058
|
+
for (const step of section.content.filter(
|
|
3059
|
+
(item) => item.type === "step"
|
|
3060
|
+
)) {
|
|
3061
|
+
for (const item of step.items.filter(
|
|
3062
|
+
(item2) => item2.type === "ingredient"
|
|
3063
|
+
)) {
|
|
3064
|
+
convertAlternatives(item.alternatives);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
|
|
3069
|
+
convertAlternatives(alternatives);
|
|
3070
|
+
}
|
|
3071
|
+
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
3072
|
+
convertAlternatives(alternatives);
|
|
3073
|
+
}
|
|
3074
|
+
newRecipe._populate_ingredient_quantities();
|
|
3075
|
+
if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
|
|
1220
3076
|
return newRecipe;
|
|
1221
3077
|
}
|
|
1222
3078
|
/**
|
|
@@ -1236,19 +3092,33 @@ var Recipe = class _Recipe {
|
|
|
1236
3092
|
*/
|
|
1237
3093
|
clone() {
|
|
1238
3094
|
const newRecipe = new _Recipe();
|
|
1239
|
-
newRecipe.
|
|
1240
|
-
newRecipe
|
|
1241
|
-
|
|
1242
|
-
);
|
|
1243
|
-
newRecipe.sections =
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
3095
|
+
newRecipe.choices = deepClone(this.choices);
|
|
3096
|
+
_Recipe.itemCounts.set(newRecipe, this.getItemCount());
|
|
3097
|
+
newRecipe.metadata = deepClone(this.metadata);
|
|
3098
|
+
newRecipe.ingredients = deepClone(this.ingredients);
|
|
3099
|
+
newRecipe.sections = this.sections.map((section) => {
|
|
3100
|
+
const newSection = new Section(section.name);
|
|
3101
|
+
newSection.content = deepClone(section.content);
|
|
3102
|
+
return newSection;
|
|
3103
|
+
});
|
|
3104
|
+
newRecipe.cookware = deepClone(this.cookware);
|
|
3105
|
+
newRecipe.timers = deepClone(this.timers);
|
|
3106
|
+
newRecipe.arbitraries = deepClone(this.arbitraries);
|
|
1248
3107
|
newRecipe.servings = this.servings;
|
|
1249
3108
|
return newRecipe;
|
|
1250
3109
|
}
|
|
1251
3110
|
};
|
|
3111
|
+
/**
|
|
3112
|
+
* External storage for unit system (not a property on instances).
|
|
3113
|
+
* Used for resolving ambiguous units during quantity addition.
|
|
3114
|
+
*/
|
|
3115
|
+
__publicField(_Recipe, "unitSystems", /* @__PURE__ */ new WeakMap());
|
|
3116
|
+
/**
|
|
3117
|
+
* External storage for item count (not a property on instances).
|
|
3118
|
+
* Used for giving ID numbers to items during parsing.
|
|
3119
|
+
*/
|
|
3120
|
+
__publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
|
|
3121
|
+
var Recipe = _Recipe;
|
|
1252
3122
|
|
|
1253
3123
|
// src/classes/shopping_list.ts
|
|
1254
3124
|
var ShoppingList = class {
|
|
@@ -1257,6 +3127,7 @@ var ShoppingList = class {
|
|
|
1257
3127
|
* @param category_config_str - The category configuration to parse.
|
|
1258
3128
|
*/
|
|
1259
3129
|
constructor(category_config_str) {
|
|
3130
|
+
// TODO: backport type change
|
|
1260
3131
|
/**
|
|
1261
3132
|
* The ingredients in the shopping list.
|
|
1262
3133
|
*/
|
|
@@ -1279,6 +3150,33 @@ var ShoppingList = class {
|
|
|
1279
3150
|
}
|
|
1280
3151
|
calculate_ingredients() {
|
|
1281
3152
|
this.ingredients = [];
|
|
3153
|
+
const addIngredientQuantity = (name, quantityTotal) => {
|
|
3154
|
+
const quantityTotalExtended = extendAllUnits(quantityTotal);
|
|
3155
|
+
const newQuantities = isAndGroup(quantityTotalExtended) ? quantityTotalExtended.and : [quantityTotalExtended];
|
|
3156
|
+
const existing = this.ingredients.find((i2) => i2.name === name);
|
|
3157
|
+
if (existing) {
|
|
3158
|
+
if (!existing.quantityTotal) {
|
|
3159
|
+
existing.quantityTotal = quantityTotal;
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
try {
|
|
3163
|
+
const existingQuantityTotalExtended = extendAllUnits(
|
|
3164
|
+
existing.quantityTotal
|
|
3165
|
+
);
|
|
3166
|
+
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
|
|
3167
|
+
existing.quantityTotal = addEquivalentsAndSimplify([
|
|
3168
|
+
...existingQuantities,
|
|
3169
|
+
...newQuantities
|
|
3170
|
+
]);
|
|
3171
|
+
return;
|
|
3172
|
+
} catch {
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
this.ingredients.push({
|
|
3176
|
+
name,
|
|
3177
|
+
quantityTotal
|
|
3178
|
+
});
|
|
3179
|
+
};
|
|
1282
3180
|
for (const addedRecipe of this.recipes) {
|
|
1283
3181
|
let scaledRecipe;
|
|
1284
3182
|
if ("factor" in addedRecipe) {
|
|
@@ -1287,67 +3185,123 @@ var ShoppingList = class {
|
|
|
1287
3185
|
} else {
|
|
1288
3186
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
1289
3187
|
}
|
|
1290
|
-
|
|
3188
|
+
const ingredients = scaledRecipe.getIngredientQuantities({
|
|
3189
|
+
choices: addedRecipe.choices
|
|
3190
|
+
});
|
|
3191
|
+
for (const ingredient of ingredients) {
|
|
1291
3192
|
if (ingredient.flags && ingredient.flags.includes("hidden")) {
|
|
1292
3193
|
continue;
|
|
1293
3194
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
if (
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
value: existingIngredient.quantity,
|
|
1304
|
-
unit: existingIngredient.unit ?? ""
|
|
1305
|
-
},
|
|
1306
|
-
{
|
|
1307
|
-
value: ingredient.quantity,
|
|
1308
|
-
unit: ingredient.unit ?? ""
|
|
1309
|
-
}
|
|
1310
|
-
);
|
|
1311
|
-
existingIngredient.quantity = newQuantity.value;
|
|
1312
|
-
if (newQuantity.unit) {
|
|
1313
|
-
existingIngredient.unit = newQuantity.unit;
|
|
3195
|
+
if (!ingredient.usedAsPrimary) {
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
if (ingredient.quantities && ingredient.quantities.length > 0) {
|
|
3199
|
+
const allQuantities = [];
|
|
3200
|
+
for (const qGroup of ingredient.quantities) {
|
|
3201
|
+
if ("and" in qGroup) {
|
|
3202
|
+
for (const qty of qGroup.and) {
|
|
3203
|
+
allQuantities.push(qty);
|
|
1314
3204
|
}
|
|
1315
3205
|
} else {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
3206
|
+
const plainQty = {
|
|
3207
|
+
quantity: qGroup.quantity
|
|
3208
|
+
};
|
|
3209
|
+
if (qGroup.unit) plainQty.unit = qGroup.unit;
|
|
3210
|
+
if (qGroup.equivalents) plainQty.equivalents = qGroup.equivalents;
|
|
3211
|
+
allQuantities.push(plainQty);
|
|
1320
3212
|
}
|
|
1321
3213
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
3214
|
+
if (allQuantities.length === 1) {
|
|
3215
|
+
addIngredientQuantity(ingredient.name, allQuantities[0]);
|
|
3216
|
+
} else {
|
|
3217
|
+
const extendedQuantities = allQuantities.map(
|
|
3218
|
+
(q) => extendAllUnits(q)
|
|
3219
|
+
);
|
|
3220
|
+
const totalQuantity = addEquivalentsAndSimplify(
|
|
3221
|
+
extendedQuantities
|
|
3222
|
+
);
|
|
3223
|
+
addIngredientQuantity(ingredient.name, totalQuantity);
|
|
1332
3224
|
}
|
|
1333
|
-
|
|
3225
|
+
} else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
|
|
3226
|
+
this.ingredients.push({ name: ingredient.name });
|
|
1334
3227
|
}
|
|
1335
3228
|
}
|
|
1336
3229
|
}
|
|
1337
3230
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
3231
|
+
/**
|
|
3232
|
+
* Adds a recipe to the shopping list, then automatically
|
|
3233
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
3234
|
+
* @param recipe - The recipe to add.
|
|
3235
|
+
* @param options - Options for adding the recipe.
|
|
3236
|
+
* @throws Error if the recipe has alternatives without corresponding choices.
|
|
3237
|
+
*/
|
|
3238
|
+
add_recipe(recipe, options = {}) {
|
|
3239
|
+
const errorMessage = this.getUnresolvedAlternativesError(
|
|
3240
|
+
recipe,
|
|
3241
|
+
options.choices
|
|
3242
|
+
);
|
|
3243
|
+
if (errorMessage) {
|
|
3244
|
+
throw new Error(errorMessage);
|
|
3245
|
+
}
|
|
3246
|
+
if (!options.scaling) {
|
|
3247
|
+
this.recipes.push({
|
|
3248
|
+
recipe,
|
|
3249
|
+
factor: options.scaling ?? 1,
|
|
3250
|
+
choices: options.choices
|
|
3251
|
+
});
|
|
1341
3252
|
} else {
|
|
1342
|
-
if ("factor" in scaling) {
|
|
1343
|
-
this.recipes.push({
|
|
3253
|
+
if ("factor" in options.scaling) {
|
|
3254
|
+
this.recipes.push({
|
|
3255
|
+
recipe,
|
|
3256
|
+
factor: options.scaling.factor,
|
|
3257
|
+
choices: options.choices
|
|
3258
|
+
});
|
|
1344
3259
|
} else {
|
|
1345
|
-
this.recipes.push({
|
|
3260
|
+
this.recipes.push({
|
|
3261
|
+
recipe,
|
|
3262
|
+
servings: options.scaling.servings,
|
|
3263
|
+
choices: options.choices
|
|
3264
|
+
});
|
|
1346
3265
|
}
|
|
1347
3266
|
}
|
|
1348
3267
|
this.calculate_ingredients();
|
|
1349
3268
|
this.categorize();
|
|
1350
3269
|
}
|
|
3270
|
+
/**
|
|
3271
|
+
* Checks if a recipe has unresolved alternatives (alternatives without provided choices).
|
|
3272
|
+
* @param recipe - The recipe to check.
|
|
3273
|
+
* @param choices - The choices provided for the recipe.
|
|
3274
|
+
* @returns An error message if there are unresolved alternatives, undefined otherwise.
|
|
3275
|
+
*/
|
|
3276
|
+
getUnresolvedAlternativesError(recipe, choices) {
|
|
3277
|
+
const missingItems = [];
|
|
3278
|
+
const missingGroups = [];
|
|
3279
|
+
for (const itemId of recipe.choices.ingredientItems.keys()) {
|
|
3280
|
+
if (!choices?.ingredientItems?.has(itemId)) {
|
|
3281
|
+
missingItems.push(itemId);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
for (const groupId of recipe.choices.ingredientGroups.keys()) {
|
|
3285
|
+
if (!choices?.ingredientGroups?.has(groupId)) {
|
|
3286
|
+
missingGroups.push(groupId);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
if (missingItems.length === 0 && missingGroups.length === 0) {
|
|
3290
|
+
return void 0;
|
|
3291
|
+
}
|
|
3292
|
+
const parts = [];
|
|
3293
|
+
if (missingItems.length > 0) {
|
|
3294
|
+
parts.push(
|
|
3295
|
+
`ingredientItems: [${missingItems.map((i2) => `'${i2}'`).join(", ")}]`
|
|
3296
|
+
);
|
|
3297
|
+
}
|
|
3298
|
+
if (missingGroups.length > 0) {
|
|
3299
|
+
parts.push(
|
|
3300
|
+
`ingredientGroups: [${missingGroups.map((g) => `'${g}'`).join(", ")}]`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
return `Recipe has unresolved alternatives. Missing choices for: ${parts.join(", ")}`;
|
|
3304
|
+
}
|
|
1351
3305
|
/**
|
|
1352
3306
|
* Removes a recipe from the shopping list, then automatically
|
|
1353
3307
|
* recalculates the quantities and recategorize the ingredients.s
|
|
@@ -1407,15 +3361,384 @@ var ShoppingList = class {
|
|
|
1407
3361
|
this.categories = categories;
|
|
1408
3362
|
}
|
|
1409
3363
|
};
|
|
3364
|
+
|
|
3365
|
+
// src/classes/shopping_cart.ts
|
|
3366
|
+
var import_yalps = require("yalps");
|
|
3367
|
+
var ShoppingCart = class {
|
|
3368
|
+
/**
|
|
3369
|
+
* Creates a new ShoppingCart instance
|
|
3370
|
+
* @param options - {@link ShoppingCartOptions | Options} for the constructor
|
|
3371
|
+
*/
|
|
3372
|
+
constructor(options) {
|
|
3373
|
+
/**
|
|
3374
|
+
* The product catalog to use for matching products
|
|
3375
|
+
*/
|
|
3376
|
+
__publicField(this, "productCatalog");
|
|
3377
|
+
/**
|
|
3378
|
+
* The shopping list to build the cart from
|
|
3379
|
+
*/
|
|
3380
|
+
__publicField(this, "shoppingList");
|
|
3381
|
+
/**
|
|
3382
|
+
* The content of the cart
|
|
3383
|
+
*/
|
|
3384
|
+
__publicField(this, "cart", []);
|
|
3385
|
+
/**
|
|
3386
|
+
* The ingredients that were successfully matched with products
|
|
3387
|
+
*/
|
|
3388
|
+
__publicField(this, "match", []);
|
|
3389
|
+
/**
|
|
3390
|
+
* The ingredients that could not be matched with products
|
|
3391
|
+
*/
|
|
3392
|
+
__publicField(this, "misMatch", []);
|
|
3393
|
+
/**
|
|
3394
|
+
* Key information about the shopping cart
|
|
3395
|
+
*/
|
|
3396
|
+
__publicField(this, "summary");
|
|
3397
|
+
if (options?.catalog) this.productCatalog = options.catalog;
|
|
3398
|
+
if (options?.list) this.shoppingList = options.list;
|
|
3399
|
+
this.summary = { totalPrice: 0, totalItems: 0 };
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Sets the product catalog to use for matching products
|
|
3403
|
+
* To use if a catalog was not provided at the creation of the instance
|
|
3404
|
+
* @param catalog - The {@link ProductCatalog} to set
|
|
3405
|
+
*/
|
|
3406
|
+
setProductCatalog(catalog) {
|
|
3407
|
+
this.productCatalog = catalog;
|
|
3408
|
+
}
|
|
3409
|
+
// TODO: harmonize recipe name to use underscores
|
|
3410
|
+
/**
|
|
3411
|
+
* Sets the shopping list to build the cart from.
|
|
3412
|
+
* To use if a shopping list was not provided at the creation of the instance
|
|
3413
|
+
* @param list - The {@link ShoppingList} to set
|
|
3414
|
+
*/
|
|
3415
|
+
setShoppingList(list) {
|
|
3416
|
+
this.shoppingList = list;
|
|
3417
|
+
}
|
|
3418
|
+
/**
|
|
3419
|
+
* Builds the cart from the shopping list and product catalog
|
|
3420
|
+
* @remarks
|
|
3421
|
+
* - 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
|
|
3422
|
+
* in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
|
|
3423
|
+
* - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
|
|
3424
|
+
* - No product is listed in the catalog for that ingredient
|
|
3425
|
+
* - The ingredient has no quantity, a text quantity
|
|
3426
|
+
* - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
|
|
3427
|
+
* @throws {@link NoProductCatalogForCartError} if no product catalog is set
|
|
3428
|
+
* @throws {@link NoShoppingListForCartError} if no shopping list is set
|
|
3429
|
+
* @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
|
|
3430
|
+
*/
|
|
3431
|
+
buildCart() {
|
|
3432
|
+
this.resetCart();
|
|
3433
|
+
if (this.productCatalog === void 0) {
|
|
3434
|
+
throw new NoProductCatalogForCartError();
|
|
3435
|
+
} else if (this.shoppingList === void 0) {
|
|
3436
|
+
throw new NoShoppingListForCartError();
|
|
3437
|
+
}
|
|
3438
|
+
for (const ingredient of this.shoppingList.ingredients) {
|
|
3439
|
+
const productOptions = this.getProductOptions(ingredient);
|
|
3440
|
+
try {
|
|
3441
|
+
const optimumMatch = this.getOptimumMatch(ingredient, productOptions);
|
|
3442
|
+
this.cart.push(...optimumMatch);
|
|
3443
|
+
this.match.push({ ingredient, selection: optimumMatch });
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
if (error instanceof NoProductMatchError) {
|
|
3446
|
+
this.misMatch.push({ ingredient, reason: error.code });
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
this.summarize();
|
|
3451
|
+
return this.misMatch.length > 0;
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* Gets the product options for a given ingredient
|
|
3455
|
+
* @param ingredient - The ingredient to get the product options for
|
|
3456
|
+
* @returns An array of {@link ProductOption}
|
|
3457
|
+
*/
|
|
3458
|
+
getProductOptions(ingredient) {
|
|
3459
|
+
return this.productCatalog.products.filter(
|
|
3460
|
+
(product) => product.ingredientName === ingredient.name || product.ingredientAliases?.includes(ingredient.name)
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Gets the optimum match for a given ingredient and product option
|
|
3465
|
+
* @param ingredient - The ingredient to match
|
|
3466
|
+
* @param options - The product options to choose from
|
|
3467
|
+
* @returns An array of {@link ProductSelection}
|
|
3468
|
+
* @throws {@link NoProductMatchError} if no match can be found
|
|
3469
|
+
*/
|
|
3470
|
+
getOptimumMatch(ingredient, options) {
|
|
3471
|
+
if (options.length === 0)
|
|
3472
|
+
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
3473
|
+
if (!ingredient.quantityTotal)
|
|
3474
|
+
throw new NoProductMatchError(ingredient.name, "noQuantity");
|
|
3475
|
+
const normalizedOptions = options.map(
|
|
3476
|
+
(option) => ({
|
|
3477
|
+
...option,
|
|
3478
|
+
sizes: option.sizes.map((s) => {
|
|
3479
|
+
const resolvedUnit = resolveUnit(s.unit);
|
|
3480
|
+
return {
|
|
3481
|
+
size: resolvedUnit && "toBase" in resolvedUnit ? multiplyQuantityValue(
|
|
3482
|
+
s.size,
|
|
3483
|
+
resolvedUnit.toBase
|
|
3484
|
+
) : s.size,
|
|
3485
|
+
unit: resolvedUnit
|
|
3486
|
+
};
|
|
3487
|
+
})
|
|
3488
|
+
})
|
|
3489
|
+
);
|
|
3490
|
+
const normalizedQuantityTotal = normalizeAllUnits(ingredient.quantityTotal);
|
|
3491
|
+
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
3492
|
+
if (isAndGroup(normalizedQuantities)) {
|
|
3493
|
+
for (const q of normalizedQuantities.and) {
|
|
3494
|
+
const result = getOptimumMatchForQuantityParts(
|
|
3495
|
+
q,
|
|
3496
|
+
normalizedOptions2,
|
|
3497
|
+
selection
|
|
3498
|
+
);
|
|
3499
|
+
selection.push(...result);
|
|
3500
|
+
}
|
|
3501
|
+
} else {
|
|
3502
|
+
const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.or : [normalizedQuantities];
|
|
3503
|
+
const solutions = [];
|
|
3504
|
+
const errors = /* @__PURE__ */ new Set();
|
|
3505
|
+
for (const alternative of alternativeUnitsOfQuantity) {
|
|
3506
|
+
if (alternative.quantity.type === "fixed" && alternative.quantity.value.type === "text") {
|
|
3507
|
+
errors.add("textValue");
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
const scaledQuantity = multiplyQuantityValue(
|
|
3511
|
+
alternative.quantity,
|
|
3512
|
+
"toBase" in alternative.unit ? alternative.unit.toBase : 1
|
|
3513
|
+
);
|
|
3514
|
+
alternative.quantity = scaledQuantity;
|
|
3515
|
+
const matchOptions = normalizedOptions2.filter(
|
|
3516
|
+
(option) => option.sizes.some(
|
|
3517
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
3518
|
+
)
|
|
3519
|
+
);
|
|
3520
|
+
if (matchOptions.length > 0) {
|
|
3521
|
+
const findCompatibleSize = (option) => option.sizes.find(
|
|
3522
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
3523
|
+
);
|
|
3524
|
+
if (matchOptions.length == 1) {
|
|
3525
|
+
const matchedOption = matchOptions[0];
|
|
3526
|
+
const compatibleSize = findCompatibleSize(matchedOption);
|
|
3527
|
+
const product = options.find(
|
|
3528
|
+
(opt) => opt.id === matchedOption.id
|
|
3529
|
+
);
|
|
3530
|
+
const targetQuantity = scaledQuantity.type === "fixed" ? scaledQuantity.value : scaledQuantity.min;
|
|
3531
|
+
const resQuantity = Math.ceil(
|
|
3532
|
+
getNumericValue(targetQuantity) / getNumericValue(compatibleSize.size.value)
|
|
3533
|
+
);
|
|
3534
|
+
solutions.push([
|
|
3535
|
+
{
|
|
3536
|
+
product,
|
|
3537
|
+
quantity: resQuantity,
|
|
3538
|
+
totalPrice: resQuantity * matchedOption.price
|
|
3539
|
+
}
|
|
3540
|
+
]);
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3543
|
+
const model = {
|
|
3544
|
+
direction: "minimize",
|
|
3545
|
+
objective: "price",
|
|
3546
|
+
integers: true,
|
|
3547
|
+
constraints: {
|
|
3548
|
+
size: {
|
|
3549
|
+
min: scaledQuantity.type === "fixed" ? getNumericValue(scaledQuantity.value) : getNumericValue(scaledQuantity.min)
|
|
3550
|
+
}
|
|
3551
|
+
},
|
|
3552
|
+
variables: matchOptions.reduce(
|
|
3553
|
+
(acc, option) => {
|
|
3554
|
+
const compatibleSize = findCompatibleSize(option);
|
|
3555
|
+
acc[option.id] = {
|
|
3556
|
+
price: option.price,
|
|
3557
|
+
size: getNumericValue(compatibleSize.size.value)
|
|
3558
|
+
};
|
|
3559
|
+
return acc;
|
|
3560
|
+
},
|
|
3561
|
+
{}
|
|
3562
|
+
)
|
|
3563
|
+
};
|
|
3564
|
+
const solution = (0, import_yalps.solve)(model);
|
|
3565
|
+
solutions.push(
|
|
3566
|
+
solution.variables.map((variable) => {
|
|
3567
|
+
const resProductSelection = {
|
|
3568
|
+
product: options.find((option) => option.id === variable[0]),
|
|
3569
|
+
quantity: variable[1]
|
|
3570
|
+
};
|
|
3571
|
+
return {
|
|
3572
|
+
...resProductSelection,
|
|
3573
|
+
totalPrice: resProductSelection.quantity * resProductSelection.product.price
|
|
3574
|
+
};
|
|
3575
|
+
})
|
|
3576
|
+
);
|
|
3577
|
+
} else {
|
|
3578
|
+
errors.add("incompatibleUnits");
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
if (solutions.length === 0) {
|
|
3582
|
+
throw new NoProductMatchError(
|
|
3583
|
+
ingredient.name,
|
|
3584
|
+
errors.size === 1 ? errors.values().next().value : "textValue_incompatibleUnits"
|
|
3585
|
+
);
|
|
3586
|
+
} else {
|
|
3587
|
+
return solutions.sort(
|
|
3588
|
+
(a2, b) => a2.reduce((acc, item) => acc + item.totalPrice, 0) - b.reduce((acc, item) => acc + item.totalPrice, 0)
|
|
3589
|
+
)[0];
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return selection;
|
|
3593
|
+
}
|
|
3594
|
+
return getOptimumMatchForQuantityParts(
|
|
3595
|
+
normalizedQuantityTotal,
|
|
3596
|
+
normalizedOptions
|
|
3597
|
+
);
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Reset the cart's properties
|
|
3601
|
+
*/
|
|
3602
|
+
resetCart() {
|
|
3603
|
+
this.cart = [];
|
|
3604
|
+
this.match = [];
|
|
3605
|
+
this.misMatch = [];
|
|
3606
|
+
this.summary = { totalPrice: 0, totalItems: 0 };
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
|
|
3610
|
+
* This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
|
|
3611
|
+
* @returns the total price and number of items in the cart
|
|
3612
|
+
*/
|
|
3613
|
+
summarize() {
|
|
3614
|
+
this.summary.totalPrice = this.cart.reduce(
|
|
3615
|
+
(acc, item) => acc + item.totalPrice,
|
|
3616
|
+
0
|
|
3617
|
+
);
|
|
3618
|
+
this.summary.totalItems = this.cart.length;
|
|
3619
|
+
return this.summary;
|
|
3620
|
+
}
|
|
3621
|
+
};
|
|
3622
|
+
|
|
3623
|
+
// src/utils/render_helpers.ts
|
|
3624
|
+
var VULGAR_FRACTIONS = {
|
|
3625
|
+
"1/2": "\xBD",
|
|
3626
|
+
"1/3": "\u2153",
|
|
3627
|
+
"2/3": "\u2154",
|
|
3628
|
+
"1/4": "\xBC",
|
|
3629
|
+
"3/4": "\xBE",
|
|
3630
|
+
"1/8": "\u215B",
|
|
3631
|
+
"3/8": "\u215C",
|
|
3632
|
+
"5/8": "\u215D",
|
|
3633
|
+
"7/8": "\u215E"
|
|
3634
|
+
};
|
|
3635
|
+
function renderFractionAsVulgar(num, den) {
|
|
3636
|
+
const wholePart = Math.floor(num / den);
|
|
3637
|
+
const remainder = num % den;
|
|
3638
|
+
if (remainder === 0) {
|
|
3639
|
+
return String(wholePart);
|
|
3640
|
+
}
|
|
3641
|
+
const fractionKey = `${remainder}/${den}`;
|
|
3642
|
+
const vulgar = VULGAR_FRACTIONS[fractionKey];
|
|
3643
|
+
if (wholePart > 0) {
|
|
3644
|
+
return vulgar ? `${wholePart}${vulgar}` : `${wholePart} ${remainder}/${den}`;
|
|
3645
|
+
}
|
|
3646
|
+
return vulgar ?? `${num}/${den}`;
|
|
3647
|
+
}
|
|
3648
|
+
function formatNumericValue(value, useVulgar = true) {
|
|
3649
|
+
if (value.type === "decimal") {
|
|
3650
|
+
return String(value.decimal);
|
|
3651
|
+
}
|
|
3652
|
+
if (useVulgar) {
|
|
3653
|
+
return renderFractionAsVulgar(value.num, value.den);
|
|
3654
|
+
}
|
|
3655
|
+
return `${value.num}/${value.den}`;
|
|
3656
|
+
}
|
|
3657
|
+
function formatSingleValue(value) {
|
|
3658
|
+
if (value.type === "text") {
|
|
3659
|
+
return value.text;
|
|
3660
|
+
}
|
|
3661
|
+
return formatNumericValue(value);
|
|
3662
|
+
}
|
|
3663
|
+
function formatQuantity(quantity) {
|
|
3664
|
+
if (quantity.type === "fixed") {
|
|
3665
|
+
return formatSingleValue(quantity.value);
|
|
3666
|
+
}
|
|
3667
|
+
const minStr = formatNumericValue(quantity.min);
|
|
3668
|
+
const maxStr = formatNumericValue(quantity.max);
|
|
3669
|
+
return `${minStr}-${maxStr}`;
|
|
3670
|
+
}
|
|
3671
|
+
function formatUnit(unit) {
|
|
3672
|
+
if (!unit) return "";
|
|
3673
|
+
if (typeof unit === "string") return unit;
|
|
3674
|
+
return unit.name;
|
|
3675
|
+
}
|
|
3676
|
+
function formatQuantityWithUnit(quantity, unit) {
|
|
3677
|
+
if (!quantity) return "";
|
|
3678
|
+
const qty = formatQuantity(quantity);
|
|
3679
|
+
const unitStr = formatUnit(unit);
|
|
3680
|
+
return unitStr ? `${qty} ${unitStr}` : qty;
|
|
3681
|
+
}
|
|
3682
|
+
function formatExtendedQuantity(item) {
|
|
3683
|
+
return formatQuantityWithUnit(item.quantity, item.unit);
|
|
3684
|
+
}
|
|
3685
|
+
function formatItemQuantity(itemQuantity, separator = " | ") {
|
|
3686
|
+
const parts = [];
|
|
3687
|
+
parts.push(formatExtendedQuantity(itemQuantity));
|
|
3688
|
+
if (itemQuantity.equivalents) {
|
|
3689
|
+
for (const eq of itemQuantity.equivalents) {
|
|
3690
|
+
parts.push(formatExtendedQuantity(eq));
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
return parts.join(separator);
|
|
3694
|
+
}
|
|
3695
|
+
function isGroupedItem(item) {
|
|
3696
|
+
return item.group !== void 0;
|
|
3697
|
+
}
|
|
3698
|
+
function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
3699
|
+
if (item.group) {
|
|
3700
|
+
const selectedIndex2 = choices?.ingredientGroups?.get(item.group);
|
|
3701
|
+
const groupAlternatives = recipe.choices.ingredientGroups.get(item.group);
|
|
3702
|
+
if (groupAlternatives && selectedIndex2 !== void 0 && selectedIndex2 < groupAlternatives.length) {
|
|
3703
|
+
const selectedItemId = groupAlternatives[selectedIndex2]?.itemId;
|
|
3704
|
+
return selectedItemId === item.id;
|
|
3705
|
+
}
|
|
3706
|
+
return false;
|
|
3707
|
+
}
|
|
3708
|
+
const selectedIndex = choices?.ingredientItems?.get(item.id);
|
|
3709
|
+
return alternativeIndex === selectedIndex;
|
|
3710
|
+
}
|
|
1410
3711
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1411
3712
|
0 && (module.exports = {
|
|
1412
3713
|
CategoryConfig,
|
|
3714
|
+
NoProductCatalogForCartError,
|
|
3715
|
+
NoShoppingListForCartError,
|
|
3716
|
+
ProductCatalog,
|
|
1413
3717
|
Recipe,
|
|
1414
3718
|
Section,
|
|
1415
|
-
|
|
3719
|
+
ShoppingCart,
|
|
3720
|
+
ShoppingList,
|
|
3721
|
+
convertQuantityToSystem,
|
|
3722
|
+
formatExtendedQuantity,
|
|
3723
|
+
formatItemQuantity,
|
|
3724
|
+
formatNumericValue,
|
|
3725
|
+
formatQuantity,
|
|
3726
|
+
formatQuantityWithUnit,
|
|
3727
|
+
formatSingleValue,
|
|
3728
|
+
formatUnit,
|
|
3729
|
+
hasAlternatives,
|
|
3730
|
+
isAlternativeSelected,
|
|
3731
|
+
isAndGroup,
|
|
3732
|
+
isGroupedItem,
|
|
3733
|
+
isSimpleGroup,
|
|
3734
|
+
renderFractionAsVulgar
|
|
1416
3735
|
});
|
|
1417
3736
|
/* v8 ignore else -- @preserve */
|
|
1418
|
-
/* v8 ignore
|
|
3737
|
+
/* v8 ignore next -- @preserve: defensive fallback for ambiguous units without toBaseBySystem */
|
|
3738
|
+
/* v8 ignore start -- @preserve: defensive fallback that shouldn't happen with valid inputs */
|
|
3739
|
+
// v8 ignore else -- @preserve
|
|
3740
|
+
// v8 ignore if -- @preserve
|
|
1419
3741
|
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
1420
|
-
/* v8 ignore
|
|
3742
|
+
/* v8 ignore if -- @preserve */
|
|
3743
|
+
// v8 ignore next -- @preserve
|
|
1421
3744
|
//# sourceMappingURL=index.cjs.map
|