@tmlmt/cooklang-parser 3.0.0-alpha.8 → 3.0.0-elpha.22
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 +2716 -585
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +952 -141
- package/dist/index.d.ts +952 -141
- package/dist/index.js +2697 -584
- package/dist/index.js.map +1 -1
- package/package.json +21 -21
package/dist/index.cjs
CHANGED
|
@@ -35,11 +35,29 @@ __export(index_exports, {
|
|
|
35
35
|
CategoryConfig: () => CategoryConfig,
|
|
36
36
|
NoProductCatalogForCartError: () => NoProductCatalogForCartError,
|
|
37
37
|
NoShoppingListForCartError: () => NoShoppingListForCartError,
|
|
38
|
+
Pantry: () => Pantry,
|
|
38
39
|
ProductCatalog: () => ProductCatalog,
|
|
39
40
|
Recipe: () => Recipe,
|
|
40
41
|
Section: () => Section,
|
|
41
42
|
ShoppingCart: () => ShoppingCart,
|
|
42
|
-
ShoppingList: () => ShoppingList
|
|
43
|
+
ShoppingList: () => ShoppingList,
|
|
44
|
+
convertQuantityToSystem: () => convertQuantityToSystem,
|
|
45
|
+
formatExtendedQuantity: () => formatExtendedQuantity,
|
|
46
|
+
formatItemQuantity: () => formatItemQuantity,
|
|
47
|
+
formatNumericValue: () => formatNumericValue,
|
|
48
|
+
formatQuantity: () => formatQuantity,
|
|
49
|
+
formatQuantityWithUnit: () => formatQuantityWithUnit,
|
|
50
|
+
formatSingleValue: () => formatSingleValue,
|
|
51
|
+
formatUnit: () => formatUnit,
|
|
52
|
+
getEffectiveChoices: () => getEffectiveChoices,
|
|
53
|
+
hasAlternatives: () => hasAlternatives,
|
|
54
|
+
isAlternativeSelected: () => isAlternativeSelected,
|
|
55
|
+
isAndGroup: () => isAndGroup,
|
|
56
|
+
isGroupedItem: () => isGroupedItem,
|
|
57
|
+
isSectionActive: () => isSectionActive,
|
|
58
|
+
isSimpleGroup: () => isSimpleGroup,
|
|
59
|
+
isStepActive: () => isStepActive,
|
|
60
|
+
renderFractionAsVulgar: () => renderFractionAsVulgar
|
|
43
61
|
});
|
|
44
62
|
module.exports = __toCommonJS(index_exports);
|
|
45
63
|
|
|
@@ -104,7 +122,7 @@ var CategoryConfig = class {
|
|
|
104
122
|
}
|
|
105
123
|
};
|
|
106
124
|
|
|
107
|
-
// src/classes/
|
|
125
|
+
// src/classes/pantry.ts
|
|
108
126
|
var import_smol_toml = __toESM(require("smol-toml"), 1);
|
|
109
127
|
|
|
110
128
|
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
@@ -306,14 +324,19 @@ var i = (() => {
|
|
|
306
324
|
})();
|
|
307
325
|
|
|
308
326
|
// src/regex.ts
|
|
327
|
+
var metadataKeyRegex = /^([^:\n]+?):/gm;
|
|
328
|
+
var numericValueRegex = /^-?\d+(\.\d+)?$/;
|
|
329
|
+
var nestedMetaVarRegex = (varName) => new RegExp(
|
|
330
|
+
`^${varName}:\\s*\\r?\\n((?:[ ]+.+(?:\\r?\\n|$))+)`,
|
|
331
|
+
"m"
|
|
332
|
+
);
|
|
309
333
|
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
310
|
-
var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
|
|
311
334
|
var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
312
335
|
var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
|
|
313
336
|
var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
|
|
314
337
|
var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
|
|
315
|
-
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();
|
|
316
|
-
var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
|
|
338
|
+
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();
|
|
339
|
+
var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict + "/").oneOrMore().endGroup().startGroup().literal("/").startNamedGroup("gIngredientSubgroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().endGroup().optional().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().startGroup().literal("[").startNamedGroup("gIngredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().toRegExp();
|
|
317
340
|
var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
|
|
318
341
|
var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
|
|
319
342
|
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();
|
|
@@ -328,12 +351,58 @@ var tokensRegex = new RegExp(
|
|
|
328
351
|
].map((r2) => r2.source).join("|"),
|
|
329
352
|
"gu"
|
|
330
353
|
);
|
|
354
|
+
var yieldPrefixPart = d().startAnchor().literal("yield").literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
|
|
355
|
+
var yieldSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
|
|
356
|
+
var yieldMetaValueWithUnitRegex = new RegExp(
|
|
357
|
+
yieldPrefixPart.source + arbitraryScalableRegex.source + yieldSuffixPart.source,
|
|
358
|
+
"m"
|
|
359
|
+
);
|
|
360
|
+
var yieldMetaValueAsQuantityRegex = d().startAnchor().literal("yield:").anyOf(" ").zeroOrMore().startNamedGroup("quantity").notAnyOf("{}|%\\n\\r").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("\\n\\r|}").oneOrMore().endGroup().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
|
|
361
|
+
var yieldMetaValueRegex = new RegExp(
|
|
362
|
+
[
|
|
363
|
+
yieldMetaValueWithUnitRegex.source,
|
|
364
|
+
yieldMetaValueAsQuantityRegex.source
|
|
365
|
+
].join("|"),
|
|
366
|
+
"m"
|
|
367
|
+
);
|
|
331
368
|
var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
|
|
332
369
|
var blockCommentRegex = d().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
|
|
333
370
|
var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
|
|
334
371
|
var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
335
372
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
336
373
|
var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
374
|
+
var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").oneOrMore().endGroup().optional().literal("]").whitespace().zeroOrMore().toRegExp();
|
|
375
|
+
var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
|
|
376
|
+
var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
|
|
377
|
+
var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
|
|
378
|
+
var mdTripleAsterisk = d().literal("***").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("***");
|
|
379
|
+
var mdTripleUnderscore = d().literal("___").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("___");
|
|
380
|
+
var mdBoldAstItalicUnd = d().literal("**_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_**");
|
|
381
|
+
var mdBoldUndItalicAst = d().literal("__*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*__");
|
|
382
|
+
var mdItalicAstBoldUnd = d().literal("*__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__*");
|
|
383
|
+
var mdItalicUndBoldAst = d().literal("_**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**_");
|
|
384
|
+
var mdBoldAsterisk = d().literal("**").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("**");
|
|
385
|
+
var mdBoldUnderscore = d().wordBoundary().literal("__").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("__").wordBoundary();
|
|
386
|
+
var mdItalicAsterisk = d().literal("*").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("*");
|
|
387
|
+
var mdItalicUnderscore = d().wordBoundary().literal("_").startCaptureGroup().anyCharacter().oneOrMore().lazy().endGroup().literal("_").wordBoundary();
|
|
388
|
+
var markdownRegex = new RegExp(
|
|
389
|
+
[
|
|
390
|
+
mdEscaped,
|
|
391
|
+
mdInlineCode,
|
|
392
|
+
mdLink,
|
|
393
|
+
mdTripleAsterisk,
|
|
394
|
+
mdTripleUnderscore,
|
|
395
|
+
mdBoldAstItalicUnd,
|
|
396
|
+
mdBoldUndItalicAst,
|
|
397
|
+
mdItalicAstBoldUnd,
|
|
398
|
+
mdItalicUndBoldAst,
|
|
399
|
+
mdBoldAsterisk,
|
|
400
|
+
mdBoldUnderscore,
|
|
401
|
+
mdItalicAsterisk,
|
|
402
|
+
mdItalicUnderscore
|
|
403
|
+
].map((r2) => r2.toRegExp().source).join("|"),
|
|
404
|
+
"g"
|
|
405
|
+
);
|
|
337
406
|
|
|
338
407
|
// src/units/definitions.ts
|
|
339
408
|
var units = [
|
|
@@ -343,7 +412,8 @@ var units = [
|
|
|
343
412
|
type: "mass",
|
|
344
413
|
system: "metric",
|
|
345
414
|
aliases: ["gram", "grams", "grammes"],
|
|
346
|
-
toBase: 1
|
|
415
|
+
toBase: 1,
|
|
416
|
+
maxValue: 999
|
|
347
417
|
},
|
|
348
418
|
{
|
|
349
419
|
name: "kg",
|
|
@@ -352,20 +422,28 @@ var units = [
|
|
|
352
422
|
aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
|
|
353
423
|
toBase: 1e3
|
|
354
424
|
},
|
|
355
|
-
// Mass (
|
|
425
|
+
// Mass (US/UK - identical in both systems)
|
|
356
426
|
{
|
|
357
427
|
name: "oz",
|
|
358
428
|
type: "mass",
|
|
359
|
-
system: "
|
|
429
|
+
system: "ambiguous",
|
|
360
430
|
aliases: ["ounce", "ounces"],
|
|
361
|
-
toBase: 28.3495
|
|
431
|
+
toBase: 28.3495,
|
|
432
|
+
// default: US (same as UK)
|
|
433
|
+
toBaseBySystem: { US: 28.3495, UK: 28.3495 },
|
|
434
|
+
maxValue: 31,
|
|
435
|
+
// 16 oz = 1 lb, allow a bit more
|
|
436
|
+
fractions: { enabled: true, denominators: [2] }
|
|
362
437
|
},
|
|
363
438
|
{
|
|
364
439
|
name: "lb",
|
|
365
440
|
type: "mass",
|
|
366
|
-
system: "
|
|
441
|
+
system: "ambiguous",
|
|
367
442
|
aliases: ["pound", "pounds"],
|
|
368
|
-
toBase: 453.592
|
|
443
|
+
toBase: 453.592,
|
|
444
|
+
// default: US (same as UK)
|
|
445
|
+
toBaseBySystem: { US: 453.592, UK: 453.592 },
|
|
446
|
+
fractions: { enabled: true, denominators: [2, 4] }
|
|
369
447
|
},
|
|
370
448
|
// Volume (Metric)
|
|
371
449
|
{
|
|
@@ -373,21 +451,26 @@ var units = [
|
|
|
373
451
|
type: "volume",
|
|
374
452
|
system: "metric",
|
|
375
453
|
aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
|
|
376
|
-
toBase: 1
|
|
454
|
+
toBase: 1,
|
|
455
|
+
maxValue: 999
|
|
377
456
|
},
|
|
378
457
|
{
|
|
379
458
|
name: "cl",
|
|
380
459
|
type: "volume",
|
|
381
460
|
system: "metric",
|
|
382
461
|
aliases: ["centiliter", "centiliters", "centilitre", "centilitres"],
|
|
383
|
-
toBase: 10
|
|
462
|
+
toBase: 10,
|
|
463
|
+
isBestUnit: false
|
|
464
|
+
// exists but not a "best" candidate
|
|
384
465
|
},
|
|
385
466
|
{
|
|
386
467
|
name: "dl",
|
|
387
468
|
type: "volume",
|
|
388
469
|
system: "metric",
|
|
389
470
|
aliases: ["deciliter", "deciliters", "decilitre", "decilitres"],
|
|
390
|
-
toBase: 100
|
|
471
|
+
toBase: 100,
|
|
472
|
+
isBestUnit: false
|
|
473
|
+
// exists but not a "best" candidate
|
|
391
474
|
},
|
|
392
475
|
{
|
|
393
476
|
name: "l",
|
|
@@ -396,55 +479,102 @@ var units = [
|
|
|
396
479
|
aliases: ["liter", "liters", "litre", "litres"],
|
|
397
480
|
toBase: 1e3
|
|
398
481
|
},
|
|
482
|
+
// Volume (JP)
|
|
483
|
+
{
|
|
484
|
+
name: "go",
|
|
485
|
+
type: "volume",
|
|
486
|
+
system: "JP",
|
|
487
|
+
aliases: ["gou", "goo", "\u5408", "rice cup"],
|
|
488
|
+
toBase: 180,
|
|
489
|
+
maxValue: 10
|
|
490
|
+
},
|
|
491
|
+
// Volume (Ambiguous: metric/US/UK)
|
|
399
492
|
{
|
|
400
493
|
name: "tsp",
|
|
401
494
|
type: "volume",
|
|
402
|
-
system: "
|
|
495
|
+
system: "ambiguous",
|
|
403
496
|
aliases: ["teaspoon", "teaspoons"],
|
|
404
|
-
toBase: 5
|
|
497
|
+
toBase: 5,
|
|
498
|
+
// default: metric
|
|
499
|
+
toBaseBySystem: { metric: 5, US: 4.929, UK: 5.919, JP: 5 },
|
|
500
|
+
maxValue: 5,
|
|
501
|
+
// 3 tsp = 1 tbsp (but allow a bit more)
|
|
502
|
+
fractions: { enabled: true, denominators: [2, 3, 4, 8] }
|
|
405
503
|
},
|
|
406
504
|
{
|
|
407
505
|
name: "tbsp",
|
|
408
506
|
type: "volume",
|
|
409
|
-
system: "
|
|
507
|
+
system: "ambiguous",
|
|
410
508
|
aliases: ["tablespoon", "tablespoons"],
|
|
411
|
-
toBase: 15
|
|
509
|
+
toBase: 15,
|
|
510
|
+
// default: metric
|
|
511
|
+
toBaseBySystem: { metric: 15, US: 14.787, UK: 17.758, JP: 15 },
|
|
512
|
+
maxValue: 4,
|
|
513
|
+
// ~16 tbsp = 1 cup
|
|
514
|
+
fractions: { enabled: true }
|
|
412
515
|
},
|
|
413
|
-
// Volume (
|
|
516
|
+
// Volume (Ambiguous: US/UK only)
|
|
414
517
|
{
|
|
415
518
|
name: "fl-oz",
|
|
416
519
|
type: "volume",
|
|
417
|
-
system: "
|
|
520
|
+
system: "ambiguous",
|
|
418
521
|
aliases: ["fluid ounce", "fluid ounces"],
|
|
419
|
-
toBase: 29.5735
|
|
522
|
+
toBase: 29.5735,
|
|
523
|
+
// default: US
|
|
524
|
+
toBaseBySystem: { US: 29.5735, UK: 28.4131 },
|
|
525
|
+
maxValue: 15,
|
|
526
|
+
// 8 fl-oz ~ 1 cup, allow more
|
|
527
|
+
fractions: { enabled: true, denominators: [2] }
|
|
420
528
|
},
|
|
421
529
|
{
|
|
422
530
|
name: "cup",
|
|
423
531
|
type: "volume",
|
|
424
|
-
system: "
|
|
532
|
+
system: "ambiguous",
|
|
425
533
|
aliases: ["cups"],
|
|
426
|
-
toBase: 236.588
|
|
534
|
+
toBase: 236.588,
|
|
535
|
+
// default: US
|
|
536
|
+
toBaseBySystem: { US: 236.588, UK: 284.131 },
|
|
537
|
+
maxValue: 15,
|
|
538
|
+
// upgrade to gallons above 15 cups
|
|
539
|
+
fractions: { enabled: true }
|
|
427
540
|
},
|
|
428
541
|
{
|
|
429
542
|
name: "pint",
|
|
430
543
|
type: "volume",
|
|
431
|
-
system: "
|
|
544
|
+
system: "ambiguous",
|
|
432
545
|
aliases: ["pints"],
|
|
433
|
-
toBase: 473.176
|
|
546
|
+
toBase: 473.176,
|
|
547
|
+
// default: US
|
|
548
|
+
toBaseBySystem: { US: 473.176, UK: 568.261 },
|
|
549
|
+
maxValue: 3,
|
|
550
|
+
// 2 pints = 1 quart
|
|
551
|
+
fractions: { enabled: true, denominators: [2] },
|
|
552
|
+
isBestUnit: false
|
|
553
|
+
// exists but not a "best" candidate
|
|
434
554
|
},
|
|
435
555
|
{
|
|
436
556
|
name: "quart",
|
|
437
557
|
type: "volume",
|
|
438
|
-
system: "
|
|
558
|
+
system: "ambiguous",
|
|
439
559
|
aliases: ["quarts"],
|
|
440
|
-
toBase: 946.353
|
|
560
|
+
toBase: 946.353,
|
|
561
|
+
// default: US
|
|
562
|
+
toBaseBySystem: { US: 946.353, UK: 1136.52 },
|
|
563
|
+
maxValue: 3,
|
|
564
|
+
// 4 quarts = 1 gallon
|
|
565
|
+
fractions: { enabled: true, denominators: [2] },
|
|
566
|
+
isBestUnit: false
|
|
567
|
+
// exists but not a "best" candidate
|
|
441
568
|
},
|
|
442
569
|
{
|
|
443
570
|
name: "gallon",
|
|
444
571
|
type: "volume",
|
|
445
|
-
system: "
|
|
572
|
+
system: "ambiguous",
|
|
446
573
|
aliases: ["gallons"],
|
|
447
|
-
toBase: 3785.41
|
|
574
|
+
toBase: 3785.41,
|
|
575
|
+
// default: US
|
|
576
|
+
toBaseBySystem: { US: 3785.41, UK: 4546.09 },
|
|
577
|
+
fractions: { enabled: true, denominators: [2] }
|
|
448
578
|
},
|
|
449
579
|
// Count units (no conversion, but recognized as a type)
|
|
450
580
|
{
|
|
@@ -452,7 +582,8 @@ var units = [
|
|
|
452
582
|
type: "count",
|
|
453
583
|
system: "metric",
|
|
454
584
|
aliases: ["pieces", "pc"],
|
|
455
|
-
toBase: 1
|
|
585
|
+
toBase: 1,
|
|
586
|
+
maxValue: 999
|
|
456
587
|
}
|
|
457
588
|
];
|
|
458
589
|
var unitMap = /* @__PURE__ */ new Map();
|
|
@@ -476,8 +607,14 @@ function isNoUnit(unit) {
|
|
|
476
607
|
return resolveUnit(unit.name).name === NO_UNIT;
|
|
477
608
|
}
|
|
478
609
|
|
|
610
|
+
// src/units/conversion.ts
|
|
611
|
+
var import_big2 = __toESM(require("big.js"), 1);
|
|
612
|
+
|
|
479
613
|
// src/quantities/numeric.ts
|
|
480
614
|
var import_big = __toESM(require("big.js"), 1);
|
|
615
|
+
var DEFAULT_DENOMINATORS = [2, 3, 4];
|
|
616
|
+
var DEFAULT_FRACTION_ACCURACY = 0.05;
|
|
617
|
+
var DEFAULT_MAX_WHOLE = 4;
|
|
481
618
|
function gcd(a2, b) {
|
|
482
619
|
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
483
620
|
}
|
|
@@ -498,6 +635,41 @@ function simplifyFraction(num, den) {
|
|
|
498
635
|
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
499
636
|
}
|
|
500
637
|
}
|
|
638
|
+
function approximateFraction(value, denominators = DEFAULT_DENOMINATORS, accuracy = DEFAULT_FRACTION_ACCURACY, maxWhole = DEFAULT_MAX_WHOLE) {
|
|
639
|
+
if (value <= 0 || !Number.isFinite(value)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
const wholePart = Math.floor(value);
|
|
643
|
+
if (wholePart > maxWhole) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
const fractionalPart = value - wholePart;
|
|
647
|
+
if (fractionalPart < 1e-4) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
let bestFraction = null;
|
|
651
|
+
for (const den of denominators) {
|
|
652
|
+
const exactNum = value * den;
|
|
653
|
+
const roundedNum = Math.round(exactNum);
|
|
654
|
+
if (roundedNum === 0) continue;
|
|
655
|
+
const approximatedValue = roundedNum / den;
|
|
656
|
+
const relativeError = Math.abs(approximatedValue - value) / value;
|
|
657
|
+
if (relativeError <= accuracy) {
|
|
658
|
+
if (!bestFraction || relativeError < bestFraction.error) {
|
|
659
|
+
bestFraction = { num: roundedNum, den, error: relativeError };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!bestFraction) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const commonDivisor = gcd(bestFraction.num, bestFraction.den);
|
|
667
|
+
return {
|
|
668
|
+
type: "fraction",
|
|
669
|
+
num: bestFraction.num / commonDivisor,
|
|
670
|
+
den: bestFraction.den / commonDivisor
|
|
671
|
+
};
|
|
672
|
+
}
|
|
501
673
|
function getNumericValue(v) {
|
|
502
674
|
if (v.type === "decimal") {
|
|
503
675
|
return v.decimal;
|
|
@@ -546,9 +718,35 @@ function addNumericValues(val1, val2) {
|
|
|
546
718
|
};
|
|
547
719
|
}
|
|
548
720
|
}
|
|
549
|
-
var toRoundedDecimal = (v) => {
|
|
721
|
+
var toRoundedDecimal = (v, precision = 3) => {
|
|
550
722
|
const value = v.type === "decimal" ? v.decimal : v.num / v.den;
|
|
551
|
-
|
|
723
|
+
if (value === 0) {
|
|
724
|
+
return { type: "decimal", decimal: 0 };
|
|
725
|
+
}
|
|
726
|
+
const absValue = Math.abs(value);
|
|
727
|
+
if (absValue >= 1e3) {
|
|
728
|
+
return { type: "decimal", decimal: Math.round(value) };
|
|
729
|
+
}
|
|
730
|
+
const magnitude = Math.floor(Math.log10(absValue));
|
|
731
|
+
const scale = Math.pow(10, precision - 1 - magnitude);
|
|
732
|
+
const rounded = Math.round(value * scale) / scale;
|
|
733
|
+
return { type: "decimal", decimal: rounded };
|
|
734
|
+
};
|
|
735
|
+
var formatOutputValue = (value, unitDef, precision = 3) => {
|
|
736
|
+
if (unitDef.fractions?.enabled) {
|
|
737
|
+
const denominators = unitDef.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
738
|
+
const maxWhole = unitDef.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
739
|
+
const fraction = approximateFraction(
|
|
740
|
+
value,
|
|
741
|
+
denominators,
|
|
742
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
743
|
+
maxWhole
|
|
744
|
+
);
|
|
745
|
+
if (fraction) {
|
|
746
|
+
return fraction;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return toRoundedDecimal({ type: "decimal", decimal: value }, precision);
|
|
552
750
|
};
|
|
553
751
|
function multiplyQuantityValue(value, factor) {
|
|
554
752
|
if (value.type === "fixed") {
|
|
@@ -582,6 +780,143 @@ function getAverageValue(q) {
|
|
|
582
780
|
}
|
|
583
781
|
}
|
|
584
782
|
|
|
783
|
+
// src/units/compatibility.ts
|
|
784
|
+
function areUnitsGroupable(u1, u2) {
|
|
785
|
+
if (u1.name === u2.name) {
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
if (u1.type === "other" || u2.type === "other") {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
if (u1.type === u2.type && u1.system === u2.system) {
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
if (u1.type === u2.type) {
|
|
795
|
+
if (u1.system === "ambiguous" && u2.system === "metric" && u1.toBaseBySystem?.metric !== void 0) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
if (u2.system === "ambiguous" && u1.system === "metric" && u2.toBaseBySystem?.metric !== void 0) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
function areUnitsConvertible(u1, u2) {
|
|
805
|
+
if (u1.name === u2.name) return true;
|
|
806
|
+
if (u1.type === "other" || u2.type === "other") return false;
|
|
807
|
+
return u1.type === u2.type;
|
|
808
|
+
}
|
|
809
|
+
function isUnitCompatibleWithSystem(unit, system) {
|
|
810
|
+
if (unit.system === system) return true;
|
|
811
|
+
if (unit.system === "ambiguous") {
|
|
812
|
+
if (unit.toBaseBySystem) {
|
|
813
|
+
return system in unit.toBaseBySystem;
|
|
814
|
+
}
|
|
815
|
+
if (system === "metric") return true;
|
|
816
|
+
}
|
|
817
|
+
if (unit.system === "metric" && system === "JP") {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/units/conversion.ts
|
|
824
|
+
var EPSILON = 0.01;
|
|
825
|
+
var DEFAULT_MAX_VALUE = 999;
|
|
826
|
+
function isCloseToInteger(value) {
|
|
827
|
+
return Math.abs(value - Math.round(value)) < EPSILON;
|
|
828
|
+
}
|
|
829
|
+
function getMaxValue(unit) {
|
|
830
|
+
return unit.maxValue ?? DEFAULT_MAX_VALUE;
|
|
831
|
+
}
|
|
832
|
+
function isValueInRange(value, unit) {
|
|
833
|
+
const maxValue = getMaxValue(unit);
|
|
834
|
+
if (value >= 1 && value <= maxValue) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
if (value > 0 && value < 1 && unit.fractions?.enabled) {
|
|
838
|
+
const denominators = unit.fractions.denominators ?? DEFAULT_DENOMINATORS;
|
|
839
|
+
const maxWhole = unit.fractions.maxWhole ?? DEFAULT_MAX_WHOLE;
|
|
840
|
+
const fraction = approximateFraction(
|
|
841
|
+
value,
|
|
842
|
+
denominators,
|
|
843
|
+
DEFAULT_FRACTION_ACCURACY,
|
|
844
|
+
maxWhole
|
|
845
|
+
);
|
|
846
|
+
return fraction !== null;
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
function findBestUnit(valueInBase, unitType, system, inputUnits) {
|
|
851
|
+
const inputUnitNames = new Set(inputUnits.map((u) => u.name));
|
|
852
|
+
const candidates = units.filter(
|
|
853
|
+
(u) => u.type === unitType && isUnitCompatibleWithSystem(u, system) && (u.isBestUnit !== false || inputUnitNames.has(u.name))
|
|
854
|
+
);
|
|
855
|
+
if (candidates.length === 0) {
|
|
856
|
+
const fallbackUnit = inputUnits[0];
|
|
857
|
+
return {
|
|
858
|
+
unit: fallbackUnit,
|
|
859
|
+
value: valueInBase / getToBase(fallbackUnit, system)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const candidatesWithValues = candidates.map((unit) => ({
|
|
863
|
+
unit,
|
|
864
|
+
value: valueInBase / getToBase(unit, system)
|
|
865
|
+
}));
|
|
866
|
+
const inRange = candidatesWithValues.filter(
|
|
867
|
+
(c) => isValueInRange(c.value, c.unit)
|
|
868
|
+
);
|
|
869
|
+
if (inRange.length > 0) {
|
|
870
|
+
const integersInInputFamily = inRange.filter(
|
|
871
|
+
(c) => isCloseToInteger(c.value) && inputUnitNames.has(c.unit.name)
|
|
872
|
+
);
|
|
873
|
+
if (integersInInputFamily.length > 0) {
|
|
874
|
+
return integersInInputFamily.sort((a2, b) => a2.value - b.value)[0];
|
|
875
|
+
}
|
|
876
|
+
const integersAny = inRange.filter((c) => isCloseToInteger(c.value));
|
|
877
|
+
if (integersAny.length > 0) {
|
|
878
|
+
return integersAny.sort((a2, b) => a2.value - b.value)[0];
|
|
879
|
+
}
|
|
880
|
+
return inRange.sort((a2, b) => {
|
|
881
|
+
const aInFamily = inputUnitNames.has(a2.unit.name) ? 0 : 1;
|
|
882
|
+
const bInFamily = inputUnitNames.has(b.unit.name) ? 0 : 1;
|
|
883
|
+
if (aInFamily !== bInFamily) return aInFamily - bInFamily;
|
|
884
|
+
return a2.value - b.value;
|
|
885
|
+
})[0];
|
|
886
|
+
}
|
|
887
|
+
return candidatesWithValues.sort((a2, b) => {
|
|
888
|
+
const aMaxValue = getMaxValue(a2.unit);
|
|
889
|
+
const bMaxValue = getMaxValue(b.unit);
|
|
890
|
+
const aDistance = a2.value < 1 ? 1 - a2.value : a2.value - aMaxValue;
|
|
891
|
+
const bDistance = b.value < 1 ? 1 - b.value : b.value - bMaxValue;
|
|
892
|
+
return aDistance - bDistance;
|
|
893
|
+
})[0];
|
|
894
|
+
}
|
|
895
|
+
function getUnitRatio(q1, q2) {
|
|
896
|
+
const q1Value = getAverageValue(q1.quantity);
|
|
897
|
+
const q2Value = getAverageValue(q2.quantity);
|
|
898
|
+
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
899
|
+
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
900
|
+
throw Error(
|
|
901
|
+
"One of both values is not a number, so a ratio cannot be computed"
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
905
|
+
}
|
|
906
|
+
function getBaseUnitRatio(q, qRef) {
|
|
907
|
+
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
908
|
+
return q.unit.toBase / qRef.unit.toBase;
|
|
909
|
+
} else {
|
|
910
|
+
return 1;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function getToBase(unit, system) {
|
|
914
|
+
if (unit.system === "ambiguous" && system && unit.toBaseBySystem) {
|
|
915
|
+
return unit.toBaseBySystem[system] ?? unit.toBase;
|
|
916
|
+
}
|
|
917
|
+
return unit.toBase;
|
|
918
|
+
}
|
|
919
|
+
|
|
585
920
|
// src/errors.ts
|
|
586
921
|
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
587
922
|
constructor(item_type, item_name, new_modifier) {
|
|
@@ -612,7 +947,7 @@ var NoProductMatchError = class extends Error {
|
|
|
612
947
|
constructor(item_name, code) {
|
|
613
948
|
const messageMap = {
|
|
614
949
|
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
615
|
-
noProduct:
|
|
950
|
+
noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
|
|
616
951
|
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
617
952
|
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
618
953
|
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`
|
|
@@ -651,20 +986,37 @@ var InvalidQuantityFormat = class extends Error {
|
|
|
651
986
|
this.name = "InvalidQuantityFormat";
|
|
652
987
|
}
|
|
653
988
|
};
|
|
989
|
+
var NoTabAsIndentError = class extends Error {
|
|
990
|
+
constructor() {
|
|
991
|
+
super(
|
|
992
|
+
`Tabs are not allowed for indentation in metadata blocks. Please use spaces only.`
|
|
993
|
+
);
|
|
994
|
+
this.name = "NoTabAsIndentError";
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
var BadIndentationError = class extends Error {
|
|
998
|
+
constructor() {
|
|
999
|
+
super(`Bad identation of a nested block. Please use spaces only.`);
|
|
1000
|
+
this.name = "BadIndentationError";
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
654
1003
|
|
|
655
1004
|
// src/utils/type_guards.ts
|
|
656
1005
|
function isGroup(x) {
|
|
657
|
-
return x
|
|
1006
|
+
return "and" in x || "or" in x;
|
|
658
1007
|
}
|
|
659
1008
|
function isOrGroup(x) {
|
|
660
|
-
return isGroup(x) &&
|
|
1009
|
+
return isGroup(x) && "or" in x;
|
|
661
1010
|
}
|
|
662
1011
|
function isAndGroup(x) {
|
|
663
|
-
return
|
|
1012
|
+
return "and" in x;
|
|
664
1013
|
}
|
|
665
1014
|
function isQuantity(x) {
|
|
666
1015
|
return x && typeof x === "object" && "quantity" in x;
|
|
667
1016
|
}
|
|
1017
|
+
function isSimpleGroup(entry) {
|
|
1018
|
+
return "quantity" in entry;
|
|
1019
|
+
}
|
|
668
1020
|
function isNumericValueIntegerLike(v) {
|
|
669
1021
|
if (v.type === "decimal") return Number.isInteger(v.decimal);
|
|
670
1022
|
return v.num % v.den === 0;
|
|
@@ -676,37 +1028,32 @@ function isValueIntegerLike(q) {
|
|
|
676
1028
|
}
|
|
677
1029
|
return isNumericValueIntegerLike(q.min) && isNumericValueIntegerLike(q.max);
|
|
678
1030
|
}
|
|
1031
|
+
function hasAlternatives(entry) {
|
|
1032
|
+
return "alternatives" in entry && Array.isArray(entry.alternatives) && entry.alternatives.length > 0;
|
|
1033
|
+
}
|
|
679
1034
|
|
|
680
1035
|
// src/quantities/mutations.ts
|
|
681
|
-
function extendAllUnits(q) {
|
|
682
|
-
if (isGroup(q)) {
|
|
683
|
-
return { ...q, entries: q.entries.map(extendAllUnits) };
|
|
684
|
-
} else {
|
|
685
|
-
const newQ = {
|
|
686
|
-
quantity: q.quantity
|
|
687
|
-
};
|
|
688
|
-
if (q.unit) {
|
|
689
|
-
newQ.unit = { name: q.unit };
|
|
690
|
-
}
|
|
691
|
-
return newQ;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
1036
|
function normalizeAllUnits(q) {
|
|
695
|
-
if (
|
|
696
|
-
return {
|
|
1037
|
+
if (isAndGroup(q)) {
|
|
1038
|
+
return { and: q.and.map(normalizeAllUnits) };
|
|
1039
|
+
} else if (isOrGroup(q)) {
|
|
1040
|
+
return { or: q.or.map(normalizeAllUnits) };
|
|
697
1041
|
} else {
|
|
698
1042
|
const newQ = {
|
|
699
1043
|
quantity: q.quantity,
|
|
700
1044
|
unit: resolveUnit(q.unit)
|
|
701
1045
|
};
|
|
1046
|
+
if (q.equivalents && q.equivalents.length > 0) {
|
|
1047
|
+
const equivalentsNormalized = q.equivalents.map(
|
|
1048
|
+
(eq) => normalizeAllUnits(eq)
|
|
1049
|
+
);
|
|
1050
|
+
return {
|
|
1051
|
+
or: [newQ, ...equivalentsNormalized]
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
702
1054
|
return newQ;
|
|
703
1055
|
}
|
|
704
1056
|
}
|
|
705
|
-
var convertQuantityValue = (value, def, targetDef) => {
|
|
706
|
-
if (def.name === targetDef.name) return value;
|
|
707
|
-
const factor = def.toBase / targetDef.toBase;
|
|
708
|
-
return multiplyQuantityValue(value, factor);
|
|
709
|
-
};
|
|
710
1057
|
function getDefaultQuantityValue() {
|
|
711
1058
|
return { type: "fixed", value: { type: "decimal", decimal: 0 } };
|
|
712
1059
|
}
|
|
@@ -733,7 +1080,7 @@ function addQuantityValues(v1, v2) {
|
|
|
733
1080
|
);
|
|
734
1081
|
return { type: "range", min: newMin, max: newMax };
|
|
735
1082
|
}
|
|
736
|
-
function addQuantities(q1, q2) {
|
|
1083
|
+
function addQuantities(q1, q2, system) {
|
|
737
1084
|
const v1 = q1.quantity;
|
|
738
1085
|
const v2 = q2.quantity;
|
|
739
1086
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
@@ -751,55 +1098,149 @@ function addQuantities(q1, q2) {
|
|
|
751
1098
|
if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
752
1099
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
753
1100
|
}
|
|
754
|
-
if (!q1.unit && !q2.unit
|
|
1101
|
+
if (!q1.unit && !q2.unit) {
|
|
1102
|
+
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
1103
|
+
}
|
|
1104
|
+
if (q1.unit && q2.unit && q1.unit.name.toLowerCase() === q2.unit.name.toLowerCase()) {
|
|
1105
|
+
if (unit1Def) {
|
|
1106
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unit1Def.system) ? unit1Def.system : "US");
|
|
1107
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit1Def, effectiveSystem, [
|
|
1108
|
+
unit1Def
|
|
1109
|
+
]);
|
|
1110
|
+
}
|
|
755
1111
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
756
1112
|
}
|
|
757
1113
|
if (unit1Def && unit2Def) {
|
|
758
|
-
if (unit1Def
|
|
1114
|
+
if (!areUnitsConvertible(unit1Def, unit2Def)) {
|
|
759
1115
|
throw new IncompatibleUnitsError(
|
|
760
1116
|
`${unit1Def.type} (${q1.unit?.name})`,
|
|
761
1117
|
`${unit2Def.type} (${q2.unit?.name})`
|
|
762
1118
|
);
|
|
763
1119
|
}
|
|
764
|
-
let
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1120
|
+
let effectiveSystem = system;
|
|
1121
|
+
if (!effectiveSystem) {
|
|
1122
|
+
if (unit1Def.system === "metric" || unit2Def.system === "metric") {
|
|
1123
|
+
effectiveSystem = "metric";
|
|
1124
|
+
} else {
|
|
1125
|
+
if (unit1Def.system === "JP" && unit2Def.system === "JP") {
|
|
1126
|
+
effectiveSystem = "JP";
|
|
1127
|
+
} else {
|
|
1128
|
+
const unit1SupportsUS = unit1Def.system === "US" || unit1Def.system === "ambiguous" && unit1Def.toBaseBySystem && "US" in unit1Def.toBaseBySystem;
|
|
1129
|
+
const unit2SupportsUS = unit2Def.system === "US" || unit2Def.system === "ambiguous" && unit2Def.toBaseBySystem && "US" in unit2Def.toBaseBySystem;
|
|
1130
|
+
effectiveSystem = unit1SupportsUS && unit2SupportsUS ? "US" : "metric";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
772
1133
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1134
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit2Def, effectiveSystem, [
|
|
1135
|
+
unit1Def,
|
|
1136
|
+
unit2Def
|
|
1137
|
+
]);
|
|
777
1138
|
}
|
|
778
1139
|
throw new IncompatibleUnitsError(
|
|
779
1140
|
q1.unit?.name,
|
|
780
1141
|
q2.unit?.name
|
|
781
1142
|
);
|
|
782
1143
|
}
|
|
1144
|
+
function addAndFindBestUnit(v1, v2, unit1Def, unit2Def, system, inputUnits) {
|
|
1145
|
+
const toBase1 = getToBase(unit1Def, system);
|
|
1146
|
+
const toBase2 = getToBase(unit2Def, system);
|
|
1147
|
+
let sumInBase;
|
|
1148
|
+
if (v1.type === "fixed" && v2.type === "fixed") {
|
|
1149
|
+
const val1 = getNumericValue(v1.value);
|
|
1150
|
+
const val2 = getNumericValue(v2.value);
|
|
1151
|
+
sumInBase = val1 * toBase1 + val2 * toBase2;
|
|
1152
|
+
} else {
|
|
1153
|
+
const avg1 = getAverageValue(v1);
|
|
1154
|
+
const avg2 = getAverageValue(v2);
|
|
1155
|
+
sumInBase = avg1 * toBase1 + avg2 * toBase2;
|
|
1156
|
+
}
|
|
1157
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1158
|
+
sumInBase,
|
|
1159
|
+
unit1Def.type,
|
|
1160
|
+
system,
|
|
1161
|
+
inputUnits
|
|
1162
|
+
);
|
|
1163
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1164
|
+
if (v1.type === "range" || v2.type === "range") {
|
|
1165
|
+
const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
|
|
1166
|
+
const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
|
|
1167
|
+
const minInBase = getNumericValue(r1.min) * toBase1 + getNumericValue(r2.min) * toBase2;
|
|
1168
|
+
const maxInBase = getNumericValue(r1.max) * toBase1 + getNumericValue(r2.max) * toBase2;
|
|
1169
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1170
|
+
const minValue = minInBase / bestToBase;
|
|
1171
|
+
const maxValue = maxInBase / bestToBase;
|
|
1172
|
+
return {
|
|
1173
|
+
quantity: {
|
|
1174
|
+
type: "range",
|
|
1175
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1176
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1177
|
+
},
|
|
1178
|
+
unit: { name: bestUnit.name }
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1183
|
+
unit: { name: bestUnit.name }
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function convertQuantityToSystem(quantity, system) {
|
|
1187
|
+
const unitDef = resolveUnit(
|
|
1188
|
+
typeof quantity.unit === "string" ? quantity.unit : quantity.unit?.name
|
|
1189
|
+
);
|
|
1190
|
+
if (unitDef.type === "other" || !("toBase" in unitDef)) {
|
|
1191
|
+
return void 0;
|
|
1192
|
+
}
|
|
1193
|
+
const avgValue = getAverageValue(quantity.quantity);
|
|
1194
|
+
if (typeof avgValue !== "number") {
|
|
1195
|
+
return void 0;
|
|
1196
|
+
}
|
|
1197
|
+
const toBase = getToBase(unitDef, system);
|
|
1198
|
+
const valueInBase = avgValue * toBase;
|
|
1199
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1200
|
+
valueInBase,
|
|
1201
|
+
unitDef.type,
|
|
1202
|
+
system,
|
|
1203
|
+
[unitDef]
|
|
1204
|
+
);
|
|
1205
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1206
|
+
if (quantity.quantity.type === "range") {
|
|
1207
|
+
const bestToBase = getToBase(bestUnit, system);
|
|
1208
|
+
const minValue = getNumericValue(quantity.quantity.min) * toBase / bestToBase;
|
|
1209
|
+
const maxValue = getNumericValue(quantity.quantity.max) * toBase / bestToBase;
|
|
1210
|
+
return {
|
|
1211
|
+
quantity: {
|
|
1212
|
+
type: "range",
|
|
1213
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1214
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1215
|
+
},
|
|
1216
|
+
unit: { name: bestUnit.name }
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
quantity: { type: "fixed", value: formattedValue },
|
|
1221
|
+
unit: { name: bestUnit.name }
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
783
1224
|
function toPlainUnit(quantity) {
|
|
784
1225
|
if (isQuantity(quantity))
|
|
785
1226
|
return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
|
|
786
|
-
else {
|
|
1227
|
+
else if (isOrGroup(quantity)) {
|
|
1228
|
+
return {
|
|
1229
|
+
or: quantity.or.map(toPlainUnit)
|
|
1230
|
+
};
|
|
1231
|
+
} else {
|
|
787
1232
|
return {
|
|
788
|
-
|
|
789
|
-
entries: quantity.entries.map(toPlainUnit)
|
|
1233
|
+
and: quantity.and.map(toPlainUnit)
|
|
790
1234
|
};
|
|
791
1235
|
}
|
|
792
1236
|
}
|
|
793
1237
|
function toExtendedUnit(q) {
|
|
794
1238
|
if (isQuantity(q)) {
|
|
795
1239
|
return q.unit ? { ...q, unit: { name: q.unit } } : q;
|
|
1240
|
+
} else if (isOrGroup(q)) {
|
|
1241
|
+
return { or: q.or.map(toExtendedUnit) };
|
|
796
1242
|
} else {
|
|
797
|
-
return {
|
|
798
|
-
...q,
|
|
799
|
-
entries: q.entries.map(
|
|
800
|
-
(entry) => isQuantity(entry) ? toExtendedUnit(entry) : toExtendedUnit(entry)
|
|
801
|
-
)
|
|
802
|
-
};
|
|
1243
|
+
return { and: q.and.map(toExtendedUnit) };
|
|
803
1244
|
}
|
|
804
1245
|
}
|
|
805
1246
|
function deNormalizeQuantity(q) {
|
|
@@ -813,26 +1254,24 @@ function deNormalizeQuantity(q) {
|
|
|
813
1254
|
}
|
|
814
1255
|
var flattenPlainUnitGroup = (summed) => {
|
|
815
1256
|
if (isOrGroup(summed)) {
|
|
816
|
-
const entries = summed.
|
|
1257
|
+
const entries = summed.or;
|
|
817
1258
|
const andGroupEntry = entries.find(
|
|
818
|
-
(e2) =>
|
|
1259
|
+
(e2) => isAndGroup(e2)
|
|
819
1260
|
);
|
|
820
1261
|
if (andGroupEntry) {
|
|
821
1262
|
const andEntries = [];
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
1263
|
+
const addGroupEntryContent = andGroupEntry.and;
|
|
1264
|
+
for (const entry of addGroupEntryContent) {
|
|
1265
|
+
andEntries.push({
|
|
1266
|
+
quantity: entry.quantity,
|
|
1267
|
+
...entry.unit && { unit: entry.unit }
|
|
1268
|
+
});
|
|
829
1269
|
}
|
|
830
1270
|
const equivalentsList = entries.filter((e2) => isQuantity(e2)).map((e2) => ({ quantity: e2.quantity, unit: e2.unit }));
|
|
831
1271
|
if (equivalentsList.length > 0) {
|
|
832
1272
|
return [
|
|
833
1273
|
{
|
|
834
|
-
|
|
835
|
-
entries: andEntries,
|
|
1274
|
+
and: andEntries,
|
|
836
1275
|
equivalents: equivalentsList
|
|
837
1276
|
}
|
|
838
1277
|
];
|
|
@@ -856,41 +1295,128 @@ var flattenPlainUnitGroup = (summed) => {
|
|
|
856
1295
|
const first = entries[0];
|
|
857
1296
|
return [{ quantity: first.quantity, unit: first.unit }];
|
|
858
1297
|
}
|
|
859
|
-
} else if (
|
|
1298
|
+
} else if (isAndGroup(summed)) {
|
|
860
1299
|
const andEntries = [];
|
|
1300
|
+
const standaloneEntries = [];
|
|
861
1301
|
const equivalentsList = [];
|
|
862
|
-
for (const entry of summed.
|
|
1302
|
+
for (const entry of summed.and) {
|
|
863
1303
|
if (isOrGroup(entry)) {
|
|
864
|
-
const orEntries = entry.
|
|
865
|
-
|
|
866
|
-
)
|
|
867
|
-
|
|
1304
|
+
const orEntries = entry.or;
|
|
1305
|
+
const firstEntry = orEntries[0];
|
|
1306
|
+
if (isAndGroup(firstEntry)) {
|
|
1307
|
+
for (const nestedEntry of firstEntry.and) {
|
|
1308
|
+
andEntries.push({
|
|
1309
|
+
quantity: nestedEntry.quantity,
|
|
1310
|
+
...nestedEntry.unit && { unit: nestedEntry.unit }
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
} else {
|
|
1314
|
+
const primary = firstEntry;
|
|
868
1315
|
andEntries.push({
|
|
869
|
-
quantity:
|
|
870
|
-
unit:
|
|
1316
|
+
quantity: primary.quantity,
|
|
1317
|
+
...primary.unit && { unit: primary.unit }
|
|
871
1318
|
});
|
|
872
|
-
equivalentsList.push(...orEntries.slice(1));
|
|
873
1319
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1320
|
+
const equivEntries = orEntries.slice(1).filter((e2) => isQuantity(e2));
|
|
1321
|
+
equivalentsList.push(
|
|
1322
|
+
...equivEntries.map((e2) => ({
|
|
1323
|
+
quantity: e2.quantity,
|
|
1324
|
+
...e2.unit && { unit: e2.unit }
|
|
1325
|
+
}))
|
|
1326
|
+
);
|
|
1327
|
+
} else {
|
|
1328
|
+
const simpleQuantityEntry = entry;
|
|
1329
|
+
standaloneEntries.push({
|
|
1330
|
+
quantity: simpleQuantityEntry.quantity,
|
|
1331
|
+
...simpleQuantityEntry.unit && { unit: simpleQuantityEntry.unit }
|
|
878
1332
|
});
|
|
879
1333
|
}
|
|
880
1334
|
}
|
|
881
1335
|
if (equivalentsList.length === 0) {
|
|
882
|
-
return andEntries;
|
|
1336
|
+
return [...andEntries, ...standaloneEntries];
|
|
883
1337
|
}
|
|
884
|
-
const result =
|
|
885
|
-
|
|
886
|
-
|
|
1338
|
+
const result = [];
|
|
1339
|
+
result.push({
|
|
1340
|
+
and: andEntries,
|
|
887
1341
|
equivalents: equivalentsList
|
|
888
|
-
};
|
|
889
|
-
|
|
1342
|
+
});
|
|
1343
|
+
result.push(...standaloneEntries);
|
|
1344
|
+
return result;
|
|
890
1345
|
} else {
|
|
891
|
-
return [
|
|
1346
|
+
return [
|
|
1347
|
+
{ quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
|
|
1348
|
+
];
|
|
892
1349
|
}
|
|
893
1350
|
};
|
|
1351
|
+
function applyBestUnit(q, system) {
|
|
1352
|
+
const extended = { quantity: q.quantity };
|
|
1353
|
+
if (q.unit) {
|
|
1354
|
+
extended.unit = typeof q.unit === "string" ? { name: q.unit } : q.unit;
|
|
1355
|
+
}
|
|
1356
|
+
if (!extended.unit?.name) {
|
|
1357
|
+
return q;
|
|
1358
|
+
}
|
|
1359
|
+
const unitDef = resolveUnit(extended.unit.name);
|
|
1360
|
+
if (unitDef.type === "other") {
|
|
1361
|
+
return q;
|
|
1362
|
+
}
|
|
1363
|
+
if (extended.quantity.type === "fixed" && extended.quantity.value.type === "text") {
|
|
1364
|
+
return q;
|
|
1365
|
+
}
|
|
1366
|
+
const avgValue = getAverageValue(extended.quantity);
|
|
1367
|
+
const effectiveSystem = system ?? (["metric", "JP"].includes(unitDef.system) ? unitDef.system : "US");
|
|
1368
|
+
const toBase = getToBase(unitDef, effectiveSystem);
|
|
1369
|
+
const valueInBase = avgValue * toBase;
|
|
1370
|
+
const { unit: bestUnit, value: bestValue } = findBestUnit(
|
|
1371
|
+
valueInBase,
|
|
1372
|
+
unitDef.type,
|
|
1373
|
+
effectiveSystem,
|
|
1374
|
+
[unitDef]
|
|
1375
|
+
);
|
|
1376
|
+
const originalCanonicalName = normalizeUnit(extended.unit.name)?.name;
|
|
1377
|
+
if (bestUnit.name === originalCanonicalName) {
|
|
1378
|
+
return q;
|
|
1379
|
+
}
|
|
1380
|
+
const formattedValue = formatOutputValue(bestValue, bestUnit);
|
|
1381
|
+
if (extended.quantity.type === "range") {
|
|
1382
|
+
const bestToBase = getToBase(bestUnit, effectiveSystem);
|
|
1383
|
+
const minValue = getNumericValue(extended.quantity.min) * toBase / bestToBase;
|
|
1384
|
+
const maxValue = getNumericValue(extended.quantity.max) * toBase / bestToBase;
|
|
1385
|
+
return {
|
|
1386
|
+
quantity: {
|
|
1387
|
+
type: "range",
|
|
1388
|
+
min: formatOutputValue(minValue, bestUnit),
|
|
1389
|
+
max: formatOutputValue(maxValue, bestUnit)
|
|
1390
|
+
},
|
|
1391
|
+
unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
return {
|
|
1395
|
+
quantity: {
|
|
1396
|
+
type: "fixed",
|
|
1397
|
+
value: formattedValue
|
|
1398
|
+
},
|
|
1399
|
+
unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
function subtractQuantities(q1, q2, options = {}) {
|
|
1403
|
+
const { clampToZero = true, system } = options;
|
|
1404
|
+
const negatedQ2 = {
|
|
1405
|
+
...q2,
|
|
1406
|
+
quantity: multiplyQuantityValue(q2.quantity, -1)
|
|
1407
|
+
};
|
|
1408
|
+
const result = addQuantities(q1, negatedQ2, system);
|
|
1409
|
+
if (clampToZero) {
|
|
1410
|
+
const avg = getAverageValue(result.quantity);
|
|
1411
|
+
if (typeof avg === "number" && avg < 0) {
|
|
1412
|
+
return {
|
|
1413
|
+
quantity: { type: "fixed", value: { type: "decimal", decimal: 0 } },
|
|
1414
|
+
unit: result.unit
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return result;
|
|
1419
|
+
}
|
|
894
1420
|
|
|
895
1421
|
// src/utils/parser_helpers.ts
|
|
896
1422
|
function flushPendingNote(section, noteItems) {
|
|
@@ -900,9 +1426,12 @@ function flushPendingNote(section, noteItems) {
|
|
|
900
1426
|
}
|
|
901
1427
|
return noteItems;
|
|
902
1428
|
}
|
|
903
|
-
function flushPendingItems(section, items) {
|
|
1429
|
+
function flushPendingItems(section, items, stepVariants, stepOptional) {
|
|
904
1430
|
if (items.length > 0) {
|
|
905
|
-
|
|
1431
|
+
const step = { type: "step", items: [...items] };
|
|
1432
|
+
if (stepVariants) step.variants = stepVariants;
|
|
1433
|
+
if (stepOptional) step.optional = true;
|
|
1434
|
+
section.content.push(step);
|
|
906
1435
|
items.length = 0;
|
|
907
1436
|
return true;
|
|
908
1437
|
}
|
|
@@ -1021,7 +1550,7 @@ function stringifyFixedValue(quantity) {
|
|
|
1021
1550
|
return String(quantity.value.decimal);
|
|
1022
1551
|
else return quantity.value.text;
|
|
1023
1552
|
}
|
|
1024
|
-
function
|
|
1553
|
+
function parseQuantityValue(input_str) {
|
|
1025
1554
|
const clean_str = String(input_str).trim();
|
|
1026
1555
|
if (rangeRegex.test(clean_str)) {
|
|
1027
1556
|
const range_parts = clean_str.split("-");
|
|
@@ -1031,19 +1560,253 @@ function parseQuantityInput(input_str) {
|
|
|
1031
1560
|
}
|
|
1032
1561
|
return { type: "fixed", value: parseFixedValue(clean_str) };
|
|
1033
1562
|
}
|
|
1563
|
+
function parseQuantityWithUnit(input) {
|
|
1564
|
+
const trimmed = input.trim();
|
|
1565
|
+
const separatorIndex = trimmed.indexOf("%");
|
|
1566
|
+
if (separatorIndex === -1) {
|
|
1567
|
+
return { value: parseQuantityValue(trimmed) };
|
|
1568
|
+
}
|
|
1569
|
+
const valuePart = trimmed.slice(0, separatorIndex).trim();
|
|
1570
|
+
const unitPart = trimmed.slice(separatorIndex + 1).trim();
|
|
1571
|
+
return {
|
|
1572
|
+
value: parseQuantityValue(valuePart),
|
|
1573
|
+
unit: unitPart || void 0
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function parseDateFromFormat(input, format) {
|
|
1577
|
+
const delimiterMatch = format.match(/[^A-Za-z]/);
|
|
1578
|
+
if (!delimiterMatch) {
|
|
1579
|
+
throw new Error(`Invalid date format: ${format}. No delimiter found.`);
|
|
1580
|
+
}
|
|
1581
|
+
const delimiter = delimiterMatch[0];
|
|
1582
|
+
const formatParts = format.split(delimiter);
|
|
1583
|
+
const inputParts = input.trim().split(delimiter);
|
|
1584
|
+
if (formatParts.length !== 3 || inputParts.length !== 3) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
`Invalid date input "${input}" for format "${format}". Expected 3 parts.`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
let day = 0, month = 0, year = 0;
|
|
1590
|
+
for (let i2 = 0; i2 < 3; i2++) {
|
|
1591
|
+
const token = formatParts[i2].toUpperCase();
|
|
1592
|
+
const value = parseInt(inputParts[i2], 10);
|
|
1593
|
+
if (isNaN(value)) {
|
|
1594
|
+
throw new Error(
|
|
1595
|
+
`Invalid date input "${input}": non-numeric part "${inputParts[i2]}".`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
if (token === "DD") day = value;
|
|
1599
|
+
else if (token === "MM") month = value;
|
|
1600
|
+
else if (token === "YYYY") year = value;
|
|
1601
|
+
else
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`Unknown token "${formatParts[i2]}" in format "${format}"`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const date = new Date(year, month - 1, day);
|
|
1607
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1608
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1609
|
+
}
|
|
1610
|
+
return date;
|
|
1611
|
+
}
|
|
1612
|
+
function disambiguateDayMonth(first, second, year) {
|
|
1613
|
+
if (second > 12 && first <= 12) {
|
|
1614
|
+
return [second, first, year];
|
|
1615
|
+
}
|
|
1616
|
+
return [first, second, year];
|
|
1617
|
+
}
|
|
1618
|
+
function parseFuzzyDate(input) {
|
|
1619
|
+
const trimmed = input.trim();
|
|
1620
|
+
const delimiterMatch = trimmed.match(/[./-]/);
|
|
1621
|
+
if (!delimiterMatch) {
|
|
1622
|
+
throw new Error(`Cannot parse date "${input}": no delimiter found.`);
|
|
1623
|
+
}
|
|
1624
|
+
const delimiter = delimiterMatch[0];
|
|
1625
|
+
const parts = trimmed.split(delimiter);
|
|
1626
|
+
if (parts.length !== 3) {
|
|
1627
|
+
throw new Error(
|
|
1628
|
+
`Cannot parse date "${input}": expected 3 parts, got ${parts.length}.`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
const nums = parts.map((p) => parseInt(p, 10));
|
|
1632
|
+
if (nums.some((n2) => isNaN(n2))) {
|
|
1633
|
+
throw new Error(`Cannot parse date "${input}": non-numeric parts found.`);
|
|
1634
|
+
}
|
|
1635
|
+
let day, month, year;
|
|
1636
|
+
if (nums[0] >= 1e3) {
|
|
1637
|
+
year = nums[0];
|
|
1638
|
+
month = nums[1];
|
|
1639
|
+
day = nums[2];
|
|
1640
|
+
} else if (nums[2] >= 1e3) {
|
|
1641
|
+
[day, month, year] = disambiguateDayMonth(nums[0], nums[1], nums[2]);
|
|
1642
|
+
} else {
|
|
1643
|
+
if (nums[2] >= 100)
|
|
1644
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1645
|
+
[day, month] = disambiguateDayMonth(nums[0], nums[1], 0);
|
|
1646
|
+
year = 2e3 + nums[2];
|
|
1647
|
+
}
|
|
1648
|
+
const date = new Date(year, month - 1, day);
|
|
1649
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
1650
|
+
throw new Error(`Invalid date: "${input}" does not form a valid date.`);
|
|
1651
|
+
}
|
|
1652
|
+
return date;
|
|
1653
|
+
}
|
|
1654
|
+
function parseMarkdownSegments(text) {
|
|
1655
|
+
const items = [];
|
|
1656
|
+
let cursor = 0;
|
|
1657
|
+
for (const match of text.matchAll(markdownRegex)) {
|
|
1658
|
+
const idx = match.index;
|
|
1659
|
+
if (idx > cursor) {
|
|
1660
|
+
items.push({ type: "text", value: text.slice(cursor, idx) });
|
|
1661
|
+
}
|
|
1662
|
+
const [
|
|
1663
|
+
,
|
|
1664
|
+
escaped,
|
|
1665
|
+
// group 1: escaped character
|
|
1666
|
+
code,
|
|
1667
|
+
// group 2: inline code
|
|
1668
|
+
linkText,
|
|
1669
|
+
// group 3: link text
|
|
1670
|
+
linkUrl,
|
|
1671
|
+
// group 4: link url
|
|
1672
|
+
tripleAst,
|
|
1673
|
+
// group 5: ***bold+italic***
|
|
1674
|
+
tripleUnd,
|
|
1675
|
+
// group 6: ___bold+italic___
|
|
1676
|
+
astUnd,
|
|
1677
|
+
// group 7: **_bold+italic_**
|
|
1678
|
+
undAst,
|
|
1679
|
+
// group 8: __*bold+italic*__
|
|
1680
|
+
astUndUnd,
|
|
1681
|
+
// group 9: *__bold+italic__*
|
|
1682
|
+
undAstAst,
|
|
1683
|
+
// group 10: _**bold+italic**_
|
|
1684
|
+
boldAst,
|
|
1685
|
+
// group 11: **bold**
|
|
1686
|
+
boldUnd,
|
|
1687
|
+
// group 12: __bold__
|
|
1688
|
+
italicAst,
|
|
1689
|
+
// group 13: *italic*
|
|
1690
|
+
italicUnd
|
|
1691
|
+
// group 14: _italic_
|
|
1692
|
+
] = match;
|
|
1693
|
+
let value;
|
|
1694
|
+
let attribute;
|
|
1695
|
+
let href;
|
|
1696
|
+
if (escaped !== void 0) {
|
|
1697
|
+
items.push({ type: "text", value: escaped });
|
|
1698
|
+
cursor = idx + match[0].length;
|
|
1699
|
+
continue;
|
|
1700
|
+
} else if (code !== void 0) {
|
|
1701
|
+
value = code;
|
|
1702
|
+
attribute = "code";
|
|
1703
|
+
} else if (linkText !== void 0) {
|
|
1704
|
+
value = linkText;
|
|
1705
|
+
attribute = "link";
|
|
1706
|
+
href = linkUrl;
|
|
1707
|
+
} else if (tripleAst !== void 0 || tripleUnd !== void 0 || astUnd !== void 0 || undAst !== void 0 || astUndUnd !== void 0 || undAstAst !== void 0) {
|
|
1708
|
+
value = tripleAst ?? tripleUnd ?? astUnd ?? undAst ?? astUndUnd ?? undAstAst;
|
|
1709
|
+
attribute = "bold+italic";
|
|
1710
|
+
} else if (boldAst !== void 0 || boldUnd !== void 0) {
|
|
1711
|
+
value = boldAst ?? boldUnd;
|
|
1712
|
+
attribute = "bold";
|
|
1713
|
+
} else {
|
|
1714
|
+
value = italicAst ?? italicUnd;
|
|
1715
|
+
attribute = "italic";
|
|
1716
|
+
}
|
|
1717
|
+
const item = { type: "text", value };
|
|
1718
|
+
if (attribute) item.attribute = attribute;
|
|
1719
|
+
if (href) item.href = href;
|
|
1720
|
+
items.push(item);
|
|
1721
|
+
cursor = idx + match[0].length;
|
|
1722
|
+
}
|
|
1723
|
+
if (cursor < text.length) {
|
|
1724
|
+
items.push({ type: "text", value: text.slice(cursor) });
|
|
1725
|
+
}
|
|
1726
|
+
return items;
|
|
1727
|
+
}
|
|
1034
1728
|
function parseSimpleMetaVar(content, varName) {
|
|
1035
1729
|
const varMatch = content.match(
|
|
1036
1730
|
new RegExp(`^${varName}:\\s*(.*(?:\\r?\\n\\s+.*)*)+`, "m")
|
|
1037
1731
|
);
|
|
1038
1732
|
return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
|
|
1039
1733
|
}
|
|
1040
|
-
function
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1734
|
+
function parseBlockScalarMetaVar(content, varName) {
|
|
1735
|
+
const match = content.match(
|
|
1736
|
+
new RegExp(
|
|
1737
|
+
`^${varName}:\\s*([|>])\\s*\\r?\\n((?:(?:[ ]+.*|\\s*)(?:\\r?\\n|$))+)`,
|
|
1738
|
+
"m"
|
|
1739
|
+
)
|
|
1740
|
+
);
|
|
1741
|
+
if (!match) return void 0;
|
|
1742
|
+
const style = match[1];
|
|
1743
|
+
const rawBlock = match[2];
|
|
1744
|
+
const lines = rawBlock.split(/\r?\n/);
|
|
1745
|
+
const firstNonEmpty = lines.find((l) => l.trim() !== "");
|
|
1746
|
+
if (!firstNonEmpty) return void 0;
|
|
1747
|
+
const baseIndent = firstNonEmpty.match(/^([ ]*)/)[1].length;
|
|
1748
|
+
const stripped = lines.map((line) => line.trim() === "" ? "" : line.slice(baseIndent)).join("\n").replace(/\n+$/, "");
|
|
1749
|
+
if (style === "|") {
|
|
1750
|
+
return stripped;
|
|
1751
|
+
}
|
|
1752
|
+
return stripped.replace(/\n\n/g, "\0").replace(/\n/g, " ").replace(/\0/g, "\n");
|
|
1753
|
+
}
|
|
1754
|
+
function parseArbitraryQuantity(raw) {
|
|
1755
|
+
const quantityMatch = raw.trim().match(quantityAlternativeRegex);
|
|
1756
|
+
if (!quantityMatch?.groups) {
|
|
1757
|
+
throw new InvalidQuantityFormat(
|
|
1758
|
+
raw,
|
|
1759
|
+
"Arbitrary quantities must have a numerical value"
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
const value = parseQuantityValue(quantityMatch.groups.quantity);
|
|
1763
|
+
const unit = quantityMatch.groups.unit;
|
|
1764
|
+
if (!value || value.type === "fixed" && value.value.type === "text") {
|
|
1765
|
+
throw new InvalidQuantityFormat(
|
|
1766
|
+
raw,
|
|
1767
|
+
"Arbitrary quantities must have a numerical value"
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
const arbitrary = {
|
|
1771
|
+
quantity: value
|
|
1772
|
+
};
|
|
1773
|
+
if (unit) arbitrary.unit = unit;
|
|
1774
|
+
return arbitrary;
|
|
1775
|
+
}
|
|
1776
|
+
function parseServingsMetaVar(content, varName) {
|
|
1777
|
+
const raw = parseSimpleMetaVar(content, varName);
|
|
1778
|
+
if (raw === void 0) return void 0;
|
|
1779
|
+
const num = Number(raw);
|
|
1780
|
+
if (isNaN(num)) {
|
|
1781
|
+
return { numericValue: 1, rawValue: raw };
|
|
1782
|
+
}
|
|
1783
|
+
return { numericValue: num, rawValue: num };
|
|
1784
|
+
}
|
|
1785
|
+
function parseYieldMetaVar(content) {
|
|
1786
|
+
const match = content.match(yieldMetaValueRegex);
|
|
1787
|
+
if (!match) return void 0;
|
|
1788
|
+
if (match.groups?.arbitraryQuantity) {
|
|
1789
|
+
const parsed = parseArbitraryQuantity(match.groups.arbitraryQuantity);
|
|
1790
|
+
const result = {
|
|
1791
|
+
quantity: parsed.quantity
|
|
1792
|
+
};
|
|
1793
|
+
if (parsed.unit) result.unit = parsed.unit;
|
|
1794
|
+
if (match.groups.servingsPrefix) {
|
|
1795
|
+
result.textBefore = match.groups.servingsPrefix;
|
|
1796
|
+
}
|
|
1797
|
+
if (match.groups.servingsSuffix) {
|
|
1798
|
+
result.textAfter = match.groups.servingsSuffix;
|
|
1799
|
+
}
|
|
1800
|
+
return result;
|
|
1801
|
+
}
|
|
1802
|
+
if (match.groups?.quantity) {
|
|
1803
|
+
const result = {
|
|
1804
|
+
quantity: parseQuantityValue(match.groups.quantity)
|
|
1805
|
+
};
|
|
1806
|
+
if (match.groups.unit) result.unit = match.groups.unit;
|
|
1807
|
+
return result;
|
|
1045
1808
|
}
|
|
1046
|
-
return
|
|
1809
|
+
return void 0;
|
|
1047
1810
|
}
|
|
1048
1811
|
function parseListMetaVar(content, varName) {
|
|
1049
1812
|
const listMatch = content.match(
|
|
@@ -1059,6 +1822,115 @@ function parseListMetaVar(content, varName) {
|
|
|
1059
1822
|
return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
|
|
1060
1823
|
}
|
|
1061
1824
|
}
|
|
1825
|
+
function extractAllMetadataKeys(content) {
|
|
1826
|
+
const keys = [];
|
|
1827
|
+
for (const match of content.matchAll(metadataKeyRegex)) {
|
|
1828
|
+
keys.push(match[1].trim());
|
|
1829
|
+
}
|
|
1830
|
+
return [...new Set(keys)];
|
|
1831
|
+
}
|
|
1832
|
+
function parseNestedMetaVar(content, varName) {
|
|
1833
|
+
const match = content.match(nestedMetaVarRegex(varName));
|
|
1834
|
+
if (!match) return void 0;
|
|
1835
|
+
const nestedContent = match[1];
|
|
1836
|
+
return parseNestedBlock(nestedContent);
|
|
1837
|
+
}
|
|
1838
|
+
function parseNestedBlock(content) {
|
|
1839
|
+
const lines = content.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
1840
|
+
if (lines.length === 0) return void 0;
|
|
1841
|
+
const baseIndentMatch = lines[0].match(/^(\s*)/);
|
|
1842
|
+
if (baseIndentMatch?.[0]?.includes(" ")) {
|
|
1843
|
+
throw new NoTabAsIndentError();
|
|
1844
|
+
}
|
|
1845
|
+
const baseIndent = baseIndentMatch?.[1]?.length;
|
|
1846
|
+
if (lines[0].trim().startsWith("- ")) return void 0;
|
|
1847
|
+
const result = {};
|
|
1848
|
+
let i2 = 0;
|
|
1849
|
+
while (i2 < lines.length) {
|
|
1850
|
+
const line = lines[i2];
|
|
1851
|
+
const leadingWhitespace = line.match(/^(\s*)/)?.[1];
|
|
1852
|
+
if (leadingWhitespace && leadingWhitespace.includes(" ")) {
|
|
1853
|
+
throw new NoTabAsIndentError();
|
|
1854
|
+
}
|
|
1855
|
+
const currentIndent = leadingWhitespace.length;
|
|
1856
|
+
if (currentIndent < baseIndent) {
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
if (currentIndent !== baseIndent) {
|
|
1860
|
+
throw new BadIndentationError();
|
|
1861
|
+
}
|
|
1862
|
+
const keyValueMatch = line.match(/^[ ]*([^:\n]+?):\s*(.*)$/);
|
|
1863
|
+
if (!keyValueMatch) {
|
|
1864
|
+
i2++;
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
const key = keyValueMatch[1].trim();
|
|
1868
|
+
const rawValue = keyValueMatch[2].trim();
|
|
1869
|
+
if (rawValue === "") {
|
|
1870
|
+
const childLines = [];
|
|
1871
|
+
let j = i2 + 1;
|
|
1872
|
+
while (j < lines.length) {
|
|
1873
|
+
const childLine = lines[j];
|
|
1874
|
+
const childIndent = childLine.match(/^([ ]*)/)?.[1]?.length;
|
|
1875
|
+
if (childIndent && childIndent > baseIndent) {
|
|
1876
|
+
childLines.push(childLine);
|
|
1877
|
+
j++;
|
|
1878
|
+
} else {
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (childLines.length > 0) {
|
|
1883
|
+
const firstChildTrimmed = childLines[0].trim();
|
|
1884
|
+
if (firstChildTrimmed.startsWith("- ")) {
|
|
1885
|
+
const reconstructedContent = `${key}:
|
|
1886
|
+
${childLines.join("\n")}`;
|
|
1887
|
+
const listResult = parseListMetaVar(reconstructedContent, key);
|
|
1888
|
+
if (listResult) {
|
|
1889
|
+
result[key] = listResult.map(
|
|
1890
|
+
(item) => parseMetadataValue(item)
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
} else {
|
|
1894
|
+
const childContent = childLines.join("\n");
|
|
1895
|
+
const nested = parseNestedBlock(childContent);
|
|
1896
|
+
if (nested) {
|
|
1897
|
+
result[key] = nested;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
i2 = j;
|
|
1902
|
+
} else {
|
|
1903
|
+
result[key] = parseMetadataValue(rawValue);
|
|
1904
|
+
i2++;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
function parseMetadataValue(rawValue) {
|
|
1910
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
1911
|
+
return rawValue.slice(1, -1).split(",").map((item) => item.trim());
|
|
1912
|
+
}
|
|
1913
|
+
if (numericValueRegex.test(rawValue)) {
|
|
1914
|
+
return Number(rawValue);
|
|
1915
|
+
}
|
|
1916
|
+
return rawValue;
|
|
1917
|
+
}
|
|
1918
|
+
function parseAnyMetaVar(content, varName) {
|
|
1919
|
+
const nested = parseNestedMetaVar(content, varName);
|
|
1920
|
+
if (nested) return nested;
|
|
1921
|
+
const list = parseListMetaVar(content, varName);
|
|
1922
|
+
if (list) return list;
|
|
1923
|
+
const simple = parseSimpleMetaVar(content, varName);
|
|
1924
|
+
if (simple) return parseMetadataValue(simple);
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
function getNumericValueFromYield(v) {
|
|
1928
|
+
if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
|
|
1929
|
+
return getNumericValue(v.quantity.value);
|
|
1930
|
+
}
|
|
1931
|
+
if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
|
|
1932
|
+
return 1;
|
|
1933
|
+
}
|
|
1062
1934
|
function extractMetadata(content) {
|
|
1063
1935
|
const metadata = {};
|
|
1064
1936
|
let servings = void 0;
|
|
@@ -1066,13 +1938,24 @@ function extractMetadata(content) {
|
|
|
1066
1938
|
if (!metadataContent) {
|
|
1067
1939
|
return { metadata };
|
|
1068
1940
|
}
|
|
1069
|
-
|
|
1941
|
+
const handledKeys = /* @__PURE__ */ new Set([
|
|
1942
|
+
// Simple string fields
|
|
1070
1943
|
"title",
|
|
1944
|
+
"author",
|
|
1945
|
+
"locale",
|
|
1946
|
+
"introduction",
|
|
1947
|
+
"description",
|
|
1948
|
+
"course",
|
|
1949
|
+
"category",
|
|
1950
|
+
"diet",
|
|
1951
|
+
"cuisine",
|
|
1952
|
+
"difficulty",
|
|
1953
|
+
// Source fields
|
|
1071
1954
|
"source",
|
|
1072
1955
|
"source.name",
|
|
1073
1956
|
"source.url",
|
|
1074
|
-
"author",
|
|
1075
1957
|
"source.author",
|
|
1958
|
+
// Time fields
|
|
1076
1959
|
"prep time",
|
|
1077
1960
|
"time.prep",
|
|
1078
1961
|
"cook time",
|
|
@@ -1080,6 +1963,24 @@ function extractMetadata(content) {
|
|
|
1080
1963
|
"time required",
|
|
1081
1964
|
"time",
|
|
1082
1965
|
"duration",
|
|
1966
|
+
// Image fields
|
|
1967
|
+
"image",
|
|
1968
|
+
"picture",
|
|
1969
|
+
"images",
|
|
1970
|
+
"pictures",
|
|
1971
|
+
// Unit system
|
|
1972
|
+
"unit system",
|
|
1973
|
+
// Scaling fields
|
|
1974
|
+
"servings",
|
|
1975
|
+
"yield",
|
|
1976
|
+
"serves",
|
|
1977
|
+
// List fields
|
|
1978
|
+
"tags",
|
|
1979
|
+
"variants"
|
|
1980
|
+
]);
|
|
1981
|
+
for (const metaVar of [
|
|
1982
|
+
"title",
|
|
1983
|
+
"author",
|
|
1083
1984
|
"locale",
|
|
1084
1985
|
"introduction",
|
|
1085
1986
|
"description",
|
|
@@ -1087,25 +1988,97 @@ function extractMetadata(content) {
|
|
|
1087
1988
|
"category",
|
|
1088
1989
|
"diet",
|
|
1089
1990
|
"cuisine",
|
|
1090
|
-
"difficulty"
|
|
1091
|
-
"image",
|
|
1092
|
-
"picture"
|
|
1991
|
+
"difficulty"
|
|
1093
1992
|
]) {
|
|
1993
|
+
if (metaVar === "description" || metaVar === "introduction") {
|
|
1994
|
+
const blockValue = parseBlockScalarMetaVar(metadataContent, metaVar);
|
|
1995
|
+
if (blockValue) {
|
|
1996
|
+
metadata[metaVar] = blockValue;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1094
2000
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1095
2001
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1096
2002
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
2003
|
+
const sourceNested = parseNestedMetaVar(metadataContent, "source");
|
|
2004
|
+
const sourceTxt = parseSimpleMetaVar(metadataContent, "source");
|
|
2005
|
+
const sourceName = parseSimpleMetaVar(metadataContent, "source.name");
|
|
2006
|
+
const sourceUrl = parseSimpleMetaVar(metadataContent, "source.url");
|
|
2007
|
+
const sourceAuthor = parseSimpleMetaVar(metadataContent, "source.author");
|
|
2008
|
+
if (sourceNested) {
|
|
2009
|
+
const source = {};
|
|
2010
|
+
if (typeof sourceNested.name === "string") source.name = sourceNested.name;
|
|
2011
|
+
if (typeof sourceNested.url === "string") source.url = sourceNested.url;
|
|
2012
|
+
if (typeof sourceNested.author === "string")
|
|
2013
|
+
source.author = sourceNested.author;
|
|
2014
|
+
if (Object.keys(source).length > 0) metadata.source = source;
|
|
2015
|
+
} else if (sourceName || sourceAuthor || sourceUrl) {
|
|
2016
|
+
const source = {};
|
|
2017
|
+
if (sourceName) source.name = sourceName;
|
|
2018
|
+
if (sourceUrl) source.url = sourceUrl;
|
|
2019
|
+
if (sourceAuthor) source.author = sourceAuthor;
|
|
2020
|
+
metadata.source = source;
|
|
2021
|
+
} else if (sourceTxt) {
|
|
2022
|
+
metadata.source = sourceTxt;
|
|
2023
|
+
}
|
|
2024
|
+
const timeNested = parseNestedMetaVar(metadataContent, "time");
|
|
2025
|
+
const prepTime = parseSimpleMetaVar(metadataContent, "prep time") ?? parseSimpleMetaVar(metadataContent, "time.prep");
|
|
2026
|
+
const cookTime = parseSimpleMetaVar(metadataContent, "cook time") ?? parseSimpleMetaVar(metadataContent, "time.cook");
|
|
2027
|
+
const totalTime = parseSimpleMetaVar(metadataContent, "time required") ?? parseSimpleMetaVar(metadataContent, "time") ?? parseSimpleMetaVar(metadataContent, "duration");
|
|
2028
|
+
if (timeNested) {
|
|
2029
|
+
const time = {};
|
|
2030
|
+
if (typeof timeNested.prep === "string") time.prep = timeNested.prep;
|
|
2031
|
+
if (typeof timeNested.cook === "string") time.cook = timeNested.cook;
|
|
2032
|
+
if (typeof timeNested.total === "string") time.total = timeNested.total;
|
|
2033
|
+
if (Object.keys(time).length > 0) metadata.time = time;
|
|
2034
|
+
} else if (prepTime || cookTime || totalTime) {
|
|
2035
|
+
const time = {};
|
|
2036
|
+
if (prepTime) time.prep = prepTime;
|
|
2037
|
+
if (cookTime) time.cook = cookTime;
|
|
2038
|
+
if (totalTime) time.total = totalTime;
|
|
2039
|
+
metadata.time = time;
|
|
2040
|
+
}
|
|
2041
|
+
const image = parseSimpleMetaVar(metadataContent, "image") ?? parseSimpleMetaVar(metadataContent, "picture");
|
|
2042
|
+
if (image) metadata.image = image;
|
|
2043
|
+
const images = parseListMetaVar(metadataContent, "images") ?? parseListMetaVar(metadataContent, "pictures");
|
|
2044
|
+
if (images) metadata.images = images;
|
|
2045
|
+
let unitSystem;
|
|
2046
|
+
const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
|
|
2047
|
+
if (unitSystemRaw) {
|
|
2048
|
+
metadata.unitSystem = unitSystemRaw;
|
|
2049
|
+
const unitSystemMap = {
|
|
2050
|
+
metric: "metric",
|
|
2051
|
+
us: "US",
|
|
2052
|
+
uk: "UK",
|
|
2053
|
+
jp: "JP"
|
|
2054
|
+
};
|
|
2055
|
+
unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
|
|
2056
|
+
}
|
|
2057
|
+
const yieldValue = parseYieldMetaVar(metadataContent);
|
|
2058
|
+
if (yieldValue) {
|
|
2059
|
+
metadata.yield = yieldValue;
|
|
2060
|
+
servings = getNumericValueFromYield(yieldValue);
|
|
2061
|
+
}
|
|
2062
|
+
for (const metaVar of ["serves", "servings"]) {
|
|
2063
|
+
const result = parseServingsMetaVar(metadataContent, metaVar);
|
|
2064
|
+
if (result !== void 0) {
|
|
2065
|
+
metadata[metaVar] = result.rawValue;
|
|
2066
|
+
servings = result.numericValue;
|
|
1102
2067
|
}
|
|
1103
2068
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
2069
|
+
const tags = parseListMetaVar(metadataContent, "tags");
|
|
2070
|
+
if (tags) metadata.tags = tags;
|
|
2071
|
+
const variants = parseListMetaVar(metadataContent, "variants");
|
|
2072
|
+
if (variants) metadata.variants = variants;
|
|
2073
|
+
const allKeys = extractAllMetadataKeys(metadataContent);
|
|
2074
|
+
for (const key of allKeys) {
|
|
2075
|
+
if (handledKeys.has(key)) continue;
|
|
2076
|
+
const value = parseAnyMetaVar(metadataContent, key);
|
|
2077
|
+
if (value !== void 0) {
|
|
2078
|
+
metadata[key] = value;
|
|
2079
|
+
}
|
|
1107
2080
|
}
|
|
1108
|
-
return { metadata, servings };
|
|
2081
|
+
return { metadata, servings, unitSystem };
|
|
1109
2082
|
}
|
|
1110
2083
|
function isPositiveIntegerString(str) {
|
|
1111
2084
|
return /^\d+$/.test(str);
|
|
@@ -1119,10 +2092,230 @@ function unionOfSets(s1, s2) {
|
|
|
1119
2092
|
}
|
|
1120
2093
|
function getAlternativeSignature(alternatives) {
|
|
1121
2094
|
if (!alternatives || alternatives.length === 0) return null;
|
|
1122
|
-
return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
2095
|
+
return alternatives.flat().map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
|
|
1123
2096
|
}
|
|
1124
2097
|
|
|
2098
|
+
// src/classes/pantry.ts
|
|
2099
|
+
var Pantry = class {
|
|
2100
|
+
/**
|
|
2101
|
+
* Creates a new Pantry instance.
|
|
2102
|
+
* @param tomlContent - Optional TOML content to parse.
|
|
2103
|
+
* @param options - Optional configuration options.
|
|
2104
|
+
*/
|
|
2105
|
+
constructor(tomlContent, options = {}) {
|
|
2106
|
+
/**
|
|
2107
|
+
* The parsed pantry items.
|
|
2108
|
+
*/
|
|
2109
|
+
__publicField(this, "items", []);
|
|
2110
|
+
/**
|
|
2111
|
+
* Options for date parsing and other configuration.
|
|
2112
|
+
*/
|
|
2113
|
+
__publicField(this, "options");
|
|
2114
|
+
/**
|
|
2115
|
+
* Optional category configuration for alias-based lookups.
|
|
2116
|
+
*/
|
|
2117
|
+
__publicField(this, "categoryConfig");
|
|
2118
|
+
this.options = options;
|
|
2119
|
+
if (tomlContent) {
|
|
2120
|
+
this.parse(tomlContent);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Parses a TOML string into pantry items.
|
|
2125
|
+
* @param tomlContent - The TOML string to parse.
|
|
2126
|
+
* @returns The parsed list of pantry items.
|
|
2127
|
+
*/
|
|
2128
|
+
parse(tomlContent) {
|
|
2129
|
+
const raw = import_smol_toml.default.parse(tomlContent);
|
|
2130
|
+
this.items = [];
|
|
2131
|
+
for (const [location, locationData] of Object.entries(raw)) {
|
|
2132
|
+
const locationTable = locationData;
|
|
2133
|
+
for (const [itemName, itemData] of Object.entries(locationTable)) {
|
|
2134
|
+
const item = this.parseItem(
|
|
2135
|
+
itemName,
|
|
2136
|
+
location,
|
|
2137
|
+
itemData
|
|
2138
|
+
);
|
|
2139
|
+
this.items.push(item);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return this.items;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Parses a single pantry item from its TOML representation.
|
|
2146
|
+
*/
|
|
2147
|
+
parseItem(name, location, data) {
|
|
2148
|
+
const item = { name, location };
|
|
2149
|
+
if (typeof data === "string") {
|
|
2150
|
+
const parsed = parseQuantityWithUnit(data);
|
|
2151
|
+
item.quantity = parsed.value;
|
|
2152
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2153
|
+
} else {
|
|
2154
|
+
if (data.quantity) {
|
|
2155
|
+
const parsed = parseQuantityWithUnit(data.quantity);
|
|
2156
|
+
item.quantity = parsed.value;
|
|
2157
|
+
if (parsed.unit) item.unit = parsed.unit;
|
|
2158
|
+
}
|
|
2159
|
+
if (data.low) {
|
|
2160
|
+
const parsed = parseQuantityWithUnit(data.low);
|
|
2161
|
+
item.low = parsed.value;
|
|
2162
|
+
if (parsed.unit) item.lowUnit = parsed.unit;
|
|
2163
|
+
}
|
|
2164
|
+
if (data.bought) {
|
|
2165
|
+
item.bought = this.parseDate(data.bought);
|
|
2166
|
+
}
|
|
2167
|
+
if (data.expire) {
|
|
2168
|
+
item.expire = this.parseDate(data.expire);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return item;
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Parses a date string using the configured format or fuzzy detection.
|
|
2175
|
+
*/
|
|
2176
|
+
parseDate(input) {
|
|
2177
|
+
if (this.options.dateFormat) {
|
|
2178
|
+
return parseDateFromFormat(input, this.options.dateFormat);
|
|
2179
|
+
}
|
|
2180
|
+
return parseFuzzyDate(input);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Sets a category configuration for alias-based item lookups.
|
|
2184
|
+
* @param config - The category configuration to use.
|
|
2185
|
+
*/
|
|
2186
|
+
setCategoryConfig(config) {
|
|
2187
|
+
this.categoryConfig = config;
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Finds a pantry item by name, using exact match first, then alias lookup
|
|
2191
|
+
* via the stored CategoryConfig.
|
|
2192
|
+
* @param name - The name to search for.
|
|
2193
|
+
* @returns The matching pantry item, or undefined if not found.
|
|
2194
|
+
*/
|
|
2195
|
+
findItem(name) {
|
|
2196
|
+
const lowerName = name.toLowerCase();
|
|
2197
|
+
const exact = this.items.find(
|
|
2198
|
+
(item) => item.name.toLowerCase() === lowerName
|
|
2199
|
+
);
|
|
2200
|
+
if (exact) return exact;
|
|
2201
|
+
if (this.categoryConfig) {
|
|
2202
|
+
for (const category of this.categoryConfig.categories) {
|
|
2203
|
+
for (const catIngredient of category.ingredients) {
|
|
2204
|
+
if (catIngredient.aliases.some(
|
|
2205
|
+
(alias) => alias.toLowerCase() === lowerName
|
|
2206
|
+
)) {
|
|
2207
|
+
const canonicalName = catIngredient.name.toLowerCase();
|
|
2208
|
+
const byCanonical = this.items.find(
|
|
2209
|
+
(item) => item.name.toLowerCase() === canonicalName
|
|
2210
|
+
);
|
|
2211
|
+
if (byCanonical) return byCanonical;
|
|
2212
|
+
for (const alias of catIngredient.aliases) {
|
|
2213
|
+
const byAlias = this.items.find(
|
|
2214
|
+
(item) => item.name.toLowerCase() === alias.toLowerCase()
|
|
2215
|
+
);
|
|
2216
|
+
if (byAlias) return byAlias;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return void 0;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Gets the numeric value of a pantry item's quantity, optionally converted to base units.
|
|
2226
|
+
* Returns undefined if the quantity has a text value or is not set.
|
|
2227
|
+
*/
|
|
2228
|
+
getItemNumericValue(quantity, unit) {
|
|
2229
|
+
if (!quantity) return void 0;
|
|
2230
|
+
let numericValue;
|
|
2231
|
+
if (quantity.type === "fixed") {
|
|
2232
|
+
if (quantity.value.type === "text") return void 0;
|
|
2233
|
+
numericValue = getNumericValue(quantity.value);
|
|
2234
|
+
} else {
|
|
2235
|
+
numericValue = (getNumericValue(quantity.min) + getNumericValue(quantity.max)) / 2;
|
|
2236
|
+
}
|
|
2237
|
+
if (unit) {
|
|
2238
|
+
const unitDef = normalizeUnit(unit);
|
|
2239
|
+
if (unitDef) {
|
|
2240
|
+
const toBase = getToBase(unitDef);
|
|
2241
|
+
numericValue *= toBase;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return numericValue;
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Returns all items that are depleted (quantity = 0) or below their low threshold.
|
|
2248
|
+
* @returns An array of depleted pantry items.
|
|
2249
|
+
*/
|
|
2250
|
+
getDepletedItems() {
|
|
2251
|
+
return this.items.filter((item) => this.isItemLow(item));
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Returns all items whose expiration date is within `nbDays` days from today
|
|
2255
|
+
* (or already passed).
|
|
2256
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0 (already expired).
|
|
2257
|
+
* @returns An array of expired pantry items.
|
|
2258
|
+
*/
|
|
2259
|
+
getExpiredItems(nbDays = 0) {
|
|
2260
|
+
return this.items.filter((item) => this.isItemExpired(item, nbDays));
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Checks if a specific item is low (quantity = 0 or below `low` threshold).
|
|
2264
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2265
|
+
* @returns true if the item is low, false otherwise. Returns false if item not found.
|
|
2266
|
+
*/
|
|
2267
|
+
isLow(itemName) {
|
|
2268
|
+
const item = this.findItem(itemName);
|
|
2269
|
+
if (!item) return false;
|
|
2270
|
+
return this.isItemLow(item);
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Checks if a specific item is expired or expires within `nbDays` days.
|
|
2274
|
+
* @param itemName - The name of the item to check (supports aliases if CategoryConfig is set).
|
|
2275
|
+
* @param nbDays - Number of days ahead to check. Defaults to 0.
|
|
2276
|
+
* @returns true if the item is expired, false otherwise. Returns false if item not found.
|
|
2277
|
+
*/
|
|
2278
|
+
isExpired(itemName, nbDays = 0) {
|
|
2279
|
+
const item = this.findItem(itemName);
|
|
2280
|
+
if (!item) return false;
|
|
2281
|
+
return this.isItemExpired(item, nbDays);
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Internal: checks if a pantry item is low.
|
|
2285
|
+
*/
|
|
2286
|
+
isItemLow(item) {
|
|
2287
|
+
if (!item.quantity) return false;
|
|
2288
|
+
const qtyValue = this.getItemNumericValue(item.quantity, item.unit);
|
|
2289
|
+
if (qtyValue === void 0) return false;
|
|
2290
|
+
if (qtyValue === 0) return true;
|
|
2291
|
+
if (item.low) {
|
|
2292
|
+
const lowValue = this.getItemNumericValue(item.low, item.lowUnit);
|
|
2293
|
+
if (lowValue !== void 0 && qtyValue <= lowValue) return true;
|
|
2294
|
+
}
|
|
2295
|
+
return false;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Internal: checks if a pantry item is expired.
|
|
2299
|
+
*/
|
|
2300
|
+
isItemExpired(item, nbDays) {
|
|
2301
|
+
if (!item.expire) return false;
|
|
2302
|
+
const now = /* @__PURE__ */ new Date();
|
|
2303
|
+
const cutoff = new Date(
|
|
2304
|
+
now.getFullYear(),
|
|
2305
|
+
now.getMonth(),
|
|
2306
|
+
now.getDate() + nbDays
|
|
2307
|
+
);
|
|
2308
|
+
const expireDay = new Date(
|
|
2309
|
+
item.expire.getFullYear(),
|
|
2310
|
+
item.expire.getMonth(),
|
|
2311
|
+
item.expire.getDate()
|
|
2312
|
+
);
|
|
2313
|
+
return expireDay <= cutoff;
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
|
|
1125
2317
|
// src/classes/product_catalog.ts
|
|
2318
|
+
var import_smol_toml2 = __toESM(require("smol-toml"), 1);
|
|
1126
2319
|
var ProductCatalog = class {
|
|
1127
2320
|
constructor(tomlContent) {
|
|
1128
2321
|
__publicField(this, "products", []);
|
|
@@ -1134,7 +2327,7 @@ var ProductCatalog = class {
|
|
|
1134
2327
|
* @returns A parsed list of `ProductOption`.
|
|
1135
2328
|
*/
|
|
1136
2329
|
parse(tomlContent) {
|
|
1137
|
-
const catalogRaw =
|
|
2330
|
+
const catalogRaw = import_smol_toml2.default.parse(tomlContent);
|
|
1138
2331
|
this.products = [];
|
|
1139
2332
|
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1140
2333
|
throw new InvalidProductCatalogFormat();
|
|
@@ -1151,7 +2344,7 @@ var ProductCatalog = class {
|
|
|
1151
2344
|
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
1152
2345
|
const sizes = sizeStrings.map((sizeStr) => {
|
|
1153
2346
|
const sizeAndUnitRaw = sizeStr.split("%");
|
|
1154
|
-
const sizeParsed =
|
|
2347
|
+
const sizeParsed = parseQuantityValue(
|
|
1155
2348
|
sizeAndUnitRaw[0]
|
|
1156
2349
|
);
|
|
1157
2350
|
const productSize = { size: sizeParsed };
|
|
@@ -1207,7 +2400,7 @@ var ProductCatalog = class {
|
|
|
1207
2400
|
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1208
2401
|
};
|
|
1209
2402
|
}
|
|
1210
|
-
return
|
|
2403
|
+
return import_smol_toml2.default.stringify(grouped);
|
|
1211
2404
|
}
|
|
1212
2405
|
/**
|
|
1213
2406
|
* Adds a product to the catalog.
|
|
@@ -1264,8 +2457,10 @@ var Section = class {
|
|
|
1264
2457
|
/**
|
|
1265
2458
|
* Creates an instance of Section.
|
|
1266
2459
|
* @param name - The name of the section. Defaults to an empty string.
|
|
2460
|
+
* @param variants - Optional variant names for this section.
|
|
2461
|
+
* @param optional - Whether the section is optional.
|
|
1267
2462
|
*/
|
|
1268
|
-
constructor(name = "") {
|
|
2463
|
+
constructor(name = "", variants, optional) {
|
|
1269
2464
|
/**
|
|
1270
2465
|
* The name of the section. Can be an empty string for the default (first) section.
|
|
1271
2466
|
* @defaultValue `""`
|
|
@@ -1273,7 +2468,13 @@ var Section = class {
|
|
|
1273
2468
|
__publicField(this, "name");
|
|
1274
2469
|
/** An array of steps and notes that make up the content of the section. */
|
|
1275
2470
|
__publicField(this, "content", []);
|
|
2471
|
+
/** Optional list of variant names this section belongs to. */
|
|
2472
|
+
__publicField(this, "variants");
|
|
2473
|
+
/** Whether the section has been marked as optional ([?]) */
|
|
2474
|
+
__publicField(this, "optional");
|
|
1276
2475
|
this.name = name;
|
|
2476
|
+
if (variants) this.variants = variants;
|
|
2477
|
+
if (optional) this.optional = true;
|
|
1277
2478
|
}
|
|
1278
2479
|
/**
|
|
1279
2480
|
* Checks if the section is blank (has no name and no content).
|
|
@@ -1286,46 +2487,16 @@ var Section = class {
|
|
|
1286
2487
|
};
|
|
1287
2488
|
|
|
1288
2489
|
// src/quantities/alternatives.ts
|
|
1289
|
-
var
|
|
1290
|
-
|
|
1291
|
-
// src/units/conversion.ts
|
|
1292
|
-
var import_big2 = __toESM(require("big.js"), 1);
|
|
1293
|
-
function getUnitRatio(q1, q2) {
|
|
1294
|
-
const q1Value = getAverageValue(q1.quantity);
|
|
1295
|
-
const q2Value = getAverageValue(q2.quantity);
|
|
1296
|
-
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
1297
|
-
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
1298
|
-
throw Error(
|
|
1299
|
-
"One of both values is not a number, so a ratio cannot be computed"
|
|
1300
|
-
);
|
|
1301
|
-
}
|
|
1302
|
-
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
1303
|
-
}
|
|
1304
|
-
function getBaseUnitRatio(q, qRef) {
|
|
1305
|
-
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
1306
|
-
return q.unit.toBase / qRef.unit.toBase;
|
|
1307
|
-
} else {
|
|
1308
|
-
return 1;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
2490
|
+
var import_big4 = __toESM(require("big.js"), 1);
|
|
1311
2491
|
|
|
1312
2492
|
// src/units/lookup.ts
|
|
1313
|
-
function areUnitsCompatible(u1, u2) {
|
|
1314
|
-
if (u1.name === u2.name) {
|
|
1315
|
-
return true;
|
|
1316
|
-
}
|
|
1317
|
-
if (u1.type !== "other" && u1.type === u2.type && u1.system === u2.system) {
|
|
1318
|
-
return true;
|
|
1319
|
-
}
|
|
1320
|
-
return false;
|
|
1321
|
-
}
|
|
1322
2493
|
function findListWithCompatibleQuantity(list, quantity) {
|
|
1323
2494
|
const quantityWithUnitDef = {
|
|
1324
2495
|
...quantity,
|
|
1325
2496
|
unit: resolveUnit(quantity.unit?.name)
|
|
1326
2497
|
};
|
|
1327
2498
|
return list.find(
|
|
1328
|
-
(l) => l.some((lq) =>
|
|
2499
|
+
(l) => l.some((lq) => areUnitsGroupable(lq.unit, quantityWithUnitDef.unit))
|
|
1329
2500
|
);
|
|
1330
2501
|
}
|
|
1331
2502
|
function findCompatibleQuantityWithinList(list, quantity) {
|
|
@@ -1339,10 +2510,14 @@ function findCompatibleQuantityWithinList(list, quantity) {
|
|
|
1339
2510
|
}
|
|
1340
2511
|
|
|
1341
2512
|
// src/utils/general.ts
|
|
2513
|
+
var import_big3 = __toESM(require("big.js"), 1);
|
|
1342
2514
|
var legacyDeepClone = (v) => {
|
|
1343
2515
|
if (v === null || typeof v !== "object") {
|
|
1344
2516
|
return v;
|
|
1345
2517
|
}
|
|
2518
|
+
if (v instanceof import_big3.default) {
|
|
2519
|
+
return new import_big3.default(v);
|
|
2520
|
+
}
|
|
1346
2521
|
if (v instanceof Map) {
|
|
1347
2522
|
return new Map(
|
|
1348
2523
|
Array.from(v.entries()).map(([k, val]) => [
|
|
@@ -1352,7 +2527,9 @@ var legacyDeepClone = (v) => {
|
|
|
1352
2527
|
);
|
|
1353
2528
|
}
|
|
1354
2529
|
if (v instanceof Set) {
|
|
1355
|
-
return new Set(
|
|
2530
|
+
return new Set(
|
|
2531
|
+
Array.from(v).map((val) => legacyDeepClone(val))
|
|
2532
|
+
);
|
|
1356
2533
|
}
|
|
1357
2534
|
if (v instanceof Date) {
|
|
1358
2535
|
return new Date(v.getTime());
|
|
@@ -1366,27 +2543,24 @@ var legacyDeepClone = (v) => {
|
|
|
1366
2543
|
}
|
|
1367
2544
|
return cloned;
|
|
1368
2545
|
};
|
|
1369
|
-
var deepClone = (v) =>
|
|
2546
|
+
var deepClone = (v) => legacyDeepClone(v);
|
|
1370
2547
|
|
|
1371
2548
|
// src/quantities/alternatives.ts
|
|
1372
2549
|
function getEquivalentUnitsLists(...quantities) {
|
|
1373
2550
|
const quantitiesCopy = deepClone(quantities);
|
|
1374
|
-
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.
|
|
2551
|
+
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
|
|
1375
2552
|
const unitLists = [];
|
|
1376
2553
|
const normalizeOrGroup = (og) => ({
|
|
1377
|
-
|
|
1378
|
-
entries: og.entries.map((q) => ({
|
|
2554
|
+
or: og.or.map((q) => ({
|
|
1379
2555
|
...q,
|
|
1380
2556
|
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
1381
2557
|
}))
|
|
1382
2558
|
});
|
|
1383
2559
|
function findLinkIndexForUnits(lists, unitsToCheck) {
|
|
1384
2560
|
return lists.findIndex((l) => {
|
|
1385
|
-
const
|
|
2561
|
+
const listItems = l.map((q) => resolveUnit(q.unit?.name));
|
|
1386
2562
|
return unitsToCheck.some(
|
|
1387
|
-
(u) =>
|
|
1388
|
-
(lu) => lu.name === u?.name || lu.system === u?.system && lu.type === u?.type && lu.type !== "other"
|
|
1389
|
-
)
|
|
2563
|
+
(u) => u && listItems.some((lu) => areUnitsGroupable(lu, u))
|
|
1390
2564
|
);
|
|
1391
2565
|
});
|
|
1392
2566
|
}
|
|
@@ -1397,17 +2571,19 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1397
2571
|
...v,
|
|
1398
2572
|
unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
|
|
1399
2573
|
};
|
|
1400
|
-
const commonQuantity = og.
|
|
1401
|
-
(q) => isQuantity(q) &&
|
|
2574
|
+
const commonQuantity = og.or.find(
|
|
2575
|
+
(q) => isQuantity(q) && areUnitsGroupable(q.unit, normalizedV.unit)
|
|
1402
2576
|
);
|
|
1403
2577
|
if (commonQuantity) {
|
|
1404
2578
|
acc.push(normalizedV);
|
|
1405
|
-
|
|
2579
|
+
if (!unitRatio) {
|
|
2580
|
+
unitRatio = getUnitRatio(normalizedV, commonQuantity);
|
|
2581
|
+
}
|
|
1406
2582
|
}
|
|
1407
2583
|
return acc;
|
|
1408
2584
|
}, []);
|
|
1409
|
-
for (const newQ of og.
|
|
1410
|
-
if (commonUnitList.some((q) =>
|
|
2585
|
+
for (const newQ of og.or) {
|
|
2586
|
+
if (commonUnitList.some((q) => areUnitsGroupable(q.unit, newQ.unit))) {
|
|
1411
2587
|
continue;
|
|
1412
2588
|
} else {
|
|
1413
2589
|
const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
|
|
@@ -1417,10 +2593,10 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1417
2593
|
}
|
|
1418
2594
|
for (const orGroup of OrGroups) {
|
|
1419
2595
|
const orGroupModified = normalizeOrGroup(orGroup);
|
|
1420
|
-
const units2 = orGroupModified.
|
|
2596
|
+
const units2 = orGroupModified.or.map((q) => q.unit);
|
|
1421
2597
|
const linkIndex = findLinkIndexForUnits(unitLists, units2);
|
|
1422
2598
|
if (linkIndex === -1) {
|
|
1423
|
-
unitLists.push(orGroupModified.
|
|
2599
|
+
unitLists.push(orGroupModified.or);
|
|
1424
2600
|
} else {
|
|
1425
2601
|
mergeOrGroupIntoList(unitLists, linkIndex, orGroupModified);
|
|
1426
2602
|
}
|
|
@@ -1516,7 +2692,7 @@ function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
|
1516
2692
|
return quantities.map((q) => {
|
|
1517
2693
|
if (isQuantity(q)) return reduceToQuantity(q);
|
|
1518
2694
|
const qListModified = sortUnitList(
|
|
1519
|
-
q.
|
|
2695
|
+
q.or.map((qq) => ({
|
|
1520
2696
|
...qq,
|
|
1521
2697
|
unit: resolveUnit(qq.unit?.name, qq.unit?.integerProtected)
|
|
1522
2698
|
}))
|
|
@@ -1524,7 +2700,7 @@ function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
|
1524
2700
|
return reduceToQuantity(qListModified[0]);
|
|
1525
2701
|
});
|
|
1526
2702
|
}
|
|
1527
|
-
function addQuantitiesOrGroups(
|
|
2703
|
+
function addQuantitiesOrGroups(quantities, system) {
|
|
1528
2704
|
if (quantities.length === 0)
|
|
1529
2705
|
return {
|
|
1530
2706
|
sum: {
|
|
@@ -1554,7 +2730,7 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1554
2730
|
unit: resolveUnit(nextQ.unit?.name)
|
|
1555
2731
|
});
|
|
1556
2732
|
} else {
|
|
1557
|
-
const sumQ = addQuantities(existingQ, nextQ);
|
|
2733
|
+
const sumQ = addQuantities(existingQ, nextQ, system);
|
|
1558
2734
|
existingQ.quantity = sumQ.quantity;
|
|
1559
2735
|
existingQ.unit = resolveUnit(sumQ.unit?.name);
|
|
1560
2736
|
}
|
|
@@ -1562,10 +2738,10 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1562
2738
|
if (sum.length === 1) {
|
|
1563
2739
|
return { sum: sum[0], unitsLists };
|
|
1564
2740
|
}
|
|
1565
|
-
return { sum: {
|
|
2741
|
+
return { sum: { and: sum }, unitsLists };
|
|
1566
2742
|
}
|
|
1567
|
-
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
1568
|
-
const sumQuantities =
|
|
2743
|
+
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
|
|
2744
|
+
const sumQuantities = isAndGroup(sum) ? sum.and : [sum];
|
|
1569
2745
|
const result = [];
|
|
1570
2746
|
const processedQuantities = /* @__PURE__ */ new Set();
|
|
1571
2747
|
for (const list of unitsLists) {
|
|
@@ -1592,10 +2768,20 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1592
2768
|
}
|
|
1593
2769
|
return main.reduce((acc, v) => {
|
|
1594
2770
|
const mainInList = findCompatibleQuantityWithinList(list, v);
|
|
2771
|
+
const conversionRatio = getBaseUnitRatio(v, mainInList);
|
|
2772
|
+
const valueInOriginalUnit = (0, import_big4.default)(getAverageValue(v.quantity)).times(
|
|
2773
|
+
conversionRatio
|
|
2774
|
+
);
|
|
1595
2775
|
const newValue = {
|
|
1596
2776
|
quantity: multiplyQuantityValue(
|
|
1597
|
-
|
|
1598
|
-
|
|
2777
|
+
{
|
|
2778
|
+
type: "fixed",
|
|
2779
|
+
value: {
|
|
2780
|
+
type: "decimal",
|
|
2781
|
+
decimal: valueInOriginalUnit.toNumber()
|
|
2782
|
+
}
|
|
2783
|
+
},
|
|
2784
|
+
(0, import_big4.default)(getAverageValue(equiv.quantity)).div(
|
|
1599
2785
|
getAverageValue(mainInList.quantity)
|
|
1600
2786
|
)
|
|
1601
2787
|
)
|
|
@@ -1603,17 +2789,15 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1603
2789
|
if (equiv.unit && !isNoUnit(equiv.unit)) {
|
|
1604
2790
|
newValue.unit = { name: equiv.unit.name };
|
|
1605
2791
|
}
|
|
1606
|
-
return addQuantities(acc, newValue);
|
|
2792
|
+
return addQuantities(acc, newValue, system);
|
|
1607
2793
|
}, initialValue);
|
|
1608
2794
|
});
|
|
1609
2795
|
if (main.length + equivalents.length > 1) {
|
|
1610
2796
|
const resultMain = main.length > 1 ? {
|
|
1611
|
-
|
|
1612
|
-
entries: main.map(deNormalizeQuantity)
|
|
2797
|
+
and: main.map(deNormalizeQuantity)
|
|
1613
2798
|
} : deNormalizeQuantity(main[0]);
|
|
1614
2799
|
result.push({
|
|
1615
|
-
|
|
1616
|
-
entries: [resultMain, ...equivalents]
|
|
2800
|
+
or: [resultMain, ...equivalents]
|
|
1617
2801
|
});
|
|
1618
2802
|
} else {
|
|
1619
2803
|
result.push(deNormalizeQuantity(main[0]));
|
|
@@ -1622,21 +2806,66 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1622
2806
|
sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
|
|
1623
2807
|
return result;
|
|
1624
2808
|
}
|
|
1625
|
-
function addEquivalentsAndSimplify(
|
|
2809
|
+
function addEquivalentsAndSimplify(quantities, system) {
|
|
1626
2810
|
if (quantities.length === 1) {
|
|
1627
2811
|
return toPlainUnit(quantities[0]);
|
|
1628
2812
|
}
|
|
1629
|
-
const { sum, unitsLists } = addQuantitiesOrGroups(
|
|
1630
|
-
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2813
|
+
const { sum, unitsLists } = addQuantitiesOrGroups(quantities, system);
|
|
2814
|
+
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2815
|
+
sum,
|
|
2816
|
+
unitsLists,
|
|
2817
|
+
system
|
|
2818
|
+
);
|
|
1631
2819
|
if (regrouped.length === 1) {
|
|
1632
2820
|
return toPlainUnit(regrouped[0]);
|
|
1633
2821
|
} else {
|
|
1634
|
-
return {
|
|
2822
|
+
return { and: regrouped.map(toPlainUnit) };
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function buildEquivalenceRatioMap(unitsLists) {
|
|
2826
|
+
const ratioMap = {};
|
|
2827
|
+
for (const list of unitsLists) {
|
|
2828
|
+
for (const equiv of list) {
|
|
2829
|
+
const equivValue = getAverageValue(equiv.quantity);
|
|
2830
|
+
for (const primary of list) {
|
|
2831
|
+
if (primary === equiv) continue;
|
|
2832
|
+
const primaryValue = getAverageValue(primary.quantity);
|
|
2833
|
+
const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
|
|
2834
|
+
const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
|
|
2835
|
+
ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
|
|
2836
|
+
ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
return ratioMap;
|
|
2841
|
+
}
|
|
2842
|
+
function recomputeEquivalents(primaries, ratioMap, equivUnits) {
|
|
2843
|
+
const equivalents = [];
|
|
2844
|
+
for (const equivUnit of equivUnits) {
|
|
2845
|
+
const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
|
|
2846
|
+
let total = 0;
|
|
2847
|
+
for (const primary of primaries) {
|
|
2848
|
+
const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
|
|
2849
|
+
const ratio = ratios[pUnit];
|
|
2850
|
+
if (ratio === void 0) continue;
|
|
2851
|
+
const pValue = getAverageValue(primary.quantity);
|
|
2852
|
+
total += pValue * ratio;
|
|
2853
|
+
}
|
|
2854
|
+
if (total > 0) {
|
|
2855
|
+
equivalents.push({
|
|
2856
|
+
quantity: {
|
|
2857
|
+
type: "fixed",
|
|
2858
|
+
value: { type: "decimal", decimal: total }
|
|
2859
|
+
},
|
|
2860
|
+
...equivUnit !== "" && { unit: equivUnit }
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
1635
2863
|
}
|
|
2864
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
1636
2865
|
}
|
|
1637
2866
|
|
|
1638
2867
|
// src/classes/recipe.ts
|
|
1639
|
-
var
|
|
2868
|
+
var import_big5 = __toESM(require("big.js"), 1);
|
|
1640
2869
|
var _Recipe = class _Recipe {
|
|
1641
2870
|
/**
|
|
1642
2871
|
* Creates a new Recipe instance.
|
|
@@ -1648,12 +2877,12 @@ var _Recipe = class _Recipe {
|
|
|
1648
2877
|
*/
|
|
1649
2878
|
__publicField(this, "metadata", {});
|
|
1650
2879
|
/**
|
|
1651
|
-
* The
|
|
1652
|
-
* Contains the full context including alternatives list and active selection index.
|
|
2880
|
+
* The possible choices of alternative ingredients for this recipe.
|
|
1653
2881
|
*/
|
|
1654
2882
|
__publicField(this, "choices", {
|
|
1655
2883
|
ingredientItems: /* @__PURE__ */ new Map(),
|
|
1656
|
-
ingredientGroups: /* @__PURE__ */ new Map()
|
|
2884
|
+
ingredientGroups: /* @__PURE__ */ new Map(),
|
|
2885
|
+
variants: []
|
|
1657
2886
|
});
|
|
1658
2887
|
/**
|
|
1659
2888
|
* The parsed recipe ingredients.
|
|
@@ -1684,10 +2913,20 @@ var _Recipe = class _Recipe {
|
|
|
1684
2913
|
*/
|
|
1685
2914
|
__publicField(this, "servings");
|
|
1686
2915
|
_Recipe.itemCounts.set(this, 0);
|
|
2916
|
+
_Recipe.subgroupIndices.set(this, /* @__PURE__ */ new Map());
|
|
1687
2917
|
if (content) {
|
|
1688
2918
|
this.parse(content);
|
|
1689
2919
|
}
|
|
1690
2920
|
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Gets the unit system specified in the recipe metadata.
|
|
2923
|
+
* Used for resolving ambiguous units like tsp, tbsp, cup, etc.
|
|
2924
|
+
*
|
|
2925
|
+
* @returns The unit system if specified, or undefined to use defaults
|
|
2926
|
+
*/
|
|
2927
|
+
get unitSystem() {
|
|
2928
|
+
return _Recipe.unitSystems.get(this);
|
|
2929
|
+
}
|
|
1691
2930
|
/**
|
|
1692
2931
|
* Gets the current item count for this recipe.
|
|
1693
2932
|
*/
|
|
@@ -1710,27 +2949,17 @@ var _Recipe = class _Recipe {
|
|
|
1710
2949
|
*/
|
|
1711
2950
|
_parseArbitraryScalable(regexMatchGroups, intoArray) {
|
|
1712
2951
|
if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
const arbitrary = {
|
|
1725
|
-
quantity: value
|
|
1726
|
-
};
|
|
1727
|
-
if (name) arbitrary.name = name;
|
|
1728
|
-
if (unit) arbitrary.unit = unit;
|
|
1729
|
-
intoArray.push({
|
|
1730
|
-
type: "arbitrary",
|
|
1731
|
-
index: this.arbitraries.push(arbitrary) - 1
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
2952
|
+
const parsed = parseArbitraryQuantity(regexMatchGroups.arbitraryQuantity);
|
|
2953
|
+
const name = regexMatchGroups.arbitraryName || void 0;
|
|
2954
|
+
const arbitrary = {
|
|
2955
|
+
quantity: parsed.quantity
|
|
2956
|
+
};
|
|
2957
|
+
if (name) arbitrary.name = name;
|
|
2958
|
+
if (parsed.unit) arbitrary.unit = parsed.unit;
|
|
2959
|
+
intoArray.push({
|
|
2960
|
+
type: "arbitrary",
|
|
2961
|
+
index: this.arbitraries.push(arbitrary) - 1
|
|
2962
|
+
});
|
|
1734
2963
|
}
|
|
1735
2964
|
/**
|
|
1736
2965
|
* Parses text for arbitrary scalables and returns NoteItem array.
|
|
@@ -1744,13 +2973,13 @@ var _Recipe = class _Recipe {
|
|
|
1744
2973
|
for (const match of text.matchAll(globalRegex)) {
|
|
1745
2974
|
const idx = match.index;
|
|
1746
2975
|
if (idx > cursor) {
|
|
1747
|
-
noteItems.push(
|
|
2976
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor, idx)));
|
|
1748
2977
|
}
|
|
1749
2978
|
this._parseArbitraryScalable(match.groups, noteItems);
|
|
1750
2979
|
cursor = idx + match[0].length;
|
|
1751
2980
|
}
|
|
1752
2981
|
if (cursor < text.length) {
|
|
1753
|
-
noteItems.push(
|
|
2982
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor)));
|
|
1754
2983
|
}
|
|
1755
2984
|
return noteItems;
|
|
1756
2985
|
}
|
|
@@ -1758,7 +2987,7 @@ var _Recipe = class _Recipe {
|
|
|
1758
2987
|
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
1759
2988
|
const quantities = [];
|
|
1760
2989
|
while (quantityMatch?.groups) {
|
|
1761
|
-
const value = quantityMatch.groups.quantity ?
|
|
2990
|
+
const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
|
|
1762
2991
|
const unit = quantityMatch.groups.unit;
|
|
1763
2992
|
if (value) {
|
|
1764
2993
|
const newQuantity = { quantity: value };
|
|
@@ -1859,7 +3088,7 @@ var _Recipe = class _Recipe {
|
|
|
1859
3088
|
alternative.note = note;
|
|
1860
3089
|
}
|
|
1861
3090
|
if (itemQuantity) {
|
|
1862
|
-
alternative
|
|
3091
|
+
Object.assign(alternative, itemQuantity);
|
|
1863
3092
|
}
|
|
1864
3093
|
alternatives.push(alternative);
|
|
1865
3094
|
testString = groups.ingredientAlternative || "";
|
|
@@ -1902,6 +3131,7 @@ var _Recipe = class _Recipe {
|
|
|
1902
3131
|
if (!match?.groups) return;
|
|
1903
3132
|
const groups = match.groups;
|
|
1904
3133
|
const groupKey = groups.gIngredientGroupKey;
|
|
3134
|
+
const subgroupKey = groups.gIngredientSubgroupKey;
|
|
1905
3135
|
let name = groups.gmIngredientName || groups.gsIngredientName;
|
|
1906
3136
|
const preparation = groups.gIngredientPreparation;
|
|
1907
3137
|
const modifiers = groups.gIngredientModifiers;
|
|
@@ -1967,9 +3197,14 @@ var _Recipe = class _Recipe {
|
|
|
1967
3197
|
displayName
|
|
1968
3198
|
};
|
|
1969
3199
|
if (itemQuantity) {
|
|
1970
|
-
alternative
|
|
3200
|
+
Object.assign(alternative, itemQuantity);
|
|
1971
3201
|
}
|
|
1972
|
-
const
|
|
3202
|
+
const note = groups.gIngredientNote?.trim();
|
|
3203
|
+
if (note) {
|
|
3204
|
+
alternative.note = note;
|
|
3205
|
+
}
|
|
3206
|
+
const existingSubgroups = this.choices.ingredientGroups.get(groupKey);
|
|
3207
|
+
const existingAlternativesFlat = existingSubgroups?.flat();
|
|
1973
3208
|
function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
|
|
1974
3209
|
const ingredient = ingredients[ingredientIdx];
|
|
1975
3210
|
if (ingredient) {
|
|
@@ -1980,8 +3215,8 @@ var _Recipe = class _Recipe {
|
|
|
1980
3215
|
}
|
|
1981
3216
|
}
|
|
1982
3217
|
}
|
|
1983
|
-
if (
|
|
1984
|
-
for (const alt of
|
|
3218
|
+
if (existingAlternativesFlat) {
|
|
3219
|
+
for (const alt of existingAlternativesFlat) {
|
|
1985
3220
|
upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
|
|
1986
3221
|
upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
|
|
1987
3222
|
}
|
|
@@ -1993,14 +3228,35 @@ var _Recipe = class _Recipe {
|
|
|
1993
3228
|
group: groupKey,
|
|
1994
3229
|
alternatives: [alternative]
|
|
1995
3230
|
};
|
|
3231
|
+
if (subgroupKey !== void 0) {
|
|
3232
|
+
newItem.subgroup = subgroupKey;
|
|
3233
|
+
}
|
|
1996
3234
|
items.push(newItem);
|
|
1997
3235
|
const choiceAlternative = deepClone(alternative);
|
|
1998
3236
|
choiceAlternative.itemId = id;
|
|
1999
3237
|
const existingChoice = this.choices.ingredientGroups.get(groupKey);
|
|
3238
|
+
const sgMap = _Recipe.subgroupIndices.get(this);
|
|
2000
3239
|
if (!existingChoice) {
|
|
2001
|
-
this.choices.ingredientGroups.set(groupKey, [choiceAlternative]);
|
|
3240
|
+
this.choices.ingredientGroups.set(groupKey, [[choiceAlternative]]);
|
|
3241
|
+
if (subgroupKey !== void 0) {
|
|
3242
|
+
sgMap.set(groupKey, /* @__PURE__ */ new Map([[subgroupKey, 0]]));
|
|
3243
|
+
}
|
|
3244
|
+
} else if (subgroupKey !== void 0) {
|
|
3245
|
+
const groupSgMap = sgMap.get(groupKey);
|
|
3246
|
+
const existingIdx = groupSgMap?.get(subgroupKey);
|
|
3247
|
+
if (existingIdx !== void 0) {
|
|
3248
|
+
existingChoice[existingIdx].push(choiceAlternative);
|
|
3249
|
+
} else {
|
|
3250
|
+
const newIdx = existingChoice.length;
|
|
3251
|
+
existingChoice.push([choiceAlternative]);
|
|
3252
|
+
if (!groupSgMap) {
|
|
3253
|
+
sgMap.set(groupKey, /* @__PURE__ */ new Map([[subgroupKey, newIdx]]));
|
|
3254
|
+
} else {
|
|
3255
|
+
groupSgMap.set(subgroupKey, newIdx);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
2002
3258
|
} else {
|
|
2003
|
-
existingChoice.push(choiceAlternative);
|
|
3259
|
+
existingChoice.push([choiceAlternative]);
|
|
2004
3260
|
}
|
|
2005
3261
|
}
|
|
2006
3262
|
/**
|
|
@@ -2014,142 +3270,230 @@ var _Recipe = class _Recipe {
|
|
|
2014
3270
|
* Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
|
|
2015
3271
|
* @internal
|
|
2016
3272
|
*/
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
3273
|
+
_populateIngredientQuantities() {
|
|
3274
|
+
for (const ing of this.ingredients) {
|
|
3275
|
+
delete ing.quantities;
|
|
3276
|
+
delete ing.usedAsPrimary;
|
|
3277
|
+
}
|
|
3278
|
+
const ingredientsWithQuantities = this.getIngredientQuantities();
|
|
3279
|
+
const matchedIndices = /* @__PURE__ */ new Set();
|
|
3280
|
+
for (const computed of ingredientsWithQuantities) {
|
|
3281
|
+
const idx = this.ingredients.findIndex(
|
|
3282
|
+
(ing2, i2) => ing2.name === computed.name && !matchedIndices.has(i2)
|
|
3283
|
+
);
|
|
3284
|
+
matchedIndices.add(idx);
|
|
3285
|
+
const ing = this.ingredients[idx];
|
|
3286
|
+
if (computed.quantities) {
|
|
3287
|
+
ing.quantities = computed.quantities;
|
|
2021
3288
|
}
|
|
2022
|
-
if (
|
|
2023
|
-
|
|
3289
|
+
if (computed.usedAsPrimary) {
|
|
3290
|
+
ing.usedAsPrimary = true;
|
|
2024
3291
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
// Type for accumulated quantities (used internally by collectQuantityGroups)
|
|
3295
|
+
// Defined as a static type alias for the private method's return type
|
|
3296
|
+
/** @internal */
|
|
3297
|
+
collectQuantityGroups(options) {
|
|
3298
|
+
const { section, step, choices } = options || {};
|
|
3299
|
+
const activeVariant = choices?.variant;
|
|
3300
|
+
const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
|
|
3301
|
+
const sectionsToProcess = section !== void 0 ? (() => {
|
|
3302
|
+
const idx = typeof section === "number" ? section : this.sections.indexOf(section);
|
|
3303
|
+
return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
|
|
3304
|
+
})() : this.sections;
|
|
2028
3305
|
const ingredientGroups = /* @__PURE__ */ new Map();
|
|
2029
|
-
|
|
2030
|
-
|
|
3306
|
+
const selectedIndices = /* @__PURE__ */ new Set();
|
|
3307
|
+
const referencedIndices = /* @__PURE__ */ new Set();
|
|
3308
|
+
const dynamicOptionalIndices = /* @__PURE__ */ new Set();
|
|
3309
|
+
for (const currentSection of sectionsToProcess) {
|
|
3310
|
+
if (currentSection.variants) {
|
|
3311
|
+
if (isDefaultVariant) {
|
|
3312
|
+
if (!currentSection.variants.includes("*")) continue;
|
|
3313
|
+
} else {
|
|
3314
|
+
if (!currentSection.variants.includes(activeVariant)) continue;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
const allSteps = currentSection.content.filter(
|
|
2031
3318
|
(item) => item.type === "step"
|
|
2032
|
-
)
|
|
2033
|
-
|
|
3319
|
+
);
|
|
3320
|
+
const isOptionalSection = currentSection.optional === true;
|
|
3321
|
+
const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
|
|
3322
|
+
for (const currentStep of stepsToProcess) {
|
|
3323
|
+
if (currentStep.variants) {
|
|
3324
|
+
if (isDefaultVariant) {
|
|
3325
|
+
if (!currentStep.variants.includes("*")) continue;
|
|
3326
|
+
} else {
|
|
3327
|
+
if (!currentStep.variants.includes(activeVariant)) continue;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
const isOptionalStep = currentStep.optional === true || isOptionalSection;
|
|
3331
|
+
for (const item of currentStep.items.filter(
|
|
2034
3332
|
(item2) => item2.type === "ingredient"
|
|
2035
3333
|
)) {
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
3334
|
+
const isGrouped = "group" in item && item.group !== void 0;
|
|
3335
|
+
const groupSubgroups = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
|
|
3336
|
+
let selectedAltIndex = 0;
|
|
3337
|
+
let isSelected;
|
|
3338
|
+
let hasExplicitChoice;
|
|
3339
|
+
if (isGrouped) {
|
|
3340
|
+
const groupChoice = choices?.ingredientGroups?.get(item.group);
|
|
3341
|
+
hasExplicitChoice = groupChoice !== void 0;
|
|
3342
|
+
if (!hasExplicitChoice && !isDefaultVariant) {
|
|
3343
|
+
const matchingSubgroupIdx = groupSubgroups?.findIndex(
|
|
3344
|
+
(sg) => sg.some(
|
|
3345
|
+
(alt) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
|
|
3346
|
+
)
|
|
3347
|
+
);
|
|
3348
|
+
if (matchingSubgroupIdx !== void 0 && matchingSubgroupIdx >= 0) {
|
|
3349
|
+
const matchedSubgroup = groupSubgroups[matchingSubgroupIdx];
|
|
3350
|
+
isSelected = matchedSubgroup.some(
|
|
3351
|
+
(alt) => alt.itemId === item.id
|
|
3352
|
+
);
|
|
3353
|
+
hasExplicitChoice = true;
|
|
3354
|
+
selectedAltIndex = 0;
|
|
3355
|
+
} else {
|
|
3356
|
+
const targetSubgroupIndex = 0;
|
|
3357
|
+
const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
|
|
3358
|
+
isSelected = selectedSubgroup.some(
|
|
3359
|
+
(alt) => alt.itemId === item.id
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
} else {
|
|
3363
|
+
const targetSubgroupIndex = groupChoice ?? 0;
|
|
3364
|
+
const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
|
|
3365
|
+
isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
|
|
2047
3366
|
}
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
{
|
|
2052
|
-
|
|
2053
|
-
|
|
3367
|
+
} else {
|
|
3368
|
+
const itemChoice = choices?.ingredientItems?.get(item.id);
|
|
3369
|
+
hasExplicitChoice = itemChoice !== void 0;
|
|
3370
|
+
if (!hasExplicitChoice && !isDefaultVariant) {
|
|
3371
|
+
const matchingIndices = item.alternatives.map((alt, idx) => ({ alt, idx })).filter(
|
|
3372
|
+
({ alt }) => alt.note && alt.note.toLowerCase().includes(activeVariant.toLowerCase())
|
|
3373
|
+
).map(({ idx }) => idx);
|
|
3374
|
+
if (matchingIndices.length > 0) {
|
|
3375
|
+
selectedAltIndex = matchingIndices[0];
|
|
3376
|
+
hasExplicitChoice = true;
|
|
3377
|
+
} else {
|
|
3378
|
+
selectedAltIndex = itemChoice ?? 0;
|
|
3379
|
+
}
|
|
3380
|
+
} else {
|
|
3381
|
+
selectedAltIndex = itemChoice ?? 0;
|
|
2054
3382
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
3383
|
+
isSelected = true;
|
|
3384
|
+
}
|
|
3385
|
+
const alternative = item.alternatives[selectedAltIndex];
|
|
3386
|
+
if (!alternative || !isSelected) continue;
|
|
3387
|
+
selectedIndices.add(alternative.index);
|
|
3388
|
+
if (isOptionalStep) {
|
|
3389
|
+
dynamicOptionalIndices.add(alternative.index);
|
|
2058
3390
|
}
|
|
2059
|
-
const
|
|
2060
|
-
const
|
|
2061
|
-
|
|
3391
|
+
const allAltsFlat = isGrouped ? groupSubgroups.flat() : item.alternatives;
|
|
3392
|
+
for (const alt of allAltsFlat) {
|
|
3393
|
+
referencedIndices.add(alt.index);
|
|
3394
|
+
}
|
|
3395
|
+
if (!alternative.quantity) continue;
|
|
3396
|
+
const baseQty = {
|
|
3397
|
+
quantity: alternative.quantity,
|
|
3398
|
+
...alternative.unit && {
|
|
3399
|
+
unit: alternative.unit
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
const quantityEntry = alternative.equivalents?.length ? { or: [baseQty, ...alternative.equivalents] } : baseQty;
|
|
2062
3403
|
let alternativeRefs;
|
|
2063
|
-
if (
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
const altQty = {
|
|
2072
|
-
quantity: otherAlt.itemQuantity.quantity
|
|
3404
|
+
if (!hasExplicitChoice && groupSubgroups && groupSubgroups.length > 1) {
|
|
3405
|
+
const currentSubgroupIdx = groupSubgroups.findIndex(
|
|
3406
|
+
(sg) => sg.some((alt) => alt.itemId === item.id)
|
|
3407
|
+
);
|
|
3408
|
+
alternativeRefs = groupSubgroups.filter((_, idx) => idx !== currentSubgroupIdx).map(
|
|
3409
|
+
(subgroup) => subgroup.map((otherAlt) => {
|
|
3410
|
+
const ref = {
|
|
3411
|
+
index: otherAlt.index
|
|
2073
3412
|
};
|
|
2074
|
-
if (otherAlt.
|
|
2075
|
-
altQty
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
3413
|
+
if (otherAlt.quantity) {
|
|
3414
|
+
const altQty = {
|
|
3415
|
+
quantity: otherAlt.quantity,
|
|
3416
|
+
...otherAlt.unit && {
|
|
3417
|
+
unit: otherAlt.unit.name
|
|
3418
|
+
},
|
|
3419
|
+
...otherAlt.equivalents && {
|
|
3420
|
+
equivalents: otherAlt.equivalents.map(
|
|
3421
|
+
(eq) => toPlainUnit(eq)
|
|
3422
|
+
)
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
ref.quantities = [altQty];
|
|
2081
3426
|
}
|
|
2082
|
-
|
|
2083
|
-
}
|
|
2084
|
-
alternativeRefs.push(newRef);
|
|
2085
|
-
}
|
|
2086
|
-
} else if (hasGroupedAlternatives) {
|
|
2087
|
-
const groupAlternatives = this.choices.ingredientGroups.get(
|
|
2088
|
-
item.group
|
|
3427
|
+
return ref;
|
|
3428
|
+
})
|
|
2089
3429
|
);
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const
|
|
2093
|
-
if (otherAlt.
|
|
3430
|
+
} else if (!hasExplicitChoice && !isGrouped && allAltsFlat.length > 1) {
|
|
3431
|
+
alternativeRefs = allAltsFlat.filter((alt) => alt.index !== alternative.index).map((otherAlt) => {
|
|
3432
|
+
const ref = { index: otherAlt.index };
|
|
3433
|
+
if (otherAlt.quantity) {
|
|
2094
3434
|
const altQty = {
|
|
2095
|
-
quantity: otherAlt.
|
|
3435
|
+
quantity: otherAlt.quantity,
|
|
3436
|
+
...otherAlt.unit && {
|
|
3437
|
+
unit: otherAlt.unit.name
|
|
3438
|
+
},
|
|
3439
|
+
...otherAlt.equivalents && {
|
|
3440
|
+
equivalents: otherAlt.equivalents.map(
|
|
3441
|
+
(eq) => toPlainUnit(eq)
|
|
3442
|
+
)
|
|
3443
|
+
}
|
|
2096
3444
|
};
|
|
2097
|
-
|
|
2098
|
-
altQty.unit = otherAlt.itemQuantity.unit.name;
|
|
2099
|
-
}
|
|
2100
|
-
if (otherAlt.itemQuantity.equivalents) {
|
|
2101
|
-
altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
|
|
2102
|
-
(eq) => toPlainUnit(eq)
|
|
2103
|
-
);
|
|
2104
|
-
}
|
|
2105
|
-
alternativeRefs.push({
|
|
2106
|
-
index: otherAlt.index,
|
|
2107
|
-
quantities: [altQty]
|
|
2108
|
-
});
|
|
3445
|
+
ref.quantities = [altQty];
|
|
2109
3446
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
3447
|
+
return [ref];
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
const altIndices = getAlternativeSignature(alternativeRefs) ?? "";
|
|
3451
|
+
let signature;
|
|
3452
|
+
if (isGrouped) {
|
|
3453
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
3454
|
+
signature = `group:${item.group}|${altIndices}|${resolvedUnit.type}`;
|
|
3455
|
+
} else if (altIndices) {
|
|
3456
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
3457
|
+
signature = `${altIndices}|${resolvedUnit.type}}`;
|
|
3458
|
+
} else {
|
|
3459
|
+
signature = null;
|
|
2114
3460
|
}
|
|
2115
3461
|
if (!ingredientGroups.has(alternative.index)) {
|
|
2116
3462
|
ingredientGroups.set(alternative.index, /* @__PURE__ */ new Map());
|
|
2117
3463
|
}
|
|
2118
|
-
const
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
groupsForIngredient.set(signature, {
|
|
3464
|
+
const groupsForIng = ingredientGroups.get(alternative.index);
|
|
3465
|
+
if (!groupsForIng.has(signature)) {
|
|
3466
|
+
groupsForIng.set(signature, {
|
|
3467
|
+
quantities: [],
|
|
2123
3468
|
alternativeQuantities: /* @__PURE__ */ new Map(),
|
|
2124
|
-
|
|
3469
|
+
alternativeSubgroups: []
|
|
2125
3470
|
});
|
|
2126
3471
|
}
|
|
2127
|
-
const group =
|
|
3472
|
+
const group = groupsForIng.get(signature);
|
|
2128
3473
|
group.quantities.push(quantityEntry);
|
|
2129
|
-
if (alternativeRefs) {
|
|
2130
|
-
|
|
3474
|
+
if (alternativeRefs && alternativeRefs.length > 0 && group.alternativeSubgroups.length === 0) {
|
|
3475
|
+
group.alternativeSubgroups = alternativeRefs.map(
|
|
3476
|
+
(subgroup) => subgroup.map((ref) => ref.index)
|
|
3477
|
+
);
|
|
3478
|
+
}
|
|
3479
|
+
for (const subgroup of alternativeRefs ?? []) {
|
|
3480
|
+
for (const ref of subgroup) {
|
|
2131
3481
|
if (!group.alternativeQuantities.has(ref.index)) {
|
|
2132
3482
|
group.alternativeQuantities.set(ref.index, []);
|
|
2133
3483
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
toExtendedUnit({
|
|
2148
|
-
quantity: altQty.quantity,
|
|
2149
|
-
unit: altQty.unit
|
|
2150
|
-
})
|
|
2151
|
-
);
|
|
2152
|
-
}
|
|
3484
|
+
for (const altQty of ref.quantities ?? []) {
|
|
3485
|
+
const extended = toExtendedUnit({
|
|
3486
|
+
quantity: altQty.quantity,
|
|
3487
|
+
unit: altQty.unit
|
|
3488
|
+
});
|
|
3489
|
+
if (altQty.equivalents?.length) {
|
|
3490
|
+
const eqEntries = [
|
|
3491
|
+
extended,
|
|
3492
|
+
...altQty.equivalents.map((eq) => toExtendedUnit(eq))
|
|
3493
|
+
];
|
|
3494
|
+
group.alternativeQuantities.get(ref.index).push({ or: eqEntries });
|
|
3495
|
+
} else {
|
|
3496
|
+
group.alternativeQuantities.get(ref.index).push(extended);
|
|
2153
3497
|
}
|
|
2154
3498
|
}
|
|
2155
3499
|
}
|
|
@@ -2157,144 +3501,219 @@ var _Recipe = class _Recipe {
|
|
|
2157
3501
|
}
|
|
2158
3502
|
}
|
|
2159
3503
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
quantityGroup.alternatives = alternatives;
|
|
2206
|
-
}
|
|
2207
|
-
quantityGroups.push(quantityGroup);
|
|
3504
|
+
return {
|
|
3505
|
+
ingredientGroups,
|
|
3506
|
+
selectedIndices,
|
|
3507
|
+
referencedIndices,
|
|
3508
|
+
dynamicOptionalIndices
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Gets the raw (unprocessed) quantity groups for each ingredient, before
|
|
3513
|
+
* any summation or equivalents simplification. This is useful for cross-recipe
|
|
3514
|
+
* aggregation (e.g., in {@link ShoppingList}), where quantities from multiple
|
|
3515
|
+
* recipes should be combined before processing.
|
|
3516
|
+
*
|
|
3517
|
+
* @param options - Options for filtering and choice selection (same as {@link getIngredientQuantities}).
|
|
3518
|
+
* @returns Array of {@link RawQuantityGroup} objects, one per ingredient with quantities.
|
|
3519
|
+
*
|
|
3520
|
+
* @example
|
|
3521
|
+
* ```typescript
|
|
3522
|
+
* const rawGroups = recipe.getRawQuantityGroups();
|
|
3523
|
+
* // Each group has: name, usedAsPrimary, flags, quantities[]
|
|
3524
|
+
* // quantities are the raw QuantityWithExtendedUnit or FlatOrGroup entries
|
|
3525
|
+
* ```
|
|
3526
|
+
*/
|
|
3527
|
+
getRawQuantityGroups(options) {
|
|
3528
|
+
const {
|
|
3529
|
+
ingredientGroups,
|
|
3530
|
+
selectedIndices,
|
|
3531
|
+
referencedIndices,
|
|
3532
|
+
dynamicOptionalIndices
|
|
3533
|
+
} = this.collectQuantityGroups(options);
|
|
3534
|
+
const result = [];
|
|
3535
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
3536
|
+
if (!referencedIndices.has(index)) continue;
|
|
3537
|
+
const orig = this.ingredients[index];
|
|
3538
|
+
const usedAsPrimary = selectedIndices.has(index);
|
|
3539
|
+
let flags = orig.flags;
|
|
3540
|
+
if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
|
|
3541
|
+
flags = [...flags ?? [], "optional"];
|
|
3542
|
+
}
|
|
3543
|
+
const quantities = [];
|
|
3544
|
+
if (usedAsPrimary) {
|
|
3545
|
+
const groupsForIng = ingredientGroups.get(index);
|
|
3546
|
+
if (groupsForIng) {
|
|
3547
|
+
for (const [, group] of groupsForIng) {
|
|
3548
|
+
quantities.push(...group.quantities);
|
|
2208
3549
|
}
|
|
2209
3550
|
}
|
|
2210
3551
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
3552
|
+
result.push({
|
|
3553
|
+
name: orig.name,
|
|
3554
|
+
...usedAsPrimary && { usedAsPrimary: true },
|
|
3555
|
+
...flags && { flags },
|
|
3556
|
+
quantities
|
|
3557
|
+
});
|
|
2214
3558
|
}
|
|
3559
|
+
return result;
|
|
2215
3560
|
}
|
|
2216
3561
|
/**
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
3562
|
+
* Gets ingredients with their quantities populated, optionally filtered by section/step
|
|
3563
|
+
* and respecting user choices for alternatives.
|
|
3564
|
+
*
|
|
3565
|
+
* When no options are provided, returns all recipe ingredients with quantities
|
|
3566
|
+
* calculated using primary alternatives (same as after parsing).
|
|
3567
|
+
*
|
|
3568
|
+
* @param options - Options for filtering and choice selection:
|
|
3569
|
+
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
3570
|
+
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
3571
|
+
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
3572
|
+
* @returns Array of Ingredient objects with quantities populated
|
|
3573
|
+
*
|
|
3574
|
+
* @example
|
|
3575
|
+
* ```typescript
|
|
3576
|
+
* // Get all ingredients with primary alternatives
|
|
3577
|
+
* const ingredients = recipe.getIngredientQuantities();
|
|
2219
3578
|
*
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
2222
|
-
*
|
|
3579
|
+
* // Get ingredients for a specific section
|
|
3580
|
+
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
3581
|
+
*
|
|
3582
|
+
* // Get ingredients with specific choices applied
|
|
3583
|
+
* const withChoices = recipe.getIngredientQuantities({
|
|
3584
|
+
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
3585
|
+
* });
|
|
3586
|
+
* ```
|
|
2223
3587
|
*/
|
|
2224
|
-
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
3588
|
+
getIngredientQuantities(options) {
|
|
3589
|
+
const {
|
|
3590
|
+
ingredientGroups,
|
|
3591
|
+
selectedIndices,
|
|
3592
|
+
referencedIndices,
|
|
3593
|
+
dynamicOptionalIndices
|
|
3594
|
+
} = this.collectQuantityGroups(options);
|
|
3595
|
+
const result = [];
|
|
3596
|
+
for (let index = 0; index < this.ingredients.length; index++) {
|
|
3597
|
+
if (!referencedIndices.has(index)) continue;
|
|
3598
|
+
const orig = this.ingredients[index];
|
|
3599
|
+
let flags = orig.flags;
|
|
3600
|
+
if (dynamicOptionalIndices.has(index) && !flags?.includes("optional")) {
|
|
3601
|
+
flags = [...flags ?? [], "optional"];
|
|
3602
|
+
}
|
|
3603
|
+
const ing = {
|
|
3604
|
+
name: orig.name,
|
|
3605
|
+
...orig.preparation && { preparation: orig.preparation },
|
|
3606
|
+
...flags && { flags },
|
|
3607
|
+
...orig.extras && { extras: orig.extras }
|
|
3608
|
+
};
|
|
3609
|
+
if (selectedIndices.has(index)) {
|
|
3610
|
+
ing.usedAsPrimary = true;
|
|
3611
|
+
const groupsForIng = ingredientGroups.get(index);
|
|
3612
|
+
if (groupsForIng) {
|
|
3613
|
+
const quantityGroups = [];
|
|
3614
|
+
for (const [, group] of groupsForIng) {
|
|
3615
|
+
const summed = addEquivalentsAndSimplify(
|
|
3616
|
+
group.quantities,
|
|
3617
|
+
this.unitSystem
|
|
3618
|
+
);
|
|
3619
|
+
const flattened = flattenPlainUnitGroup(summed);
|
|
3620
|
+
let alternatives;
|
|
3621
|
+
if (group.alternativeSubgroups.length > 0) {
|
|
3622
|
+
alternatives = group.alternativeSubgroups.map(
|
|
3623
|
+
(subgroupIndices) => subgroupIndices.map((altIdx) => {
|
|
3624
|
+
const altQtys = group.alternativeQuantities.get(altIdx);
|
|
3625
|
+
return {
|
|
3626
|
+
index: altIdx,
|
|
3627
|
+
...altQtys.length > 0 && {
|
|
3628
|
+
quantities: flattenPlainUnitGroup(
|
|
3629
|
+
addEquivalentsAndSimplify(altQtys, this.unitSystem)
|
|
3630
|
+
).flatMap(
|
|
3631
|
+
/* v8 ignore next -- item.and branch requires complex nested AND-with-equivalents structure */
|
|
3632
|
+
(item) => "quantity" in item ? [item] : item.and
|
|
3633
|
+
)
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
})
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
for (const gq of flattened) {
|
|
3640
|
+
if ("and" in gq) {
|
|
3641
|
+
quantityGroups.push({
|
|
3642
|
+
and: gq.and,
|
|
3643
|
+
...gq.equivalents?.length && {
|
|
3644
|
+
equivalents: gq.equivalents
|
|
3645
|
+
},
|
|
3646
|
+
...alternatives?.length && { alternatives }
|
|
3647
|
+
});
|
|
3648
|
+
} else {
|
|
3649
|
+
quantityGroups.push({
|
|
3650
|
+
...gq,
|
|
3651
|
+
...alternatives?.length && { alternatives }
|
|
3652
|
+
});
|
|
2269
3653
|
}
|
|
2270
3654
|
}
|
|
2271
3655
|
}
|
|
3656
|
+
if (quantityGroups.length > 0) {
|
|
3657
|
+
ing.quantities = quantityGroups;
|
|
3658
|
+
}
|
|
2272
3659
|
}
|
|
2273
3660
|
}
|
|
3661
|
+
result.push(ing);
|
|
2274
3662
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
3663
|
+
return result;
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Returns the list of cookware items that are used in the active variant.
|
|
3667
|
+
* Cookware in steps/sections not matching the active variant are excluded.
|
|
3668
|
+
* Hidden cookware is always excluded.
|
|
3669
|
+
*
|
|
3670
|
+
* @param options - Options for filtering:
|
|
3671
|
+
* - `choices`: The choices to apply (only `variant` is used)
|
|
3672
|
+
* @returns Array of Cookware objects referenced by active steps
|
|
3673
|
+
*
|
|
3674
|
+
* @example
|
|
3675
|
+
* ```typescript
|
|
3676
|
+
* // Get all cookware for the default variant
|
|
3677
|
+
* const cookware = recipe.getCookwareForVariant();
|
|
3678
|
+
*
|
|
3679
|
+
* // Get cookware for a specific variant
|
|
3680
|
+
* const veganCookware = recipe.getCookwareForVariant({ choices: { variant: 'vegan' } });
|
|
3681
|
+
* ```
|
|
3682
|
+
*/
|
|
3683
|
+
getCookwareForVariant(options) {
|
|
3684
|
+
const { choices } = options || {};
|
|
3685
|
+
const activeVariant = choices?.variant;
|
|
3686
|
+
const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
|
|
3687
|
+
const cookwareIndices = /* @__PURE__ */ new Set();
|
|
3688
|
+
for (const currentSection of this.sections) {
|
|
3689
|
+
if (currentSection.variants) {
|
|
3690
|
+
if (isDefaultVariant) {
|
|
3691
|
+
if (!currentSection.variants.includes("*")) continue;
|
|
3692
|
+
} else {
|
|
3693
|
+
if (!currentSection.variants.includes(activeVariant)) continue;
|
|
3694
|
+
}
|
|
2290
3695
|
}
|
|
2291
|
-
const
|
|
2292
|
-
|
|
2293
|
-
|
|
3696
|
+
const allSteps = currentSection.content.filter(
|
|
3697
|
+
(item) => item.type === "step"
|
|
3698
|
+
);
|
|
3699
|
+
for (const currentStep of allSteps) {
|
|
3700
|
+
if (currentStep.variants) {
|
|
3701
|
+
if (isDefaultVariant) {
|
|
3702
|
+
if (!currentStep.variants.includes("*")) continue;
|
|
3703
|
+
} else {
|
|
3704
|
+
if (!currentStep.variants.includes(activeVariant)) continue;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
for (const item of currentStep.items) {
|
|
3708
|
+
if (item.type === "cookware") {
|
|
3709
|
+
cookwareIndices.add(item.index);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
2294
3712
|
}
|
|
2295
|
-
computedIngredients.push(computed);
|
|
2296
3713
|
}
|
|
2297
|
-
return
|
|
3714
|
+
return this.cookware.filter(
|
|
3715
|
+
(cw, idx) => cookwareIndices.has(idx) && !cw.flags?.includes("hidden")
|
|
3716
|
+
);
|
|
2298
3717
|
}
|
|
2299
3718
|
/**
|
|
2300
3719
|
* Parses a recipe from a string.
|
|
@@ -2302,17 +3721,23 @@ var _Recipe = class _Recipe {
|
|
|
2302
3721
|
*/
|
|
2303
3722
|
parse(content) {
|
|
2304
3723
|
const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
|
|
2305
|
-
const { metadata, servings } = extractMetadata(content);
|
|
3724
|
+
const { metadata, servings, unitSystem } = extractMetadata(content);
|
|
2306
3725
|
this.metadata = metadata;
|
|
2307
3726
|
this.servings = servings;
|
|
3727
|
+
if (unitSystem) _Recipe.unitSystems.set(this, unitSystem);
|
|
2308
3728
|
let blankLineBefore = true;
|
|
2309
3729
|
let section = new Section();
|
|
2310
3730
|
const items = [];
|
|
2311
3731
|
let noteText = "";
|
|
2312
3732
|
let inNote = false;
|
|
3733
|
+
let stepVariants;
|
|
3734
|
+
let stepOptional;
|
|
3735
|
+
const discoveredVariants = /* @__PURE__ */ new Set();
|
|
2313
3736
|
for (const line of cleanContent) {
|
|
2314
3737
|
if (line.trim().length === 0) {
|
|
2315
|
-
flushPendingItems(section, items);
|
|
3738
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3739
|
+
stepVariants = void 0;
|
|
3740
|
+
stepOptional = void 0;
|
|
2316
3741
|
flushPendingNote(
|
|
2317
3742
|
section,
|
|
2318
3743
|
noteText ? this._parseNoteText(noteText) : []
|
|
@@ -2323,30 +3748,42 @@ var _Recipe = class _Recipe {
|
|
|
2323
3748
|
continue;
|
|
2324
3749
|
}
|
|
2325
3750
|
if (line.startsWith("=")) {
|
|
2326
|
-
flushPendingItems(section, items);
|
|
3751
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3752
|
+
stepVariants = void 0;
|
|
3753
|
+
stepOptional = void 0;
|
|
2327
3754
|
flushPendingNote(
|
|
2328
3755
|
section,
|
|
2329
3756
|
noteText ? this._parseNoteText(noteText) : []
|
|
2330
3757
|
);
|
|
2331
3758
|
noteText = "";
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
3759
|
+
let sectionName = line.replace(/^=+|=+$/g, "").trim();
|
|
3760
|
+
let sectionVariants;
|
|
3761
|
+
let sectionOptional;
|
|
3762
|
+
const sectionVarMatch = sectionName.match(variantTagRegex);
|
|
3763
|
+
if (sectionVarMatch?.groups) {
|
|
3764
|
+
const isOptionalPrefix = sectionVarMatch.groups.variantOptionalPrefix === "?";
|
|
3765
|
+
const names = (sectionVarMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
|
|
3766
|
+
if (names.length > 0) {
|
|
3767
|
+
sectionVariants = names;
|
|
3768
|
+
for (const v of names) discoveredVariants.add(v);
|
|
3769
|
+
}
|
|
3770
|
+
if (isOptionalPrefix) {
|
|
3771
|
+
sectionOptional = true;
|
|
2337
3772
|
}
|
|
2338
|
-
|
|
3773
|
+
sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
|
|
2339
3774
|
}
|
|
3775
|
+
if (!section.isBlank()) {
|
|
3776
|
+
this.sections.push(section);
|
|
3777
|
+
}
|
|
3778
|
+
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
2340
3779
|
blankLineBefore = true;
|
|
2341
3780
|
inNote = false;
|
|
2342
3781
|
continue;
|
|
2343
3782
|
}
|
|
2344
3783
|
if (blankLineBefore && line.startsWith(">")) {
|
|
2345
|
-
flushPendingItems(section, items);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
noteText ? this._parseNoteText(noteText) : []
|
|
2349
|
-
);
|
|
3784
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3785
|
+
stepVariants = void 0;
|
|
3786
|
+
stepOptional = void 0;
|
|
2350
3787
|
noteText = line.substring(1).trim();
|
|
2351
3788
|
inNote = true;
|
|
2352
3789
|
blankLineBefore = false;
|
|
@@ -2361,13 +3798,31 @@ var _Recipe = class _Recipe {
|
|
|
2361
3798
|
blankLineBefore = false;
|
|
2362
3799
|
continue;
|
|
2363
3800
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
3801
|
+
let currentLine = line;
|
|
3802
|
+
if (items.length === 0) {
|
|
3803
|
+
const varMatch = currentLine.match(variantTagRegex);
|
|
3804
|
+
if (varMatch?.groups) {
|
|
3805
|
+
const isOptionalPrefix = varMatch.groups.variantOptionalPrefix === "?";
|
|
3806
|
+
const names = (varMatch.groups.variantNames ?? "").split(",").map((n2) => n2.trim()).filter((n2) => n2.length > 0);
|
|
3807
|
+
if (names.length > 0) {
|
|
3808
|
+
stepVariants = names;
|
|
3809
|
+
for (const v of names) discoveredVariants.add(v);
|
|
3810
|
+
}
|
|
3811
|
+
if (isOptionalPrefix) {
|
|
3812
|
+
stepOptional = true;
|
|
3813
|
+
}
|
|
3814
|
+
currentLine = currentLine.slice(varMatch[0].length);
|
|
3815
|
+
if (currentLine.trim().length === 0) {
|
|
3816
|
+
blankLineBefore = false;
|
|
3817
|
+
continue;
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
2366
3821
|
let cursor = 0;
|
|
2367
|
-
for (const match of
|
|
3822
|
+
for (const match of currentLine.matchAll(tokensRegex)) {
|
|
2368
3823
|
const idx = match.index;
|
|
2369
3824
|
if (idx > cursor) {
|
|
2370
|
-
items.push(
|
|
3825
|
+
items.push(...parseMarkdownSegments(currentLine.slice(cursor, idx)));
|
|
2371
3826
|
}
|
|
2372
3827
|
const groups = match.groups;
|
|
2373
3828
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
@@ -2386,7 +3841,7 @@ var _Recipe = class _Recipe {
|
|
|
2386
3841
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
2387
3842
|
flags.push("hidden");
|
|
2388
3843
|
}
|
|
2389
|
-
const quantity = quantityRaw ?
|
|
3844
|
+
const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
|
|
2390
3845
|
const newCookware = {
|
|
2391
3846
|
name
|
|
2392
3847
|
};
|
|
@@ -2418,7 +3873,7 @@ var _Recipe = class _Recipe {
|
|
|
2418
3873
|
throw new Error("Timer missing unit");
|
|
2419
3874
|
}
|
|
2420
3875
|
const name = groups.timerName || void 0;
|
|
2421
|
-
const duration =
|
|
3876
|
+
const duration = parseQuantityValue(durationStr);
|
|
2422
3877
|
const timerObj = {
|
|
2423
3878
|
name,
|
|
2424
3879
|
duration,
|
|
@@ -2428,17 +3883,22 @@ var _Recipe = class _Recipe {
|
|
|
2428
3883
|
}
|
|
2429
3884
|
cursor = idx + match[0].length;
|
|
2430
3885
|
}
|
|
2431
|
-
if (cursor <
|
|
2432
|
-
items.push(
|
|
3886
|
+
if (cursor < currentLine.length) {
|
|
3887
|
+
items.push(...parseMarkdownSegments(currentLine.slice(cursor)));
|
|
2433
3888
|
}
|
|
2434
3889
|
blankLineBefore = false;
|
|
2435
3890
|
}
|
|
2436
|
-
flushPendingItems(section, items);
|
|
3891
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
2437
3892
|
flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
|
|
2438
3893
|
if (!section.isBlank()) {
|
|
2439
3894
|
this.sections.push(section);
|
|
2440
3895
|
}
|
|
2441
|
-
this.
|
|
3896
|
+
const metaVariants = this.metadata.variants ?? [];
|
|
3897
|
+
const allVariants = /* @__PURE__ */ new Set([...metaVariants, ...discoveredVariants]);
|
|
3898
|
+
if (allVariants.size > 0) {
|
|
3899
|
+
this.choices.variants = [...allVariants];
|
|
3900
|
+
}
|
|
3901
|
+
this._populateIngredientQuantities();
|
|
2442
3902
|
}
|
|
2443
3903
|
/**
|
|
2444
3904
|
* Scales the recipe to a new number of servings. In practice, it calls
|
|
@@ -2453,7 +3913,7 @@ var _Recipe = class _Recipe {
|
|
|
2453
3913
|
if (originalServings === void 0 || originalServings === 0) {
|
|
2454
3914
|
originalServings = 1;
|
|
2455
3915
|
}
|
|
2456
|
-
const factor = (0,
|
|
3916
|
+
const factor = (0, import_big5.default)(newServings).div(originalServings);
|
|
2457
3917
|
return this.scaleBy(factor);
|
|
2458
3918
|
}
|
|
2459
3919
|
/**
|
|
@@ -2468,18 +3928,19 @@ var _Recipe = class _Recipe {
|
|
|
2468
3928
|
if (originalServings === void 0 || originalServings === 0) {
|
|
2469
3929
|
originalServings = 1;
|
|
2470
3930
|
}
|
|
3931
|
+
const unitSystem = this.unitSystem;
|
|
2471
3932
|
function scaleAlternativesBy(alternatives, factor2) {
|
|
2472
3933
|
for (const alternative of alternatives) {
|
|
2473
|
-
if (alternative.
|
|
2474
|
-
const scaleFactor = alternative.
|
|
2475
|
-
if (alternative.
|
|
2476
|
-
alternative.
|
|
2477
|
-
alternative.
|
|
3934
|
+
if (alternative.quantity) {
|
|
3935
|
+
const scaleFactor = alternative.scalable ? (0, import_big5.default)(factor2) : 1;
|
|
3936
|
+
if (alternative.quantity.type !== "fixed" || alternative.quantity.value.type !== "text") {
|
|
3937
|
+
alternative.quantity = multiplyQuantityValue(
|
|
3938
|
+
alternative.quantity,
|
|
2478
3939
|
scaleFactor
|
|
2479
3940
|
);
|
|
2480
3941
|
}
|
|
2481
|
-
if (alternative.
|
|
2482
|
-
alternative.
|
|
3942
|
+
if (alternative.equivalents) {
|
|
3943
|
+
alternative.equivalents = alternative.equivalents.map(
|
|
2483
3944
|
(altQuantity) => {
|
|
2484
3945
|
if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
|
|
2485
3946
|
return altQuantity;
|
|
@@ -2495,6 +3956,20 @@ var _Recipe = class _Recipe {
|
|
|
2495
3956
|
}
|
|
2496
3957
|
);
|
|
2497
3958
|
}
|
|
3959
|
+
const optimizedPrimary = applyBestUnit(
|
|
3960
|
+
{
|
|
3961
|
+
quantity: alternative.quantity,
|
|
3962
|
+
unit: alternative.unit
|
|
3963
|
+
},
|
|
3964
|
+
unitSystem
|
|
3965
|
+
);
|
|
3966
|
+
alternative.quantity = optimizedPrimary.quantity;
|
|
3967
|
+
alternative.unit = optimizedPrimary.unit;
|
|
3968
|
+
if (alternative.equivalents) {
|
|
3969
|
+
alternative.equivalents = alternative.equivalents.map(
|
|
3970
|
+
(eq) => applyBestUnit(eq, unitSystem)
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
2498
3973
|
}
|
|
2499
3974
|
}
|
|
2500
3975
|
}
|
|
@@ -2509,8 +3984,10 @@ var _Recipe = class _Recipe {
|
|
|
2509
3984
|
}
|
|
2510
3985
|
}
|
|
2511
3986
|
}
|
|
2512
|
-
for (const
|
|
2513
|
-
|
|
3987
|
+
for (const subgroups of newRecipe.choices.ingredientGroups.values()) {
|
|
3988
|
+
for (const subgroup of subgroups) {
|
|
3989
|
+
scaleAlternativesBy(subgroup, factor);
|
|
3990
|
+
}
|
|
2514
3991
|
}
|
|
2515
3992
|
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
2516
3993
|
scaleAlternativesBy(alternatives, factor);
|
|
@@ -2520,39 +3997,198 @@ var _Recipe = class _Recipe {
|
|
|
2520
3997
|
arbitrary.quantity,
|
|
2521
3998
|
factor
|
|
2522
3999
|
);
|
|
4000
|
+
const optimized = applyBestUnit(
|
|
4001
|
+
{ quantity: arbitrary.quantity, unit: arbitrary.unit },
|
|
4002
|
+
unitSystem
|
|
4003
|
+
);
|
|
4004
|
+
arbitrary.quantity = optimized.quantity;
|
|
4005
|
+
arbitrary.unit = optimized.unit;
|
|
2523
4006
|
}
|
|
2524
|
-
newRecipe.
|
|
2525
|
-
newRecipe.servings = (0,
|
|
2526
|
-
|
|
2527
|
-
if (
|
|
2528
|
-
|
|
2529
|
-
String(this.metadata.servings).replace(",", ".")
|
|
2530
|
-
);
|
|
2531
|
-
newRecipe.metadata.servings = String(
|
|
2532
|
-
(0, import_big4.default)(servingsValue).times(factor).toNumber()
|
|
2533
|
-
);
|
|
4007
|
+
newRecipe._populateIngredientQuantities();
|
|
4008
|
+
newRecipe.servings = (0, import_big5.default)(originalServings).times(factor).toNumber();
|
|
4009
|
+
for (const metaVar of ["servings", "serves"]) {
|
|
4010
|
+
if (typeof newRecipe.metadata[metaVar] === "number") {
|
|
4011
|
+
newRecipe.metadata[metaVar] = (0, import_big5.default)(newRecipe.metadata[metaVar]).times(factor).toNumber();
|
|
2534
4012
|
}
|
|
2535
4013
|
}
|
|
2536
4014
|
if (newRecipe.metadata.yield && this.metadata.yield) {
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
4015
|
+
const original = this.metadata.yield;
|
|
4016
|
+
if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
|
|
4017
|
+
} else {
|
|
4018
|
+
const scaledQuantity = multiplyQuantityValue(
|
|
4019
|
+
original.quantity,
|
|
4020
|
+
factor
|
|
2540
4021
|
);
|
|
2541
|
-
|
|
2542
|
-
|
|
4022
|
+
const optimized = applyBestUnit(
|
|
4023
|
+
{ quantity: scaledQuantity, unit: original.unit },
|
|
4024
|
+
unitSystem
|
|
2543
4025
|
);
|
|
4026
|
+
const scaled = {
|
|
4027
|
+
quantity: optimized.quantity
|
|
4028
|
+
};
|
|
4029
|
+
if (optimized.unit) scaled.unit = optimized.unit;
|
|
4030
|
+
if (original.textBefore) scaled.textBefore = original.textBefore;
|
|
4031
|
+
if (original.textAfter) scaled.textAfter = original.textAfter;
|
|
4032
|
+
newRecipe.metadata.yield = scaled;
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
return newRecipe;
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Converts all ingredient quantities in the recipe to a target unit system.
|
|
4039
|
+
*
|
|
4040
|
+
* @param system - The target unit system to convert to (metric, US, UK, JP)
|
|
4041
|
+
* @param method - How to handle existing quantities:
|
|
4042
|
+
* - "keep": Keep all existing equivalents (swap if needed, or add converted)
|
|
4043
|
+
* - "replace": Replace primary with target system quantity, discard equivalent used for conversion
|
|
4044
|
+
* - "remove": Only keep target system quantity, delete all equivalents
|
|
4045
|
+
* @returns A new Recipe instance with converted quantities
|
|
4046
|
+
*
|
|
4047
|
+
* @example
|
|
4048
|
+
* ```typescript
|
|
4049
|
+
* // Convert a recipe to metric, keeping original units as equivalents
|
|
4050
|
+
* const metricRecipe = recipe.convertTo("metric", "keep");
|
|
4051
|
+
*
|
|
4052
|
+
* // Convert to US units, removing all other equivalents
|
|
4053
|
+
* const usRecipe = recipe.convertTo("US", "remove");
|
|
4054
|
+
* ```
|
|
4055
|
+
*/
|
|
4056
|
+
convertTo(system, method) {
|
|
4057
|
+
const newRecipe = this.clone();
|
|
4058
|
+
function buildNewPrimary(convertedQty, oldPrimary, remainingEquivalents, scalable, integerProtected, source) {
|
|
4059
|
+
const newUnit = integerProtected && convertedQty.unit ? { name: convertedQty.unit.name, integerProtected: true } : convertedQty.unit;
|
|
4060
|
+
const newPrimary = {
|
|
4061
|
+
quantity: convertedQty.quantity,
|
|
4062
|
+
unit: newUnit,
|
|
4063
|
+
scalable
|
|
4064
|
+
};
|
|
4065
|
+
if (method === "remove") {
|
|
4066
|
+
return newPrimary;
|
|
4067
|
+
} else if (method === "replace") {
|
|
4068
|
+
if (source === "converted") remainingEquivalents.push(oldPrimary);
|
|
4069
|
+
if (remainingEquivalents.length > 0) {
|
|
4070
|
+
newPrimary.equivalents = remainingEquivalents;
|
|
4071
|
+
}
|
|
4072
|
+
} else {
|
|
4073
|
+
newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
|
|
2544
4074
|
}
|
|
4075
|
+
return newPrimary;
|
|
2545
4076
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
4077
|
+
function convertAlternativeQuantity(alternative) {
|
|
4078
|
+
const primaryUnit = resolveUnit(alternative.unit?.name);
|
|
4079
|
+
const equivalents = alternative.equivalents ?? [];
|
|
4080
|
+
const oldPrimary = {
|
|
4081
|
+
quantity: alternative.quantity,
|
|
4082
|
+
unit: alternative.unit
|
|
4083
|
+
};
|
|
4084
|
+
if (primaryUnit.type !== "other" && isUnitCompatibleWithSystem(primaryUnit, system)) {
|
|
4085
|
+
if (method === "remove") {
|
|
4086
|
+
return {
|
|
4087
|
+
quantity: alternative.quantity,
|
|
4088
|
+
unit: alternative.unit,
|
|
4089
|
+
scalable: alternative.scalable
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
return {
|
|
4093
|
+
quantity: alternative.quantity,
|
|
4094
|
+
unit: alternative.unit,
|
|
4095
|
+
scalable: alternative.scalable,
|
|
4096
|
+
equivalents
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
const targetEquivIndex = equivalents.findIndex((eq) => {
|
|
4100
|
+
const eqUnit = resolveUnit(eq.unit?.name);
|
|
4101
|
+
return eqUnit.type !== "other" && isUnitCompatibleWithSystem(eqUnit, system);
|
|
4102
|
+
});
|
|
4103
|
+
if (targetEquivIndex !== -1) {
|
|
4104
|
+
const targetEquiv = equivalents[targetEquivIndex];
|
|
4105
|
+
const remainingEquivalents = equivalents.filter(
|
|
4106
|
+
(_, i2) => i2 !== targetEquivIndex
|
|
2550
4107
|
);
|
|
2551
|
-
|
|
2552
|
-
|
|
4108
|
+
return buildNewPrimary(
|
|
4109
|
+
targetEquiv,
|
|
4110
|
+
oldPrimary,
|
|
4111
|
+
remainingEquivalents,
|
|
4112
|
+
alternative.scalable,
|
|
4113
|
+
targetEquiv.unit?.integerProtected,
|
|
4114
|
+
"swapped"
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
const converted = convertQuantityToSystem(oldPrimary, system);
|
|
4118
|
+
if (converted && converted.unit) {
|
|
4119
|
+
return buildNewPrimary(
|
|
4120
|
+
converted,
|
|
4121
|
+
oldPrimary,
|
|
4122
|
+
equivalents,
|
|
4123
|
+
alternative.scalable,
|
|
4124
|
+
alternative.unit?.integerProtected,
|
|
4125
|
+
"swapped"
|
|
2553
4126
|
);
|
|
2554
4127
|
}
|
|
4128
|
+
for (let i2 = 0; i2 < equivalents.length; i2++) {
|
|
4129
|
+
const equiv = equivalents[i2];
|
|
4130
|
+
const convertedEquiv = convertQuantityToSystem(equiv, system);
|
|
4131
|
+
if (convertedEquiv && convertedEquiv.unit) {
|
|
4132
|
+
const remainingEquivalents = method === "keep" ? equivalents : equivalents.filter((_, idx) => idx !== i2);
|
|
4133
|
+
return buildNewPrimary(
|
|
4134
|
+
convertedEquiv,
|
|
4135
|
+
oldPrimary,
|
|
4136
|
+
remainingEquivalents,
|
|
4137
|
+
alternative.scalable,
|
|
4138
|
+
equiv.unit?.integerProtected,
|
|
4139
|
+
"converted"
|
|
4140
|
+
);
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
if (method === "remove") {
|
|
4144
|
+
return {
|
|
4145
|
+
quantity: alternative.quantity,
|
|
4146
|
+
unit: alternative.unit,
|
|
4147
|
+
scalable: alternative.scalable
|
|
4148
|
+
};
|
|
4149
|
+
} else {
|
|
4150
|
+
return {
|
|
4151
|
+
quantity: alternative.quantity,
|
|
4152
|
+
unit: alternative.unit,
|
|
4153
|
+
scalable: alternative.scalable,
|
|
4154
|
+
equivalents
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
function convertAlternatives(alternatives) {
|
|
4159
|
+
for (const alternative of alternatives) {
|
|
4160
|
+
if (alternative.quantity) {
|
|
4161
|
+
const converted = convertAlternativeQuantity(
|
|
4162
|
+
alternative
|
|
4163
|
+
);
|
|
4164
|
+
alternative.quantity = converted.quantity;
|
|
4165
|
+
alternative.unit = converted.unit;
|
|
4166
|
+
alternative.scalable = converted.scalable;
|
|
4167
|
+
alternative.equivalents = converted.equivalents;
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
2555
4170
|
}
|
|
4171
|
+
for (const section of newRecipe.sections) {
|
|
4172
|
+
for (const step of section.content.filter(
|
|
4173
|
+
(item) => item.type === "step"
|
|
4174
|
+
)) {
|
|
4175
|
+
for (const item of step.items.filter(
|
|
4176
|
+
(item2) => item2.type === "ingredient"
|
|
4177
|
+
)) {
|
|
4178
|
+
convertAlternatives(item.alternatives);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
for (const subgroups of newRecipe.choices.ingredientGroups.values()) {
|
|
4183
|
+
for (const subgroup of subgroups) {
|
|
4184
|
+
convertAlternatives(subgroup);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
4188
|
+
convertAlternatives(alternatives);
|
|
4189
|
+
}
|
|
4190
|
+
newRecipe._populateIngredientQuantities();
|
|
4191
|
+
if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
|
|
2556
4192
|
return newRecipe;
|
|
2557
4193
|
}
|
|
2558
4194
|
/**
|
|
@@ -2577,7 +4213,11 @@ var _Recipe = class _Recipe {
|
|
|
2577
4213
|
newRecipe.metadata = deepClone(this.metadata);
|
|
2578
4214
|
newRecipe.ingredients = deepClone(this.ingredients);
|
|
2579
4215
|
newRecipe.sections = this.sections.map((section) => {
|
|
2580
|
-
const newSection = new Section(
|
|
4216
|
+
const newSection = new Section(
|
|
4217
|
+
section.name,
|
|
4218
|
+
section.variants,
|
|
4219
|
+
section.optional
|
|
4220
|
+
);
|
|
2581
4221
|
newSection.content = deepClone(section.content);
|
|
2582
4222
|
return newSection;
|
|
2583
4223
|
});
|
|
@@ -2588,21 +4228,30 @@ var _Recipe = class _Recipe {
|
|
|
2588
4228
|
return newRecipe;
|
|
2589
4229
|
}
|
|
2590
4230
|
};
|
|
4231
|
+
/**
|
|
4232
|
+
* External storage for unit system (not a property on instances).
|
|
4233
|
+
* Used for resolving ambiguous units during quantity addition.
|
|
4234
|
+
*/
|
|
4235
|
+
__publicField(_Recipe, "unitSystems", /* @__PURE__ */ new WeakMap());
|
|
2591
4236
|
/**
|
|
2592
4237
|
* External storage for item count (not a property on instances).
|
|
2593
4238
|
* Used for giving ID numbers to items during parsing.
|
|
2594
4239
|
*/
|
|
2595
4240
|
__publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
|
|
4241
|
+
/**
|
|
4242
|
+
* External storage for subgroup index tracking during parsing.
|
|
4243
|
+
* Maps groupKey → subgroupKey → index within the subgroups array.
|
|
4244
|
+
*/
|
|
4245
|
+
__publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
|
|
2596
4246
|
var Recipe = _Recipe;
|
|
2597
4247
|
|
|
2598
4248
|
// src/classes/shopping_list.ts
|
|
2599
4249
|
var ShoppingList = class {
|
|
2600
4250
|
/**
|
|
2601
4251
|
* Creates a new ShoppingList instance
|
|
2602
|
-
* @param
|
|
4252
|
+
* @param categoryConfigStr - The category configuration to parse.
|
|
2603
4253
|
*/
|
|
2604
|
-
constructor(
|
|
2605
|
-
// TODO: backport type change
|
|
4254
|
+
constructor(categoryConfigStr) {
|
|
2606
4255
|
/**
|
|
2607
4256
|
* The ingredients in the shopping list.
|
|
2608
4257
|
*/
|
|
@@ -2614,43 +4263,43 @@ var ShoppingList = class {
|
|
|
2614
4263
|
/**
|
|
2615
4264
|
* The category configuration for the shopping list.
|
|
2616
4265
|
*/
|
|
2617
|
-
__publicField(this, "
|
|
4266
|
+
__publicField(this, "categoryConfig");
|
|
2618
4267
|
/**
|
|
2619
4268
|
* The categorized ingredients in the shopping list.
|
|
2620
4269
|
*/
|
|
2621
4270
|
__publicField(this, "categories");
|
|
2622
|
-
|
|
2623
|
-
|
|
4271
|
+
/**
|
|
4272
|
+
* The unit system to use for quantity simplification.
|
|
4273
|
+
* When set, overrides per-recipe unit systems.
|
|
4274
|
+
*/
|
|
4275
|
+
__publicField(this, "unitSystem");
|
|
4276
|
+
/**
|
|
4277
|
+
* Per-ingredient equivalence ratio maps for recomputing equivalents
|
|
4278
|
+
* after pantry subtraction. Keyed by ingredient name.
|
|
4279
|
+
* @internal
|
|
4280
|
+
*/
|
|
4281
|
+
__publicField(this, "equivalenceRatios", /* @__PURE__ */ new Map());
|
|
4282
|
+
/**
|
|
4283
|
+
* The original pantry (never mutated by recipe calculations).
|
|
4284
|
+
*/
|
|
4285
|
+
__publicField(this, "pantry");
|
|
4286
|
+
/**
|
|
4287
|
+
* The pantry with quantities updated after subtracting recipe needs.
|
|
4288
|
+
* Recomputed on every {@link ShoppingList.calculateIngredients | calculateIngredients()} call.
|
|
4289
|
+
*/
|
|
4290
|
+
__publicField(this, "resultingPantry");
|
|
4291
|
+
if (categoryConfigStr) {
|
|
4292
|
+
this.setCategoryConfig(categoryConfigStr);
|
|
2624
4293
|
}
|
|
2625
4294
|
}
|
|
2626
|
-
|
|
4295
|
+
calculateIngredients() {
|
|
2627
4296
|
this.ingredients = [];
|
|
2628
|
-
const
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (!existing.quantityTotal) {
|
|
2634
|
-
existing.quantityTotal = quantityTotal;
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
try {
|
|
2638
|
-
const existingQuantityTotalExtended = extendAllUnits(
|
|
2639
|
-
existing.quantityTotal
|
|
2640
|
-
);
|
|
2641
|
-
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.entries : [existingQuantityTotalExtended];
|
|
2642
|
-
existing.quantityTotal = addEquivalentsAndSimplify(
|
|
2643
|
-
...existingQuantities,
|
|
2644
|
-
...newQuantities
|
|
2645
|
-
);
|
|
2646
|
-
return;
|
|
2647
|
-
} catch {
|
|
2648
|
-
}
|
|
4297
|
+
const rawQuantitiesMap = /* @__PURE__ */ new Map();
|
|
4298
|
+
const nameOrder = [];
|
|
4299
|
+
const trackName = (name) => {
|
|
4300
|
+
if (!nameOrder.includes(name)) {
|
|
4301
|
+
nameOrder.push(name);
|
|
2649
4302
|
}
|
|
2650
|
-
this.ingredients.push({
|
|
2651
|
-
name,
|
|
2652
|
-
quantityTotal
|
|
2653
|
-
});
|
|
2654
4303
|
};
|
|
2655
4304
|
for (const addedRecipe of this.recipes) {
|
|
2656
4305
|
let scaledRecipe;
|
|
@@ -2660,28 +4309,261 @@ var ShoppingList = class {
|
|
|
2660
4309
|
} else {
|
|
2661
4310
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
2662
4311
|
}
|
|
2663
|
-
const
|
|
2664
|
-
addedRecipe.choices
|
|
2665
|
-
);
|
|
2666
|
-
for (const
|
|
2667
|
-
if (
|
|
4312
|
+
const rawGroups = scaledRecipe.getRawQuantityGroups({
|
|
4313
|
+
choices: addedRecipe.choices
|
|
4314
|
+
});
|
|
4315
|
+
for (const group of rawGroups) {
|
|
4316
|
+
if (group.flags?.includes("hidden") || !group.usedAsPrimary) {
|
|
2668
4317
|
continue;
|
|
2669
4318
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
4319
|
+
trackName(group.name);
|
|
4320
|
+
if (group.quantities.length > 0) {
|
|
4321
|
+
const existing = rawQuantitiesMap.get(group.name) ?? [];
|
|
4322
|
+
existing.push(...group.quantities);
|
|
4323
|
+
rawQuantitiesMap.set(group.name, existing);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
this.equivalenceRatios.clear();
|
|
4328
|
+
for (const name of nameOrder) {
|
|
4329
|
+
const rawQuantities = rawQuantitiesMap.get(name);
|
|
4330
|
+
if (!rawQuantities || rawQuantities.length === 0) {
|
|
4331
|
+
this.ingredients.push({ name });
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
const textEntries = [];
|
|
4335
|
+
const numericEntries = [];
|
|
4336
|
+
for (const q of rawQuantities) {
|
|
4337
|
+
if ("quantity" in q && q.quantity.type === "fixed" && q.quantity.value.type === "text") {
|
|
4338
|
+
textEntries.push(q);
|
|
4339
|
+
} else {
|
|
4340
|
+
numericEntries.push(q);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
if (numericEntries.length > 1) {
|
|
4344
|
+
const ratioMap = buildEquivalenceRatioMap(
|
|
4345
|
+
getEquivalentUnitsLists(...numericEntries)
|
|
4346
|
+
);
|
|
4347
|
+
if (Object.keys(ratioMap).length > 0) {
|
|
4348
|
+
this.equivalenceRatios.set(name, ratioMap);
|
|
2674
4349
|
}
|
|
2675
4350
|
}
|
|
4351
|
+
const resultQuantities = [];
|
|
4352
|
+
for (const t2 of textEntries) {
|
|
4353
|
+
resultQuantities.push(toPlainUnit(t2));
|
|
4354
|
+
}
|
|
4355
|
+
if (numericEntries.length > 0) {
|
|
4356
|
+
resultQuantities.push(
|
|
4357
|
+
...flattenPlainUnitGroup(
|
|
4358
|
+
addEquivalentsAndSimplify(numericEntries, this.unitSystem)
|
|
4359
|
+
)
|
|
4360
|
+
);
|
|
4361
|
+
}
|
|
4362
|
+
this.ingredients.push({
|
|
4363
|
+
name,
|
|
4364
|
+
quantities: resultQuantities
|
|
4365
|
+
});
|
|
2676
4366
|
}
|
|
4367
|
+
this.applyPantrySubtraction();
|
|
4368
|
+
}
|
|
4369
|
+
/**
|
|
4370
|
+
* Subtracts pantry item quantities from calculated ingredient quantities
|
|
4371
|
+
* and updates the resultingPantry to reflect consumed stock.
|
|
4372
|
+
*/
|
|
4373
|
+
applyPantrySubtraction() {
|
|
4374
|
+
if (!this.pantry) {
|
|
4375
|
+
this.resultingPantry = void 0;
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
const clonedPantry = new Pantry();
|
|
4379
|
+
clonedPantry.items = deepClone(this.pantry.items);
|
|
4380
|
+
if (this.categoryConfig) {
|
|
4381
|
+
clonedPantry.setCategoryConfig(this.categoryConfig);
|
|
4382
|
+
}
|
|
4383
|
+
for (const ingredient of this.ingredients) {
|
|
4384
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
4385
|
+
continue;
|
|
4386
|
+
const pantryItem = clonedPantry.findItem(ingredient.name);
|
|
4387
|
+
if (!pantryItem || !pantryItem.quantity) continue;
|
|
4388
|
+
let pantryExtended = {
|
|
4389
|
+
quantity: pantryItem.quantity,
|
|
4390
|
+
...pantryItem.unit && { unit: { name: pantryItem.unit } }
|
|
4391
|
+
};
|
|
4392
|
+
for (let i2 = 0; i2 < ingredient.quantities.length; i2++) {
|
|
4393
|
+
const entry = ingredient.quantities[i2];
|
|
4394
|
+
const leaves = "and" in entry ? entry.and : [entry];
|
|
4395
|
+
for (const leaf of leaves) {
|
|
4396
|
+
const ingredientExtended = toExtendedUnit(leaf);
|
|
4397
|
+
const leafHasUnit = leaf.unit !== void 0 && leaf.unit !== "";
|
|
4398
|
+
const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
|
|
4399
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4400
|
+
const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
|
|
4401
|
+
const leafDef = normalizeUnit(leaf.unit);
|
|
4402
|
+
const pantryDef = normalizeUnit(pantryExtended.unit?.name);
|
|
4403
|
+
if (unitMismatch) {
|
|
4404
|
+
const leafUnit = leaf.unit ?? NO_UNIT;
|
|
4405
|
+
const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
|
|
4406
|
+
const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
|
|
4407
|
+
if (ratioFromPantry !== void 0) {
|
|
4408
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4409
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
4410
|
+
if (typeof pantryValue === "number" && typeof leafValue === "number") {
|
|
4411
|
+
const pantryInLeafUnits = pantryValue * ratioFromPantry;
|
|
4412
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
4413
|
+
const remainingLeafValue = Math.max(
|
|
4414
|
+
leafValue - pantryInLeafUnits,
|
|
4415
|
+
0
|
|
4416
|
+
);
|
|
4417
|
+
leaf.quantity = {
|
|
4418
|
+
type: "fixed",
|
|
4419
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4420
|
+
};
|
|
4421
|
+
const consumedInPantryUnits = subtracted / ratioFromPantry;
|
|
4422
|
+
const remainingPantryValue = Math.max(
|
|
4423
|
+
pantryValue - consumedInPantryUnits,
|
|
4424
|
+
0
|
|
4425
|
+
);
|
|
4426
|
+
pantryExtended = {
|
|
4427
|
+
quantity: {
|
|
4428
|
+
type: "fixed",
|
|
4429
|
+
value: {
|
|
4430
|
+
type: "decimal",
|
|
4431
|
+
decimal: remainingPantryValue
|
|
4432
|
+
}
|
|
4433
|
+
},
|
|
4434
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
4435
|
+
};
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
} else {
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
} else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
|
|
4442
|
+
const remaining = subtractQuantities(
|
|
4443
|
+
ingredientExtended,
|
|
4444
|
+
pantryExtended,
|
|
4445
|
+
{ clampToZero: true }
|
|
4446
|
+
);
|
|
4447
|
+
const consumed = subtractQuantities(
|
|
4448
|
+
pantryExtended,
|
|
4449
|
+
ingredientExtended,
|
|
4450
|
+
{ clampToZero: true }
|
|
4451
|
+
);
|
|
4452
|
+
pantryExtended = consumed;
|
|
4453
|
+
const updated = toPlainUnit(remaining);
|
|
4454
|
+
leaf.quantity = updated.quantity;
|
|
4455
|
+
leaf.unit = updated.unit;
|
|
4456
|
+
} else if (ratioMap) {
|
|
4457
|
+
const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
|
|
4458
|
+
const leafValue = getAverageValue(ingredientExtended.quantity);
|
|
4459
|
+
const pantryValue = getAverageValue(pantryExtended.quantity);
|
|
4460
|
+
if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
|
|
4461
|
+
for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
|
|
4462
|
+
const ratio = ratios[canonicalLeaf];
|
|
4463
|
+
if (ratio === void 0) continue;
|
|
4464
|
+
const equivDef = normalizeUnit(equivUnit);
|
|
4465
|
+
if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
|
|
4466
|
+
continue;
|
|
4467
|
+
const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
|
|
4468
|
+
const pantryInLeafUnits = pantryInEquiv / ratio;
|
|
4469
|
+
const subtracted = Math.min(pantryInLeafUnits, leafValue);
|
|
4470
|
+
const remainingLeafValue = Math.max(
|
|
4471
|
+
leafValue - pantryInLeafUnits,
|
|
4472
|
+
0
|
|
4473
|
+
);
|
|
4474
|
+
leaf.quantity = {
|
|
4475
|
+
type: "fixed",
|
|
4476
|
+
value: { type: "decimal", decimal: remainingLeafValue }
|
|
4477
|
+
};
|
|
4478
|
+
const consumedInEquiv = subtracted * ratio;
|
|
4479
|
+
const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
|
|
4480
|
+
const remainingPantryValue = Math.max(
|
|
4481
|
+
pantryValue - consumedInPantryUnits,
|
|
4482
|
+
0
|
|
4483
|
+
);
|
|
4484
|
+
pantryExtended = {
|
|
4485
|
+
quantity: {
|
|
4486
|
+
type: "fixed",
|
|
4487
|
+
value: {
|
|
4488
|
+
type: "decimal",
|
|
4489
|
+
decimal: remainingPantryValue
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
4492
|
+
...pantryExtended.unit && { unit: pantryExtended.unit }
|
|
4493
|
+
};
|
|
4494
|
+
break;
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
if ("and" in entry) {
|
|
4500
|
+
const nonZero = entry.and.filter(
|
|
4501
|
+
(leaf) => leaf.quantity.type !== "fixed" || leaf.quantity.value.type !== "decimal" || leaf.quantity.value.decimal !== 0
|
|
4502
|
+
);
|
|
4503
|
+
entry.and.length = 0;
|
|
4504
|
+
entry.and.push(...nonZero);
|
|
4505
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4506
|
+
if (entry.equivalents && ratioMap) {
|
|
4507
|
+
const equivUnits = entry.equivalents.map((e2) => e2.unit);
|
|
4508
|
+
entry.equivalents = recomputeEquivalents(
|
|
4509
|
+
entry.and,
|
|
4510
|
+
ratioMap,
|
|
4511
|
+
equivUnits
|
|
4512
|
+
);
|
|
4513
|
+
}
|
|
4514
|
+
if (entry.and.length === 1) {
|
|
4515
|
+
const single = entry.and[0];
|
|
4516
|
+
ingredient.quantities[i2] = {
|
|
4517
|
+
quantity: single.quantity,
|
|
4518
|
+
...single.unit && { unit: single.unit },
|
|
4519
|
+
...entry.equivalents && { equivalents: entry.equivalents }
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
} else if ("equivalents" in entry && entry.equivalents) {
|
|
4523
|
+
const ratioMap = this.equivalenceRatios.get(ingredient.name);
|
|
4524
|
+
if (ratioMap) {
|
|
4525
|
+
const equivUnits = entry.equivalents.map(
|
|
4526
|
+
(e2) => e2.unit
|
|
4527
|
+
// equivalents always have units
|
|
4528
|
+
);
|
|
4529
|
+
const recomputed = recomputeEquivalents(
|
|
4530
|
+
[entry],
|
|
4531
|
+
ratioMap,
|
|
4532
|
+
equivUnits
|
|
4533
|
+
);
|
|
4534
|
+
entry.equivalents = recomputed;
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
ingredient.quantities = ingredient.quantities.filter((entry) => {
|
|
4539
|
+
if ("and" in entry) return entry.and.length > 0;
|
|
4540
|
+
return !(entry.quantity.type === "fixed" && entry.quantity.value.type === "decimal" && entry.quantity.value.decimal === 0);
|
|
4541
|
+
});
|
|
4542
|
+
if (ingredient.quantities.length === 0) {
|
|
4543
|
+
ingredient.quantities = void 0;
|
|
4544
|
+
}
|
|
4545
|
+
pantryItem.quantity = pantryExtended.quantity;
|
|
4546
|
+
if (pantryExtended.unit) {
|
|
4547
|
+
pantryItem.unit = pantryExtended.unit.name;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
this.resultingPantry = clonedPantry;
|
|
2677
4551
|
}
|
|
2678
4552
|
/**
|
|
2679
4553
|
* Adds a recipe to the shopping list, then automatically
|
|
2680
4554
|
* recalculates the quantities and recategorize the ingredients.
|
|
2681
4555
|
* @param recipe - The recipe to add.
|
|
2682
4556
|
* @param options - Options for adding the recipe.
|
|
4557
|
+
* @throws Error if the recipe has alternatives without corresponding choices.
|
|
2683
4558
|
*/
|
|
2684
|
-
|
|
4559
|
+
addRecipe(recipe, options = {}) {
|
|
4560
|
+
const errorMessage = this.getUnresolvedAlternativesError(
|
|
4561
|
+
recipe,
|
|
4562
|
+
options.choices
|
|
4563
|
+
);
|
|
4564
|
+
if (errorMessage) {
|
|
4565
|
+
throw new Error(errorMessage);
|
|
4566
|
+
}
|
|
2685
4567
|
if (!options.scaling) {
|
|
2686
4568
|
this.recipes.push({
|
|
2687
4569
|
recipe,
|
|
@@ -2703,32 +4585,102 @@ var ShoppingList = class {
|
|
|
2703
4585
|
});
|
|
2704
4586
|
}
|
|
2705
4587
|
}
|
|
2706
|
-
this.
|
|
4588
|
+
this.calculateIngredients();
|
|
2707
4589
|
this.categorize();
|
|
2708
4590
|
}
|
|
4591
|
+
/**
|
|
4592
|
+
* Checks if a recipe has unresolved alternatives (alternatives without provided choices).
|
|
4593
|
+
* @param recipe - The recipe to check.
|
|
4594
|
+
* @param choices - The choices provided for the recipe.
|
|
4595
|
+
* @returns An error message if there are unresolved alternatives, undefined otherwise.
|
|
4596
|
+
*/
|
|
4597
|
+
getUnresolvedAlternativesError(recipe, choices) {
|
|
4598
|
+
const missingItems = [];
|
|
4599
|
+
const missingGroups = [];
|
|
4600
|
+
for (const itemId of recipe.choices.ingredientItems.keys()) {
|
|
4601
|
+
if (!choices?.ingredientItems?.has(itemId)) {
|
|
4602
|
+
missingItems.push(itemId);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
for (const groupId of recipe.choices.ingredientGroups.keys()) {
|
|
4606
|
+
if (!choices?.ingredientGroups?.has(groupId)) {
|
|
4607
|
+
missingGroups.push(groupId);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
if (missingItems.length === 0 && missingGroups.length === 0) {
|
|
4611
|
+
return void 0;
|
|
4612
|
+
}
|
|
4613
|
+
const parts = [];
|
|
4614
|
+
if (missingItems.length > 0) {
|
|
4615
|
+
parts.push(
|
|
4616
|
+
`ingredientItems: [${missingItems.map((i2) => `'${i2}'`).join(", ")}]`
|
|
4617
|
+
);
|
|
4618
|
+
}
|
|
4619
|
+
if (missingGroups.length > 0) {
|
|
4620
|
+
parts.push(
|
|
4621
|
+
`ingredientGroups: [${missingGroups.map((g) => `'${g}'`).join(", ")}]`
|
|
4622
|
+
);
|
|
4623
|
+
}
|
|
4624
|
+
return `Recipe has unresolved alternatives. Missing choices for: ${parts.join(", ")}`;
|
|
4625
|
+
}
|
|
2709
4626
|
/**
|
|
2710
4627
|
* Removes a recipe from the shopping list, then automatically
|
|
2711
|
-
* recalculates the quantities and recategorize the ingredients.
|
|
4628
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
2712
4629
|
* @param index - The index of the recipe to remove.
|
|
2713
4630
|
*/
|
|
2714
|
-
|
|
4631
|
+
removeRecipe(index) {
|
|
2715
4632
|
if (index < 0 || index >= this.recipes.length) {
|
|
2716
4633
|
throw new Error("Index out of bounds");
|
|
2717
4634
|
}
|
|
2718
4635
|
this.recipes.splice(index, 1);
|
|
2719
|
-
this.
|
|
4636
|
+
this.calculateIngredients();
|
|
2720
4637
|
this.categorize();
|
|
2721
4638
|
}
|
|
4639
|
+
/**
|
|
4640
|
+
* Adds a pantry to the shopping list. On-hand pantry quantities will be
|
|
4641
|
+
* subtracted from recipe ingredient needs on each recalculation.
|
|
4642
|
+
* @param pantry - A Pantry instance or a TOML string to parse.
|
|
4643
|
+
* @param options - Options for pantry parsing (only used when providing a TOML string).
|
|
4644
|
+
*/
|
|
4645
|
+
addPantry(pantry, options) {
|
|
4646
|
+
if (typeof pantry === "string") {
|
|
4647
|
+
this.pantry = new Pantry(pantry, options);
|
|
4648
|
+
} else if (pantry instanceof Pantry) {
|
|
4649
|
+
this.pantry = pantry;
|
|
4650
|
+
} else {
|
|
4651
|
+
throw new Error(
|
|
4652
|
+
"Invalid pantry: expected a Pantry instance or TOML string"
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
if (this.categoryConfig) {
|
|
4656
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4657
|
+
}
|
|
4658
|
+
this.calculateIngredients();
|
|
4659
|
+
this.categorize();
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* Returns the resulting pantry with quantities updated to reflect
|
|
4663
|
+
* what was consumed by the shopping list's recipes.
|
|
4664
|
+
* Returns undefined if no pantry was added.
|
|
4665
|
+
* @returns The resulting Pantry, or undefined.
|
|
4666
|
+
*/
|
|
4667
|
+
getPantry() {
|
|
4668
|
+
return this.resultingPantry;
|
|
4669
|
+
}
|
|
2722
4670
|
/**
|
|
2723
4671
|
* Sets the category configuration for the shopping list
|
|
2724
4672
|
* and automatically categorize current ingredients from the list.
|
|
4673
|
+
* Also propagates the configuration to the pantry if one is set.
|
|
2725
4674
|
* @param config - The category configuration to parse.
|
|
2726
4675
|
*/
|
|
2727
|
-
|
|
4676
|
+
setCategoryConfig(config) {
|
|
2728
4677
|
if (typeof config === "string")
|
|
2729
|
-
this.
|
|
2730
|
-
else if (config instanceof CategoryConfig) this.
|
|
4678
|
+
this.categoryConfig = new CategoryConfig(config);
|
|
4679
|
+
else if (config instanceof CategoryConfig) this.categoryConfig = config;
|
|
2731
4680
|
else throw new Error("Invalid category configuration");
|
|
4681
|
+
if (this.pantry) {
|
|
4682
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4683
|
+
}
|
|
2732
4684
|
this.categorize();
|
|
2733
4685
|
}
|
|
2734
4686
|
/**
|
|
@@ -2736,17 +4688,17 @@ var ShoppingList = class {
|
|
|
2736
4688
|
* Will use the category config if any, otherwise all ingredients will be placed in the "other" category
|
|
2737
4689
|
*/
|
|
2738
4690
|
categorize() {
|
|
2739
|
-
if (!this.
|
|
4691
|
+
if (!this.categoryConfig) {
|
|
2740
4692
|
this.categories = { other: this.ingredients };
|
|
2741
4693
|
return;
|
|
2742
4694
|
}
|
|
2743
4695
|
const categories = { other: [] };
|
|
2744
|
-
for (const category of this.
|
|
4696
|
+
for (const category of this.categoryConfig.categories) {
|
|
2745
4697
|
categories[category.name] = [];
|
|
2746
4698
|
}
|
|
2747
4699
|
for (const ingredient of this.ingredients) {
|
|
2748
4700
|
let found = false;
|
|
2749
|
-
for (const category of this.
|
|
4701
|
+
for (const category of this.categoryConfig.categories) {
|
|
2750
4702
|
for (const categoryIngredient of category.ingredients) {
|
|
2751
4703
|
if (categoryIngredient.aliases.includes(ingredient.name)) {
|
|
2752
4704
|
categories[category.name].push(ingredient);
|
|
@@ -2810,7 +4762,6 @@ var ShoppingCart = class {
|
|
|
2810
4762
|
setProductCatalog(catalog) {
|
|
2811
4763
|
this.productCatalog = catalog;
|
|
2812
4764
|
}
|
|
2813
|
-
// TODO: harmonize recipe name to use underscores
|
|
2814
4765
|
/**
|
|
2815
4766
|
* Sets the shopping list to build the cart from.
|
|
2816
4767
|
* To use if a shopping list was not provided at the creation of the instance
|
|
@@ -2874,8 +4825,27 @@ var ShoppingCart = class {
|
|
|
2874
4825
|
getOptimumMatch(ingredient, options) {
|
|
2875
4826
|
if (options.length === 0)
|
|
2876
4827
|
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
2877
|
-
if (!ingredient.
|
|
4828
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
2878
4829
|
throw new NoProductMatchError(ingredient.name, "noQuantity");
|
|
4830
|
+
const allPlainEntries = [];
|
|
4831
|
+
for (const q of ingredient.quantities) {
|
|
4832
|
+
if ("and" in q) {
|
|
4833
|
+
allPlainEntries.push({ and: q.and });
|
|
4834
|
+
} else {
|
|
4835
|
+
const entry = {
|
|
4836
|
+
quantity: q.quantity,
|
|
4837
|
+
...q.unit && { unit: q.unit },
|
|
4838
|
+
...q.equivalents && { equivalents: q.equivalents }
|
|
4839
|
+
};
|
|
4840
|
+
allPlainEntries.push(entry);
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
let quantityTotal;
|
|
4844
|
+
if (allPlainEntries.length === 1) {
|
|
4845
|
+
quantityTotal = allPlainEntries[0];
|
|
4846
|
+
} else {
|
|
4847
|
+
quantityTotal = { and: allPlainEntries };
|
|
4848
|
+
}
|
|
2879
4849
|
const normalizedOptions = options.map(
|
|
2880
4850
|
(option) => ({
|
|
2881
4851
|
...option,
|
|
@@ -2891,10 +4861,10 @@ var ShoppingCart = class {
|
|
|
2891
4861
|
})
|
|
2892
4862
|
})
|
|
2893
4863
|
);
|
|
2894
|
-
const normalizedQuantityTotal = normalizeAllUnits(
|
|
4864
|
+
const normalizedQuantityTotal = normalizeAllUnits(quantityTotal);
|
|
2895
4865
|
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
2896
4866
|
if (isAndGroup(normalizedQuantities)) {
|
|
2897
|
-
for (const q of normalizedQuantities.
|
|
4867
|
+
for (const q of normalizedQuantities.and) {
|
|
2898
4868
|
const result = getOptimumMatchForQuantityParts(
|
|
2899
4869
|
q,
|
|
2900
4870
|
normalizedOptions2,
|
|
@@ -2903,7 +4873,7 @@ var ShoppingCart = class {
|
|
|
2903
4873
|
selection.push(...result);
|
|
2904
4874
|
}
|
|
2905
4875
|
} else {
|
|
2906
|
-
const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.
|
|
4876
|
+
const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.or : [normalizedQuantities];
|
|
2907
4877
|
const solutions = [];
|
|
2908
4878
|
const errors = /* @__PURE__ */ new Set();
|
|
2909
4879
|
for (const alternative of alternativeUnitsOfQuantity) {
|
|
@@ -2918,12 +4888,12 @@ var ShoppingCart = class {
|
|
|
2918
4888
|
alternative.quantity = scaledQuantity;
|
|
2919
4889
|
const matchOptions = normalizedOptions2.filter(
|
|
2920
4890
|
(option) => option.sizes.some(
|
|
2921
|
-
(s) =>
|
|
4891
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2922
4892
|
)
|
|
2923
4893
|
);
|
|
2924
4894
|
if (matchOptions.length > 0) {
|
|
2925
4895
|
const findCompatibleSize = (option) => option.sizes.find(
|
|
2926
|
-
(s) =>
|
|
4896
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2927
4897
|
);
|
|
2928
4898
|
if (matchOptions.length == 1) {
|
|
2929
4899
|
const matchedOption = matchOptions[0];
|
|
@@ -3023,18 +4993,179 @@ var ShoppingCart = class {
|
|
|
3023
4993
|
return this.summary;
|
|
3024
4994
|
}
|
|
3025
4995
|
};
|
|
4996
|
+
|
|
4997
|
+
// src/utils/render_helpers.ts
|
|
4998
|
+
var VULGAR_FRACTIONS = {
|
|
4999
|
+
"1/2": "\xBD",
|
|
5000
|
+
"1/3": "\u2153",
|
|
5001
|
+
"2/3": "\u2154",
|
|
5002
|
+
"1/4": "\xBC",
|
|
5003
|
+
"3/4": "\xBE",
|
|
5004
|
+
"1/8": "\u215B",
|
|
5005
|
+
"3/8": "\u215C",
|
|
5006
|
+
"5/8": "\u215D",
|
|
5007
|
+
"7/8": "\u215E"
|
|
5008
|
+
};
|
|
5009
|
+
function renderFractionAsVulgar(num, den) {
|
|
5010
|
+
const wholePart = Math.floor(num / den);
|
|
5011
|
+
const remainder = num % den;
|
|
5012
|
+
if (remainder === 0) {
|
|
5013
|
+
return String(wholePart);
|
|
5014
|
+
}
|
|
5015
|
+
const fractionKey = `${remainder}/${den}`;
|
|
5016
|
+
const vulgar = VULGAR_FRACTIONS[fractionKey];
|
|
5017
|
+
if (wholePart > 0) {
|
|
5018
|
+
return vulgar ? `${wholePart}${vulgar}` : `${wholePart} ${remainder}/${den}`;
|
|
5019
|
+
}
|
|
5020
|
+
return vulgar ?? `${num}/${den}`;
|
|
5021
|
+
}
|
|
5022
|
+
function formatNumericValue(value, useVulgar = true) {
|
|
5023
|
+
if (value.type === "decimal") {
|
|
5024
|
+
return String(value.decimal);
|
|
5025
|
+
}
|
|
5026
|
+
if (useVulgar) {
|
|
5027
|
+
return renderFractionAsVulgar(value.num, value.den);
|
|
5028
|
+
}
|
|
5029
|
+
return `${value.num}/${value.den}`;
|
|
5030
|
+
}
|
|
5031
|
+
function formatSingleValue(value) {
|
|
5032
|
+
if (value.type === "text") {
|
|
5033
|
+
return value.text;
|
|
5034
|
+
}
|
|
5035
|
+
return formatNumericValue(value);
|
|
5036
|
+
}
|
|
5037
|
+
function formatQuantity(quantity) {
|
|
5038
|
+
if (quantity.type === "fixed") {
|
|
5039
|
+
return formatSingleValue(quantity.value);
|
|
5040
|
+
}
|
|
5041
|
+
const minStr = formatNumericValue(quantity.min);
|
|
5042
|
+
const maxStr = formatNumericValue(quantity.max);
|
|
5043
|
+
return `${minStr}-${maxStr}`;
|
|
5044
|
+
}
|
|
5045
|
+
function formatUnit(unit) {
|
|
5046
|
+
if (!unit) return "";
|
|
5047
|
+
if (typeof unit === "string") return unit;
|
|
5048
|
+
return unit.name;
|
|
5049
|
+
}
|
|
5050
|
+
function formatQuantityWithUnit(quantity, unit) {
|
|
5051
|
+
if (!quantity) return "";
|
|
5052
|
+
const qty = formatQuantity(quantity);
|
|
5053
|
+
const unitStr = formatUnit(unit);
|
|
5054
|
+
return unitStr ? `${qty} ${unitStr}` : qty;
|
|
5055
|
+
}
|
|
5056
|
+
function formatExtendedQuantity(item) {
|
|
5057
|
+
return formatQuantityWithUnit(item.quantity, item.unit);
|
|
5058
|
+
}
|
|
5059
|
+
function formatItemQuantity(itemQuantity, separator = " | ") {
|
|
5060
|
+
const parts = [];
|
|
5061
|
+
parts.push(formatExtendedQuantity(itemQuantity));
|
|
5062
|
+
if (itemQuantity.equivalents) {
|
|
5063
|
+
for (const eq of itemQuantity.equivalents) {
|
|
5064
|
+
parts.push(formatExtendedQuantity(eq));
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
return parts.join(separator);
|
|
5068
|
+
}
|
|
5069
|
+
function isGroupedItem(item) {
|
|
5070
|
+
return item.group !== void 0;
|
|
5071
|
+
}
|
|
5072
|
+
function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
5073
|
+
if (item.group) {
|
|
5074
|
+
const selectedIndex2 = choices?.ingredientGroups?.get(item.group);
|
|
5075
|
+
const groupSubgroups = recipe.choices.ingredientGroups.get(item.group);
|
|
5076
|
+
if (groupSubgroups && selectedIndex2 !== void 0 && selectedIndex2 < groupSubgroups.length) {
|
|
5077
|
+
const selectedSubgroup = groupSubgroups[selectedIndex2];
|
|
5078
|
+
return selectedSubgroup?.some((alt) => alt.itemId === item.id);
|
|
5079
|
+
}
|
|
5080
|
+
return false;
|
|
5081
|
+
}
|
|
5082
|
+
const selectedIndex = choices?.ingredientItems?.get(item.id);
|
|
5083
|
+
return alternativeIndex === selectedIndex;
|
|
5084
|
+
}
|
|
5085
|
+
function isSectionActive(section, variant) {
|
|
5086
|
+
if (!section.variants) return true;
|
|
5087
|
+
const isDefault = variant === void 0 || variant === "*";
|
|
5088
|
+
if (isDefault) {
|
|
5089
|
+
return section.variants.includes("*");
|
|
5090
|
+
}
|
|
5091
|
+
return section.variants.includes(variant);
|
|
5092
|
+
}
|
|
5093
|
+
function isStepActive(step, variant) {
|
|
5094
|
+
if (!step.variants) return true;
|
|
5095
|
+
const isDefault = variant === void 0 || variant === "*";
|
|
5096
|
+
if (isDefault) {
|
|
5097
|
+
return step.variants.includes("*");
|
|
5098
|
+
}
|
|
5099
|
+
return step.variants.includes(variant);
|
|
5100
|
+
}
|
|
5101
|
+
function getEffectiveChoices(recipe, variant) {
|
|
5102
|
+
const choices = { variant };
|
|
5103
|
+
if (variant === void 0 || variant === "*") return choices;
|
|
5104
|
+
const variantLower = variant.toLowerCase();
|
|
5105
|
+
for (const [itemId, alternatives] of recipe.choices.ingredientItems) {
|
|
5106
|
+
const matchIdx = alternatives.findIndex(
|
|
5107
|
+
(alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
|
|
5108
|
+
);
|
|
5109
|
+
if (matchIdx >= 0) {
|
|
5110
|
+
if (!choices.ingredientItems) choices.ingredientItems = /* @__PURE__ */ new Map();
|
|
5111
|
+
choices.ingredientItems.set(itemId, matchIdx);
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
for (const [groupId, subgroups] of recipe.choices.ingredientGroups) {
|
|
5115
|
+
const matchIdx = subgroups.findIndex(
|
|
5116
|
+
(sg) => sg.some(
|
|
5117
|
+
(alt) => alt.note && alt.note.toLowerCase().includes(variantLower)
|
|
5118
|
+
)
|
|
5119
|
+
);
|
|
5120
|
+
if (matchIdx >= 0) {
|
|
5121
|
+
if (!choices.ingredientGroups) choices.ingredientGroups = /* @__PURE__ */ new Map();
|
|
5122
|
+
choices.ingredientGroups.set(groupId, matchIdx);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
return choices;
|
|
5126
|
+
}
|
|
3026
5127
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3027
5128
|
0 && (module.exports = {
|
|
3028
5129
|
CategoryConfig,
|
|
3029
5130
|
NoProductCatalogForCartError,
|
|
3030
5131
|
NoShoppingListForCartError,
|
|
5132
|
+
Pantry,
|
|
3031
5133
|
ProductCatalog,
|
|
3032
5134
|
Recipe,
|
|
3033
5135
|
Section,
|
|
3034
5136
|
ShoppingCart,
|
|
3035
|
-
ShoppingList
|
|
5137
|
+
ShoppingList,
|
|
5138
|
+
convertQuantityToSystem,
|
|
5139
|
+
formatExtendedQuantity,
|
|
5140
|
+
formatItemQuantity,
|
|
5141
|
+
formatNumericValue,
|
|
5142
|
+
formatQuantity,
|
|
5143
|
+
formatQuantityWithUnit,
|
|
5144
|
+
formatSingleValue,
|
|
5145
|
+
formatUnit,
|
|
5146
|
+
getEffectiveChoices,
|
|
5147
|
+
hasAlternatives,
|
|
5148
|
+
isAlternativeSelected,
|
|
5149
|
+
isAndGroup,
|
|
5150
|
+
isGroupedItem,
|
|
5151
|
+
isSectionActive,
|
|
5152
|
+
isSimpleGroup,
|
|
5153
|
+
isStepActive,
|
|
5154
|
+
renderFractionAsVulgar
|
|
3036
5155
|
});
|
|
3037
5156
|
/* v8 ignore else -- @preserve */
|
|
5157
|
+
/* v8 ignore next -- @preserve: defensive fallback for ambiguous units without toBaseBySystem */
|
|
5158
|
+
/* v8 ignore start -- @preserve: defensive fallback that shouldn't happen with valid inputs */
|
|
5159
|
+
// v8 ignore else -- @preserve
|
|
5160
|
+
// v8 ignore if -- @preserve
|
|
3038
5161
|
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
5162
|
+
/* v8 ignore next 4 -- @preserve: defensive guard; regex always matches */
|
|
5163
|
+
// v8 ignore if -- @preserve: defensive type guard
|
|
3039
5164
|
/* v8 ignore if -- @preserve */
|
|
5165
|
+
// v8 ignore next -- @preserve
|
|
5166
|
+
// v8 ignore else -- @preserve: text quantities never reach the equivalence path
|
|
5167
|
+
// v8 ignore else --@preserve: defensive type guard
|
|
5168
|
+
// v8 ignore else -- @preserve: detection if
|
|
5169
|
+
/* v8 ignore else -- @preserve: only act when there are matches */
|
|
5170
|
+
/* v8 ignore else -- @preserve: initialization pattern */
|
|
3040
5171
|
//# sourceMappingURL=index.cjs.map
|