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.js
CHANGED
|
@@ -147,6 +147,7 @@ function normalizeRecipe(input) {
|
|
|
147
147
|
}
|
|
148
148
|
if (recipe && typeof recipe === "object" && "version" in recipe && !recipe.recipeVersion && typeof recipe.version === "string") {
|
|
149
149
|
recipe.recipeVersion = recipe.version;
|
|
150
|
+
delete recipe.version;
|
|
150
151
|
warnings.push("'version' is deprecated; mapped to 'recipeVersion'.");
|
|
151
152
|
}
|
|
152
153
|
normalizeTime(recipe);
|
|
@@ -216,6 +217,51 @@ function normalizeTime(recipe) {
|
|
|
216
217
|
});
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
// src/specVersion.ts
|
|
221
|
+
var SOUSTACK_SPEC_VERSION = "0.0.2";
|
|
222
|
+
|
|
223
|
+
// src/schemaMetadata.ts
|
|
224
|
+
var CANONICAL_SCHEMA_ID = "https://soustack.spec/soustack.schema.json";
|
|
225
|
+
var LEGACY_SCHEMA_ID = `http://soustack.org/schema/v${SOUSTACK_SPEC_VERSION}`;
|
|
226
|
+
var RAW_SPEC_BASE = "https://raw.githubusercontent.com/soustack/soustack-spec";
|
|
227
|
+
var RAW_SPEC_FORK_BASE = "https://raw.githubusercontent.com/RichardHerold/soustack-spec";
|
|
228
|
+
var SCHEMA_ALIAS_MAP = /* @__PURE__ */ new Map([
|
|
229
|
+
[CANONICAL_SCHEMA_ID, CANONICAL_SCHEMA_ID],
|
|
230
|
+
[LEGACY_SCHEMA_ID, CANONICAL_SCHEMA_ID],
|
|
231
|
+
[`${LEGACY_SCHEMA_ID}/`, CANONICAL_SCHEMA_ID],
|
|
232
|
+
["https://soustack.org/schema/v0.0.2", CANONICAL_SCHEMA_ID],
|
|
233
|
+
["https://soustack.org/schema/v0.0.2/", CANONICAL_SCHEMA_ID],
|
|
234
|
+
[`${RAW_SPEC_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
235
|
+
[`${RAW_SPEC_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
236
|
+
[`${RAW_SPEC_FORK_BASE}/main/soustack.schema.json`, CANONICAL_SCHEMA_ID],
|
|
237
|
+
[`${RAW_SPEC_FORK_BASE}/v${SOUSTACK_SPEC_VERSION}/soustack.schema.json`, CANONICAL_SCHEMA_ID]
|
|
238
|
+
]);
|
|
239
|
+
function resolveSchemaHint(value) {
|
|
240
|
+
if (typeof value !== "string" || !value) {
|
|
241
|
+
return { canonicalId: void 0, isSoustackSchema: false, wasAlias: false };
|
|
242
|
+
}
|
|
243
|
+
const trimmed = value.replace(/#$/, "");
|
|
244
|
+
const mapped = SCHEMA_ALIAS_MAP.get(trimmed) ?? trimmed;
|
|
245
|
+
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/");
|
|
246
|
+
return {
|
|
247
|
+
canonicalId: mapped,
|
|
248
|
+
isSoustackSchema,
|
|
249
|
+
wasAlias: mapped !== trimmed || SCHEMA_ALIAS_MAP.has(trimmed)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function withCanonicalSchema(value) {
|
|
253
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
const existing = typeof value.$schema === "string" ? value.$schema : void 0;
|
|
257
|
+
const resolved = resolveSchemaHint(existing);
|
|
258
|
+
const schemaId = resolved.isSoustackSchema ? resolved.canonicalId : CANONICAL_SCHEMA_ID;
|
|
259
|
+
return {
|
|
260
|
+
...value,
|
|
261
|
+
$schema: schemaId ?? CANONICAL_SCHEMA_ID
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
// src/fromSchemaOrg.ts
|
|
220
266
|
function fromSchemaOrg(input) {
|
|
221
267
|
const recipeNode = extractRecipeNode(input);
|
|
@@ -231,23 +277,18 @@ function fromSchemaOrg(input) {
|
|
|
231
277
|
const source = convertSource(recipeNode);
|
|
232
278
|
const dateModified = recipeNode.dateModified || void 0;
|
|
233
279
|
const nutrition = convertNutrition(recipeNode.nutrition);
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const times = convertTimes(time);
|
|
280
|
+
const images = toArray(normalizeImage(recipeNode.image));
|
|
281
|
+
const videos = normalizeMediaList(recipeNode.video);
|
|
282
|
+
const profile = recipeYield && time ? "base" : "lite";
|
|
238
283
|
const stacks = {};
|
|
239
|
-
if (attribution) stacks.attribution = 1;
|
|
240
|
-
if (taxonomy) stacks.taxonomy = 1;
|
|
241
|
-
if (media) stacks.media = 1;
|
|
242
|
-
if (nutrition) stacks.nutrition = 1;
|
|
243
|
-
if (times) stacks.times = 1;
|
|
244
284
|
const rawRecipe = {
|
|
245
285
|
"@type": "Recipe",
|
|
246
|
-
profile
|
|
286
|
+
profile,
|
|
247
287
|
stacks,
|
|
248
288
|
name: recipeNode.name.trim(),
|
|
249
289
|
description: recipeNode.description?.trim() || void 0,
|
|
250
|
-
|
|
290
|
+
images: images.length ? images : void 0,
|
|
291
|
+
videos: videos.length ? videos : void 0,
|
|
251
292
|
category,
|
|
252
293
|
tags: tags.length ? tags : void 0,
|
|
253
294
|
source,
|
|
@@ -257,14 +298,10 @@ function fromSchemaOrg(input) {
|
|
|
257
298
|
ingredients,
|
|
258
299
|
instructions,
|
|
259
300
|
...dateModified ? { dateModified } : {},
|
|
260
|
-
...nutrition ? { nutrition } : {}
|
|
261
|
-
...attribution ? { attribution } : {},
|
|
262
|
-
...taxonomy ? { taxonomy } : {},
|
|
263
|
-
...media ? { media } : {},
|
|
264
|
-
...times ? { times } : {}
|
|
301
|
+
...nutrition ? { nutrition } : {}
|
|
265
302
|
};
|
|
266
303
|
const { recipe } = normalizeRecipe(rawRecipe);
|
|
267
|
-
return recipe;
|
|
304
|
+
return withCanonicalSchema(recipe);
|
|
268
305
|
}
|
|
269
306
|
function extractRecipeNode(input) {
|
|
270
307
|
if (!input) return null;
|
|
@@ -308,7 +345,10 @@ function isValidName(name) {
|
|
|
308
345
|
function convertIngredients(value) {
|
|
309
346
|
if (!value) return [];
|
|
310
347
|
const normalized = Array.isArray(value) ? value : [value];
|
|
311
|
-
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean)
|
|
348
|
+
return normalized.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean).map((name) => ({
|
|
349
|
+
name,
|
|
350
|
+
scaling: { mode: "linear" }
|
|
351
|
+
}));
|
|
312
352
|
}
|
|
313
353
|
function convertInstructions(value) {
|
|
314
354
|
if (!value) return [];
|
|
@@ -327,8 +367,8 @@ function convertInstructions(value) {
|
|
|
327
367
|
const subsectionItems = extractSectionItems(entry.itemListElement);
|
|
328
368
|
if (subsectionItems.length) {
|
|
329
369
|
result.push({
|
|
330
|
-
|
|
331
|
-
|
|
370
|
+
section: entry.name?.trim() || "Section",
|
|
371
|
+
steps: subsectionItems
|
|
332
372
|
});
|
|
333
373
|
}
|
|
334
374
|
continue;
|
|
@@ -384,7 +424,7 @@ function convertHowToStep(step) {
|
|
|
384
424
|
}
|
|
385
425
|
const instruction = { text };
|
|
386
426
|
if (id) instruction.id = id;
|
|
387
|
-
if (image) instruction.
|
|
427
|
+
if (image) instruction.images = Array.isArray(image) ? image : [image];
|
|
388
428
|
if (timing) instruction.timing = timing;
|
|
389
429
|
return instruction;
|
|
390
430
|
}
|
|
@@ -394,7 +434,13 @@ function extractInstructionTiming(step) {
|
|
|
394
434
|
return void 0;
|
|
395
435
|
}
|
|
396
436
|
const parsed = smartParseDuration(duration);
|
|
397
|
-
|
|
437
|
+
if (parsed === null || parsed === void 0) {
|
|
438
|
+
return void 0;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
activity: "active",
|
|
442
|
+
duration: { minutes: parsed }
|
|
443
|
+
};
|
|
398
444
|
}
|
|
399
445
|
function extractInstructionId(step) {
|
|
400
446
|
const raw = step["@id"] || step.id || step.url;
|
|
@@ -411,14 +457,22 @@ function isHowToSection(value) {
|
|
|
411
457
|
return Boolean(value) && typeof value === "object" && value["@type"] === "HowToSection" && Array.isArray(value.itemListElement);
|
|
412
458
|
}
|
|
413
459
|
function convertTime(recipe) {
|
|
460
|
+
const total = smartParseDuration(recipe.totalTime ?? "");
|
|
414
461
|
const prep = smartParseDuration(recipe.prepTime ?? "");
|
|
415
462
|
const cook = smartParseDuration(recipe.cookTime ?? "");
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
|
|
463
|
+
const minutes = isPositiveDuration(total) ? total : [prep, cook].filter(isPositiveDuration).reduce((sum, value) => {
|
|
464
|
+
if (sum === null) return value;
|
|
465
|
+
return sum + value;
|
|
466
|
+
}, null);
|
|
467
|
+
if (!isPositiveDuration(minutes)) {
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
total: { minutes }
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function isPositiveDuration(value) {
|
|
475
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
422
476
|
}
|
|
423
477
|
function collectTags(cuisine, keywords) {
|
|
424
478
|
const tags = /* @__PURE__ */ new Set();
|
|
@@ -476,23 +530,6 @@ function extractEntityName(value) {
|
|
|
476
530
|
}
|
|
477
531
|
return void 0;
|
|
478
532
|
}
|
|
479
|
-
function convertAttribution(recipe) {
|
|
480
|
-
const attribution = {};
|
|
481
|
-
const url = (recipe.url || recipe.mainEntityOfPage)?.trim();
|
|
482
|
-
const author = extractEntityName(recipe.author);
|
|
483
|
-
const datePublished = recipe.datePublished?.trim();
|
|
484
|
-
if (url) attribution.url = url;
|
|
485
|
-
if (author) attribution.author = author;
|
|
486
|
-
if (datePublished) attribution.datePublished = datePublished;
|
|
487
|
-
return Object.keys(attribution).length ? attribution : void 0;
|
|
488
|
-
}
|
|
489
|
-
function convertTaxonomy(keywords, category, cuisine) {
|
|
490
|
-
const taxonomy = {};
|
|
491
|
-
if (keywords.length) taxonomy.keywords = keywords;
|
|
492
|
-
if (category) taxonomy.category = category;
|
|
493
|
-
if (cuisine) taxonomy.cuisine = cuisine;
|
|
494
|
-
return Object.keys(taxonomy).length ? taxonomy : void 0;
|
|
495
|
-
}
|
|
496
533
|
function normalizeMediaList(value) {
|
|
497
534
|
if (!value) return [];
|
|
498
535
|
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
@@ -503,28 +540,18 @@ function normalizeMediaList(value) {
|
|
|
503
540
|
return url ? [url] : [];
|
|
504
541
|
}
|
|
505
542
|
function extractMediaUrl(value) {
|
|
506
|
-
if (value && typeof value === "object"
|
|
507
|
-
const
|
|
508
|
-
|
|
543
|
+
if (value && typeof value === "object") {
|
|
544
|
+
const urlValue = typeof value.url === "string" ? value.url : typeof value.contentUrl === "string" ? value.contentUrl : void 0;
|
|
545
|
+
if (typeof urlValue === "string") {
|
|
546
|
+
const trimmed = urlValue.trim();
|
|
547
|
+
return trimmed || void 0;
|
|
548
|
+
}
|
|
509
549
|
}
|
|
510
550
|
return void 0;
|
|
511
551
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const videos = normalizeMediaList(video);
|
|
516
|
-
const media = {};
|
|
517
|
-
if (images.length) media.images = images;
|
|
518
|
-
if (videos.length) media.videos = videos;
|
|
519
|
-
return Object.keys(media).length ? media : void 0;
|
|
520
|
-
}
|
|
521
|
-
function convertTimes(time) {
|
|
522
|
-
if (!time) return void 0;
|
|
523
|
-
const times = {};
|
|
524
|
-
if (typeof time.prep === "number") times.prepMinutes = time.prep;
|
|
525
|
-
if (typeof time.active === "number") times.cookMinutes = time.active;
|
|
526
|
-
if (typeof time.total === "number") times.totalMinutes = time.total;
|
|
527
|
-
return Object.keys(times).length ? times : void 0;
|
|
552
|
+
function toArray(value) {
|
|
553
|
+
if (!value) return [];
|
|
554
|
+
return Array.isArray(value) ? value : [value];
|
|
528
555
|
}
|
|
529
556
|
function convertNutrition(nutrition) {
|
|
530
557
|
if (!nutrition || typeof nutrition !== "object") {
|