Skip to content

Commit 6f9f0e8

Browse files
authored
feat!: alternative units and ingredients (#80)
# Description Introduction of alternative units and ingredients Resolves: #66 Resolves: #69 ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] This change requires a documentation update
1 parent 59a3484 commit 6f9f0e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+8904
-2802
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ dist
33
coverage
44
docs/.vitepress/cache
55
docs/.vitepress/dist
6-
docs/api
6+
docs/api
7+
.nuxt

docs/guide-extensions.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,55 @@ When referencing ingredients, the added quantities will be converted to [Decimal
9696
```
9797

9898
Also works with Cookware and Timers
99-
99+
100100
## Cookware quantities
101101

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

105+
## Alternative units
106+
107+
You can define equivalent quantities in different units for the same ingredient using the pipe `|` separator within the curly braces.
108+
109+
Usage: `@flour{100%g|3.5%oz}`
110+
111+
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.
112+
113+
## Ingredient alternatives
114+
115+
### Inline alternatives
116+
117+
You can specify alternative ingredients directly in the ingredient syntax using pipes: `@baseName{quantity}|altName{altQuantity}[note]|...`
118+
119+
This allows users to select from multiple alternatives when computing recipe quantities.
120+
121+
Use cases:
122+
- `@milk{200%ml}|almond milk{100%ml}[vegan version]|soy milk{150%ml}[another vegan option]`
123+
- `@sugar{100%g}|brown sugar{100%g}[for a richer flavor]`
124+
125+
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.
126+
127+
All modifiers (`&`, `-`, `?`) work with inline alternatives:
128+
`@&milk{200%ml}|-almond milk{100%ml}[vegan version]|?soy milk{150%ml}[another vegan option]`
129+
130+
### Grouped alternatives
131+
132+
Ingredients can also have alternatives by grouping them with the same group key using the syntax: `@|groupKey|ingredientName{}`
133+
134+
This is useful when you want to provide alternative choices in your recipe text naturally:
135+
136+
Use cases:
137+
- `Add @|milk|milk{200%ml} or @|milk|almond milk{100%ml} or @|milk|oat milk{150%ml} for a vegan version`
138+
- `Add some @|spices|salt{} or maybe some @|spices|pepper{}`
139+
140+
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.
141+
142+
All modifiers (`&`, `-`, `?`) work with grouped alternatives:
143+
```
144+
Add @|flour|&flour tipo 00{100%g} or @|flour|flour tipo 1{50%g}
145+
Add @|spices|-salt{} or @|spices|?pepper{}
146+
```
147+
105148
## Ingredient aliases
106149

107150
- `@ingredientName|displayAlias{}` will add the ingredient as "ingredientName" in the ingredients list, but will display is as "displayAlias" in the preparation step.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ packages:
22
- .
33

44
onlyBuiltDependencies:
5+
- '@parcel/watcher'
56
- esbuild
7+
- vue-demi

src/classes/product_catalog.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import type {
33
FixedNumericValue,
44
ProductOption,
55
ProductOptionToml,
6+
ProductSize,
67
} from "../types";
78
import type { TomlTable } from "smol-toml";
89
import {
910
isPositiveIntegerString,
1011
parseQuantityInput,
1112
stringifyQuantityValue,
12-
} from "../parser_helpers";
13+
} from "../utils/parser_helpers";
1314
import { InvalidProductCatalogFormat } from "../errors";
1415

1516
/**
@@ -76,27 +77,32 @@ export class ProductCatalog {
7677
const { name, size, price, ...rest } =
7778
productData as unknown as ProductOptionToml;
7879

79-
const sizeAndUnitRaw = size.split("%");
80-
const sizeParsed = parseQuantityInput(
81-
sizeAndUnitRaw[0]!,
82-
) as FixedNumericValue;
80+
// Handle size as string or string[]
81+
const sizeStrings = Array.isArray(size) ? size : [size];
82+
const sizes: ProductSize[] = sizeStrings.map((sizeStr) => {
83+
const sizeAndUnitRaw = sizeStr.split("%");
84+
const sizeParsed = parseQuantityInput(
85+
sizeAndUnitRaw[0]!,
86+
) as FixedNumericValue;
87+
const productSize: ProductSize = { size: sizeParsed };
88+
if (sizeAndUnitRaw.length > 1) {
89+
productSize.unit = sizeAndUnitRaw[1]!;
90+
}
91+
return productSize;
92+
});
8393

8494
const productOption: ProductOption = {
8595
id: productId,
8696
productName: name,
8797
ingredientName: ingredientName,
8898
price: price,
89-
size: sizeParsed,
99+
sizes,
90100
...rest,
91101
};
92102
if (aliases) {
93103
productOption.ingredientAliases = aliases;
94104
}
95105

96-
if (sizeAndUnitRaw.length > 1) {
97-
productOption.unit = sizeAndUnitRaw[1]!;
98-
}
99-
100106
this.products.push(productOption);
101107
}
102108
}
@@ -116,8 +122,7 @@ export class ProductCatalog {
116122
id,
117123
ingredientName,
118124
ingredientAliases,
119-
size,
120-
unit,
125+
sizes,
121126
productName,
122127
...rest
123128
} = product;
@@ -127,12 +132,19 @@ export class ProductCatalog {
127132
if (ingredientAliases && !grouped[ingredientName].aliases) {
128133
grouped[ingredientName].aliases = ingredientAliases;
129134
}
135+
136+
// Stringify each size as "value%unit" or just "value"
137+
const sizeStrings = sizes.map((s) =>
138+
s.unit
139+
? `${stringifyQuantityValue(s.size)}%${s.unit}`
140+
: stringifyQuantityValue(s.size),
141+
);
142+
130143
grouped[ingredientName][id] = {
131144
...rest,
132145
name: productName,
133-
size: unit
134-
? `${stringifyQuantityValue(size)}%${unit}`
135-
: stringifyQuantityValue(size),
146+
// Use array if multiple sizes, otherwise single string
147+
size: sizeStrings.length === 1 ? sizeStrings[0]! : sizeStrings,
136148
};
137149
}
138150

@@ -184,7 +196,11 @@ export class ProductCatalog {
184196
}
185197

186198
const hasProductName = typeof record.name === "string";
187-
const hasSize = typeof record.size === "string";
199+
// Size can be a string or an array of strings
200+
const hasSize =
201+
typeof record.size === "string" ||
202+
(Array.isArray(record.size) &&
203+
record.size.every((s) => typeof s === "string"));
188204
const hasPrice = typeof record.price === "number";
189205

190206
if (!(hasProductName && hasSize && hasPrice)) {

0 commit comments

Comments
 (0)