Skip to content

Commit f88bd2c

Browse files
committed
feat: export helpers in ts
1 parent 5904923 commit f88bd2c

File tree

2 files changed

+349
-3
lines changed

2 files changed

+349
-3
lines changed

typescript/index.ts

Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,218 @@ import {
55
RecipeTime,
66
Servings, Section, Ingredient, Cookware, Timer, Quantity, ScaledRecipeWithReport, GroupedQuantity,
77
ingredient_should_be_listed, ingredient_display_name, grouped_quantity_is_empty, grouped_quantity_display,
8-
cookware_should_be_listed, cookware_display_name, Content, Step, quantity_display, GroupedIndexAndQuantity
8+
cookware_should_be_listed, cookware_display_name, Content, Step, quantity_display, GroupedIndexAndQuantity,
9+
Value, Item
910
} from "./pkg/cooklang_wasm.js";
1011

11-
export {version, Parser};
12-
export type {ScaledRecipeWithReport} from "./pkg/cooklang_wasm.js";
12+
export {
13+
version,
14+
Parser,
15+
ingredient_should_be_listed,
16+
ingredient_display_name,
17+
grouped_quantity_is_empty,
18+
grouped_quantity_display,
19+
cookware_should_be_listed,
20+
cookware_display_name,
21+
quantity_display
22+
};
23+
export type {ScaledRecipeWithReport, Value, Quantity, Ingredient, Cookware, Timer, Section, Content, Step, Item} from "./pkg/cooklang_wasm.js";
24+
25+
// ============================================================================
26+
// Numeric Value Extraction Helpers
27+
// ============================================================================
28+
29+
/**
30+
* Extract a numeric value from a WASM Value type.
31+
*
32+
* For ranges, returns the start value.
33+
* For text values, returns null.
34+
*
35+
* @param value - The Value to extract from
36+
* @returns The numeric value or null if not a number/range
37+
*
38+
* @example
39+
* ```typescript
40+
* const value = ingredient.quantity?.value;
41+
* const numeric = getNumericValue(value); // 2.5
42+
* ```
43+
*/
44+
export function getNumericValue(value: Value | null | undefined): number | null {
45+
if (!value) {
46+
return null;
47+
}
48+
49+
if (value.type === 'number') {
50+
// WASM returns nested structure: { type: "number", value: { type: "regular", value: 3 } }
51+
// The type definitions are incomplete, so we need to cast
52+
const numValue = value.value as any;
53+
return Number(numValue.value);
54+
} else if (value.type === 'range') {
55+
// Range structure: { type: "range", value: { start: { type: "regular", value: X }, end: { ... } } }
56+
// Return start of range
57+
const rangeValue = value.value as any;
58+
return Number(rangeValue.start.value);
59+
}
60+
return null;
61+
}
62+
63+
/**
64+
* Extract the numeric value from a Quantity.
65+
*
66+
* Convenience wrapper around getNumericValue for Quantity objects.
67+
*
68+
* @param quantity - The Quantity to extract from
69+
* @returns The numeric value or null
70+
*
71+
* @example
72+
* ```typescript
73+
* const qty = getQuantityValue(ingredient.quantity); // 2.5
74+
* ```
75+
*/
76+
export function getQuantityValue(quantity: Quantity | null | undefined): number | null {
77+
return quantity ? getNumericValue(quantity.value) : null;
78+
}
79+
80+
/**
81+
* Extract the unit string from a Quantity.
82+
*
83+
* @param quantity - The Quantity to extract from
84+
* @returns The unit string or null if no unit
85+
*
86+
* @example
87+
* ```typescript
88+
* const unit = getQuantityUnit(ingredient.quantity); // "cups"
89+
* ```
90+
*/
91+
export function getQuantityUnit(quantity: Quantity | null | undefined): string | null {
92+
return quantity?.unit ?? null;
93+
}
94+
95+
// ============================================================================
96+
// Flat List Helpers
97+
// ============================================================================
98+
99+
/**
100+
* Simple ingredient with display-ready values.
101+
* For more control, use recipe.groupedIngredients with the display functions.
102+
*/
103+
export interface FlatIngredient {
104+
/** Display name of the ingredient */
105+
name: string;
106+
/** Numeric quantity (start of range if range), or null if none */
107+
quantity: number | null;
108+
/** Unit string, or null if none */
109+
unit: string | null;
110+
/** Formatted display text for the quantity (e.g., "1-2 cups", "3/4 tsp") */
111+
displayText: string | null;
112+
/** Optional note/modifier (e.g., "finely chopped") */
113+
note: string | null;
114+
}
115+
116+
/**
117+
* Simple cookware with display-ready values.
118+
*/
119+
export interface FlatCookware {
120+
/** Display name of the cookware */
121+
name: string;
122+
/** Numeric quantity, or null if none */
123+
quantity: number | null;
124+
/** Formatted display text for the quantity */
125+
displayText: string | null;
126+
/** Optional note/modifier */
127+
note: string | null;
128+
}
129+
130+
/**
131+
* Simple timer with display-ready values.
132+
*/
133+
export interface FlatTimer {
134+
/** Optional timer name */
135+
name: string | null;
136+
/** Numeric quantity (in seconds after unit conversion), or null if none */
137+
quantity: number | null;
138+
/** Unit string (e.g., "minutes", "hours"), or null if none */
139+
unit: string | null;
140+
/** Formatted display text for the quantity */
141+
displayText: string | null;
142+
}
143+
144+
/**
145+
* Get a flat list of all ingredients with simple, display-ready values.
146+
*
147+
* This is a convenience function for simple use cases. For more control over
148+
* grouping and display, use recipe.groupedIngredients with the display functions.
149+
*
150+
* @param recipe - The parsed recipe
151+
* @returns Array of flat ingredient objects
152+
*
153+
* @example
154+
* ```typescript
155+
* const ingredients = getFlatIngredients(recipe);
156+
* ingredients.forEach(ing => {
157+
* console.log(`${ing.displayText || ''} ${ing.name}`);
158+
* });
159+
* ```
160+
*/
161+
export function getFlatIngredients(recipe: CooklangRecipe): FlatIngredient[] {
162+
return recipe.ingredients.map(ing => ({
163+
name: ingredient_display_name(ing),
164+
quantity: getQuantityValue(ing.quantity),
165+
unit: getQuantityUnit(ing.quantity),
166+
displayText: ing.quantity ? quantity_display(ing.quantity) : null,
167+
note: ing.note
168+
}));
169+
}
170+
171+
/**
172+
* Get a flat list of all cookware with simple, display-ready values.
173+
*
174+
* @param recipe - The parsed recipe
175+
* @returns Array of flat cookware objects
176+
*
177+
* @example
178+
* ```typescript
179+
* const cookware = getFlatCookware(recipe);
180+
* cookware.forEach(cw => {
181+
* console.log(`${cw.displayText || ''} ${cw.name}`);
182+
* });
183+
* ```
184+
*/
185+
export function getFlatCookware(recipe: CooklangRecipe): FlatCookware[] {
186+
return recipe.cookware.map(cw => ({
187+
name: cookware_display_name(cw),
188+
quantity: getQuantityValue(cw.quantity),
189+
displayText: cw.quantity ? quantity_display(cw.quantity) : null,
190+
note: cw.note
191+
}));
192+
}
193+
194+
/**
195+
* Get a flat list of all timers from the recipe.
196+
*
197+
* @param recipe - The parsed recipe
198+
* @returns Array of flat timer objects
199+
*
200+
* @example
201+
* ```typescript
202+
* const timers = getFlatTimers(recipe);
203+
* timers.forEach(timer => {
204+
* console.log(`${timer.name}: ${timer.displayText}`);
205+
* });
206+
* ```
207+
*/
208+
export function getFlatTimers(recipe: CooklangRecipe): FlatTimer[] {
209+
return recipe.timers.map(tm => ({
210+
name: tm.name,
211+
quantity: getQuantityValue(tm.quantity),
212+
unit: getQuantityUnit(tm.quantity),
213+
displayText: tm.quantity ? quantity_display(tm.quantity) : null
214+
}));
215+
}
13216

217+
// ============================================================================
218+
// Recipe and Parser Classes
219+
// ============================================================================
14220

15221
export class CooklangRecipe {
16222
// Metadata

typescript/test/helpers.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {it, expect, describe} from "vitest";
2+
import {
3+
CooklangParser,
4+
getNumericValue,
5+
getQuantityValue,
6+
getQuantityUnit,
7+
getFlatIngredients,
8+
getFlatCookware,
9+
getFlatTimers
10+
} from "../index.js";
11+
12+
describe("Numeric Value Helpers", () => {
13+
it("extracts numeric values from quantities", () => {
14+
const input = `
15+
Mix @flour{2%cups} with @water{500%ml}.
16+
`;
17+
18+
const parser = new CooklangParser();
19+
const [recipe] = parser.parse(input);
20+
21+
// Test getQuantityValue
22+
const flourQty = getQuantityValue(recipe.ingredients[0].quantity);
23+
expect(flourQty).toEqual(2);
24+
25+
const waterQty = getQuantityValue(recipe.ingredients[1].quantity);
26+
expect(waterQty).toEqual(500);
27+
});
28+
29+
it("extracts units from quantities", () => {
30+
const input = `
31+
Mix @flour{2%cups} with @water{500%ml}.
32+
`;
33+
34+
const parser = new CooklangParser();
35+
const [recipe] = parser.parse(input);
36+
37+
const flourUnit = getQuantityUnit(recipe.ingredients[0].quantity);
38+
expect(flourUnit).toEqual("cups");
39+
40+
const waterUnit = getQuantityUnit(recipe.ingredients[1].quantity);
41+
expect(waterUnit).toEqual("ml");
42+
});
43+
44+
it("handles null quantities", () => {
45+
const input = `
46+
Mix @flour with @water.
47+
`;
48+
49+
const parser = new CooklangParser();
50+
const [recipe] = parser.parse(input);
51+
52+
const flourQty = getQuantityValue(recipe.ingredients[0].quantity);
53+
expect(flourQty).toBeNull();
54+
55+
const flourUnit = getQuantityUnit(recipe.ingredients[0].quantity);
56+
expect(flourUnit).toBeNull();
57+
});
58+
59+
it("extracts start value from ranges", () => {
60+
const input = `
61+
Add @sugar{1-2%cups}.
62+
`;
63+
64+
const parser = new CooklangParser();
65+
const [recipe] = parser.parse(input);
66+
67+
const value = getNumericValue(recipe.ingredients[0].quantity?.value);
68+
expect(value).toEqual(1); // Should return start of range
69+
});
70+
});
71+
72+
describe("Flat List Helpers", () => {
73+
it("creates flat ingredient list", () => {
74+
const input = `
75+
Mix @flour{2%cups} with @water{500%ml} and @salt.
76+
`;
77+
78+
const parser = new CooklangParser();
79+
const [recipe] = parser.parse(input);
80+
81+
const ingredients = getFlatIngredients(recipe);
82+
83+
expect(ingredients).toHaveLength(3);
84+
85+
expect(ingredients[0].name).toEqual("flour");
86+
expect(ingredients[0].quantity).toEqual(2);
87+
expect(ingredients[0].unit).toEqual("cups");
88+
expect(ingredients[0].displayText).toBeTruthy();
89+
90+
expect(ingredients[1].name).toEqual("water");
91+
expect(ingredients[1].quantity).toEqual(500);
92+
expect(ingredients[1].unit).toEqual("ml");
93+
94+
expect(ingredients[2].name).toEqual("salt");
95+
expect(ingredients[2].quantity).toBeNull();
96+
expect(ingredients[2].unit).toBeNull();
97+
});
98+
99+
it("creates flat cookware list", () => {
100+
const input = `
101+
Use #pot and #pan{2}.
102+
`;
103+
104+
const parser = new CooklangParser();
105+
const [recipe] = parser.parse(input);
106+
107+
const cookware = getFlatCookware(recipe);
108+
109+
expect(cookware).toHaveLength(2);
110+
111+
expect(cookware[0].name).toEqual("pot");
112+
expect(cookware[0].quantity).toBeNull();
113+
114+
expect(cookware[1].name).toEqual("pan");
115+
expect(cookware[1].quantity).toEqual(2);
116+
});
117+
118+
it("creates flat timer list", () => {
119+
const input = `
120+
Cook for ~{10%minutes} then ~bake{30%minutes}.
121+
`;
122+
123+
const parser = new CooklangParser();
124+
const [recipe] = parser.parse(input);
125+
126+
const timers = getFlatTimers(recipe);
127+
128+
expect(timers).toHaveLength(2);
129+
130+
expect(timers[0].name).toBeNull();
131+
expect(timers[0].quantity).toEqual(10);
132+
expect(timers[0].unit).toEqual("minutes");
133+
expect(timers[0].displayText).toBeTruthy();
134+
135+
expect(timers[1].name).toEqual("bake");
136+
expect(timers[1].quantity).toEqual(30);
137+
expect(timers[1].unit).toEqual("minutes");
138+
});
139+
140+
});

0 commit comments

Comments
 (0)