@tmlmt/cooklang-parser 3.0.0-alpha.9 → 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 +2520 -420
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +868 -129
- package/dist/index.d.ts +868 -129
- package/dist/index.js +2502 -419
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
package/dist/index.cjs
CHANGED
|
@@ -35,12 +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
43
|
ShoppingList: () => ShoppingList,
|
|
43
|
-
|
|
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
|
|
44
61
|
});
|
|
45
62
|
module.exports = __toCommonJS(index_exports);
|
|
46
63
|
|
|
@@ -105,7 +122,7 @@ var CategoryConfig = class {
|
|
|
105
122
|
}
|
|
106
123
|
};
|
|
107
124
|
|
|
108
|
-
// src/classes/
|
|
125
|
+
// src/classes/pantry.ts
|
|
109
126
|
var import_smol_toml = __toESM(require("smol-toml"), 1);
|
|
110
127
|
|
|
111
128
|
// node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
|
|
@@ -307,14 +324,19 @@ var i = (() => {
|
|
|
307
324
|
})();
|
|
308
325
|
|
|
309
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
|
+
);
|
|
310
333
|
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
311
|
-
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();
|
|
312
334
|
var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
|
|
313
335
|
var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
|
|
314
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();
|
|
315
337
|
var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
|
|
316
|
-
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();
|
|
317
|
-
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();
|
|
318
340
|
var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
|
|
319
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();
|
|
320
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();
|
|
@@ -329,12 +351,58 @@ var tokensRegex = new RegExp(
|
|
|
329
351
|
].map((r2) => r2.source).join("|"),
|
|
330
352
|
"gu"
|
|
331
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
|
+
);
|
|
332
368
|
var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
|
|
333
369
|
var blockCommentRegex = d().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
|
|
334
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();
|
|
335
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();
|
|
336
372
|
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
337
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
|
+
);
|
|
338
406
|
|
|
339
407
|
// src/units/definitions.ts
|
|
340
408
|
var units = [
|
|
@@ -344,7 +412,8 @@ var units = [
|
|
|
344
412
|
type: "mass",
|
|
345
413
|
system: "metric",
|
|
346
414
|
aliases: ["gram", "grams", "grammes"],
|
|
347
|
-
toBase: 1
|
|
415
|
+
toBase: 1,
|
|
416
|
+
maxValue: 999
|
|
348
417
|
},
|
|
349
418
|
{
|
|
350
419
|
name: "kg",
|
|
@@ -353,20 +422,28 @@ var units = [
|
|
|
353
422
|
aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
|
|
354
423
|
toBase: 1e3
|
|
355
424
|
},
|
|
356
|
-
// Mass (
|
|
425
|
+
// Mass (US/UK - identical in both systems)
|
|
357
426
|
{
|
|
358
427
|
name: "oz",
|
|
359
428
|
type: "mass",
|
|
360
|
-
system: "
|
|
429
|
+
system: "ambiguous",
|
|
361
430
|
aliases: ["ounce", "ounces"],
|
|
362
|
-
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] }
|
|
363
437
|
},
|
|
364
438
|
{
|
|
365
439
|
name: "lb",
|
|
366
440
|
type: "mass",
|
|
367
|
-
system: "
|
|
441
|
+
system: "ambiguous",
|
|
368
442
|
aliases: ["pound", "pounds"],
|
|
369
|
-
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] }
|
|
370
447
|
},
|
|
371
448
|
// Volume (Metric)
|
|
372
449
|
{
|
|
@@ -374,21 +451,26 @@ var units = [
|
|
|
374
451
|
type: "volume",
|
|
375
452
|
system: "metric",
|
|
376
453
|
aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
|
|
377
|
-
toBase: 1
|
|
454
|
+
toBase: 1,
|
|
455
|
+
maxValue: 999
|
|
378
456
|
},
|
|
379
457
|
{
|
|
380
458
|
name: "cl",
|
|
381
459
|
type: "volume",
|
|
382
460
|
system: "metric",
|
|
383
461
|
aliases: ["centiliter", "centiliters", "centilitre", "centilitres"],
|
|
384
|
-
toBase: 10
|
|
462
|
+
toBase: 10,
|
|
463
|
+
isBestUnit: false
|
|
464
|
+
// exists but not a "best" candidate
|
|
385
465
|
},
|
|
386
466
|
{
|
|
387
467
|
name: "dl",
|
|
388
468
|
type: "volume",
|
|
389
469
|
system: "metric",
|
|
390
470
|
aliases: ["deciliter", "deciliters", "decilitre", "decilitres"],
|
|
391
|
-
toBase: 100
|
|
471
|
+
toBase: 100,
|
|
472
|
+
isBestUnit: false
|
|
473
|
+
// exists but not a "best" candidate
|
|
392
474
|
},
|
|
393
475
|
{
|
|
394
476
|
name: "l",
|
|
@@ -397,55 +479,102 @@ var units = [
|
|
|
397
479
|
aliases: ["liter", "liters", "litre", "litres"],
|
|
398
480
|
toBase: 1e3
|
|
399
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)
|
|
400
492
|
{
|
|
401
493
|
name: "tsp",
|
|
402
494
|
type: "volume",
|
|
403
|
-
system: "
|
|
495
|
+
system: "ambiguous",
|
|
404
496
|
aliases: ["teaspoon", "teaspoons"],
|
|
405
|
-
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] }
|
|
406
503
|
},
|
|
407
504
|
{
|
|
408
505
|
name: "tbsp",
|
|
409
506
|
type: "volume",
|
|
410
|
-
system: "
|
|
507
|
+
system: "ambiguous",
|
|
411
508
|
aliases: ["tablespoon", "tablespoons"],
|
|
412
|
-
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 }
|
|
413
515
|
},
|
|
414
|
-
// Volume (
|
|
516
|
+
// Volume (Ambiguous: US/UK only)
|
|
415
517
|
{
|
|
416
518
|
name: "fl-oz",
|
|
417
519
|
type: "volume",
|
|
418
|
-
system: "
|
|
520
|
+
system: "ambiguous",
|
|
419
521
|
aliases: ["fluid ounce", "fluid ounces"],
|
|
420
|
-
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] }
|
|
421
528
|
},
|
|
422
529
|
{
|
|
423
530
|
name: "cup",
|
|
424
531
|
type: "volume",
|
|
425
|
-
system: "
|
|
532
|
+
system: "ambiguous",
|
|
426
533
|
aliases: ["cups"],
|
|
427
|
-
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 }
|
|
428
540
|
},
|
|
429
541
|
{
|
|
430
542
|
name: "pint",
|
|
431
543
|
type: "volume",
|
|
432
|
-
system: "
|
|
544
|
+
system: "ambiguous",
|
|
433
545
|
aliases: ["pints"],
|
|
434
|
-
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
|
|
435
554
|
},
|
|
436
555
|
{
|
|
437
556
|
name: "quart",
|
|
438
557
|
type: "volume",
|
|
439
|
-
system: "
|
|
558
|
+
system: "ambiguous",
|
|
440
559
|
aliases: ["quarts"],
|
|
441
|
-
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
|
|
442
568
|
},
|
|
443
569
|
{
|
|
444
570
|
name: "gallon",
|
|
445
571
|
type: "volume",
|
|
446
|
-
system: "
|
|
572
|
+
system: "ambiguous",
|
|
447
573
|
aliases: ["gallons"],
|
|
448
|
-
toBase: 3785.41
|
|
574
|
+
toBase: 3785.41,
|
|
575
|
+
// default: US
|
|
576
|
+
toBaseBySystem: { US: 3785.41, UK: 4546.09 },
|
|
577
|
+
fractions: { enabled: true, denominators: [2] }
|
|
449
578
|
},
|
|
450
579
|
// Count units (no conversion, but recognized as a type)
|
|
451
580
|
{
|
|
@@ -453,7 +582,8 @@ var units = [
|
|
|
453
582
|
type: "count",
|
|
454
583
|
system: "metric",
|
|
455
584
|
aliases: ["pieces", "pc"],
|
|
456
|
-
toBase: 1
|
|
585
|
+
toBase: 1,
|
|
586
|
+
maxValue: 999
|
|
457
587
|
}
|
|
458
588
|
];
|
|
459
589
|
var unitMap = /* @__PURE__ */ new Map();
|
|
@@ -477,8 +607,14 @@ function isNoUnit(unit) {
|
|
|
477
607
|
return resolveUnit(unit.name).name === NO_UNIT;
|
|
478
608
|
}
|
|
479
609
|
|
|
610
|
+
// src/units/conversion.ts
|
|
611
|
+
var import_big2 = __toESM(require("big.js"), 1);
|
|
612
|
+
|
|
480
613
|
// src/quantities/numeric.ts
|
|
481
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;
|
|
482
618
|
function gcd(a2, b) {
|
|
483
619
|
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
484
620
|
}
|
|
@@ -499,6 +635,41 @@ function simplifyFraction(num, den) {
|
|
|
499
635
|
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
500
636
|
}
|
|
501
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
|
+
}
|
|
502
673
|
function getNumericValue(v) {
|
|
503
674
|
if (v.type === "decimal") {
|
|
504
675
|
return v.decimal;
|
|
@@ -547,9 +718,35 @@ function addNumericValues(val1, val2) {
|
|
|
547
718
|
};
|
|
548
719
|
}
|
|
549
720
|
}
|
|
550
|
-
var toRoundedDecimal = (v) => {
|
|
721
|
+
var toRoundedDecimal = (v, precision = 3) => {
|
|
551
722
|
const value = v.type === "decimal" ? v.decimal : v.num / v.den;
|
|
552
|
-
|
|
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);
|
|
553
750
|
};
|
|
554
751
|
function multiplyQuantityValue(value, factor) {
|
|
555
752
|
if (value.type === "fixed") {
|
|
@@ -583,6 +780,143 @@ function getAverageValue(q) {
|
|
|
583
780
|
}
|
|
584
781
|
}
|
|
585
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
|
+
|
|
586
920
|
// src/errors.ts
|
|
587
921
|
var ReferencedItemCannotBeRedefinedError = class extends Error {
|
|
588
922
|
constructor(item_type, item_name, new_modifier) {
|
|
@@ -613,7 +947,7 @@ var NoProductMatchError = class extends Error {
|
|
|
613
947
|
constructor(item_name, code) {
|
|
614
948
|
const messageMap = {
|
|
615
949
|
incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
|
|
616
|
-
noProduct:
|
|
950
|
+
noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
|
|
617
951
|
textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
|
|
618
952
|
noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
|
|
619
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`
|
|
@@ -652,6 +986,20 @@ var InvalidQuantityFormat = class extends Error {
|
|
|
652
986
|
this.name = "InvalidQuantityFormat";
|
|
653
987
|
}
|
|
654
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
|
+
};
|
|
655
1003
|
|
|
656
1004
|
// src/utils/type_guards.ts
|
|
657
1005
|
function isGroup(x) {
|
|
@@ -661,11 +1009,14 @@ function isOrGroup(x) {
|
|
|
661
1009
|
return isGroup(x) && "or" in x;
|
|
662
1010
|
}
|
|
663
1011
|
function isAndGroup(x) {
|
|
664
|
-
return
|
|
1012
|
+
return "and" in x;
|
|
665
1013
|
}
|
|
666
1014
|
function isQuantity(x) {
|
|
667
1015
|
return x && typeof x === "object" && "quantity" in x;
|
|
668
1016
|
}
|
|
1017
|
+
function isSimpleGroup(entry) {
|
|
1018
|
+
return "quantity" in entry;
|
|
1019
|
+
}
|
|
669
1020
|
function isNumericValueIntegerLike(v) {
|
|
670
1021
|
if (v.type === "decimal") return Number.isInteger(v.decimal);
|
|
671
1022
|
return v.num % v.den === 0;
|
|
@@ -677,23 +1028,11 @@ function isValueIntegerLike(q) {
|
|
|
677
1028
|
}
|
|
678
1029
|
return isNumericValueIntegerLike(q.min) && isNumericValueIntegerLike(q.max);
|
|
679
1030
|
}
|
|
1031
|
+
function hasAlternatives(entry) {
|
|
1032
|
+
return "alternatives" in entry && Array.isArray(entry.alternatives) && entry.alternatives.length > 0;
|
|
1033
|
+
}
|
|
680
1034
|
|
|
681
1035
|
// src/quantities/mutations.ts
|
|
682
|
-
function extendAllUnits(q) {
|
|
683
|
-
if (isAndGroup(q)) {
|
|
684
|
-
return { and: q.and.map(extendAllUnits) };
|
|
685
|
-
} else if (isOrGroup(q)) {
|
|
686
|
-
return { or: q.or.map(extendAllUnits) };
|
|
687
|
-
} else {
|
|
688
|
-
const newQ = {
|
|
689
|
-
quantity: q.quantity
|
|
690
|
-
};
|
|
691
|
-
if (q.unit) {
|
|
692
|
-
newQ.unit = { name: q.unit };
|
|
693
|
-
}
|
|
694
|
-
return newQ;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
1036
|
function normalizeAllUnits(q) {
|
|
698
1037
|
if (isAndGroup(q)) {
|
|
699
1038
|
return { and: q.and.map(normalizeAllUnits) };
|
|
@@ -715,11 +1054,6 @@ function normalizeAllUnits(q) {
|
|
|
715
1054
|
return newQ;
|
|
716
1055
|
}
|
|
717
1056
|
}
|
|
718
|
-
var convertQuantityValue = (value, def, targetDef) => {
|
|
719
|
-
if (def.name === targetDef.name) return value;
|
|
720
|
-
const factor = def.toBase / targetDef.toBase;
|
|
721
|
-
return multiplyQuantityValue(value, factor);
|
|
722
|
-
};
|
|
723
1057
|
function getDefaultQuantityValue() {
|
|
724
1058
|
return { type: "fixed", value: { type: "decimal", decimal: 0 } };
|
|
725
1059
|
}
|
|
@@ -746,7 +1080,7 @@ function addQuantityValues(v1, v2) {
|
|
|
746
1080
|
);
|
|
747
1081
|
return { type: "range", min: newMin, max: newMax };
|
|
748
1082
|
}
|
|
749
|
-
function addQuantities(q1, q2) {
|
|
1083
|
+
function addQuantities(q1, q2, system) {
|
|
750
1084
|
const v1 = q1.quantity;
|
|
751
1085
|
const v2 = q2.quantity;
|
|
752
1086
|
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
@@ -764,35 +1098,129 @@ function addQuantities(q1, q2) {
|
|
|
764
1098
|
if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
|
|
765
1099
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
766
1100
|
}
|
|
767
|
-
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
|
+
}
|
|
768
1111
|
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
769
1112
|
}
|
|
770
1113
|
if (unit1Def && unit2Def) {
|
|
771
|
-
if (unit1Def
|
|
1114
|
+
if (!areUnitsConvertible(unit1Def, unit2Def)) {
|
|
772
1115
|
throw new IncompatibleUnitsError(
|
|
773
1116
|
`${unit1Def.type} (${q1.unit?.name})`,
|
|
774
1117
|
`${unit2Def.type} (${q2.unit?.name})`
|
|
775
1118
|
);
|
|
776
1119
|
}
|
|
777
|
-
let
|
|
778
|
-
if (
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
}
|
|
785
1133
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1134
|
+
return addAndFindBestUnit(v1, v2, unit1Def, unit2Def, effectiveSystem, [
|
|
1135
|
+
unit1Def,
|
|
1136
|
+
unit2Def
|
|
1137
|
+
]);
|
|
790
1138
|
}
|
|
791
1139
|
throw new IncompatibleUnitsError(
|
|
792
1140
|
q1.unit?.name,
|
|
793
1141
|
q2.unit?.name
|
|
794
1142
|
);
|
|
795
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
|
+
}
|
|
796
1224
|
function toPlainUnit(quantity) {
|
|
797
1225
|
if (isQuantity(quantity))
|
|
798
1226
|
return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
|
|
@@ -869,36 +1297,126 @@ var flattenPlainUnitGroup = (summed) => {
|
|
|
869
1297
|
}
|
|
870
1298
|
} else if (isAndGroup(summed)) {
|
|
871
1299
|
const andEntries = [];
|
|
1300
|
+
const standaloneEntries = [];
|
|
872
1301
|
const equivalentsList = [];
|
|
873
1302
|
for (const entry of summed.and) {
|
|
874
1303
|
if (isOrGroup(entry)) {
|
|
875
1304
|
const orEntries = entry.or;
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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;
|
|
1315
|
+
andEntries.push({
|
|
1316
|
+
quantity: primary.quantity,
|
|
1317
|
+
...primary.unit && { unit: primary.unit }
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
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 }
|
|
885
1332
|
});
|
|
886
1333
|
}
|
|
887
1334
|
}
|
|
888
1335
|
if (equivalentsList.length === 0) {
|
|
889
|
-
return andEntries;
|
|
1336
|
+
return [...andEntries, ...standaloneEntries];
|
|
890
1337
|
}
|
|
891
|
-
const result =
|
|
1338
|
+
const result = [];
|
|
1339
|
+
result.push({
|
|
892
1340
|
and: andEntries,
|
|
893
1341
|
equivalents: equivalentsList
|
|
894
|
-
};
|
|
895
|
-
|
|
1342
|
+
});
|
|
1343
|
+
result.push(...standaloneEntries);
|
|
1344
|
+
return result;
|
|
896
1345
|
} else {
|
|
897
1346
|
return [
|
|
898
1347
|
{ quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
|
|
899
1348
|
];
|
|
900
1349
|
}
|
|
901
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
|
+
}
|
|
902
1420
|
|
|
903
1421
|
// src/utils/parser_helpers.ts
|
|
904
1422
|
function flushPendingNote(section, noteItems) {
|
|
@@ -908,9 +1426,12 @@ function flushPendingNote(section, noteItems) {
|
|
|
908
1426
|
}
|
|
909
1427
|
return noteItems;
|
|
910
1428
|
}
|
|
911
|
-
function flushPendingItems(section, items) {
|
|
1429
|
+
function flushPendingItems(section, items, stepVariants, stepOptional) {
|
|
912
1430
|
if (items.length > 0) {
|
|
913
|
-
|
|
1431
|
+
const step = { type: "step", items: [...items] };
|
|
1432
|
+
if (stepVariants) step.variants = stepVariants;
|
|
1433
|
+
if (stepOptional) step.optional = true;
|
|
1434
|
+
section.content.push(step);
|
|
914
1435
|
items.length = 0;
|
|
915
1436
|
return true;
|
|
916
1437
|
}
|
|
@@ -1029,7 +1550,7 @@ function stringifyFixedValue(quantity) {
|
|
|
1029
1550
|
return String(quantity.value.decimal);
|
|
1030
1551
|
else return quantity.value.text;
|
|
1031
1552
|
}
|
|
1032
|
-
function
|
|
1553
|
+
function parseQuantityValue(input_str) {
|
|
1033
1554
|
const clean_str = String(input_str).trim();
|
|
1034
1555
|
if (rangeRegex.test(clean_str)) {
|
|
1035
1556
|
const range_parts = clean_str.split("-");
|
|
@@ -1039,19 +1560,253 @@ function parseQuantityInput(input_str) {
|
|
|
1039
1560
|
}
|
|
1040
1561
|
return { type: "fixed", value: parseFixedValue(clean_str) };
|
|
1041
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
|
+
}
|
|
1042
1728
|
function parseSimpleMetaVar(content, varName) {
|
|
1043
1729
|
const varMatch = content.match(
|
|
1044
1730
|
new RegExp(`^${varName}:\\s*(.*(?:\\r?\\n\\s+.*)*)+`, "m")
|
|
1045
1731
|
);
|
|
1046
1732
|
return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
|
|
1047
1733
|
}
|
|
1048
|
-
function
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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;
|
|
1053
1808
|
}
|
|
1054
|
-
return
|
|
1809
|
+
return void 0;
|
|
1055
1810
|
}
|
|
1056
1811
|
function parseListMetaVar(content, varName) {
|
|
1057
1812
|
const listMatch = content.match(
|
|
@@ -1067,6 +1822,115 @@ function parseListMetaVar(content, varName) {
|
|
|
1067
1822
|
return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
|
|
1068
1823
|
}
|
|
1069
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
|
+
}
|
|
1070
1934
|
function extractMetadata(content) {
|
|
1071
1935
|
const metadata = {};
|
|
1072
1936
|
let servings = void 0;
|
|
@@ -1074,20 +1938,49 @@ function extractMetadata(content) {
|
|
|
1074
1938
|
if (!metadataContent) {
|
|
1075
1939
|
return { metadata };
|
|
1076
1940
|
}
|
|
1077
|
-
|
|
1941
|
+
const handledKeys = /* @__PURE__ */ new Set([
|
|
1942
|
+
// Simple string fields
|
|
1078
1943
|
"title",
|
|
1079
|
-
"source",
|
|
1080
|
-
"source.name",
|
|
1081
|
-
"source.url",
|
|
1082
1944
|
"author",
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"
|
|
1945
|
+
"locale",
|
|
1946
|
+
"introduction",
|
|
1947
|
+
"description",
|
|
1948
|
+
"course",
|
|
1949
|
+
"category",
|
|
1950
|
+
"diet",
|
|
1951
|
+
"cuisine",
|
|
1952
|
+
"difficulty",
|
|
1953
|
+
// Source fields
|
|
1954
|
+
"source",
|
|
1955
|
+
"source.name",
|
|
1956
|
+
"source.url",
|
|
1957
|
+
"source.author",
|
|
1958
|
+
// Time fields
|
|
1959
|
+
"prep time",
|
|
1960
|
+
"time.prep",
|
|
1086
1961
|
"cook time",
|
|
1087
1962
|
"time.cook",
|
|
1088
1963
|
"time required",
|
|
1089
1964
|
"time",
|
|
1090
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",
|
|
1091
1984
|
"locale",
|
|
1092
1985
|
"introduction",
|
|
1093
1986
|
"description",
|
|
@@ -1095,25 +1988,97 @@ function extractMetadata(content) {
|
|
|
1095
1988
|
"category",
|
|
1096
1989
|
"diet",
|
|
1097
1990
|
"cuisine",
|
|
1098
|
-
"difficulty"
|
|
1099
|
-
"image",
|
|
1100
|
-
"picture"
|
|
1991
|
+
"difficulty"
|
|
1101
1992
|
]) {
|
|
1993
|
+
if (metaVar === "description" || metaVar === "introduction") {
|
|
1994
|
+
const blockValue = parseBlockScalarMetaVar(metadataContent, metaVar);
|
|
1995
|
+
if (blockValue) {
|
|
1996
|
+
metadata[metaVar] = blockValue;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1102
2000
|
const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
|
|
1103
2001
|
if (stringMetaValue) metadata[metaVar] = stringMetaValue;
|
|
1104
2002
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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;
|
|
1110
2067
|
}
|
|
1111
2068
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
+
}
|
|
1115
2080
|
}
|
|
1116
|
-
return { metadata, servings };
|
|
2081
|
+
return { metadata, servings, unitSystem };
|
|
1117
2082
|
}
|
|
1118
2083
|
function isPositiveIntegerString(str) {
|
|
1119
2084
|
return /^\d+$/.test(str);
|
|
@@ -1127,10 +2092,230 @@ function unionOfSets(s1, s2) {
|
|
|
1127
2092
|
}
|
|
1128
2093
|
function getAlternativeSignature(alternatives) {
|
|
1129
2094
|
if (!alternatives || alternatives.length === 0) return null;
|
|
1130
|
-
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(",");
|
|
1131
2096
|
}
|
|
1132
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
|
+
|
|
1133
2317
|
// src/classes/product_catalog.ts
|
|
2318
|
+
var import_smol_toml2 = __toESM(require("smol-toml"), 1);
|
|
1134
2319
|
var ProductCatalog = class {
|
|
1135
2320
|
constructor(tomlContent) {
|
|
1136
2321
|
__publicField(this, "products", []);
|
|
@@ -1142,7 +2327,7 @@ var ProductCatalog = class {
|
|
|
1142
2327
|
* @returns A parsed list of `ProductOption`.
|
|
1143
2328
|
*/
|
|
1144
2329
|
parse(tomlContent) {
|
|
1145
|
-
const catalogRaw =
|
|
2330
|
+
const catalogRaw = import_smol_toml2.default.parse(tomlContent);
|
|
1146
2331
|
this.products = [];
|
|
1147
2332
|
if (!this.isValidTomlContent(catalogRaw)) {
|
|
1148
2333
|
throw new InvalidProductCatalogFormat();
|
|
@@ -1159,7 +2344,7 @@ var ProductCatalog = class {
|
|
|
1159
2344
|
const sizeStrings = Array.isArray(size) ? size : [size];
|
|
1160
2345
|
const sizes = sizeStrings.map((sizeStr) => {
|
|
1161
2346
|
const sizeAndUnitRaw = sizeStr.split("%");
|
|
1162
|
-
const sizeParsed =
|
|
2347
|
+
const sizeParsed = parseQuantityValue(
|
|
1163
2348
|
sizeAndUnitRaw[0]
|
|
1164
2349
|
);
|
|
1165
2350
|
const productSize = { size: sizeParsed };
|
|
@@ -1215,7 +2400,7 @@ var ProductCatalog = class {
|
|
|
1215
2400
|
size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
|
|
1216
2401
|
};
|
|
1217
2402
|
}
|
|
1218
|
-
return
|
|
2403
|
+
return import_smol_toml2.default.stringify(grouped);
|
|
1219
2404
|
}
|
|
1220
2405
|
/**
|
|
1221
2406
|
* Adds a product to the catalog.
|
|
@@ -1272,8 +2457,10 @@ var Section = class {
|
|
|
1272
2457
|
/**
|
|
1273
2458
|
* Creates an instance of Section.
|
|
1274
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.
|
|
1275
2462
|
*/
|
|
1276
|
-
constructor(name = "") {
|
|
2463
|
+
constructor(name = "", variants, optional) {
|
|
1277
2464
|
/**
|
|
1278
2465
|
* The name of the section. Can be an empty string for the default (first) section.
|
|
1279
2466
|
* @defaultValue `""`
|
|
@@ -1281,7 +2468,13 @@ var Section = class {
|
|
|
1281
2468
|
__publicField(this, "name");
|
|
1282
2469
|
/** An array of steps and notes that make up the content of the section. */
|
|
1283
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");
|
|
1284
2475
|
this.name = name;
|
|
2476
|
+
if (variants) this.variants = variants;
|
|
2477
|
+
if (optional) this.optional = true;
|
|
1285
2478
|
}
|
|
1286
2479
|
/**
|
|
1287
2480
|
* Checks if the section is blank (has no name and no content).
|
|
@@ -1294,46 +2487,16 @@ var Section = class {
|
|
|
1294
2487
|
};
|
|
1295
2488
|
|
|
1296
2489
|
// src/quantities/alternatives.ts
|
|
1297
|
-
var
|
|
1298
|
-
|
|
1299
|
-
// src/units/conversion.ts
|
|
1300
|
-
var import_big2 = __toESM(require("big.js"), 1);
|
|
1301
|
-
function getUnitRatio(q1, q2) {
|
|
1302
|
-
const q1Value = getAverageValue(q1.quantity);
|
|
1303
|
-
const q2Value = getAverageValue(q2.quantity);
|
|
1304
|
-
const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
|
|
1305
|
-
if (typeof q1Value !== "number" || typeof q2Value !== "number") {
|
|
1306
|
-
throw Error(
|
|
1307
|
-
"One of both values is not a number, so a ratio cannot be computed"
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
|
|
1311
|
-
}
|
|
1312
|
-
function getBaseUnitRatio(q, qRef) {
|
|
1313
|
-
if ("toBase" in q.unit && "toBase" in qRef.unit) {
|
|
1314
|
-
return q.unit.toBase / qRef.unit.toBase;
|
|
1315
|
-
} else {
|
|
1316
|
-
return 1;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
2490
|
+
var import_big4 = __toESM(require("big.js"), 1);
|
|
1319
2491
|
|
|
1320
2492
|
// src/units/lookup.ts
|
|
1321
|
-
function areUnitsCompatible(u1, u2) {
|
|
1322
|
-
if (u1.name === u2.name) {
|
|
1323
|
-
return true;
|
|
1324
|
-
}
|
|
1325
|
-
if (u1.type !== "other" && u1.type === u2.type && u1.system === u2.system) {
|
|
1326
|
-
return true;
|
|
1327
|
-
}
|
|
1328
|
-
return false;
|
|
1329
|
-
}
|
|
1330
2493
|
function findListWithCompatibleQuantity(list, quantity) {
|
|
1331
2494
|
const quantityWithUnitDef = {
|
|
1332
2495
|
...quantity,
|
|
1333
2496
|
unit: resolveUnit(quantity.unit?.name)
|
|
1334
2497
|
};
|
|
1335
2498
|
return list.find(
|
|
1336
|
-
(l) => l.some((lq) =>
|
|
2499
|
+
(l) => l.some((lq) => areUnitsGroupable(lq.unit, quantityWithUnitDef.unit))
|
|
1337
2500
|
);
|
|
1338
2501
|
}
|
|
1339
2502
|
function findCompatibleQuantityWithinList(list, quantity) {
|
|
@@ -1347,10 +2510,14 @@ function findCompatibleQuantityWithinList(list, quantity) {
|
|
|
1347
2510
|
}
|
|
1348
2511
|
|
|
1349
2512
|
// src/utils/general.ts
|
|
2513
|
+
var import_big3 = __toESM(require("big.js"), 1);
|
|
1350
2514
|
var legacyDeepClone = (v) => {
|
|
1351
2515
|
if (v === null || typeof v !== "object") {
|
|
1352
2516
|
return v;
|
|
1353
2517
|
}
|
|
2518
|
+
if (v instanceof import_big3.default) {
|
|
2519
|
+
return new import_big3.default(v);
|
|
2520
|
+
}
|
|
1354
2521
|
if (v instanceof Map) {
|
|
1355
2522
|
return new Map(
|
|
1356
2523
|
Array.from(v.entries()).map(([k, val]) => [
|
|
@@ -1360,7 +2527,9 @@ var legacyDeepClone = (v) => {
|
|
|
1360
2527
|
);
|
|
1361
2528
|
}
|
|
1362
2529
|
if (v instanceof Set) {
|
|
1363
|
-
return new Set(
|
|
2530
|
+
return new Set(
|
|
2531
|
+
Array.from(v).map((val) => legacyDeepClone(val))
|
|
2532
|
+
);
|
|
1364
2533
|
}
|
|
1365
2534
|
if (v instanceof Date) {
|
|
1366
2535
|
return new Date(v.getTime());
|
|
@@ -1374,7 +2543,7 @@ var legacyDeepClone = (v) => {
|
|
|
1374
2543
|
}
|
|
1375
2544
|
return cloned;
|
|
1376
2545
|
};
|
|
1377
|
-
var deepClone = (v) =>
|
|
2546
|
+
var deepClone = (v) => legacyDeepClone(v);
|
|
1378
2547
|
|
|
1379
2548
|
// src/quantities/alternatives.ts
|
|
1380
2549
|
function getEquivalentUnitsLists(...quantities) {
|
|
@@ -1382,7 +2551,6 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1382
2551
|
const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
|
|
1383
2552
|
const unitLists = [];
|
|
1384
2553
|
const normalizeOrGroup = (og) => ({
|
|
1385
|
-
...og,
|
|
1386
2554
|
or: og.or.map((q) => ({
|
|
1387
2555
|
...q,
|
|
1388
2556
|
unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
|
|
@@ -1390,11 +2558,9 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1390
2558
|
});
|
|
1391
2559
|
function findLinkIndexForUnits(lists, unitsToCheck) {
|
|
1392
2560
|
return lists.findIndex((l) => {
|
|
1393
|
-
const
|
|
2561
|
+
const listItems = l.map((q) => resolveUnit(q.unit?.name));
|
|
1394
2562
|
return unitsToCheck.some(
|
|
1395
|
-
(u) =>
|
|
1396
|
-
(lu) => lu.name === u?.name || lu.system === u?.system && lu.type === u?.type && lu.type !== "other"
|
|
1397
|
-
)
|
|
2563
|
+
(u) => u && listItems.some((lu) => areUnitsGroupable(lu, u))
|
|
1398
2564
|
);
|
|
1399
2565
|
});
|
|
1400
2566
|
}
|
|
@@ -1406,16 +2572,18 @@ function getEquivalentUnitsLists(...quantities) {
|
|
|
1406
2572
|
unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
|
|
1407
2573
|
};
|
|
1408
2574
|
const commonQuantity = og.or.find(
|
|
1409
|
-
(q) => isQuantity(q) &&
|
|
2575
|
+
(q) => isQuantity(q) && areUnitsGroupable(q.unit, normalizedV.unit)
|
|
1410
2576
|
);
|
|
1411
2577
|
if (commonQuantity) {
|
|
1412
2578
|
acc.push(normalizedV);
|
|
1413
|
-
|
|
2579
|
+
if (!unitRatio) {
|
|
2580
|
+
unitRatio = getUnitRatio(normalizedV, commonQuantity);
|
|
2581
|
+
}
|
|
1414
2582
|
}
|
|
1415
2583
|
return acc;
|
|
1416
2584
|
}, []);
|
|
1417
2585
|
for (const newQ of og.or) {
|
|
1418
|
-
if (commonUnitList.some((q) =>
|
|
2586
|
+
if (commonUnitList.some((q) => areUnitsGroupable(q.unit, newQ.unit))) {
|
|
1419
2587
|
continue;
|
|
1420
2588
|
} else {
|
|
1421
2589
|
const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
|
|
@@ -1532,7 +2700,7 @@ function reduceOrsToFirstEquivalent(unitList, quantities) {
|
|
|
1532
2700
|
return reduceToQuantity(qListModified[0]);
|
|
1533
2701
|
});
|
|
1534
2702
|
}
|
|
1535
|
-
function addQuantitiesOrGroups(
|
|
2703
|
+
function addQuantitiesOrGroups(quantities, system) {
|
|
1536
2704
|
if (quantities.length === 0)
|
|
1537
2705
|
return {
|
|
1538
2706
|
sum: {
|
|
@@ -1562,7 +2730,7 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1562
2730
|
unit: resolveUnit(nextQ.unit?.name)
|
|
1563
2731
|
});
|
|
1564
2732
|
} else {
|
|
1565
|
-
const sumQ = addQuantities(existingQ, nextQ);
|
|
2733
|
+
const sumQ = addQuantities(existingQ, nextQ, system);
|
|
1566
2734
|
existingQ.quantity = sumQ.quantity;
|
|
1567
2735
|
existingQ.unit = resolveUnit(sumQ.unit?.name);
|
|
1568
2736
|
}
|
|
@@ -1572,7 +2740,7 @@ function addQuantitiesOrGroups(...quantities) {
|
|
|
1572
2740
|
}
|
|
1573
2741
|
return { sum: { and: sum }, unitsLists };
|
|
1574
2742
|
}
|
|
1575
|
-
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
2743
|
+
function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
|
|
1576
2744
|
const sumQuantities = isAndGroup(sum) ? sum.and : [sum];
|
|
1577
2745
|
const result = [];
|
|
1578
2746
|
const processedQuantities = /* @__PURE__ */ new Set();
|
|
@@ -1600,10 +2768,20 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1600
2768
|
}
|
|
1601
2769
|
return main.reduce((acc, v) => {
|
|
1602
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
|
+
);
|
|
1603
2775
|
const newValue = {
|
|
1604
2776
|
quantity: multiplyQuantityValue(
|
|
1605
|
-
|
|
1606
|
-
|
|
2777
|
+
{
|
|
2778
|
+
type: "fixed",
|
|
2779
|
+
value: {
|
|
2780
|
+
type: "decimal",
|
|
2781
|
+
decimal: valueInOriginalUnit.toNumber()
|
|
2782
|
+
}
|
|
2783
|
+
},
|
|
2784
|
+
(0, import_big4.default)(getAverageValue(equiv.quantity)).div(
|
|
1607
2785
|
getAverageValue(mainInList.quantity)
|
|
1608
2786
|
)
|
|
1609
2787
|
)
|
|
@@ -1611,7 +2789,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1611
2789
|
if (equiv.unit && !isNoUnit(equiv.unit)) {
|
|
1612
2790
|
newValue.unit = { name: equiv.unit.name };
|
|
1613
2791
|
}
|
|
1614
|
-
return addQuantities(acc, newValue);
|
|
2792
|
+
return addQuantities(acc, newValue, system);
|
|
1615
2793
|
}, initialValue);
|
|
1616
2794
|
});
|
|
1617
2795
|
if (main.length + equivalents.length > 1) {
|
|
@@ -1628,21 +2806,66 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
|
|
|
1628
2806
|
sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
|
|
1629
2807
|
return result;
|
|
1630
2808
|
}
|
|
1631
|
-
function addEquivalentsAndSimplify(
|
|
2809
|
+
function addEquivalentsAndSimplify(quantities, system) {
|
|
1632
2810
|
if (quantities.length === 1) {
|
|
1633
2811
|
return toPlainUnit(quantities[0]);
|
|
1634
2812
|
}
|
|
1635
|
-
const { sum, unitsLists } = addQuantitiesOrGroups(
|
|
1636
|
-
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2813
|
+
const { sum, unitsLists } = addQuantitiesOrGroups(quantities, system);
|
|
2814
|
+
const regrouped = regroupQuantitiesAndExpandEquivalents(
|
|
2815
|
+
sum,
|
|
2816
|
+
unitsLists,
|
|
2817
|
+
system
|
|
2818
|
+
);
|
|
1637
2819
|
if (regrouped.length === 1) {
|
|
1638
2820
|
return toPlainUnit(regrouped[0]);
|
|
1639
2821
|
} else {
|
|
1640
2822
|
return { and: regrouped.map(toPlainUnit) };
|
|
1641
2823
|
}
|
|
1642
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
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return equivalents.length > 0 ? equivalents : void 0;
|
|
2865
|
+
}
|
|
1643
2866
|
|
|
1644
2867
|
// src/classes/recipe.ts
|
|
1645
|
-
var
|
|
2868
|
+
var import_big5 = __toESM(require("big.js"), 1);
|
|
1646
2869
|
var _Recipe = class _Recipe {
|
|
1647
2870
|
/**
|
|
1648
2871
|
* Creates a new Recipe instance.
|
|
@@ -1658,7 +2881,8 @@ var _Recipe = class _Recipe {
|
|
|
1658
2881
|
*/
|
|
1659
2882
|
__publicField(this, "choices", {
|
|
1660
2883
|
ingredientItems: /* @__PURE__ */ new Map(),
|
|
1661
|
-
ingredientGroups: /* @__PURE__ */ new Map()
|
|
2884
|
+
ingredientGroups: /* @__PURE__ */ new Map(),
|
|
2885
|
+
variants: []
|
|
1662
2886
|
});
|
|
1663
2887
|
/**
|
|
1664
2888
|
* The parsed recipe ingredients.
|
|
@@ -1689,10 +2913,20 @@ var _Recipe = class _Recipe {
|
|
|
1689
2913
|
*/
|
|
1690
2914
|
__publicField(this, "servings");
|
|
1691
2915
|
_Recipe.itemCounts.set(this, 0);
|
|
2916
|
+
_Recipe.subgroupIndices.set(this, /* @__PURE__ */ new Map());
|
|
1692
2917
|
if (content) {
|
|
1693
2918
|
this.parse(content);
|
|
1694
2919
|
}
|
|
1695
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
|
+
}
|
|
1696
2930
|
/**
|
|
1697
2931
|
* Gets the current item count for this recipe.
|
|
1698
2932
|
*/
|
|
@@ -1715,27 +2949,17 @@ var _Recipe = class _Recipe {
|
|
|
1715
2949
|
*/
|
|
1716
2950
|
_parseArbitraryScalable(regexMatchGroups, intoArray) {
|
|
1717
2951
|
if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
const arbitrary = {
|
|
1730
|
-
quantity: value
|
|
1731
|
-
};
|
|
1732
|
-
if (name) arbitrary.name = name;
|
|
1733
|
-
if (unit) arbitrary.unit = unit;
|
|
1734
|
-
intoArray.push({
|
|
1735
|
-
type: "arbitrary",
|
|
1736
|
-
index: this.arbitraries.push(arbitrary) - 1
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
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
|
+
});
|
|
1739
2963
|
}
|
|
1740
2964
|
/**
|
|
1741
2965
|
* Parses text for arbitrary scalables and returns NoteItem array.
|
|
@@ -1749,13 +2973,13 @@ var _Recipe = class _Recipe {
|
|
|
1749
2973
|
for (const match of text.matchAll(globalRegex)) {
|
|
1750
2974
|
const idx = match.index;
|
|
1751
2975
|
if (idx > cursor) {
|
|
1752
|
-
noteItems.push(
|
|
2976
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor, idx)));
|
|
1753
2977
|
}
|
|
1754
2978
|
this._parseArbitraryScalable(match.groups, noteItems);
|
|
1755
2979
|
cursor = idx + match[0].length;
|
|
1756
2980
|
}
|
|
1757
2981
|
if (cursor < text.length) {
|
|
1758
|
-
noteItems.push(
|
|
2982
|
+
noteItems.push(...parseMarkdownSegments(text.slice(cursor)));
|
|
1759
2983
|
}
|
|
1760
2984
|
return noteItems;
|
|
1761
2985
|
}
|
|
@@ -1763,7 +2987,7 @@ var _Recipe = class _Recipe {
|
|
|
1763
2987
|
let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
|
|
1764
2988
|
const quantities = [];
|
|
1765
2989
|
while (quantityMatch?.groups) {
|
|
1766
|
-
const value = quantityMatch.groups.quantity ?
|
|
2990
|
+
const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
|
|
1767
2991
|
const unit = quantityMatch.groups.unit;
|
|
1768
2992
|
if (value) {
|
|
1769
2993
|
const newQuantity = { quantity: value };
|
|
@@ -1864,7 +3088,7 @@ var _Recipe = class _Recipe {
|
|
|
1864
3088
|
alternative.note = note;
|
|
1865
3089
|
}
|
|
1866
3090
|
if (itemQuantity) {
|
|
1867
|
-
alternative
|
|
3091
|
+
Object.assign(alternative, itemQuantity);
|
|
1868
3092
|
}
|
|
1869
3093
|
alternatives.push(alternative);
|
|
1870
3094
|
testString = groups.ingredientAlternative || "";
|
|
@@ -1907,6 +3131,7 @@ var _Recipe = class _Recipe {
|
|
|
1907
3131
|
if (!match?.groups) return;
|
|
1908
3132
|
const groups = match.groups;
|
|
1909
3133
|
const groupKey = groups.gIngredientGroupKey;
|
|
3134
|
+
const subgroupKey = groups.gIngredientSubgroupKey;
|
|
1910
3135
|
let name = groups.gmIngredientName || groups.gsIngredientName;
|
|
1911
3136
|
const preparation = groups.gIngredientPreparation;
|
|
1912
3137
|
const modifiers = groups.gIngredientModifiers;
|
|
@@ -1972,9 +3197,14 @@ var _Recipe = class _Recipe {
|
|
|
1972
3197
|
displayName
|
|
1973
3198
|
};
|
|
1974
3199
|
if (itemQuantity) {
|
|
1975
|
-
alternative
|
|
3200
|
+
Object.assign(alternative, itemQuantity);
|
|
3201
|
+
}
|
|
3202
|
+
const note = groups.gIngredientNote?.trim();
|
|
3203
|
+
if (note) {
|
|
3204
|
+
alternative.note = note;
|
|
1976
3205
|
}
|
|
1977
|
-
const
|
|
3206
|
+
const existingSubgroups = this.choices.ingredientGroups.get(groupKey);
|
|
3207
|
+
const existingAlternativesFlat = existingSubgroups?.flat();
|
|
1978
3208
|
function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
|
|
1979
3209
|
const ingredient = ingredients[ingredientIdx];
|
|
1980
3210
|
if (ingredient) {
|
|
@@ -1985,8 +3215,8 @@ var _Recipe = class _Recipe {
|
|
|
1985
3215
|
}
|
|
1986
3216
|
}
|
|
1987
3217
|
}
|
|
1988
|
-
if (
|
|
1989
|
-
for (const alt of
|
|
3218
|
+
if (existingAlternativesFlat) {
|
|
3219
|
+
for (const alt of existingAlternativesFlat) {
|
|
1990
3220
|
upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
|
|
1991
3221
|
upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
|
|
1992
3222
|
}
|
|
@@ -1998,14 +3228,35 @@ var _Recipe = class _Recipe {
|
|
|
1998
3228
|
group: groupKey,
|
|
1999
3229
|
alternatives: [alternative]
|
|
2000
3230
|
};
|
|
3231
|
+
if (subgroupKey !== void 0) {
|
|
3232
|
+
newItem.subgroup = subgroupKey;
|
|
3233
|
+
}
|
|
2001
3234
|
items.push(newItem);
|
|
2002
3235
|
const choiceAlternative = deepClone(alternative);
|
|
2003
3236
|
choiceAlternative.itemId = id;
|
|
2004
3237
|
const existingChoice = this.choices.ingredientGroups.get(groupKey);
|
|
3238
|
+
const sgMap = _Recipe.subgroupIndices.get(this);
|
|
2005
3239
|
if (!existingChoice) {
|
|
2006
|
-
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
|
+
}
|
|
2007
3258
|
} else {
|
|
2008
|
-
existingChoice.push(choiceAlternative);
|
|
3259
|
+
existingChoice.push([choiceAlternative]);
|
|
2009
3260
|
}
|
|
2010
3261
|
}
|
|
2011
3262
|
/**
|
|
@@ -2019,7 +3270,7 @@ var _Recipe = class _Recipe {
|
|
|
2019
3270
|
* Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
|
|
2020
3271
|
* @internal
|
|
2021
3272
|
*/
|
|
2022
|
-
|
|
3273
|
+
_populateIngredientQuantities() {
|
|
2023
3274
|
for (const ing of this.ingredients) {
|
|
2024
3275
|
delete ing.quantities;
|
|
2025
3276
|
delete ing.usedAsPrimary;
|
|
@@ -2040,35 +3291,13 @@ var _Recipe = class _Recipe {
|
|
|
2040
3291
|
}
|
|
2041
3292
|
}
|
|
2042
3293
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
* When no options are provided, returns all recipe ingredients with quantities
|
|
2048
|
-
* calculated using primary alternatives (same as after parsing).
|
|
2049
|
-
*
|
|
2050
|
-
* @param options - Options for filtering and choice selection:
|
|
2051
|
-
* - `section`: Filter to a specific section (Section object or 0-based index)
|
|
2052
|
-
* - `step`: Filter to a specific step (Step object or 0-based index)
|
|
2053
|
-
* - `choices`: Choices for alternative ingredients (defaults to primary)
|
|
2054
|
-
* @returns Array of Ingredient objects with quantities populated
|
|
2055
|
-
*
|
|
2056
|
-
* @example
|
|
2057
|
-
* ```typescript
|
|
2058
|
-
* // Get all ingredients with primary alternatives
|
|
2059
|
-
* const ingredients = recipe.getIngredientQuantities();
|
|
2060
|
-
*
|
|
2061
|
-
* // Get ingredients for a specific section
|
|
2062
|
-
* const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
|
|
2063
|
-
*
|
|
2064
|
-
* // Get ingredients with specific choices applied
|
|
2065
|
-
* const withChoices = recipe.getIngredientQuantities({
|
|
2066
|
-
* choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
|
|
2067
|
-
* });
|
|
2068
|
-
* ```
|
|
2069
|
-
*/
|
|
2070
|
-
getIngredientQuantities(options) {
|
|
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) {
|
|
2071
3298
|
const { section, step, choices } = options || {};
|
|
3299
|
+
const activeVariant = choices?.variant;
|
|
3300
|
+
const isDefaultVariant = activeVariant === void 0 || activeVariant === "*";
|
|
2072
3301
|
const sectionsToProcess = section !== void 0 ? (() => {
|
|
2073
3302
|
const idx = typeof section === "number" ? section : this.sections.indexOf(section);
|
|
2074
3303
|
return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
|
|
@@ -2076,80 +3305,155 @@ var _Recipe = class _Recipe {
|
|
|
2076
3305
|
const ingredientGroups = /* @__PURE__ */ new Map();
|
|
2077
3306
|
const selectedIndices = /* @__PURE__ */ new Set();
|
|
2078
3307
|
const referencedIndices = /* @__PURE__ */ new Set();
|
|
3308
|
+
const dynamicOptionalIndices = /* @__PURE__ */ new Set();
|
|
2079
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
|
+
}
|
|
2080
3317
|
const allSteps = currentSection.content.filter(
|
|
2081
3318
|
(item) => item.type === "step"
|
|
2082
3319
|
);
|
|
3320
|
+
const isOptionalSection = currentSection.optional === true;
|
|
2083
3321
|
const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
|
|
2084
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;
|
|
2085
3331
|
for (const item of currentStep.items.filter(
|
|
2086
3332
|
(item2) => item2.type === "ingredient"
|
|
2087
3333
|
)) {
|
|
2088
3334
|
const isGrouped = "group" in item && item.group !== void 0;
|
|
2089
|
-
const
|
|
3335
|
+
const groupSubgroups = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
|
|
2090
3336
|
let selectedAltIndex = 0;
|
|
2091
|
-
let isSelected
|
|
2092
|
-
let hasExplicitChoice
|
|
3337
|
+
let isSelected;
|
|
3338
|
+
let hasExplicitChoice;
|
|
2093
3339
|
if (isGrouped) {
|
|
2094
3340
|
const groupChoice = choices?.ingredientGroups?.get(item.group);
|
|
2095
3341
|
hasExplicitChoice = groupChoice !== void 0;
|
|
2096
|
-
|
|
2097
|
-
|
|
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;
|
|
3366
|
+
}
|
|
2098
3367
|
} else {
|
|
2099
3368
|
const itemChoice = choices?.ingredientItems?.get(item.id);
|
|
2100
3369
|
hasExplicitChoice = itemChoice !== void 0;
|
|
2101
|
-
|
|
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;
|
|
3382
|
+
}
|
|
2102
3383
|
isSelected = true;
|
|
2103
3384
|
}
|
|
2104
3385
|
const alternative = item.alternatives[selectedAltIndex];
|
|
2105
3386
|
if (!alternative || !isSelected) continue;
|
|
2106
3387
|
selectedIndices.add(alternative.index);
|
|
2107
|
-
|
|
2108
|
-
|
|
3388
|
+
if (isOptionalStep) {
|
|
3389
|
+
dynamicOptionalIndices.add(alternative.index);
|
|
3390
|
+
}
|
|
3391
|
+
const allAltsFlat = isGrouped ? groupSubgroups.flat() : item.alternatives;
|
|
3392
|
+
for (const alt of allAltsFlat) {
|
|
2109
3393
|
referencedIndices.add(alt.index);
|
|
2110
3394
|
}
|
|
2111
|
-
if (!alternative.
|
|
3395
|
+
if (!alternative.quantity) continue;
|
|
2112
3396
|
const baseQty = {
|
|
2113
|
-
quantity: alternative.
|
|
2114
|
-
...alternative.
|
|
2115
|
-
unit: alternative.
|
|
3397
|
+
quantity: alternative.quantity,
|
|
3398
|
+
...alternative.unit && {
|
|
3399
|
+
unit: alternative.unit
|
|
2116
3400
|
}
|
|
2117
3401
|
};
|
|
2118
|
-
const quantityEntry = alternative.
|
|
3402
|
+
const quantityEntry = alternative.equivalents?.length ? { or: [baseQty, ...alternative.equivalents] } : baseQty;
|
|
2119
3403
|
let alternativeRefs;
|
|
2120
|
-
if (!hasExplicitChoice &&
|
|
2121
|
-
|
|
2122
|
-
(
|
|
2123
|
-
)
|
|
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
|
|
3412
|
+
};
|
|
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];
|
|
3426
|
+
}
|
|
3427
|
+
return ref;
|
|
3428
|
+
})
|
|
3429
|
+
);
|
|
3430
|
+
} else if (!hasExplicitChoice && !isGrouped && allAltsFlat.length > 1) {
|
|
3431
|
+
alternativeRefs = allAltsFlat.filter((alt) => alt.index !== alternative.index).map((otherAlt) => {
|
|
2124
3432
|
const ref = { index: otherAlt.index };
|
|
2125
|
-
if (otherAlt.
|
|
3433
|
+
if (otherAlt.quantity) {
|
|
2126
3434
|
const altQty = {
|
|
2127
|
-
quantity: otherAlt.
|
|
2128
|
-
...otherAlt.
|
|
2129
|
-
unit: otherAlt.
|
|
3435
|
+
quantity: otherAlt.quantity,
|
|
3436
|
+
...otherAlt.unit && {
|
|
3437
|
+
unit: otherAlt.unit.name
|
|
2130
3438
|
},
|
|
2131
|
-
...otherAlt.
|
|
2132
|
-
equivalents: otherAlt.
|
|
3439
|
+
...otherAlt.equivalents && {
|
|
3440
|
+
equivalents: otherAlt.equivalents.map(
|
|
2133
3441
|
(eq) => toPlainUnit(eq)
|
|
2134
3442
|
)
|
|
2135
3443
|
}
|
|
2136
3444
|
};
|
|
2137
3445
|
ref.quantities = [altQty];
|
|
2138
3446
|
}
|
|
2139
|
-
return ref;
|
|
3447
|
+
return [ref];
|
|
2140
3448
|
});
|
|
2141
3449
|
}
|
|
2142
3450
|
const altIndices = getAlternativeSignature(alternativeRefs) ?? "";
|
|
2143
3451
|
let signature;
|
|
2144
3452
|
if (isGrouped) {
|
|
2145
|
-
const resolvedUnit = resolveUnit(
|
|
2146
|
-
alternative.itemQuantity.unit?.name
|
|
2147
|
-
);
|
|
3453
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
2148
3454
|
signature = `group:${item.group}|${altIndices}|${resolvedUnit.type}`;
|
|
2149
3455
|
} else if (altIndices) {
|
|
2150
|
-
const resolvedUnit = resolveUnit(
|
|
2151
|
-
alternative.itemQuantity.unit?.name
|
|
2152
|
-
);
|
|
3456
|
+
const resolvedUnit = resolveUnit(alternative.unit?.name);
|
|
2153
3457
|
signature = `${altIndices}|${resolvedUnit.type}}`;
|
|
2154
3458
|
} else {
|
|
2155
3459
|
signature = null;
|
|
@@ -2161,42 +3465,145 @@ var _Recipe = class _Recipe {
|
|
|
2161
3465
|
if (!groupsForIng.has(signature)) {
|
|
2162
3466
|
groupsForIng.set(signature, {
|
|
2163
3467
|
quantities: [],
|
|
2164
|
-
alternativeQuantities: /* @__PURE__ */ new Map()
|
|
3468
|
+
alternativeQuantities: /* @__PURE__ */ new Map(),
|
|
3469
|
+
alternativeSubgroups: []
|
|
2165
3470
|
});
|
|
2166
3471
|
}
|
|
2167
3472
|
const group = groupsForIng.get(signature);
|
|
2168
3473
|
group.quantities.push(quantityEntry);
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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) {
|
|
3481
|
+
if (!group.alternativeQuantities.has(ref.index)) {
|
|
3482
|
+
group.alternativeQuantities.set(ref.index, []);
|
|
3483
|
+
}
|
|
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);
|
|
3497
|
+
}
|
|
2186
3498
|
}
|
|
2187
3499
|
}
|
|
2188
3500
|
}
|
|
2189
3501
|
}
|
|
2190
3502
|
}
|
|
2191
3503
|
}
|
|
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);
|
|
2192
3534
|
const result = [];
|
|
2193
3535
|
for (let index = 0; index < this.ingredients.length; index++) {
|
|
2194
3536
|
if (!referencedIndices.has(index)) continue;
|
|
2195
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);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
result.push({
|
|
3553
|
+
name: orig.name,
|
|
3554
|
+
...usedAsPrimary && { usedAsPrimary: true },
|
|
3555
|
+
...flags && { flags },
|
|
3556
|
+
quantities
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
return result;
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
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();
|
|
3578
|
+
*
|
|
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
|
+
* ```
|
|
3587
|
+
*/
|
|
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
|
+
}
|
|
2196
3603
|
const ing = {
|
|
2197
3604
|
name: orig.name,
|
|
2198
3605
|
...orig.preparation && { preparation: orig.preparation },
|
|
2199
|
-
...
|
|
3606
|
+
...flags && { flags },
|
|
2200
3607
|
...orig.extras && { extras: orig.extras }
|
|
2201
3608
|
};
|
|
2202
3609
|
if (selectedIndices.has(index)) {
|
|
@@ -2205,19 +3612,30 @@ var _Recipe = class _Recipe {
|
|
|
2205
3612
|
if (groupsForIng) {
|
|
2206
3613
|
const quantityGroups = [];
|
|
2207
3614
|
for (const [, group] of groupsForIng) {
|
|
2208
|
-
const summed = addEquivalentsAndSimplify(
|
|
3615
|
+
const summed = addEquivalentsAndSimplify(
|
|
3616
|
+
group.quantities,
|
|
3617
|
+
this.unitSystem
|
|
3618
|
+
);
|
|
2209
3619
|
const flattened = flattenPlainUnitGroup(summed);
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
+
}
|
|
2221
3639
|
for (const gq of flattened) {
|
|
2222
3640
|
if ("and" in gq) {
|
|
2223
3641
|
quantityGroups.push({
|
|
@@ -2244,23 +3662,82 @@ var _Recipe = class _Recipe {
|
|
|
2244
3662
|
}
|
|
2245
3663
|
return result;
|
|
2246
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
|
+
}
|
|
3695
|
+
}
|
|
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
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
return this.cookware.filter(
|
|
3715
|
+
(cw, idx) => cookwareIndices.has(idx) && !cw.flags?.includes("hidden")
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
2247
3718
|
/**
|
|
2248
3719
|
* Parses a recipe from a string.
|
|
2249
3720
|
* @param content - The recipe content to parse.
|
|
2250
3721
|
*/
|
|
2251
3722
|
parse(content) {
|
|
2252
3723
|
const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
|
|
2253
|
-
const { metadata, servings } = extractMetadata(content);
|
|
3724
|
+
const { metadata, servings, unitSystem } = extractMetadata(content);
|
|
2254
3725
|
this.metadata = metadata;
|
|
2255
3726
|
this.servings = servings;
|
|
3727
|
+
if (unitSystem) _Recipe.unitSystems.set(this, unitSystem);
|
|
2256
3728
|
let blankLineBefore = true;
|
|
2257
3729
|
let section = new Section();
|
|
2258
3730
|
const items = [];
|
|
2259
3731
|
let noteText = "";
|
|
2260
3732
|
let inNote = false;
|
|
3733
|
+
let stepVariants;
|
|
3734
|
+
let stepOptional;
|
|
3735
|
+
const discoveredVariants = /* @__PURE__ */ new Set();
|
|
2261
3736
|
for (const line of cleanContent) {
|
|
2262
3737
|
if (line.trim().length === 0) {
|
|
2263
|
-
flushPendingItems(section, items);
|
|
3738
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3739
|
+
stepVariants = void 0;
|
|
3740
|
+
stepOptional = void 0;
|
|
2264
3741
|
flushPendingNote(
|
|
2265
3742
|
section,
|
|
2266
3743
|
noteText ? this._parseNoteText(noteText) : []
|
|
@@ -2271,26 +3748,42 @@ var _Recipe = class _Recipe {
|
|
|
2271
3748
|
continue;
|
|
2272
3749
|
}
|
|
2273
3750
|
if (line.startsWith("=")) {
|
|
2274
|
-
flushPendingItems(section, items);
|
|
3751
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3752
|
+
stepVariants = void 0;
|
|
3753
|
+
stepOptional = void 0;
|
|
2275
3754
|
flushPendingNote(
|
|
2276
3755
|
section,
|
|
2277
3756
|
noteText ? this._parseNoteText(noteText) : []
|
|
2278
3757
|
);
|
|
2279
3758
|
noteText = "";
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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;
|
|
2285
3772
|
}
|
|
2286
|
-
|
|
3773
|
+
sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
|
|
2287
3774
|
}
|
|
3775
|
+
if (!section.isBlank()) {
|
|
3776
|
+
this.sections.push(section);
|
|
3777
|
+
}
|
|
3778
|
+
section = new Section(sectionName, sectionVariants, sectionOptional);
|
|
2288
3779
|
blankLineBefore = true;
|
|
2289
3780
|
inNote = false;
|
|
2290
3781
|
continue;
|
|
2291
3782
|
}
|
|
2292
3783
|
if (blankLineBefore && line.startsWith(">")) {
|
|
2293
|
-
flushPendingItems(section, items);
|
|
3784
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
3785
|
+
stepVariants = void 0;
|
|
3786
|
+
stepOptional = void 0;
|
|
2294
3787
|
noteText = line.substring(1).trim();
|
|
2295
3788
|
inNote = true;
|
|
2296
3789
|
blankLineBefore = false;
|
|
@@ -2305,11 +3798,31 @@ var _Recipe = class _Recipe {
|
|
|
2305
3798
|
blankLineBefore = false;
|
|
2306
3799
|
continue;
|
|
2307
3800
|
}
|
|
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
|
+
}
|
|
2308
3821
|
let cursor = 0;
|
|
2309
|
-
for (const match of
|
|
3822
|
+
for (const match of currentLine.matchAll(tokensRegex)) {
|
|
2310
3823
|
const idx = match.index;
|
|
2311
3824
|
if (idx > cursor) {
|
|
2312
|
-
items.push(
|
|
3825
|
+
items.push(...parseMarkdownSegments(currentLine.slice(cursor, idx)));
|
|
2313
3826
|
}
|
|
2314
3827
|
const groups = match.groups;
|
|
2315
3828
|
if (groups.mIngredientName || groups.sIngredientName) {
|
|
@@ -2328,7 +3841,7 @@ var _Recipe = class _Recipe {
|
|
|
2328
3841
|
if (modifiers !== void 0 && modifiers.includes("-")) {
|
|
2329
3842
|
flags.push("hidden");
|
|
2330
3843
|
}
|
|
2331
|
-
const quantity = quantityRaw ?
|
|
3844
|
+
const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
|
|
2332
3845
|
const newCookware = {
|
|
2333
3846
|
name
|
|
2334
3847
|
};
|
|
@@ -2360,7 +3873,7 @@ var _Recipe = class _Recipe {
|
|
|
2360
3873
|
throw new Error("Timer missing unit");
|
|
2361
3874
|
}
|
|
2362
3875
|
const name = groups.timerName || void 0;
|
|
2363
|
-
const duration =
|
|
3876
|
+
const duration = parseQuantityValue(durationStr);
|
|
2364
3877
|
const timerObj = {
|
|
2365
3878
|
name,
|
|
2366
3879
|
duration,
|
|
@@ -2370,17 +3883,22 @@ var _Recipe = class _Recipe {
|
|
|
2370
3883
|
}
|
|
2371
3884
|
cursor = idx + match[0].length;
|
|
2372
3885
|
}
|
|
2373
|
-
if (cursor <
|
|
2374
|
-
items.push(
|
|
3886
|
+
if (cursor < currentLine.length) {
|
|
3887
|
+
items.push(...parseMarkdownSegments(currentLine.slice(cursor)));
|
|
2375
3888
|
}
|
|
2376
3889
|
blankLineBefore = false;
|
|
2377
3890
|
}
|
|
2378
|
-
flushPendingItems(section, items);
|
|
3891
|
+
flushPendingItems(section, items, stepVariants, stepOptional);
|
|
2379
3892
|
flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
|
|
2380
3893
|
if (!section.isBlank()) {
|
|
2381
3894
|
this.sections.push(section);
|
|
2382
3895
|
}
|
|
2383
|
-
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();
|
|
2384
3902
|
}
|
|
2385
3903
|
/**
|
|
2386
3904
|
* Scales the recipe to a new number of servings. In practice, it calls
|
|
@@ -2395,7 +3913,7 @@ var _Recipe = class _Recipe {
|
|
|
2395
3913
|
if (originalServings === void 0 || originalServings === 0) {
|
|
2396
3914
|
originalServings = 1;
|
|
2397
3915
|
}
|
|
2398
|
-
const factor = (0,
|
|
3916
|
+
const factor = (0, import_big5.default)(newServings).div(originalServings);
|
|
2399
3917
|
return this.scaleBy(factor);
|
|
2400
3918
|
}
|
|
2401
3919
|
/**
|
|
@@ -2410,18 +3928,19 @@ var _Recipe = class _Recipe {
|
|
|
2410
3928
|
if (originalServings === void 0 || originalServings === 0) {
|
|
2411
3929
|
originalServings = 1;
|
|
2412
3930
|
}
|
|
3931
|
+
const unitSystem = this.unitSystem;
|
|
2413
3932
|
function scaleAlternativesBy(alternatives, factor2) {
|
|
2414
3933
|
for (const alternative of alternatives) {
|
|
2415
|
-
if (alternative.
|
|
2416
|
-
const scaleFactor = alternative.
|
|
2417
|
-
if (alternative.
|
|
2418
|
-
alternative.
|
|
2419
|
-
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,
|
|
2420
3939
|
scaleFactor
|
|
2421
3940
|
);
|
|
2422
3941
|
}
|
|
2423
|
-
if (alternative.
|
|
2424
|
-
alternative.
|
|
3942
|
+
if (alternative.equivalents) {
|
|
3943
|
+
alternative.equivalents = alternative.equivalents.map(
|
|
2425
3944
|
(altQuantity) => {
|
|
2426
3945
|
if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
|
|
2427
3946
|
return altQuantity;
|
|
@@ -2437,6 +3956,20 @@ var _Recipe = class _Recipe {
|
|
|
2437
3956
|
}
|
|
2438
3957
|
);
|
|
2439
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
|
+
}
|
|
2440
3973
|
}
|
|
2441
3974
|
}
|
|
2442
3975
|
}
|
|
@@ -2451,8 +3984,10 @@ var _Recipe = class _Recipe {
|
|
|
2451
3984
|
}
|
|
2452
3985
|
}
|
|
2453
3986
|
}
|
|
2454
|
-
for (const
|
|
2455
|
-
|
|
3987
|
+
for (const subgroups of newRecipe.choices.ingredientGroups.values()) {
|
|
3988
|
+
for (const subgroup of subgroups) {
|
|
3989
|
+
scaleAlternativesBy(subgroup, factor);
|
|
3990
|
+
}
|
|
2456
3991
|
}
|
|
2457
3992
|
for (const alternatives of newRecipe.choices.ingredientItems.values()) {
|
|
2458
3993
|
scaleAlternativesBy(alternatives, factor);
|
|
@@ -2462,39 +3997,198 @@ var _Recipe = class _Recipe {
|
|
|
2462
3997
|
arbitrary.quantity,
|
|
2463
3998
|
factor
|
|
2464
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;
|
|
2465
4006
|
}
|
|
2466
|
-
newRecipe.
|
|
2467
|
-
newRecipe.servings = (0,
|
|
2468
|
-
|
|
2469
|
-
if (
|
|
2470
|
-
|
|
2471
|
-
String(this.metadata.servings).replace(",", ".")
|
|
2472
|
-
);
|
|
2473
|
-
newRecipe.metadata.servings = String(
|
|
2474
|
-
(0, import_big4.default)(servingsValue).times(factor).toNumber()
|
|
2475
|
-
);
|
|
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();
|
|
2476
4012
|
}
|
|
2477
4013
|
}
|
|
2478
4014
|
if (newRecipe.metadata.yield && this.metadata.yield) {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
|
2482
4021
|
);
|
|
2483
|
-
|
|
2484
|
-
|
|
4022
|
+
const optimized = applyBestUnit(
|
|
4023
|
+
{ quantity: scaledQuantity, unit: original.unit },
|
|
4024
|
+
unitSystem
|
|
2485
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;
|
|
2486
4033
|
}
|
|
2487
4034
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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];
|
|
4074
|
+
}
|
|
4075
|
+
return newPrimary;
|
|
4076
|
+
}
|
|
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
|
|
4107
|
+
);
|
|
4108
|
+
return buildNewPrimary(
|
|
4109
|
+
targetEquiv,
|
|
4110
|
+
oldPrimary,
|
|
4111
|
+
remainingEquivalents,
|
|
4112
|
+
alternative.scalable,
|
|
4113
|
+
targetEquiv.unit?.integerProtected,
|
|
4114
|
+
"swapped"
|
|
2492
4115
|
);
|
|
2493
|
-
|
|
2494
|
-
|
|
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"
|
|
2495
4126
|
);
|
|
2496
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
|
+
}
|
|
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
|
+
}
|
|
2497
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);
|
|
2498
4192
|
return newRecipe;
|
|
2499
4193
|
}
|
|
2500
4194
|
/**
|
|
@@ -2519,7 +4213,11 @@ var _Recipe = class _Recipe {
|
|
|
2519
4213
|
newRecipe.metadata = deepClone(this.metadata);
|
|
2520
4214
|
newRecipe.ingredients = deepClone(this.ingredients);
|
|
2521
4215
|
newRecipe.sections = this.sections.map((section) => {
|
|
2522
|
-
const newSection = new Section(
|
|
4216
|
+
const newSection = new Section(
|
|
4217
|
+
section.name,
|
|
4218
|
+
section.variants,
|
|
4219
|
+
section.optional
|
|
4220
|
+
);
|
|
2523
4221
|
newSection.content = deepClone(section.content);
|
|
2524
4222
|
return newSection;
|
|
2525
4223
|
});
|
|
@@ -2530,21 +4228,30 @@ var _Recipe = class _Recipe {
|
|
|
2530
4228
|
return newRecipe;
|
|
2531
4229
|
}
|
|
2532
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());
|
|
2533
4236
|
/**
|
|
2534
4237
|
* External storage for item count (not a property on instances).
|
|
2535
4238
|
* Used for giving ID numbers to items during parsing.
|
|
2536
4239
|
*/
|
|
2537
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());
|
|
2538
4246
|
var Recipe = _Recipe;
|
|
2539
4247
|
|
|
2540
4248
|
// src/classes/shopping_list.ts
|
|
2541
4249
|
var ShoppingList = class {
|
|
2542
4250
|
/**
|
|
2543
4251
|
* Creates a new ShoppingList instance
|
|
2544
|
-
* @param
|
|
4252
|
+
* @param categoryConfigStr - The category configuration to parse.
|
|
2545
4253
|
*/
|
|
2546
|
-
constructor(
|
|
2547
|
-
// TODO: backport type change
|
|
4254
|
+
constructor(categoryConfigStr) {
|
|
2548
4255
|
/**
|
|
2549
4256
|
* The ingredients in the shopping list.
|
|
2550
4257
|
*/
|
|
@@ -2556,43 +4263,43 @@ var ShoppingList = class {
|
|
|
2556
4263
|
/**
|
|
2557
4264
|
* The category configuration for the shopping list.
|
|
2558
4265
|
*/
|
|
2559
|
-
__publicField(this, "
|
|
4266
|
+
__publicField(this, "categoryConfig");
|
|
2560
4267
|
/**
|
|
2561
4268
|
* The categorized ingredients in the shopping list.
|
|
2562
4269
|
*/
|
|
2563
4270
|
__publicField(this, "categories");
|
|
2564
|
-
|
|
2565
|
-
|
|
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);
|
|
2566
4293
|
}
|
|
2567
4294
|
}
|
|
2568
|
-
|
|
4295
|
+
calculateIngredients() {
|
|
2569
4296
|
this.ingredients = [];
|
|
2570
|
-
const
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
if (!existing.quantityTotal) {
|
|
2576
|
-
existing.quantityTotal = quantityTotal;
|
|
2577
|
-
return;
|
|
2578
|
-
}
|
|
2579
|
-
try {
|
|
2580
|
-
const existingQuantityTotalExtended = extendAllUnits(
|
|
2581
|
-
existing.quantityTotal
|
|
2582
|
-
);
|
|
2583
|
-
const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
|
|
2584
|
-
existing.quantityTotal = addEquivalentsAndSimplify(
|
|
2585
|
-
...existingQuantities,
|
|
2586
|
-
...newQuantities
|
|
2587
|
-
);
|
|
2588
|
-
return;
|
|
2589
|
-
} catch {
|
|
2590
|
-
}
|
|
4297
|
+
const rawQuantitiesMap = /* @__PURE__ */ new Map();
|
|
4298
|
+
const nameOrder = [];
|
|
4299
|
+
const trackName = (name) => {
|
|
4300
|
+
if (!nameOrder.includes(name)) {
|
|
4301
|
+
nameOrder.push(name);
|
|
2591
4302
|
}
|
|
2592
|
-
this.ingredients.push({
|
|
2593
|
-
name,
|
|
2594
|
-
quantityTotal
|
|
2595
|
-
});
|
|
2596
4303
|
};
|
|
2597
4304
|
for (const addedRecipe of this.recipes) {
|
|
2598
4305
|
let scaledRecipe;
|
|
@@ -2602,48 +4309,245 @@ var ShoppingList = class {
|
|
|
2602
4309
|
} else {
|
|
2603
4310
|
scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
|
|
2604
4311
|
}
|
|
2605
|
-
const
|
|
4312
|
+
const rawGroups = scaledRecipe.getRawQuantityGroups({
|
|
2606
4313
|
choices: addedRecipe.choices
|
|
2607
4314
|
});
|
|
2608
|
-
for (const
|
|
2609
|
-
if (
|
|
4315
|
+
for (const group of rawGroups) {
|
|
4316
|
+
if (group.flags?.includes("hidden") || !group.usedAsPrimary) {
|
|
2610
4317
|
continue;
|
|
2611
4318
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
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);
|
|
2614
4349
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
+
});
|
|
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;
|
|
2621
4437
|
}
|
|
2622
4438
|
} else {
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
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
|
+
}
|
|
2629
4496
|
}
|
|
2630
4497
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
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
|
|
2636
4528
|
);
|
|
2637
|
-
const
|
|
2638
|
-
|
|
4529
|
+
const recomputed = recomputeEquivalents(
|
|
4530
|
+
[entry],
|
|
4531
|
+
ratioMap,
|
|
4532
|
+
equivUnits
|
|
2639
4533
|
);
|
|
2640
|
-
|
|
4534
|
+
entry.equivalents = recomputed;
|
|
2641
4535
|
}
|
|
2642
|
-
} else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
|
|
2643
|
-
this.ingredients.push({ name: ingredient.name });
|
|
2644
4536
|
}
|
|
2645
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
|
+
}
|
|
2646
4549
|
}
|
|
4550
|
+
this.resultingPantry = clonedPantry;
|
|
2647
4551
|
}
|
|
2648
4552
|
/**
|
|
2649
4553
|
* Adds a recipe to the shopping list, then automatically
|
|
@@ -2652,7 +4556,7 @@ var ShoppingList = class {
|
|
|
2652
4556
|
* @param options - Options for adding the recipe.
|
|
2653
4557
|
* @throws Error if the recipe has alternatives without corresponding choices.
|
|
2654
4558
|
*/
|
|
2655
|
-
|
|
4559
|
+
addRecipe(recipe, options = {}) {
|
|
2656
4560
|
const errorMessage = this.getUnresolvedAlternativesError(
|
|
2657
4561
|
recipe,
|
|
2658
4562
|
options.choices
|
|
@@ -2681,7 +4585,7 @@ var ShoppingList = class {
|
|
|
2681
4585
|
});
|
|
2682
4586
|
}
|
|
2683
4587
|
}
|
|
2684
|
-
this.
|
|
4588
|
+
this.calculateIngredients();
|
|
2685
4589
|
this.categorize();
|
|
2686
4590
|
}
|
|
2687
4591
|
/**
|
|
@@ -2721,27 +4625,62 @@ var ShoppingList = class {
|
|
|
2721
4625
|
}
|
|
2722
4626
|
/**
|
|
2723
4627
|
* Removes a recipe from the shopping list, then automatically
|
|
2724
|
-
* recalculates the quantities and recategorize the ingredients.
|
|
4628
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
2725
4629
|
* @param index - The index of the recipe to remove.
|
|
2726
4630
|
*/
|
|
2727
|
-
|
|
4631
|
+
removeRecipe(index) {
|
|
2728
4632
|
if (index < 0 || index >= this.recipes.length) {
|
|
2729
4633
|
throw new Error("Index out of bounds");
|
|
2730
4634
|
}
|
|
2731
4635
|
this.recipes.splice(index, 1);
|
|
2732
|
-
this.
|
|
4636
|
+
this.calculateIngredients();
|
|
4637
|
+
this.categorize();
|
|
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();
|
|
2733
4659
|
this.categorize();
|
|
2734
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
|
+
}
|
|
2735
4670
|
/**
|
|
2736
4671
|
* Sets the category configuration for the shopping list
|
|
2737
4672
|
* and automatically categorize current ingredients from the list.
|
|
4673
|
+
* Also propagates the configuration to the pantry if one is set.
|
|
2738
4674
|
* @param config - The category configuration to parse.
|
|
2739
4675
|
*/
|
|
2740
|
-
|
|
4676
|
+
setCategoryConfig(config) {
|
|
2741
4677
|
if (typeof config === "string")
|
|
2742
|
-
this.
|
|
2743
|
-
else if (config instanceof CategoryConfig) this.
|
|
4678
|
+
this.categoryConfig = new CategoryConfig(config);
|
|
4679
|
+
else if (config instanceof CategoryConfig) this.categoryConfig = config;
|
|
2744
4680
|
else throw new Error("Invalid category configuration");
|
|
4681
|
+
if (this.pantry) {
|
|
4682
|
+
this.pantry.setCategoryConfig(this.categoryConfig);
|
|
4683
|
+
}
|
|
2745
4684
|
this.categorize();
|
|
2746
4685
|
}
|
|
2747
4686
|
/**
|
|
@@ -2749,17 +4688,17 @@ var ShoppingList = class {
|
|
|
2749
4688
|
* Will use the category config if any, otherwise all ingredients will be placed in the "other" category
|
|
2750
4689
|
*/
|
|
2751
4690
|
categorize() {
|
|
2752
|
-
if (!this.
|
|
4691
|
+
if (!this.categoryConfig) {
|
|
2753
4692
|
this.categories = { other: this.ingredients };
|
|
2754
4693
|
return;
|
|
2755
4694
|
}
|
|
2756
4695
|
const categories = { other: [] };
|
|
2757
|
-
for (const category of this.
|
|
4696
|
+
for (const category of this.categoryConfig.categories) {
|
|
2758
4697
|
categories[category.name] = [];
|
|
2759
4698
|
}
|
|
2760
4699
|
for (const ingredient of this.ingredients) {
|
|
2761
4700
|
let found = false;
|
|
2762
|
-
for (const category of this.
|
|
4701
|
+
for (const category of this.categoryConfig.categories) {
|
|
2763
4702
|
for (const categoryIngredient of category.ingredients) {
|
|
2764
4703
|
if (categoryIngredient.aliases.includes(ingredient.name)) {
|
|
2765
4704
|
categories[category.name].push(ingredient);
|
|
@@ -2823,7 +4762,6 @@ var ShoppingCart = class {
|
|
|
2823
4762
|
setProductCatalog(catalog) {
|
|
2824
4763
|
this.productCatalog = catalog;
|
|
2825
4764
|
}
|
|
2826
|
-
// TODO: harmonize recipe name to use underscores
|
|
2827
4765
|
/**
|
|
2828
4766
|
* Sets the shopping list to build the cart from.
|
|
2829
4767
|
* To use if a shopping list was not provided at the creation of the instance
|
|
@@ -2887,8 +4825,27 @@ var ShoppingCart = class {
|
|
|
2887
4825
|
getOptimumMatch(ingredient, options) {
|
|
2888
4826
|
if (options.length === 0)
|
|
2889
4827
|
throw new NoProductMatchError(ingredient.name, "noProduct");
|
|
2890
|
-
if (!ingredient.
|
|
4828
|
+
if (!ingredient.quantities || ingredient.quantities.length === 0)
|
|
2891
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
|
+
}
|
|
2892
4849
|
const normalizedOptions = options.map(
|
|
2893
4850
|
(option) => ({
|
|
2894
4851
|
...option,
|
|
@@ -2904,7 +4861,7 @@ var ShoppingCart = class {
|
|
|
2904
4861
|
})
|
|
2905
4862
|
})
|
|
2906
4863
|
);
|
|
2907
|
-
const normalizedQuantityTotal = normalizeAllUnits(
|
|
4864
|
+
const normalizedQuantityTotal = normalizeAllUnits(quantityTotal);
|
|
2908
4865
|
function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
|
|
2909
4866
|
if (isAndGroup(normalizedQuantities)) {
|
|
2910
4867
|
for (const q of normalizedQuantities.and) {
|
|
@@ -2931,12 +4888,12 @@ var ShoppingCart = class {
|
|
|
2931
4888
|
alternative.quantity = scaledQuantity;
|
|
2932
4889
|
const matchOptions = normalizedOptions2.filter(
|
|
2933
4890
|
(option) => option.sizes.some(
|
|
2934
|
-
(s) =>
|
|
4891
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2935
4892
|
)
|
|
2936
4893
|
);
|
|
2937
4894
|
if (matchOptions.length > 0) {
|
|
2938
4895
|
const findCompatibleSize = (option) => option.sizes.find(
|
|
2939
|
-
(s) =>
|
|
4896
|
+
(s) => areUnitsGroupable(alternative.unit, s.unit)
|
|
2940
4897
|
);
|
|
2941
4898
|
if (matchOptions.length == 1) {
|
|
2942
4899
|
const matchedOption = matchOptions[0];
|
|
@@ -3038,34 +4995,177 @@ var ShoppingCart = class {
|
|
|
3038
4995
|
};
|
|
3039
4996
|
|
|
3040
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
|
+
}
|
|
3041
5072
|
function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
|
|
3042
5073
|
if (item.group) {
|
|
3043
5074
|
const selectedIndex2 = choices?.ingredientGroups?.get(item.group);
|
|
3044
|
-
const
|
|
3045
|
-
if (
|
|
3046
|
-
const
|
|
3047
|
-
return
|
|
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);
|
|
3048
5079
|
}
|
|
3049
5080
|
return false;
|
|
3050
5081
|
}
|
|
3051
5082
|
const selectedIndex = choices?.ingredientItems?.get(item.id);
|
|
3052
5083
|
return alternativeIndex === selectedIndex;
|
|
3053
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
|
+
}
|
|
3054
5127
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3055
5128
|
0 && (module.exports = {
|
|
3056
5129
|
CategoryConfig,
|
|
3057
5130
|
NoProductCatalogForCartError,
|
|
3058
5131
|
NoShoppingListForCartError,
|
|
5132
|
+
Pantry,
|
|
3059
5133
|
ProductCatalog,
|
|
3060
5134
|
Recipe,
|
|
3061
5135
|
Section,
|
|
3062
5136
|
ShoppingCart,
|
|
3063
5137
|
ShoppingList,
|
|
3064
|
-
|
|
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
|
|
3065
5155
|
});
|
|
3066
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 */
|
|
3067
5159
|
// v8 ignore else -- @preserve
|
|
3068
|
-
/* v8 ignore else -- expliciting error type -- @preserve */
|
|
3069
5160
|
// v8 ignore if -- @preserve
|
|
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
|
|
3070
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 */
|
|
3071
5171
|
//# sourceMappingURL=index.cjs.map
|