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