Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2bd4e86
define types, define regex
tmlmt Dec 9, 2025
2dfccab
parse ingredients with in-line alternative and parse alternative quan…
tmlmt Dec 9, 2025
ce5b7e4
alternative units
tmlmt Dec 18, 2025
50c42ba
fix: recipe anchor token name for in-line alternative ingredients
tmlmt Dec 21, 2025
38e2b31
parse ingredient alternatives grouped by key
tmlmt Dec 22, 2025
b7964a8
scale recipes and fix some bugs
tmlmt Dec 22, 2025
a9e5750
adapt tests, fix some bugs and improve types
tmlmt Dec 24, 2025
43a64aa
refactor cookware without quantityParts, fix clone() function, adjust…
tmlmt Dec 26, 2025
757a545
chore!: rename field names to avoid value.value.value
tmlmt Dec 27, 2025
00e2bcd
adapt ShoppingList
tmlmt Dec 28, 2025
27a2175
adapt shoppingCart and fix stuff along the way
tmlmt Dec 29, 2025
ab90147
lint
tmlmt Dec 29, 2025
b52ea2a
refactor: move all types to types.ts
tmlmt Dec 29, 2025
95f3517
refactor: move all errors to errors.ts
tmlmt Dec 29, 2025
3b31222
start refactoring
tmlmt Dec 29, 2025
faaa2f4
continue refactoring
tmlmt Dec 30, 2025
f4ef4f7
finalize refactor
tmlmt Dec 30, 2025
4d5b1eb
rename test file
tmlmt Dec 30, 2025
a3f276f
increase coverage and fix along the way
tmlmt Dec 30, 2025
5b9bb36
chore: remove duplicate and wrongly names NestedAndGroup type
tmlmt Dec 30, 2025
f19a05c
refactor: move isValueIntegerLike to type_guards
tmlmt Dec 31, 2025
6af0a06
test: increase coverage
tmlmt Dec 31, 2025
f59741f
refactor: include preservation of integerProtected flag in resolveUnit
tmlmt Jan 1, 2026
3d88fbf
chore: remove useless reference to __no-unit__
tmlmt Jan 1, 2026
f7e2156
fix(sortUnitList): no-unit ranked higher than integer-protected units
tmlmt Jan 1, 2026
3180353
chore: ensure consistant sorting regardless of host locale
tmlmt Jan 2, 2026
a0050c8
test: add more tests for quantities/alternatives.ts
tmlmt Jan 2, 2026
1799306
fix(parsing): ingredient name and groupkey not detected for alternati…
tmlmt Jan 3, 2026
d1ea21e
test(shoppingList): maximize coverge
tmlmt Jan 3, 2026
05f19ba
fix(parsing): ingredient-recipe anchor for alternative ingredients wi…
tmlmt Jan 3, 2026
92e0b86
fix(parsing): alternatives of ingredient items should not contain itemId
tmlmt Jan 3, 2026
f6d269b
style(regex): remove leading @ from in-line ingredient alternatives
tmlmt Jan 3, 2026
7f96641
chore: remove console.log
tmlmt Jan 3, 2026
b4a8bfb
chore(parsing): do not expect a note for ingredient alternatives with…
tmlmt Jan 3, 2026
b886930
fix(parsing): alternative units not detected properly
tmlmt Jan 3, 2026
2c545b3
fix(Recipe): clone function missing choices property
tmlmt Jan 4, 2026
9369f69
reach 100% coverage and fix issues along the way
tmlmt Jan 4, 2026
3c12f53
feat(ProductCatalog): allow multiple product sizes
tmlmt Jan 4, 2026
f4ee806
chore: authorize parcel and vue-demi with approve-builds
tmlmt Jan 6, 2026
bf6e588
chore(Recipe): hide _itemCount from instance properties
tmlmt Jan 7, 2026
4ccd2f8
chore(gitignore): ignore .nuxt
tmlmt Jan 8, 2026
65d1ffb
feat: show all options in ingredients list and enable calculating wit…
tmlmt Jan 9, 2026
594fe15
docs: add missing types and fine-tune docstrings
tmlmt Jan 11, 2026
88649ea
fix: add alternative units to ingredients list
tmlmt Jan 11, 2026
39eb2ab
refactor: separate primary quantity from equivalents across the Recip…
tmlmt Jan 12, 2026
22d84c4
fix: capture alternative units of alternative ingredients, and avoid …
tmlmt Jan 12, 2026
8e3acf2
fix: handle unit-less alternatives
tmlmt Jan 12, 2026
d829a5c
fix: add equivalents and simplify ingredients too
tmlmt Jan 13, 2026
c1c07bb
fix: main And groups of quantities that are added and simplified
tmlmt Jan 13, 2026
a102817
fix: export new quantity types
tmlmt Jan 13, 2026
986c800
chore(test): fix parser_helpers tests wrt types
tmlmt Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist
coverage
docs/.vitepress/cache
docs/.vitepress/dist
docs/api
docs/api
.nuxt
45 changes: 44 additions & 1 deletion docs/guide-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,55 @@ When referencing ingredients, the added quantities will be converted to [Decimal
```

Also works with Cookware and Timers

## Cookware quantities

- Cookware can also be quantified (without any unit, e.g. `#bowls{2}`)
- Quantities will be added similarly as ingredients if cookware is referenced, e.g. `#&bowls{2}`

## Alternative units

You can define equivalent quantities in different units for the same ingredient using the pipe `|` separator within the curly braces.

Usage: `@flour{100%g|3.5%oz}`

This is useful for providing multiple units (e.g. metric and imperial) simultaneously for the same ingredient. The first unit is considered the primary unit.

## Ingredient alternatives

### Inline alternatives

You can specify alternative ingredients directly in the ingredient syntax using pipes: `@baseName{quantity}|altName{altQuantity}[note]|...`

This allows users to select from multiple alternatives when computing recipe quantities.

Use cases:
- `@milk{200%ml}|almond milk{100%ml}[vegan version]|soy milk{150%ml}[another vegan option]`
- `@sugar{100%g}|brown sugar{100%g}[for a richer flavor]`

When inline alternatives are defined, the recipe's [`choices`](/api/interfaces/RecipeChoices) property will be populated. You can then use the `calc_ingredient_quantities()` method to compute quantities corresponding to the user's choices.

All modifiers (`&`, `-`, `?`) work with inline alternatives:
`@&milk{200%ml}|-almond milk{100%ml}[vegan version]|?soy milk{150%ml}[another vegan option]`

### Grouped alternatives

Ingredients can also have alternatives by grouping them with the same group key using the syntax: `@|groupKey|ingredientName{}`

This is useful when you want to provide alternative choices in your recipe text naturally:

Use cases:
- `Add @|milk|milk{200%ml} or @|milk|almond milk{100%ml} or @|milk|oat milk{150%ml} for a vegan version`
- `Add some @|spices|salt{} or maybe some @|spices|pepper{}`

When grouped alternatives are defined, the recipe's [`choices`](/api/interfaces/RecipeChoices) property will be populated with available alternatives for each group. You can then use the `calc_ingredient_quantities()` method to compute quantities corresponding to the user's choices.

All modifiers (`&`, `-`, `?`) work with grouped alternatives:
```
Add @|flour|&flour tipo 00{100%g} or @|flour|flour tipo 1{50%g}
Add @|spices|-salt{} or @|spices|?pepper{}
```

## Ingredient aliases

- `@ingredientName|displayAlias{}` will add the ingredient as "ingredientName" in the ingredients list, but will display is as "displayAlias" in the preparation step.
Expand Down
2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ packages:
- .

onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild
- vue-demi
48 changes: 32 additions & 16 deletions src/classes/product_catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type {
FixedNumericValue,
ProductOption,
ProductOptionToml,
ProductSize,
} from "../types";
import type { TomlTable } from "smol-toml";
import {
isPositiveIntegerString,
parseQuantityInput,
stringifyQuantityValue,
} from "../parser_helpers";
} from "../utils/parser_helpers";
import { InvalidProductCatalogFormat } from "../errors";

/**
Expand Down Expand Up @@ -76,27 +77,32 @@ export class ProductCatalog {
const { name, size, price, ...rest } =
productData as unknown as ProductOptionToml;

const sizeAndUnitRaw = size.split("%");
const sizeParsed = parseQuantityInput(
sizeAndUnitRaw[0]!,
) as FixedNumericValue;
// Handle size as string or string[]
const sizeStrings = Array.isArray(size) ? size : [size];
const sizes: ProductSize[] = sizeStrings.map((sizeStr) => {
const sizeAndUnitRaw = sizeStr.split("%");
const sizeParsed = parseQuantityInput(
sizeAndUnitRaw[0]!,
) as FixedNumericValue;
const productSize: ProductSize = { size: sizeParsed };
if (sizeAndUnitRaw.length > 1) {
productSize.unit = sizeAndUnitRaw[1]!;
}
return productSize;
});

const productOption: ProductOption = {
id: productId,
productName: name,
ingredientName: ingredientName,
price: price,
size: sizeParsed,
sizes,
...rest,
};
if (aliases) {
productOption.ingredientAliases = aliases;
}

if (sizeAndUnitRaw.length > 1) {
productOption.unit = sizeAndUnitRaw[1]!;
}

this.products.push(productOption);
}
}
Expand All @@ -116,8 +122,7 @@ export class ProductCatalog {
id,
ingredientName,
ingredientAliases,
size,
unit,
sizes,
productName,
...rest
} = product;
Expand All @@ -127,12 +132,19 @@ export class ProductCatalog {
if (ingredientAliases && !grouped[ingredientName].aliases) {
grouped[ingredientName].aliases = ingredientAliases;
}

// Stringify each size as "value%unit" or just "value"
const sizeStrings = sizes.map((s) =>
s.unit
? `${stringifyQuantityValue(s.size)}%${s.unit}`
: stringifyQuantityValue(s.size),
);

grouped[ingredientName][id] = {
...rest,
name: productName,
size: unit
? `${stringifyQuantityValue(size)}%${unit}`
: stringifyQuantityValue(size),
// Use array if multiple sizes, otherwise single string
size: sizeStrings.length === 1 ? sizeStrings[0]! : sizeStrings,
};
}

Expand Down Expand Up @@ -184,7 +196,11 @@ export class ProductCatalog {
}

const hasProductName = typeof record.name === "string";
const hasSize = typeof record.size === "string";
// Size can be a string or an array of strings
const hasSize =
typeof record.size === "string" ||
(Array.isArray(record.size) &&
record.size.every((s) => typeof s === "string"));
const hasPrice = typeof record.price === "number";

if (!(hasProductName && hasSize && hasPrice)) {
Expand Down
Loading