soustack 0.4.0 → 0.4.1
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 +4 -4
- package/dist/cli/index.js +4412 -1275
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +106 -80
- package/dist/index.d.ts +106 -80
- package/dist/index.js +4527 -1360
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4527 -1360
- package/dist/index.mjs.map +1 -1
- package/dist/scrape/index.d.mts +86 -74
- package/dist/scrape/index.d.ts +86 -74
- package/dist/scrape/index.js +91 -64
- package/dist/scrape/index.js.map +1 -1
- package/dist/scrape/index.mjs +91 -64
- package/dist/scrape/index.mjs.map +1 -1
- package/package.json +15 -6
- package/spec/.sync-meta.json +149 -0
- package/spec/SOUSTACK_SPEC_VERSION +1 -0
- package/spec/defs/common.schema.json +46 -0
- package/spec/defs/duration.schema.json +33 -0
- package/spec/defs/entities.schema.json +111 -0
- package/spec/defs/ingredientQuantified.schema.json +9 -0
- package/spec/defs/quantity.schema.json +16 -0
- package/spec/defs/scalingRule.schema.json +127 -0
- package/spec/defs/temperature.schema.json +63 -0
- package/spec/fixtures/content/illustrated-step.valid.json +24 -0
- package/spec/fixtures/invalid/equipment-unknown-reference.invalid.json +38 -0
- package/spec/fixtures/invalid/mise-en-place-unknown-equipment.invalid.json +37 -0
- package/spec/fixtures/invalid/mise-en-place-unknown-input.invalid.json +41 -0
- package/spec/fixtures/invalid/storage-leftovers-missing-method.invalid.json +31 -0
- package/spec/fixtures/invalid/storage-leftovers-wrong-type.invalid.json +23 -0
- package/spec/fixtures/level/base-full.valid.json +162 -0
- package/spec/fixtures/level/base-missing-yield.invalid.json +12 -0
- package/spec/fixtures/level/lite-min.valid.json +14 -0
- package/spec/fixtures/profile/profile-base.valid.json +20 -0
- package/spec/fixtures/profile/profile-equipped.valid.json +28 -0
- package/spec/fixtures/profile/profile-illustrated.valid.json +28 -0
- package/spec/fixtures/profile/profile-lite.valid.json +13 -0
- package/spec/fixtures/profile/profile-prepped.valid.json +31 -0
- package/spec/fixtures/profile/profile-scalable-missing-scaling.invalid.json +29 -0
- package/spec/fixtures/profile/profile-scalable.valid.json +49 -0
- package/spec/fixtures/profile/profile-timed-missing-structured.invalid.json +30 -0
- package/spec/fixtures/scaling/bakers-percent-missing-ref.invalid.json +41 -0
- package/spec/fixtures/scaling/bakers-percent.valid.json +51 -0
- package/spec/fixtures/scaling/discrete-range.invalid.json +36 -0
- package/spec/fixtures/scaling/missing-quantified.invalid.json +40 -0
- package/spec/fixtures/scaling/reject-bakersPercentage.invalid.json +50 -0
- package/spec/fixtures/stacks/compute-missing-timed.invalid.json +32 -0
- package/spec/fixtures/stacks/dietary-no-signal.invalid.json +16 -0
- package/spec/fixtures/stacks/illustrated-empty.invalid.json +13 -0
- package/spec/fixtures/stacks/quantified-string.invalid.json +22 -0
- package/spec/fixtures/stacks/referenced-missing-input.invalid.json +32 -0
- package/spec/fixtures/stacks/storage-min.valid.json +20 -0
- package/spec/fixtures/stacks/storage-no-duration.invalid.json +16 -0
- package/spec/fixtures/stacks/timed-implies-structured.valid.json +50 -0
- package/spec/fixtures/stacks/timed-range.invalid.json +33 -0
- package/spec/fixtures/valid/equipment-scaling-rules.valid.json +76 -0
- package/spec/fixtures/valid/equipment-strings.valid.json +31 -0
- package/spec/fixtures/valid/equipment-structured-uses.valid.json +47 -0
- package/spec/fixtures/valid/mise-en-place-basic.valid.json +31 -0
- package/spec/fixtures/valid/mise-en-place-referenced-equipment.valid.json +51 -0
- package/spec/fixtures/valid/prep-ingredient-strings.valid.json +48 -0
- package/spec/fixtures/valid/prep-ingredient-structured.valid.json +45 -0
- package/spec/fixtures/valid/profile-equipped.valid.json +29 -0
- package/spec/fixtures/valid/profile-prepped.valid.json +32 -0
- package/spec/fixtures/valid/quantified-nested-ingredient-sections.valid.json +61 -0
- package/spec/fixtures/valid/referenced-scaling.valid.json +67 -0
- package/spec/fixtures/valid/storage-leftovers-simple.valid.json +27 -0
- package/spec/fixtures/valid/storage-leftovers-structured.valid.json +43 -0
- package/spec/fixtures/valid/structured-nested-step-sections.valid.json +84 -0
- package/spec/schemas/stacks-registry.schema.json +108 -0
- package/spec/soustack.schema.json +2379 -0
- package/spec/stacks/compute.schema.json +7 -0
- package/spec/stacks/compute@1.md +22 -0
- package/spec/stacks/dietary.schema.json +45 -0
- package/spec/stacks/dietary@1.md +24 -0
- package/spec/stacks/equipment.schema.json +98 -0
- package/spec/stacks/equipment@1.md +244 -0
- package/spec/stacks/illustrated.schema.json +54 -0
- package/spec/stacks/illustrated@1.md +24 -0
- package/spec/stacks/prep.schema.json +76 -0
- package/spec/stacks/prep@1.md +276 -0
- package/spec/stacks/quantified.schema.json +74 -0
- package/spec/stacks/quantified@1.md +24 -0
- package/spec/stacks/referenced.schema.json +96 -0
- package/spec/stacks/referenced@1.md +23 -0
- package/spec/stacks/registry.json +112 -0
- package/spec/stacks/scaling.schema.json +99 -0
- package/spec/stacks/scaling@1.md +238 -0
- package/spec/stacks/storage.schema.json +132 -0
- package/spec/stacks/storage@1.md +256 -0
- package/spec/stacks/structured.schema.json +48 -0
- package/spec/stacks/structured@1.md +24 -0
- package/spec/stacks/substitutions.schema.json +43 -0
- package/spec/stacks/substitutions@1.md +24 -0
- package/spec/stacks/techniques.schema.json +28 -0
- package/spec/stacks/techniques@1.md +23 -0
- package/spec/stacks/timed.schema.json +60 -0
- package/spec/stacks/timed@1.md +23 -0
- package/src/defs/common.schema.json +46 -0
- package/src/defs/duration.schema.json +33 -0
- package/src/defs/entities.schema.json +111 -0
- package/src/defs/ingredientQuantified.schema.json +9 -0
- package/src/defs/quantity.schema.json +16 -0
- package/src/defs/scalingRule.schema.json +127 -0
- package/src/defs/temperature.schema.json +63 -0
- package/src/profiles/base.schema.json +2 -2
- package/src/profiles/equipped.schema.json +10 -0
- package/src/profiles/illustrated.schema.json +4 -4
- package/src/profiles/lite.schema.json +10 -0
- package/src/profiles/prepped.schema.json +10 -0
- package/src/profiles/scalable.schema.json +6 -6
- package/src/profiles/timed.schema.json +10 -0
- package/src/schema.json +2271 -248
- package/src/schemas/stacks-registry.schema.json +108 -0
- package/src/soustack.schema.json +2271 -248
- package/src/stacks/compute.schema.json +7 -0
- package/src/stacks/compute@1.md +22 -0
- package/src/stacks/dietary.schema.json +45 -0
- package/src/stacks/dietary@1.md +24 -0
- package/src/stacks/equipment.schema.json +98 -0
- package/src/stacks/equipment@1.md +244 -0
- package/src/stacks/illustrated.schema.json +54 -0
- package/src/stacks/illustrated@1.md +24 -0
- package/src/stacks/prep.schema.json +76 -0
- package/src/stacks/prep@1.md +276 -0
- package/src/stacks/quantified.schema.json +74 -0
- package/src/stacks/quantified@1.md +24 -0
- package/src/stacks/referenced.schema.json +96 -0
- package/src/stacks/referenced@1.md +23 -0
- package/src/stacks/registry.json +112 -0
- package/src/stacks/scaling.schema.json +99 -0
- package/src/stacks/scaling@1.md +238 -0
- package/src/stacks/storage.schema.json +132 -0
- package/src/stacks/storage@1.md +256 -0
- package/src/stacks/structured.schema.json +48 -0
- package/src/stacks/structured@1.md +24 -0
- package/src/stacks/substitutions.schema.json +43 -0
- package/src/stacks/substitutions@1.md +24 -0
- package/src/stacks/techniques.schema.json +28 -0
- package/src/stacks/techniques@1.md +23 -0
- package/src/stacks/timed.schema.json +60 -0
- package/src/stacks/timed@1.md +23 -0
- package/src/profiles/cookable.schema.json +0 -18
- package/src/profiles/quantified.schema.json +0 -43
- package/src/profiles/schedulable.schema.json +0 -43
package/dist/scrape/index.mjs
CHANGED
|
@@ -145,6 +145,7 @@ function normalizeRecipe(input) {
|
|
|
145
145
|
}
|
|
146
146
|
if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
|
|
147
147
|
recipe.recipeVersion = recipe.version;
|
|
148
|
+
delete recipe.version;
|
|
148
149
|
warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
|
|
149
150
|
}
|
|
150
151
|
normalizeTime(recipe);
|
|
@@ -214,6 +215,51 @@ function normalizeTime(recipe) {
|
|
|
214
215
|
});
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
// src/specVersion.ts
|
|
219
|
+
var SOUSTACK_SPEC_VERSION = "0.0.2";
|
|
220
|
+
|
|
221
|
+
// src/schemaMetadata.ts
|
|
222
|
+
var CANONICAL_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
|
|
223
|
+
var LEGACY_SCHEMA_ID = `http://soustack.org/schema/v${SOUSTACK_SPEC_VERSION}`;
|
|
224
|
+
var RAW_SPEC_BASE = "https://raw.githubusercontent.com/soustack/soustack-spec";
|
|
225
|
+
var RAW_SPEC_FORK_BASE = "https://raw.githubusercontent.com/RichardHerold/soustack-spec";
|
|
226
|
+
var SCHEMA_ALIAS_MAP = /* @__PURE__ */ new Map([
|
|
227
|
+
[CANONICAL_SCHEMA_ID, CANONICAL_SCHEMA_ID],
|
|
228
|
+
[LEGACY_SCHEMA_ID, CANONICAL_SCHEMA_ID],
|
|
229
|
+
[`${LEGACY_SCHEMA_ID}/`, CANONICAL_SCHEMA_ID],
|
|
230
|
+
["https://soustack.org/schema/v0.0.2", CANONICAL_SCHEMA_ID],
|
|
231
|
+
["https://soustack.org/schema/v0.0.2/", CANONICAL_SCHEMA_ID],
|
|
232
|
+
[`${RAW_SPEC_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
233
|
+
[`${RAW_SPEC_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
234
|
+
[`${RAW_SPEC_FORK_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
235
|
+
[`${RAW_SPEC_FORK_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID]
|
|
236
|
+
]);
|
|
237
|
+
function resolveSchemaHint(value) {
|
|
238
|
+
if (typeof value !== "string" || !value) {
|
|
239
|
+
return { canonicalId: void 0, isSoustackSchema: false, wasAlias: false };
|
|
240
|
+
}
|
|
241
|
+
const trimmed = value.replace(/#$/, "");
|
|
242
|
+
const mapped = SCHEMA_ALIAS_MAP.get(trimmed) ?? trimmed;
|
|
243
|
+
const isSoustackSchema = SCHEMA_ALIAS_MAP.has(trimmed) || mapped.startsWith("http://soustack.org/schema") || mapped.startsWith("https://soustack.org/schema") || mapped.startsWith("https://soustack.spec/") || mapped.startsWith("https://soustack.org/schemas/");
|
|
244
|
+
return {
|
|
245
|
+
canonicalId: mapped,
|
|
246
|
+
isSoustackSchema,
|
|
247
|
+
wasAlias: mapped !== trimmed || SCHEMA_ALIAS_MAP.has(trimmed)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function withCanonicalSchema(value) {
|
|
251
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
const existing = typeof value.$schema === "string" ? value.$schema : void 0;
|
|
255
|
+
const resolved = resolveSchemaHint(existing);
|
|
256
|
+
const schemaId = resolved.isSoustackSchema ? resolved.canonicalId : CANONICAL_SCHEMA_ID;
|
|
257
|
+
return {
|
|
258
|
+
...value,
|
|
259
|
+
$schema: schemaId ?? CANONICAL_SCHEMA_ID
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
217
263
|
// src/fromSchemaOrg.ts
|
|
218
264
|
function fromSchemaOrg(input) {
|
|
219
265
|
const recipeNode = extractRecipeNode(input);
|
|
@@ -229,23 +275,18 @@ function fromSchemaOrg(input) {
|
|
|
229
275
|
const source = convertSource(recipeNode);
|
|
230
276
|
const dateModified = recipeNode.dateModified || void 0;
|
|
231
277
|
const nutrition = convertNutrition(recipeNode.nutrition);
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const times = convertTimes(time);
|
|
278
|
+
const images = toArray(normalizeImage(recipeNode.image));
|
|
279
|
+
const videos = normalizeMediaList(recipeNode.video);
|
|
280
|
+
const profile = recipeYield && time ? "base" : "lite";
|
|
236
281
|
const stacks = {};
|
|
237
|
-
if (attribution) stacks.attribution = 1;
|
|
238
|
-
if (taxonomy) stacks.taxonomy = 1;
|
|
239
|
-
if (media) stacks.media = 1;
|
|
240
|
-
if (nutrition) stacks.nutrition = 1;
|
|
241
|
-
if (times) stacks.times = 1;
|
|
242
282
|
const rawRecipe = {
|
|
243
283
|
"@type": "Recipe",
|
|
244
|
-
profile
|
|
284
|
+
profile,
|
|
245
285
|
stacks,
|
|
246
286
|
name: recipeNode.name.trim(),
|
|
247
287
|
description: recipeNode.description?.trim() || void 0,
|
|
248
|
-
|
|
288
|
+
images: images.length ? images : void 0,
|
|
289
|
+
videos: videos.length ? videos : void 0,
|
|
249
290
|
category,
|
|
250
291
|
tags: tags.length ? tags : void 0,
|
|
251
292
|
source,
|
|
@@ -255,14 +296,10 @@ function fromSchemaOrg(input) {
|
|
|
255
296
|
ingredients,
|
|
256
297
|
instructions,
|
|
257
298
|
...dateModified ? { dateModified } : {},
|
|
258
|
-
...nutrition ? { nutrition } : {}
|
|
259
|
-
...attribution ? { attribution } : {},
|
|
260
|
-
...taxonomy ? { taxonomy } : {},
|
|
261
|
-
...media ? { media } : {},
|
|
262
|
-
...times ? { times } : {}
|
|
299
|
+
...nutrition ? { nutrition } : {}
|
|
263
300
|
};
|
|
264
301
|
const { recipe } = normalizeRecipe(rawRecipe);
|
|
265
|
-
return recipe;
|
|
302
|
+
return withCanonicalSchema(recipe);
|
|
266
303
|
}
|
|
267
304
|
function extractRecipeNode(input) {
|
|
268
305
|
if (!input) return null;
|
|
@@ -306,7 +343,10 @@ function isValidName(name) {
|
|
|
306
343
|
function convertIngredients(value) {
|
|
307
344
|
if (!value) return [];
|
|
308
345
|
const normalized = Array.isArray(value) ? value : [value];
|
|
309
|
-
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
|
|
346
|
+
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean).map((name) => ({
|
|
347
|
+
name,
|
|
348
|
+
scaling: { mode: "linear" }
|
|
349
|
+
}));
|
|
310
350
|
}
|
|
311
351
|
function convertInstructions(value) {
|
|
312
352
|
if (!value) return [];
|
|
@@ -325,8 +365,8 @@ function convertInstructions(value) {
|
|
|
325
365
|
const subsectionItems = extractSectionItems(entry.itemListElement);
|
|
326
366
|
if (subsectionItems.length) {
|
|
327
367
|
result.push({
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
section: entry.name?.trim() || "Section",
|
|
369
|
+
steps: subsectionItems
|
|
330
370
|
});
|
|
331
371
|
}
|
|
332
372
|
continue;
|
|
@@ -382,7 +422,7 @@ function convertHowToStep(step) {
|
|
|
382
422
|
}
|
|
383
423
|
const instruction = { text };
|
|
384
424
|
if (id) instruction.id = id;
|
|
385
|
-
if (image) instruction.
|
|
425
|
+
if (image) instruction.images = Array.isArray(image) ? image : [image];
|
|
386
426
|
if (timing) instruction.timing = timing;
|
|
387
427
|
return instruction;
|
|
388
428
|
}
|
|
@@ -392,7 +432,13 @@ function extractInstructionTiming(step) {
|
|
|
392
432
|
return void 0;
|
|
393
433
|
}
|
|
394
434
|
const parsed = smartParseDuration(duration);
|
|
395
|
-
|
|
435
|
+
if (parsed === null || parsed === void 0) {
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
activity: "active",
|
|
440
|
+
duration: { minutes: parsed }
|
|
441
|
+
};
|
|
396
442
|
}
|
|
397
443
|
function extractInstructionId(step) {
|
|
398
444
|
const raw = step["@id"] || step.id || step.url;
|
|
@@ -409,14 +455,22 @@ function isHowToSection(value) {
|
|
|
409
455
|
return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
|
|
410
456
|
}
|
|
411
457
|
function convertTime(recipe) {
|
|
458
|
+
const total = smartParseDuration(recipe.totalTime ?? "");
|
|
412
459
|
const prep = smartParseDuration(recipe.prepTime ?? "");
|
|
413
460
|
const cook = smartParseDuration(recipe.cookTime ?? "");
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
|
|
461
|
+
const minutes = isPositiveDuration(total) ? total : [prep, cook].filter(isPositiveDuration).reduce((sum, value) => {
|
|
462
|
+
if (sum === null) return value;
|
|
463
|
+
return sum + value;
|
|
464
|
+
}, null);
|
|
465
|
+
if (!isPositiveDuration(minutes)) {
|
|
466
|
+
return void 0;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
total: { minutes }
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function isPositiveDuration(value) {
|
|
473
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
420
474
|
}
|
|
421
475
|
function collectTags(cuisine, keywords) {
|
|
422
476
|
const tags = /* @__PURE__ */ new Set();
|
|
@@ -474,23 +528,6 @@ function extractEntityName(value) {
|
|
|
474
528
|
}
|
|
475
529
|
return void 0;
|
|
476
530
|
}
|
|
477
|
-
function convertAttribution(recipe) {
|
|
478
|
-
const attribution = {};
|
|
479
|
-
const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
|
|
480
|
-
const author = extractEntityName(recipe.author);
|
|
481
|
-
const datePublished = recipe.datePublished?.trim();
|
|
482
|
-
if (url) attribution.url = url;
|
|
483
|
-
if (author) attribution.author = author;
|
|
484
|
-
if (datePublished) attribution.datePublished = datePublished;
|
|
485
|
-
return Object.keys(attribution).length ? attribution : void 0;
|
|
486
|
-
}
|
|
487
|
-
function convertTaxonomy(keywords, category, cuisine) {
|
|
488
|
-
const taxonomy = {};
|
|
489
|
-
if (keywords.length) taxonomy.keywords = keywords;
|
|
490
|
-
if (category) taxonomy.category = category;
|
|
491
|
-
if (cuisine) taxonomy.cuisine = cuisine;
|
|
492
|
-
return Object.keys(taxonomy).length ? taxonomy : void 0;
|
|
493
|
-
}
|
|
494
531
|
function normalizeMediaList(value) {
|
|
495
532
|
if (!value) return [];
|
|
496
533
|
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
@@ -501,28 +538,18 @@ function normalizeMediaList(value) {
|
|
|
501
538
|
return url ? [url] : [];
|
|
502
539
|
}
|
|
503
540
|
function extractMediaUrl(value) {
|
|
504
|
-
if (value && typeof value === "object"
|
|
505
|
-
const
|
|
506
|
-
|
|
541
|
+
if (value && typeof value === "object") {
|
|
542
|
+
const urlValue = typeof value.url === "string" ? value.url : typeof value.contentUrl === "string" ? value.contentUrl : void 0;
|
|
543
|
+
if (typeof urlValue === "string") {
|
|
544
|
+
const trimmed = urlValue.trim();
|
|
545
|
+
return trimmed || void 0;
|
|
546
|
+
}
|
|
507
547
|
}
|
|
508
548
|
return void 0;
|
|
509
549
|
}
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const videos = normalizeMediaList(video);
|
|
514
|
-
const media = {};
|
|
515
|
-
if (images.length) media.images = images;
|
|
516
|
-
if (videos.length) media.videos = videos;
|
|
517
|
-
return Object.keys(media).length ? media : void 0;
|
|
518
|
-
}
|
|
519
|
-
function convertTimes(time) {
|
|
520
|
-
if (!time) return void 0;
|
|
521
|
-
const times = {};
|
|
522
|
-
if (typeof time.prep === "number") times.prepMinutes = time.prep;
|
|
523
|
-
if (typeof time.active === "number") times.cookMinutes = time.active;
|
|
524
|
-
if (typeof time.total === "number") times.totalMinutes = time.total;
|
|
525
|
-
return Object.keys(times).length ? times : void 0;
|
|
550
|
+
function toArray(value) {
|
|
551
|
+
if (!value) return [];
|
|
552
|
+
return Array.isArray(value) ? value : [value];
|
|
526
553
|
}
|
|
527
554
|
function convertNutrition(nutrition) {
|
|
528
555
|
if (!nutrition || typeof nutrition !== "object") {
|