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.
- package/LICENSE +21 -21
- package/README.md +394 -244
- package/dist/cli/index.js +1672 -1387
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +159 -151
- package/dist/index.d.ts +159 -151
- package/dist/index.js +1731 -1641
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1725 -1628
- package/dist/index.mjs.map +1 -1
- package/dist/scrape.d.mts +334 -0
- package/dist/scrape.d.ts +334 -0
- package/dist/scrape.js +921 -0
- package/dist/scrape.js.map +1 -0
- package/dist/scrape.mjs +916 -0
- package/dist/scrape.mjs.map +1 -0
- package/package.json +89 -75
- package/src/profiles/.gitkeep +0 -0
- package/src/profiles/base.schema.json +9 -0
- package/src/profiles/cookable.schema.json +18 -0
- package/src/profiles/illustrated.schema.json +48 -0
- package/src/profiles/quantified.schema.json +43 -0
- package/src/profiles/scalable.schema.json +75 -0
- package/src/profiles/schedulable.schema.json +43 -0
- package/src/schema.json +56 -23
- package/src/soustack.schema.json +356 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Soustack Recipe Schema v0.
|
|
2
|
+
* Soustack Recipe Schema v0.3.0
|
|
3
3
|
* A portable, scalable, interoperable recipe format.
|
|
4
4
|
*/
|
|
5
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;
|
|
6
22
|
/** Unique identifier (slug or UUID) */
|
|
7
23
|
id?: string;
|
|
24
|
+
/** Optional display title */
|
|
25
|
+
title?: string;
|
|
8
26
|
/** The title of the recipe */
|
|
9
27
|
name: string;
|
|
10
28
|
/** Semantic versioning (e.g., 1.0.0) */
|
|
29
|
+
recipeVersion?: string;
|
|
30
|
+
/** Deprecated alias for recipeVersion */
|
|
11
31
|
version?: string;
|
|
12
32
|
description?: string;
|
|
13
33
|
/** Primary category (e.g., "Main Course") */
|
|
@@ -29,6 +49,8 @@ interface SoustackRecipe {
|
|
|
29
49
|
storage?: Storage;
|
|
30
50
|
substitutions?: Substitution[];
|
|
31
51
|
nutrition?: NutritionFacts;
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
[k: `x-${string}`]: unknown;
|
|
32
54
|
}
|
|
33
55
|
type Recipe = SoustackRecipe;
|
|
34
56
|
interface Source {
|
|
@@ -69,25 +91,25 @@ interface Equipment {
|
|
|
69
91
|
name: string;
|
|
70
92
|
required?: boolean;
|
|
71
93
|
label?: string;
|
|
72
|
-
capacity?: Quantity;
|
|
94
|
+
capacity?: Quantity$1;
|
|
73
95
|
scalingLimit?: number;
|
|
74
96
|
alternatives?: string[];
|
|
75
97
|
}
|
|
76
|
-
interface Quantity {
|
|
98
|
+
interface Quantity$1 {
|
|
77
99
|
amount: number;
|
|
78
100
|
/** Unit string (e.g. "g", "cup") or null for count-based items (e.g. "2 eggs") */
|
|
79
101
|
unit: string | null;
|
|
80
102
|
}
|
|
81
|
-
type IngredientItem = string | Ingredient | IngredientSubsection;
|
|
103
|
+
type IngredientItem = string | Ingredient$1 | IngredientSubsection;
|
|
82
104
|
interface IngredientSubsection {
|
|
83
105
|
subsection: string;
|
|
84
|
-
items: (string | Ingredient)[];
|
|
106
|
+
items: (string | Ingredient$1)[];
|
|
85
107
|
}
|
|
86
|
-
interface Ingredient {
|
|
108
|
+
interface Ingredient$1 {
|
|
87
109
|
id?: string;
|
|
88
110
|
/** Full human-readable text (e.g. "2 cups flour") */
|
|
89
111
|
item: string;
|
|
90
|
-
quantity?: Quantity;
|
|
112
|
+
quantity?: Quantity$1;
|
|
91
113
|
name?: string;
|
|
92
114
|
aisle?: string;
|
|
93
115
|
/** Required prep state (e.g. "diced") */
|
|
@@ -162,7 +184,7 @@ interface SoustackInstruction {
|
|
|
162
184
|
}
|
|
163
185
|
type Instruction = SoustackInstruction;
|
|
164
186
|
interface StepTiming {
|
|
165
|
-
duration: number;
|
|
187
|
+
duration: number | string;
|
|
166
188
|
type: "active" | "passive";
|
|
167
189
|
scaling?: "linear" | "fixed" | "sqrt";
|
|
168
190
|
}
|
|
@@ -200,57 +222,66 @@ interface Alternative {
|
|
|
200
222
|
dietary?: string[];
|
|
201
223
|
}
|
|
202
224
|
interface NutritionFacts {
|
|
203
|
-
calories?:
|
|
204
|
-
|
|
205
|
-
carbohydrateContent?: string;
|
|
206
|
-
proteinContent?: string;
|
|
207
|
-
fiberContent?: string;
|
|
208
|
-
sugarContent?: string;
|
|
209
|
-
sodiumContent?: string;
|
|
210
|
-
servingSize?: string;
|
|
211
|
-
[key: string]: string | number | null | string[] | undefined;
|
|
225
|
+
calories?: number;
|
|
226
|
+
protein_g?: number;
|
|
212
227
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
*/
|
|
218
|
-
interface ComputedRecipe {
|
|
219
|
-
metadata: {
|
|
220
|
-
targetYield: number;
|
|
221
|
-
baseYield: number;
|
|
222
|
-
multiplier: number;
|
|
223
|
-
};
|
|
224
|
-
ingredients: ComputedIngredient[];
|
|
225
|
-
instructions: ComputedInstruction[];
|
|
226
|
-
timing: {
|
|
227
|
-
active: number;
|
|
228
|
-
passive: number;
|
|
229
|
-
total: number;
|
|
230
|
-
};
|
|
228
|
+
interface AttributionModule {
|
|
229
|
+
url?: string;
|
|
230
|
+
author?: string;
|
|
231
|
+
datePublished?: string;
|
|
231
232
|
}
|
|
232
|
-
interface
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
unit: string | null;
|
|
237
|
-
text: string;
|
|
238
|
-
notes?: string;
|
|
233
|
+
interface TaxonomyModule {
|
|
234
|
+
keywords?: string[];
|
|
235
|
+
category?: string;
|
|
236
|
+
cuisine?: string;
|
|
239
237
|
}
|
|
240
|
-
interface
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
238
|
+
interface MediaModule {
|
|
239
|
+
images?: string[];
|
|
240
|
+
videos?: string[];
|
|
241
|
+
}
|
|
242
|
+
interface TimesModule {
|
|
243
|
+
prepMinutes?: number;
|
|
244
|
+
cookMinutes?: number;
|
|
245
|
+
totalMinutes?: number;
|
|
245
246
|
}
|
|
246
|
-
declare function scaleRecipe(recipe: Recipe, targetYieldAmount: number): ComputedRecipe;
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
interface ScaleRecipeOptions {
|
|
249
|
+
multiplier?: number;
|
|
250
|
+
targetYield?: {
|
|
251
|
+
amount: number;
|
|
252
|
+
unit?: string;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
declare function scaleRecipe(recipe: Recipe, options?: ScaleRecipeOptions): Recipe;
|
|
256
|
+
|
|
257
|
+
type ProfileName = "minimal" | "core";
|
|
258
|
+
interface NormalizedError {
|
|
259
|
+
path: string;
|
|
260
|
+
message: string;
|
|
261
|
+
keyword?: string;
|
|
262
|
+
}
|
|
263
|
+
interface NormalizedWarning {
|
|
264
|
+
path: string;
|
|
265
|
+
message: string;
|
|
266
|
+
}
|
|
267
|
+
interface ValidateOptions {
|
|
268
|
+
profile?: ProfileName;
|
|
269
|
+
schema?: string;
|
|
270
|
+
collectAllErrors?: boolean;
|
|
271
|
+
}
|
|
272
|
+
interface ValidationResult {
|
|
273
|
+
valid: boolean;
|
|
274
|
+
errors: NormalizedError[];
|
|
275
|
+
warnings: NormalizedWarning[];
|
|
276
|
+
normalized?: Recipe;
|
|
277
|
+
}
|
|
278
|
+
declare function validateRecipe(input: any, options?: ValidateOptions): ValidationResult;
|
|
279
|
+
declare function detectProfiles(recipe: any): ProfileName[];
|
|
249
280
|
|
|
250
281
|
declare function fromSchemaOrg(input: unknown): Recipe | null;
|
|
251
282
|
|
|
252
283
|
interface SchemaOrgRecipe$1 {
|
|
253
|
-
'@context'?: string
|
|
284
|
+
'@context'?: string | Array<string | Record<string, unknown>> | Record<string, unknown>;
|
|
254
285
|
'@type'?: string | string[];
|
|
255
286
|
name: string;
|
|
256
287
|
description?: string;
|
|
@@ -271,6 +302,7 @@ interface SchemaOrgRecipe$1 {
|
|
|
271
302
|
datePublished?: string;
|
|
272
303
|
dateModified?: string;
|
|
273
304
|
nutrition?: NutritionInformation;
|
|
305
|
+
video?: SchemaOrgImage;
|
|
274
306
|
'@graph'?: unknown;
|
|
275
307
|
}
|
|
276
308
|
type SchemaOrgIngredientList = string | string[];
|
|
@@ -295,6 +327,12 @@ interface HowToStep$1 {
|
|
|
295
327
|
name?: string;
|
|
296
328
|
url?: string;
|
|
297
329
|
image?: SchemaOrgImage;
|
|
330
|
+
'@id'?: string;
|
|
331
|
+
id?: string;
|
|
332
|
+
totalTime?: string;
|
|
333
|
+
performTime?: string;
|
|
334
|
+
prepTime?: string;
|
|
335
|
+
duration?: string;
|
|
298
336
|
}
|
|
299
337
|
interface HowToSection {
|
|
300
338
|
'@type': 'HowToSection';
|
|
@@ -310,6 +348,14 @@ interface NutritionInformation {
|
|
|
310
348
|
[key: string]: string | number | null | undefined;
|
|
311
349
|
}
|
|
312
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Convert a Soustack recipe to Schema.org JSON-LD format.
|
|
353
|
+
*
|
|
354
|
+
* BREAKING CHANGE in v0.3.0: This function now targets the "minimal" profile
|
|
355
|
+
* and only includes modules that are schemaOrgMappable (as defined in the
|
|
356
|
+
* modules registry). Non-mappable modules (e.g., nutrition@1, schedule@1)
|
|
357
|
+
* are excluded from the conversion.
|
|
358
|
+
*/
|
|
313
359
|
declare function toSchemaOrg(recipe: Recipe): SchemaOrgRecipe$1;
|
|
314
360
|
|
|
315
361
|
interface HowToStep {
|
|
@@ -334,110 +380,72 @@ interface SchemaOrgRecipe {
|
|
|
334
380
|
aggregateRating?: unknown;
|
|
335
381
|
[key: string]: unknown;
|
|
336
382
|
}
|
|
337
|
-
interface FetchRequestInit {
|
|
338
|
-
headers?: Record<string, string>;
|
|
339
|
-
signal?: AbortSignal;
|
|
340
|
-
redirect?: 'follow' | 'error' | 'manual';
|
|
341
|
-
}
|
|
342
|
-
interface FetchResponse {
|
|
343
|
-
ok: boolean;
|
|
344
|
-
status: number;
|
|
345
|
-
statusText: string;
|
|
346
|
-
text(): Promise<string>;
|
|
347
|
-
}
|
|
348
|
-
type FetchImplementation = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>;
|
|
349
|
-
interface FetchOptions {
|
|
350
|
-
timeout?: number;
|
|
351
|
-
userAgent?: string;
|
|
352
|
-
maxRetries?: number;
|
|
353
|
-
fetchFn?: FetchImplementation;
|
|
354
|
-
}
|
|
355
|
-
interface ScrapeRecipeOptions extends FetchOptions {
|
|
356
|
-
}
|
|
357
383
|
|
|
358
|
-
/**
|
|
359
|
-
* Scrapes a recipe from a URL (Node.js only).
|
|
360
|
-
*
|
|
361
|
-
* ⚠️ Not available in browser environments due to CORS restrictions.
|
|
362
|
-
* For browser usage, fetch the HTML yourself and use extractRecipeFromHTML().
|
|
363
|
-
*
|
|
364
|
-
* @param url - The URL of the recipe page to scrape
|
|
365
|
-
* @param options - Fetch options (timeout, userAgent, maxRetries)
|
|
366
|
-
* @returns A Soustack recipe object
|
|
367
|
-
* @throws Error if no recipe is found
|
|
368
|
-
*/
|
|
369
|
-
declare function scrapeRecipe(url: string, options?: ScrapeRecipeOptions): Promise<Recipe>;
|
|
370
|
-
/**
|
|
371
|
-
* Extracts a recipe from HTML string (browser and Node.js compatible).
|
|
372
|
-
*
|
|
373
|
-
* This function works in both environments and doesn't require network access.
|
|
374
|
-
* Perfect for browser usage where you fetch HTML yourself (with cookies/session).
|
|
375
|
-
*
|
|
376
|
-
* @example
|
|
377
|
-
* ```ts
|
|
378
|
-
* // In browser:
|
|
379
|
-
* const response = await fetch('https://example.com/recipe');
|
|
380
|
-
* const html = await response.text();
|
|
381
|
-
* const recipe = extractRecipeFromHTML(html);
|
|
382
|
-
* ```
|
|
383
|
-
*
|
|
384
|
-
* @param html - The HTML string containing Schema.org recipe data
|
|
385
|
-
* @returns A Soustack recipe object
|
|
386
|
-
* @throws Error if no recipe is found
|
|
387
|
-
*/
|
|
388
|
-
declare function extractRecipeFromHTML(html: string): Recipe;
|
|
389
|
-
/**
|
|
390
|
-
* Extract Schema.org recipe data from HTML string (browser-compatible).
|
|
391
|
-
*
|
|
392
|
-
* Returns the raw Schema.org recipe object, which can then be converted
|
|
393
|
-
* to Soustack format using fromSchemaOrg(). This gives you access to the
|
|
394
|
-
* original Schema.org data for inspection, debugging, or custom transformations.
|
|
395
|
-
*
|
|
396
|
-
* @param html - HTML string containing Schema.org recipe data
|
|
397
|
-
* @returns Schema.org recipe object, or null if not found
|
|
398
|
-
*
|
|
399
|
-
* @example
|
|
400
|
-
* ```ts
|
|
401
|
-
* // In browser:
|
|
402
|
-
* const response = await fetch('https://example.com/recipe');
|
|
403
|
-
* const html = await response.text();
|
|
404
|
-
* const schemaOrgRecipe = extractSchemaOrgRecipeFromHTML(html);
|
|
405
|
-
*
|
|
406
|
-
* if (schemaOrgRecipe) {
|
|
407
|
-
* // Inspect or modify Schema.org data before converting
|
|
408
|
-
* console.log('Found recipe:', schemaOrgRecipe.name);
|
|
409
|
-
*
|
|
410
|
-
* // Convert to Soustack format
|
|
411
|
-
* const soustackRecipe = fromSchemaOrg(schemaOrgRecipe);
|
|
412
|
-
* }
|
|
413
|
-
* ```
|
|
414
|
-
*/
|
|
415
384
|
declare function extractSchemaOrgRecipeFromHTML(html: string): SchemaOrgRecipe | null;
|
|
416
385
|
|
|
417
|
-
declare
|
|
418
|
-
declare function parseIngredient(text: string): ParsedIngredient;
|
|
419
|
-
declare function parseIngredientLine(text: string): ParsedIngredient;
|
|
420
|
-
declare function parseIngredients(texts: string[]): ParsedIngredient[];
|
|
421
|
-
|
|
422
|
-
declare function parseDuration(iso: string): number | null;
|
|
423
|
-
declare function parseDuration(iso: string | null | undefined): number | null;
|
|
424
|
-
declare function formatDuration(minutes: number): string;
|
|
425
|
-
declare function formatDuration(minutes: number | null | undefined): string;
|
|
426
|
-
declare function parseHumanDuration(text: string): number | null;
|
|
427
|
-
declare function parseHumanDuration(text: string | null | undefined): number | null;
|
|
428
|
-
declare function smartParseDuration(input: string): number | null;
|
|
429
|
-
declare function smartParseDuration(input: string | null | undefined): number | null;
|
|
386
|
+
declare const SOUSTACK_SPEC_VERSION = "0.3.0";
|
|
430
387
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
388
|
+
type ConvertTarget = 'metric';
|
|
389
|
+
type ConvertMode = 'volume' | 'mass';
|
|
390
|
+
type RoundMode = 'none' | 'sane';
|
|
391
|
+
interface LineItem {
|
|
392
|
+
ingredient: string;
|
|
393
|
+
quantity: number;
|
|
394
|
+
unit: string | null;
|
|
395
|
+
}
|
|
396
|
+
interface ConvertedLineItem extends LineItem {
|
|
397
|
+
notes?: string;
|
|
398
|
+
}
|
|
399
|
+
declare class UnknownUnitError extends Error {
|
|
400
|
+
readonly unit: string;
|
|
401
|
+
constructor(unit: string);
|
|
402
|
+
}
|
|
403
|
+
declare class UnsupportedConversionError extends Error {
|
|
404
|
+
readonly unit: string;
|
|
405
|
+
readonly mode: ConvertMode;
|
|
406
|
+
constructor(unit: string, mode: ConvertMode);
|
|
407
|
+
}
|
|
408
|
+
declare class MissingEquivalencyError extends Error {
|
|
409
|
+
readonly ingredient: string;
|
|
410
|
+
readonly unit: string;
|
|
411
|
+
constructor(ingredient: string, unit: string);
|
|
412
|
+
}
|
|
413
|
+
declare function convertLineItemToMetric(item: LineItem, mode: ConvertMode, opts?: {
|
|
414
|
+
round?: RoundMode;
|
|
415
|
+
}): ConvertedLineItem;
|
|
434
416
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
417
|
+
interface Quantity {
|
|
418
|
+
amount: number;
|
|
419
|
+
unit?: string | null;
|
|
420
|
+
}
|
|
421
|
+
interface Ingredient {
|
|
422
|
+
id?: string;
|
|
423
|
+
item: string;
|
|
424
|
+
quantity?: Quantity;
|
|
425
|
+
name?: string;
|
|
426
|
+
prep?: string;
|
|
427
|
+
prepAction?: string;
|
|
428
|
+
prepActions?: string[];
|
|
429
|
+
form?: string;
|
|
430
|
+
prepTime?: number;
|
|
431
|
+
optional?: boolean;
|
|
432
|
+
notes?: string;
|
|
433
|
+
}
|
|
434
|
+
interface MiseEnPlaceTask {
|
|
435
|
+
category: 'prep' | 'state' | 'measure' | 'other';
|
|
436
|
+
action?: string;
|
|
437
|
+
form?: string;
|
|
438
|
+
items: Array<{
|
|
439
|
+
ingredient: string;
|
|
440
|
+
quantity?: Quantity;
|
|
441
|
+
optional?: boolean;
|
|
442
|
+
notes?: string;
|
|
443
|
+
}>;
|
|
444
|
+
}
|
|
445
|
+
interface MiseEnPlacePlan {
|
|
446
|
+
tasks: MiseEnPlaceTask[];
|
|
447
|
+
ungrouped: Ingredient[];
|
|
448
|
+
}
|
|
449
|
+
declare function miseEnPlace(ingredients: Ingredient[]): MiseEnPlacePlan;
|
|
442
450
|
|
|
443
|
-
export { type Alternative, type
|
|
451
|
+
export { type Alternative, type AttributionModule, type ConvertMode, type ConvertTarget, type ConvertedLineItem, type Equipment, type FrozenStorageMethod, type Ingredient$1 as Ingredient, type IngredientItem, type IngredientSubsection, type Instruction, type InstructionItem, type InstructionSubsection, type LineItem, type MakeAheadComponent, type MediaModule, type Ingredient as MiseEnPlaceIngredient, type MiseEnPlacePlan, type Quantity as MiseEnPlaceQuantity, type MiseEnPlaceTask, MissingEquivalencyError, type NutritionFacts, type ParsedIngredient, type ParsedYield, type Quantity$1 as Quantity, type Recipe, type RoundMode, SOUSTACK_SPEC_VERSION, type Scaling, type ScalingBakersPercentage, type ScalingBase, type ScalingDiscrete, type ScalingFixed, type ScalingLinear, type ScalingProportional, type SimpleTime, type Source, type SoustackInstruction, type SoustackRecipe, type StepTiming, type Storage, type StorageMethod, type StructuredTime, type Substitution, type TaxonomyModule, type Time, type TimesModule, UnknownUnitError, UnsupportedConversionError, type Yield, convertLineItemToMetric, detectProfiles, extractSchemaOrgRecipeFromHTML, fromSchemaOrg, miseEnPlace, scaleRecipe, toSchemaOrg, validateRecipe };
|