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/README.md +100 -7
- package/dist/cli/index.js +738 -793
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +117 -19
- package/dist/index.d.ts +117 -19
- package/dist/index.js +1264 -801
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1260 -802
- package/dist/index.mjs.map +1 -1
- package/dist/scrape.d.mts +36 -10
- package/dist/scrape.d.ts +36 -10
- package/dist/scrape.js +105 -3
- package/dist/scrape.js.map +1 -1
- package/dist/scrape.mjs +105 -3
- package/dist/scrape.mjs.map +1 -1
- package/package.json +7 -4
- package/src/profiles/base.schema.json +2 -2
- package/src/profiles/cookable.schema.json +4 -4
- package/src/profiles/illustrated.schema.json +4 -4
- package/src/profiles/quantified.schema.json +4 -4
- package/src/profiles/scalable.schema.json +6 -6
- package/src/profiles/schedulable.schema.json +4 -4
- package/src/schema.json +15 -3
- package/src/soustack.schema.json +15 -3
package/dist/scrape.d.mts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
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';
|
|
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?:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
* 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?:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
-
|
|
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 = [
|