soustack 0.2.3 → 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/dist/scrape.d.mts CHANGED
@@ -1,10 +1,24 @@
1
1
  /**
2
- * Soustack Recipe Schema v0.2.1
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';
6
8
  /** Optional $schema pointer for profile-aware validation */
7
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;
8
22
  /** Unique identifier (slug or UUID) */
9
23
  id?: string;
10
24
  /** Optional display title */
@@ -190,15 +204,27 @@ interface Alternative {
190
204
  dietary?: string[];
191
205
  }
192
206
  interface NutritionFacts {
193
- calories?: string;
194
- fatContent?: string;
195
- carbohydrateContent?: string;
196
- proteinContent?: string;
197
- fiberContent?: string;
198
- sugarContent?: string;
199
- sodiumContent?: string;
200
- servingSize?: string;
201
- [key: string]: string | number | null | string[] | undefined;
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;
202
228
  }
203
229
 
204
230
  interface HowToStep {
package/dist/scrape.d.ts CHANGED
@@ -1,10 +1,24 @@
1
1
  /**
2
- * Soustack Recipe Schema v0.2.1
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';
6
8
  /** Optional $schema pointer for profile-aware validation */
7
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;
8
22
  /** Unique identifier (slug or UUID) */
9
23
  id?: string;
10
24
  /** Optional display title */
@@ -190,15 +204,27 @@ interface Alternative {
190
204
  dietary?: string[];
191
205
  }
192
206
  interface NutritionFacts {
193
- calories?: string;
194
- fatContent?: string;
195
- carbohydrateContent?: string;
196
- proteinContent?: string;
197
- fiberContent?: string;
198
- sugarContent?: string;
199
- sodiumContent?: string;
200
- servingSize?: string;
201
- [key: string]: string | number | null | string[] | undefined;
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;
202
228
  }
203
229
 
204
230
  interface HowToStep {
package/dist/scrape.js CHANGED
@@ -143,8 +143,22 @@ function fromSchemaOrg(input) {
143
143
  const tags = collectTags(recipeNode.recipeCuisine, recipeNode.keywords);
144
144
  const category = extractFirst(recipeNode.recipeCategory);
145
145
  const source = convertSource(recipeNode);
146
- const nutrition = recipeNode.nutrition && typeof recipeNode.nutrition === "object" ? recipeNode.nutrition : void 0;
146
+ const dateModified = recipeNode.dateModified || void 0;
147
+ const nutrition = convertNutrition(recipeNode.nutrition);
148
+ const attribution = convertAttribution(recipeNode);
149
+ const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
150
+ const media = convertMedia(recipeNode.image, recipeNode.video);
151
+ const times = convertTimes(time);
152
+ const modules = [];
153
+ if (attribution) modules.push("attribution@1");
154
+ if (taxonomy) modules.push("taxonomy@1");
155
+ if (media) modules.push("media@1");
156
+ if (nutrition) modules.push("nutrition@1");
157
+ if (times) modules.push("times@1");
147
158
  return {
159
+ "@type": "Recipe",
160
+ profile: "minimal",
161
+ modules: modules.sort(),
148
162
  name: recipeNode.name.trim(),
149
163
  description: recipeNode.description?.trim() || void 0,
150
164
  image: normalizeImage(recipeNode.image),
@@ -152,12 +166,16 @@ function fromSchemaOrg(input) {
152
166
  tags: tags.length ? tags : void 0,
153
167
  source,
154
168
  dateAdded: recipeNode.datePublished || void 0,
155
- dateModified: recipeNode.dateModified || void 0,
156
169
  yield: recipeYield,
157
170
  time,
158
171
  ingredients,
159
172
  instructions,
160
- nutrition
173
+ ...dateModified ? { dateModified } : {},
174
+ ...nutrition ? { nutrition } : {},
175
+ ...attribution ? { attribution } : {},
176
+ ...taxonomy ? { taxonomy } : {},
177
+ ...media ? { media } : {},
178
+ ...times ? { times } : {}
161
179
  };
162
180
  }
163
181
  function extractRecipeNode(input) {
@@ -370,6 +388,90 @@ function extractEntityName(value) {
370
388
  }
371
389
  return void 0;
372
390
  }
391
+ function convertAttribution(recipe) {
392
+ const attribution = {};
393
+ const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
394
+ const author = extractEntityName(recipe.author);
395
+ const datePublished = recipe.datePublished?.trim();
396
+ if (url) attribution.url = url;
397
+ if (author) attribution.author = author;
398
+ if (datePublished) attribution.datePublished = datePublished;
399
+ return Object.keys(attribution).length ? attribution : void 0;
400
+ }
401
+ function convertTaxonomy(keywords, category, cuisine) {
402
+ const taxonomy = {};
403
+ if (keywords.length) taxonomy.keywords = keywords;
404
+ if (category) taxonomy.category = category;
405
+ if (cuisine) taxonomy.cuisine = cuisine;
406
+ return Object.keys(taxonomy).length ? taxonomy : void 0;
407
+ }
408
+ function normalizeMediaList(value) {
409
+ if (!value) return [];
410
+ if (typeof value === "string") return [value.trim()].filter(Boolean);
411
+ if (Array.isArray(value)) {
412
+ return value.map((item) => typeof item === "string" ? item.trim() : extractMediaUrl(item)).filter((entry) => Boolean(entry?.length));
413
+ }
414
+ const url = extractMediaUrl(value);
415
+ return url ? [url] : [];
416
+ }
417
+ function extractMediaUrl(value) {
418
+ if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
419
+ const trimmed = value.url.trim();
420
+ return trimmed || void 0;
421
+ }
422
+ return void 0;
423
+ }
424
+ function convertMedia(image, video) {
425
+ const normalizedImage = normalizeImage(image);
426
+ const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
427
+ const videos = normalizeMediaList(video);
428
+ const media = {};
429
+ if (images.length) media.images = images;
430
+ if (videos.length) media.videos = videos;
431
+ return Object.keys(media).length ? media : void 0;
432
+ }
433
+ function convertTimes(time) {
434
+ if (!time) return void 0;
435
+ const times = {};
436
+ if (typeof time.prep === "number") times.prepMinutes = time.prep;
437
+ if (typeof time.active === "number") times.cookMinutes = time.active;
438
+ if (typeof time.total === "number") times.totalMinutes = time.total;
439
+ return Object.keys(times).length ? times : void 0;
440
+ }
441
+ function convertNutrition(nutrition) {
442
+ if (!nutrition || typeof nutrition !== "object") {
443
+ return void 0;
444
+ }
445
+ const result = {};
446
+ let hasData = false;
447
+ if ("calories" in nutrition) {
448
+ const calories = nutrition.calories;
449
+ if (typeof calories === "number") {
450
+ result.calories = calories;
451
+ hasData = true;
452
+ } else if (typeof calories === "string") {
453
+ const parsed = parseFloat(calories.replace(/[^\d.-]/g, ""));
454
+ if (!isNaN(parsed)) {
455
+ result.calories = parsed;
456
+ hasData = true;
457
+ }
458
+ }
459
+ }
460
+ if ("proteinContent" in nutrition || "protein_g" in nutrition) {
461
+ const protein = nutrition.proteinContent || nutrition.protein_g;
462
+ if (typeof protein === "number") {
463
+ result.protein_g = protein;
464
+ hasData = true;
465
+ } else if (typeof protein === "string") {
466
+ const parsed = parseFloat(protein.replace(/[^\d.-]/g, ""));
467
+ if (!isNaN(parsed)) {
468
+ result.protein_g = parsed;
469
+ hasData = true;
470
+ }
471
+ }
472
+ }
473
+ return hasData ? result : void 0;
474
+ }
373
475
 
374
476
  // src/scraper/fetch.ts
375
477
  var DEFAULT_USER_AGENTS = [