soustack 0.2.1 → 0.3.0

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.
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Soustack Recipe Schema v0.3.0
3
+ * A portable, scalable, interoperable recipe format.
4
+ */
5
+ interface SoustackRecipe {
6
+ /** Document marker for Soustack recipes */
7
+ '@type'?: 'Recipe';
8
+ /** Optional $schema pointer for profile-aware validation */
9
+ $schema?: string;
10
+ /** Optional declared validation profile */
11
+ profile?: string;
12
+ /** Enabled module identifiers (e.g., "nutrition@1") */
13
+ modules?: string[];
14
+ /** Attribution module payload */
15
+ attribution?: AttributionModule;
16
+ /** Taxonomy module payload */
17
+ taxonomy?: TaxonomyModule;
18
+ /** Media module payload */
19
+ media?: MediaModule;
20
+ /** Times module payload */
21
+ times?: TimesModule;
22
+ /** Unique identifier (slug or UUID) */
23
+ id?: string;
24
+ /** Optional display title */
25
+ title?: string;
26
+ /** The title of the recipe */
27
+ name: string;
28
+ /** Semantic versioning (e.g., 1.0.0) */
29
+ recipeVersion?: string;
30
+ /** Deprecated alias for recipeVersion */
31
+ version?: string;
32
+ description?: string;
33
+ /** Primary category (e.g., "Main Course") */
34
+ category?: string;
35
+ /** Additional tags for filtering */
36
+ tags?: string[];
37
+ /** URL(s) to recipe image(s) */
38
+ image?: string | string[];
39
+ /** ISO 8601 date string */
40
+ dateAdded?: string;
41
+ /** Last updated timestamp */
42
+ dateModified?: string;
43
+ source?: Source;
44
+ yield?: Yield;
45
+ time?: Time;
46
+ equipment?: Equipment[];
47
+ ingredients: IngredientItem[];
48
+ instructions: InstructionItem[];
49
+ storage?: Storage;
50
+ substitutions?: Substitution[];
51
+ nutrition?: NutritionFacts;
52
+ metadata?: Record<string, unknown>;
53
+ [k: `x-${string}`]: unknown;
54
+ }
55
+ type Recipe = SoustackRecipe;
56
+ interface Source {
57
+ author?: string;
58
+ url?: string;
59
+ name?: string;
60
+ adapted?: boolean;
61
+ }
62
+ interface Yield {
63
+ amount: number;
64
+ unit: string;
65
+ servings?: number;
66
+ description?: string;
67
+ }
68
+ /**
69
+ * Time can be structured (machine-readable) or simple (strings).
70
+ * Structured time takes precedence if both exist.
71
+ */
72
+ type Time = StructuredTime | SimpleTime;
73
+ interface StructuredTime {
74
+ prep?: number;
75
+ active?: number;
76
+ passive?: number;
77
+ total?: number;
78
+ }
79
+ interface SimpleTime {
80
+ prepTime?: string;
81
+ cookTime?: string;
82
+ }
83
+ interface Equipment {
84
+ id?: string;
85
+ name: string;
86
+ required?: boolean;
87
+ label?: string;
88
+ capacity?: Quantity;
89
+ scalingLimit?: number;
90
+ alternatives?: string[];
91
+ }
92
+ interface Quantity {
93
+ amount: number;
94
+ /** Unit string (e.g. "g", "cup") or null for count-based items (e.g. "2 eggs") */
95
+ unit: string | null;
96
+ }
97
+ type IngredientItem = string | Ingredient | IngredientSubsection;
98
+ interface IngredientSubsection {
99
+ subsection: string;
100
+ items: (string | Ingredient)[];
101
+ }
102
+ interface Ingredient {
103
+ id?: string;
104
+ /** Full human-readable text (e.g. "2 cups flour") */
105
+ item: string;
106
+ quantity?: Quantity;
107
+ name?: string;
108
+ aisle?: string;
109
+ /** Required prep state (e.g. "diced") */
110
+ prep?: string;
111
+ prepAction?: string;
112
+ prepTime?: number;
113
+ /** ID of equipment where this ingredient goes */
114
+ destination?: string;
115
+ scaling?: Scaling;
116
+ critical?: boolean;
117
+ optional?: boolean;
118
+ notes?: string;
119
+ }
120
+ /**
121
+ * Intelligent Scaling Logic
122
+ * Defines how an ingredient behaves when the recipe yield changes.
123
+ */
124
+ type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingBakersPercentage;
125
+ interface ScalingBase {
126
+ min?: number;
127
+ max?: number;
128
+ }
129
+ interface ScalingLinear extends ScalingBase {
130
+ type: "linear";
131
+ }
132
+ interface ScalingDiscrete extends ScalingBase {
133
+ type: "discrete";
134
+ roundTo?: number;
135
+ }
136
+ interface ScalingProportional extends ScalingBase {
137
+ type: "proportional";
138
+ factor?: number;
139
+ }
140
+ interface ScalingFixed extends ScalingBase {
141
+ type: "fixed";
142
+ }
143
+ interface ScalingBakersPercentage extends ScalingBase {
144
+ type: 'bakers_percentage';
145
+ /** The ID of the flour/base ingredient this is relative to */
146
+ referenceId: string;
147
+ /** The percentage relative to the reference (e.g. 0.02 for 2%) */
148
+ factor?: number;
149
+ }
150
+ type InstructionItem = string | Instruction | InstructionSubsection;
151
+ interface InstructionSubsection {
152
+ subsection: string;
153
+ items: (string | Instruction)[];
154
+ }
155
+ interface SoustackInstruction {
156
+ id?: string;
157
+ text: string;
158
+ destination?: string;
159
+ /** IDs of steps that must complete before this one starts */
160
+ dependsOn?: string[];
161
+ /** IDs of ingredients used in this step */
162
+ inputs?: string[];
163
+ timing?: StepTiming;
164
+ /** Optional image URL for this instruction */
165
+ image?: string;
166
+ }
167
+ type Instruction = SoustackInstruction;
168
+ interface StepTiming {
169
+ duration: number | string;
170
+ type: "active" | "passive";
171
+ scaling?: "linear" | "fixed" | "sqrt";
172
+ }
173
+ interface Storage {
174
+ roomTemp?: StorageMethod;
175
+ refrigerated?: StorageMethod;
176
+ frozen?: FrozenStorageMethod;
177
+ reheating?: string;
178
+ makeAhead?: MakeAheadComponent[];
179
+ }
180
+ interface StorageMethod {
181
+ /** ISO 8601 duration (e.g. P3D) */
182
+ duration: string;
183
+ method?: string;
184
+ notes?: string;
185
+ }
186
+ interface FrozenStorageMethod extends StorageMethod {
187
+ thawing?: string;
188
+ }
189
+ interface MakeAheadComponent extends StorageMethod {
190
+ component: string;
191
+ storage: "roomTemp" | "refrigerated" | "frozen";
192
+ }
193
+ interface Substitution {
194
+ ingredient: string;
195
+ critical?: boolean;
196
+ notes?: string;
197
+ alternatives?: Alternative[];
198
+ }
199
+ interface Alternative {
200
+ name: string;
201
+ ratio: string;
202
+ notes?: string;
203
+ impact?: string;
204
+ dietary?: string[];
205
+ }
206
+ interface NutritionFacts {
207
+ calories?: number;
208
+ protein_g?: number;
209
+ }
210
+ interface AttributionModule {
211
+ url?: string;
212
+ author?: string;
213
+ datePublished?: string;
214
+ }
215
+ interface TaxonomyModule {
216
+ keywords?: string[];
217
+ category?: string;
218
+ cuisine?: string;
219
+ }
220
+ interface MediaModule {
221
+ images?: string[];
222
+ videos?: string[];
223
+ }
224
+ interface TimesModule {
225
+ prepMinutes?: number;
226
+ cookMinutes?: number;
227
+ totalMinutes?: number;
228
+ }
229
+
230
+ interface HowToStep {
231
+ '@type'?: 'HowToStep' | 'HowToSection' | string;
232
+ name?: string;
233
+ text?: string;
234
+ itemListElement?: Array<string | HowToStep>;
235
+ }
236
+ interface SchemaOrgRecipe {
237
+ '@type': string | string[];
238
+ name?: string;
239
+ description?: string;
240
+ image?: string | string[];
241
+ recipeIngredient?: string[];
242
+ recipeInstructions?: Array<string | HowToStep>;
243
+ recipeYield?: string | number;
244
+ prepTime?: string;
245
+ cookTime?: string;
246
+ totalTime?: string;
247
+ author?: unknown;
248
+ datePublished?: string;
249
+ aggregateRating?: unknown;
250
+ [key: string]: unknown;
251
+ }
252
+ interface FetchRequestInit {
253
+ headers?: Record<string, string>;
254
+ signal?: AbortSignal;
255
+ redirect?: 'follow' | 'error' | 'manual';
256
+ }
257
+ interface FetchResponse {
258
+ ok: boolean;
259
+ status: number;
260
+ statusText: string;
261
+ text(): Promise<string>;
262
+ }
263
+ type FetchImplementation = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>;
264
+ interface FetchOptions {
265
+ timeout?: number;
266
+ userAgent?: string;
267
+ maxRetries?: number;
268
+ fetchFn?: FetchImplementation;
269
+ }
270
+ interface ScrapeRecipeOptions extends FetchOptions {
271
+ }
272
+
273
+ /**
274
+ * Scrapes a recipe from a URL (Node.js only).
275
+ *
276
+ * ⚠️ Not available in browser environments due to CORS restrictions.
277
+ * For browser usage, fetch the HTML yourself and use extractRecipeFromHTML().
278
+ *
279
+ * @param url - The URL of the recipe page to scrape
280
+ * @param options - Fetch options (timeout, userAgent, maxRetries)
281
+ * @returns A Soustack recipe object
282
+ * @throws Error if no recipe is found
283
+ */
284
+ declare function scrapeRecipe(url: string, options?: ScrapeRecipeOptions): Promise<Recipe>;
285
+ /**
286
+ * Extracts a recipe from HTML string (browser and Node.js compatible).
287
+ *
288
+ * This function works in both environments and doesn't require network access.
289
+ * Perfect for browser usage where you fetch HTML yourself (with cookies/session).
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * // In browser:
294
+ * const response = await fetch('https://example.com/recipe');
295
+ * const html = await response.text();
296
+ * const recipe = extractRecipeFromHTML(html);
297
+ * ```
298
+ *
299
+ * @param html - The HTML string containing Schema.org recipe data
300
+ * @returns A Soustack recipe object
301
+ * @throws Error if no recipe is found
302
+ */
303
+ declare function extractRecipeFromHTML(html: string): Recipe;
304
+ /**
305
+ * Extract Schema.org recipe data from HTML string (browser-compatible).
306
+ *
307
+ * Returns the raw Schema.org recipe object, which can then be converted
308
+ * to Soustack format using fromSchemaOrg(). This gives you access to the
309
+ * original Schema.org data for inspection, debugging, or custom transformations.
310
+ *
311
+ * @param html - HTML string containing Schema.org recipe data
312
+ * @returns Schema.org recipe object, or null if not found
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * // In browser:
317
+ * const response = await fetch('https://example.com/recipe');
318
+ * const html = await response.text();
319
+ * const schemaOrgRecipe = extractSchemaOrgRecipeFromHTML(html);
320
+ *
321
+ * if (schemaOrgRecipe) {
322
+ * // Inspect or modify Schema.org data before converting
323
+ * console.log('Found recipe:', schemaOrgRecipe.name);
324
+ *
325
+ * // Convert to Soustack format
326
+ * const soustackRecipe = fromSchemaOrg(schemaOrgRecipe);
327
+ * }
328
+ * ```
329
+ */
330
+ declare function extractSchemaOrgRecipeFromHTML(html: string): SchemaOrgRecipe | null;
331
+
332
+ declare function fetchPage(url: string, options?: FetchOptions): Promise<string>;
333
+
334
+ export { type FetchImplementation, type FetchOptions, type SchemaOrgRecipe, type ScrapeRecipeOptions, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, fetchPage, scrapeRecipe };
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Soustack Recipe Schema v0.3.0
3
+ * A portable, scalable, interoperable recipe format.
4
+ */
5
+ interface SoustackRecipe {
6
+ /** Document marker for Soustack recipes */
7
+ '@type'?: 'Recipe';
8
+ /** Optional $schema pointer for profile-aware validation */
9
+ $schema?: string;
10
+ /** Optional declared validation profile */
11
+ profile?: string;
12
+ /** Enabled module identifiers (e.g., "nutrition@1") */
13
+ modules?: string[];
14
+ /** Attribution module payload */
15
+ attribution?: AttributionModule;
16
+ /** Taxonomy module payload */
17
+ taxonomy?: TaxonomyModule;
18
+ /** Media module payload */
19
+ media?: MediaModule;
20
+ /** Times module payload */
21
+ times?: TimesModule;
22
+ /** Unique identifier (slug or UUID) */
23
+ id?: string;
24
+ /** Optional display title */
25
+ title?: string;
26
+ /** The title of the recipe */
27
+ name: string;
28
+ /** Semantic versioning (e.g., 1.0.0) */
29
+ recipeVersion?: string;
30
+ /** Deprecated alias for recipeVersion */
31
+ version?: string;
32
+ description?: string;
33
+ /** Primary category (e.g., "Main Course") */
34
+ category?: string;
35
+ /** Additional tags for filtering */
36
+ tags?: string[];
37
+ /** URL(s) to recipe image(s) */
38
+ image?: string | string[];
39
+ /** ISO 8601 date string */
40
+ dateAdded?: string;
41
+ /** Last updated timestamp */
42
+ dateModified?: string;
43
+ source?: Source;
44
+ yield?: Yield;
45
+ time?: Time;
46
+ equipment?: Equipment[];
47
+ ingredients: IngredientItem[];
48
+ instructions: InstructionItem[];
49
+ storage?: Storage;
50
+ substitutions?: Substitution[];
51
+ nutrition?: NutritionFacts;
52
+ metadata?: Record<string, unknown>;
53
+ [k: `x-${string}`]: unknown;
54
+ }
55
+ type Recipe = SoustackRecipe;
56
+ interface Source {
57
+ author?: string;
58
+ url?: string;
59
+ name?: string;
60
+ adapted?: boolean;
61
+ }
62
+ interface Yield {
63
+ amount: number;
64
+ unit: string;
65
+ servings?: number;
66
+ description?: string;
67
+ }
68
+ /**
69
+ * Time can be structured (machine-readable) or simple (strings).
70
+ * Structured time takes precedence if both exist.
71
+ */
72
+ type Time = StructuredTime | SimpleTime;
73
+ interface StructuredTime {
74
+ prep?: number;
75
+ active?: number;
76
+ passive?: number;
77
+ total?: number;
78
+ }
79
+ interface SimpleTime {
80
+ prepTime?: string;
81
+ cookTime?: string;
82
+ }
83
+ interface Equipment {
84
+ id?: string;
85
+ name: string;
86
+ required?: boolean;
87
+ label?: string;
88
+ capacity?: Quantity;
89
+ scalingLimit?: number;
90
+ alternatives?: string[];
91
+ }
92
+ interface Quantity {
93
+ amount: number;
94
+ /** Unit string (e.g. "g", "cup") or null for count-based items (e.g. "2 eggs") */
95
+ unit: string | null;
96
+ }
97
+ type IngredientItem = string | Ingredient | IngredientSubsection;
98
+ interface IngredientSubsection {
99
+ subsection: string;
100
+ items: (string | Ingredient)[];
101
+ }
102
+ interface Ingredient {
103
+ id?: string;
104
+ /** Full human-readable text (e.g. "2 cups flour") */
105
+ item: string;
106
+ quantity?: Quantity;
107
+ name?: string;
108
+ aisle?: string;
109
+ /** Required prep state (e.g. "diced") */
110
+ prep?: string;
111
+ prepAction?: string;
112
+ prepTime?: number;
113
+ /** ID of equipment where this ingredient goes */
114
+ destination?: string;
115
+ scaling?: Scaling;
116
+ critical?: boolean;
117
+ optional?: boolean;
118
+ notes?: string;
119
+ }
120
+ /**
121
+ * Intelligent Scaling Logic
122
+ * Defines how an ingredient behaves when the recipe yield changes.
123
+ */
124
+ type Scaling = ScalingLinear | ScalingDiscrete | ScalingProportional | ScalingFixed | ScalingBakersPercentage;
125
+ interface ScalingBase {
126
+ min?: number;
127
+ max?: number;
128
+ }
129
+ interface ScalingLinear extends ScalingBase {
130
+ type: "linear";
131
+ }
132
+ interface ScalingDiscrete extends ScalingBase {
133
+ type: "discrete";
134
+ roundTo?: number;
135
+ }
136
+ interface ScalingProportional extends ScalingBase {
137
+ type: "proportional";
138
+ factor?: number;
139
+ }
140
+ interface ScalingFixed extends ScalingBase {
141
+ type: "fixed";
142
+ }
143
+ interface ScalingBakersPercentage extends ScalingBase {
144
+ type: 'bakers_percentage';
145
+ /** The ID of the flour/base ingredient this is relative to */
146
+ referenceId: string;
147
+ /** The percentage relative to the reference (e.g. 0.02 for 2%) */
148
+ factor?: number;
149
+ }
150
+ type InstructionItem = string | Instruction | InstructionSubsection;
151
+ interface InstructionSubsection {
152
+ subsection: string;
153
+ items: (string | Instruction)[];
154
+ }
155
+ interface SoustackInstruction {
156
+ id?: string;
157
+ text: string;
158
+ destination?: string;
159
+ /** IDs of steps that must complete before this one starts */
160
+ dependsOn?: string[];
161
+ /** IDs of ingredients used in this step */
162
+ inputs?: string[];
163
+ timing?: StepTiming;
164
+ /** Optional image URL for this instruction */
165
+ image?: string;
166
+ }
167
+ type Instruction = SoustackInstruction;
168
+ interface StepTiming {
169
+ duration: number | string;
170
+ type: "active" | "passive";
171
+ scaling?: "linear" | "fixed" | "sqrt";
172
+ }
173
+ interface Storage {
174
+ roomTemp?: StorageMethod;
175
+ refrigerated?: StorageMethod;
176
+ frozen?: FrozenStorageMethod;
177
+ reheating?: string;
178
+ makeAhead?: MakeAheadComponent[];
179
+ }
180
+ interface StorageMethod {
181
+ /** ISO 8601 duration (e.g. P3D) */
182
+ duration: string;
183
+ method?: string;
184
+ notes?: string;
185
+ }
186
+ interface FrozenStorageMethod extends StorageMethod {
187
+ thawing?: string;
188
+ }
189
+ interface MakeAheadComponent extends StorageMethod {
190
+ component: string;
191
+ storage: "roomTemp" | "refrigerated" | "frozen";
192
+ }
193
+ interface Substitution {
194
+ ingredient: string;
195
+ critical?: boolean;
196
+ notes?: string;
197
+ alternatives?: Alternative[];
198
+ }
199
+ interface Alternative {
200
+ name: string;
201
+ ratio: string;
202
+ notes?: string;
203
+ impact?: string;
204
+ dietary?: string[];
205
+ }
206
+ interface NutritionFacts {
207
+ calories?: number;
208
+ protein_g?: number;
209
+ }
210
+ interface AttributionModule {
211
+ url?: string;
212
+ author?: string;
213
+ datePublished?: string;
214
+ }
215
+ interface TaxonomyModule {
216
+ keywords?: string[];
217
+ category?: string;
218
+ cuisine?: string;
219
+ }
220
+ interface MediaModule {
221
+ images?: string[];
222
+ videos?: string[];
223
+ }
224
+ interface TimesModule {
225
+ prepMinutes?: number;
226
+ cookMinutes?: number;
227
+ totalMinutes?: number;
228
+ }
229
+
230
+ interface HowToStep {
231
+ '@type'?: 'HowToStep' | 'HowToSection' | string;
232
+ name?: string;
233
+ text?: string;
234
+ itemListElement?: Array<string | HowToStep>;
235
+ }
236
+ interface SchemaOrgRecipe {
237
+ '@type': string | string[];
238
+ name?: string;
239
+ description?: string;
240
+ image?: string | string[];
241
+ recipeIngredient?: string[];
242
+ recipeInstructions?: Array<string | HowToStep>;
243
+ recipeYield?: string | number;
244
+ prepTime?: string;
245
+ cookTime?: string;
246
+ totalTime?: string;
247
+ author?: unknown;
248
+ datePublished?: string;
249
+ aggregateRating?: unknown;
250
+ [key: string]: unknown;
251
+ }
252
+ interface FetchRequestInit {
253
+ headers?: Record<string, string>;
254
+ signal?: AbortSignal;
255
+ redirect?: 'follow' | 'error' | 'manual';
256
+ }
257
+ interface FetchResponse {
258
+ ok: boolean;
259
+ status: number;
260
+ statusText: string;
261
+ text(): Promise<string>;
262
+ }
263
+ type FetchImplementation = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>;
264
+ interface FetchOptions {
265
+ timeout?: number;
266
+ userAgent?: string;
267
+ maxRetries?: number;
268
+ fetchFn?: FetchImplementation;
269
+ }
270
+ interface ScrapeRecipeOptions extends FetchOptions {
271
+ }
272
+
273
+ /**
274
+ * Scrapes a recipe from a URL (Node.js only).
275
+ *
276
+ * ⚠️ Not available in browser environments due to CORS restrictions.
277
+ * For browser usage, fetch the HTML yourself and use extractRecipeFromHTML().
278
+ *
279
+ * @param url - The URL of the recipe page to scrape
280
+ * @param options - Fetch options (timeout, userAgent, maxRetries)
281
+ * @returns A Soustack recipe object
282
+ * @throws Error if no recipe is found
283
+ */
284
+ declare function scrapeRecipe(url: string, options?: ScrapeRecipeOptions): Promise<Recipe>;
285
+ /**
286
+ * Extracts a recipe from HTML string (browser and Node.js compatible).
287
+ *
288
+ * This function works in both environments and doesn't require network access.
289
+ * Perfect for browser usage where you fetch HTML yourself (with cookies/session).
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * // In browser:
294
+ * const response = await fetch('https://example.com/recipe');
295
+ * const html = await response.text();
296
+ * const recipe = extractRecipeFromHTML(html);
297
+ * ```
298
+ *
299
+ * @param html - The HTML string containing Schema.org recipe data
300
+ * @returns A Soustack recipe object
301
+ * @throws Error if no recipe is found
302
+ */
303
+ declare function extractRecipeFromHTML(html: string): Recipe;
304
+ /**
305
+ * Extract Schema.org recipe data from HTML string (browser-compatible).
306
+ *
307
+ * Returns the raw Schema.org recipe object, which can then be converted
308
+ * to Soustack format using fromSchemaOrg(). This gives you access to the
309
+ * original Schema.org data for inspection, debugging, or custom transformations.
310
+ *
311
+ * @param html - HTML string containing Schema.org recipe data
312
+ * @returns Schema.org recipe object, or null if not found
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * // In browser:
317
+ * const response = await fetch('https://example.com/recipe');
318
+ * const html = await response.text();
319
+ * const schemaOrgRecipe = extractSchemaOrgRecipeFromHTML(html);
320
+ *
321
+ * if (schemaOrgRecipe) {
322
+ * // Inspect or modify Schema.org data before converting
323
+ * console.log('Found recipe:', schemaOrgRecipe.name);
324
+ *
325
+ * // Convert to Soustack format
326
+ * const soustackRecipe = fromSchemaOrg(schemaOrgRecipe);
327
+ * }
328
+ * ```
329
+ */
330
+ declare function extractSchemaOrgRecipeFromHTML(html: string): SchemaOrgRecipe | null;
331
+
332
+ declare function fetchPage(url: string, options?: FetchOptions): Promise<string>;
333
+
334
+ export { type FetchImplementation, type FetchOptions, type SchemaOrgRecipe, type ScrapeRecipeOptions, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, fetchPage, scrapeRecipe };