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/index.js
CHANGED
|
@@ -247,894 +247,523 @@ function toDurationMinutes(duration) {
|
|
|
247
247
|
return 0;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// src/schema.json
|
|
251
|
-
var
|
|
250
|
+
// src/schemas/recipe/base.schema.json
|
|
251
|
+
var base_schema_default = {
|
|
252
252
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
253
|
-
$id: "http://soustack.org/schema/
|
|
254
|
-
title: "Soustack Recipe Schema
|
|
255
|
-
description: "
|
|
253
|
+
$id: "http://soustack.org/schema/recipe/base.schema.json",
|
|
254
|
+
title: "Soustack Recipe Base Schema",
|
|
255
|
+
description: "Base document shape for Soustack recipe documents. Profiles and modules build on this baseline.",
|
|
256
256
|
type: "object",
|
|
257
|
-
|
|
258
|
-
additionalProperties: false,
|
|
259
|
-
patternProperties: {
|
|
260
|
-
"^x-": {}
|
|
261
|
-
},
|
|
257
|
+
additionalProperties: true,
|
|
262
258
|
properties: {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
description: "Optional schema hint for tooling compatibility"
|
|
267
|
-
},
|
|
268
|
-
id: {
|
|
269
|
-
type: "string",
|
|
270
|
-
description: "Unique identifier (slug or UUID)"
|
|
271
|
-
},
|
|
272
|
-
name: {
|
|
273
|
-
type: "string",
|
|
274
|
-
description: "The title of the recipe"
|
|
275
|
-
},
|
|
276
|
-
title: {
|
|
277
|
-
type: "string",
|
|
278
|
-
description: "Optional display title; alias for name"
|
|
259
|
+
"@type": {
|
|
260
|
+
const: "Recipe",
|
|
261
|
+
description: "Document marker for Soustack recipes"
|
|
279
262
|
},
|
|
280
|
-
|
|
263
|
+
profile: {
|
|
281
264
|
type: "string",
|
|
282
|
-
|
|
283
|
-
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
265
|
+
description: "Profile identifier applied to this recipe"
|
|
284
266
|
},
|
|
285
|
-
|
|
286
|
-
type: "string",
|
|
287
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
288
|
-
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
289
|
-
},
|
|
290
|
-
description: {
|
|
291
|
-
type: "string"
|
|
292
|
-
},
|
|
293
|
-
category: {
|
|
294
|
-
type: "string",
|
|
295
|
-
examples: ["Main Course", "Dessert"]
|
|
296
|
-
},
|
|
297
|
-
tags: {
|
|
267
|
+
modules: {
|
|
298
268
|
type: "array",
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
description: "Recipe-level hero image(s)",
|
|
303
|
-
anyOf: [
|
|
304
|
-
{
|
|
305
|
-
type: "string",
|
|
306
|
-
format: "uri"
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
type: "array",
|
|
310
|
-
minItems: 1,
|
|
311
|
-
items: {
|
|
312
|
-
type: "string",
|
|
313
|
-
format: "uri"
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
]
|
|
317
|
-
},
|
|
318
|
-
dateAdded: {
|
|
319
|
-
type: "string",
|
|
320
|
-
format: "date-time"
|
|
321
|
-
},
|
|
322
|
-
metadata: {
|
|
323
|
-
type: "object",
|
|
324
|
-
additionalProperties: true,
|
|
325
|
-
description: "Free-form vendor metadata"
|
|
326
|
-
},
|
|
327
|
-
source: {
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {
|
|
330
|
-
author: { type: "string" },
|
|
331
|
-
url: { type: "string", format: "uri" },
|
|
332
|
-
name: { type: "string" },
|
|
333
|
-
adapted: { type: "boolean" }
|
|
269
|
+
description: "List of module identifiers applied to this recipe",
|
|
270
|
+
items: {
|
|
271
|
+
type: "string"
|
|
334
272
|
}
|
|
335
273
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
time: {
|
|
340
|
-
$ref: "#/definitions/time"
|
|
341
|
-
},
|
|
342
|
-
equipment: {
|
|
343
|
-
type: "array",
|
|
344
|
-
items: { $ref: "#/definitions/equipment" }
|
|
274
|
+
name: {
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "Human-readable recipe name"
|
|
345
277
|
},
|
|
346
278
|
ingredients: {
|
|
347
279
|
type: "array",
|
|
348
|
-
|
|
349
|
-
anyOf: [
|
|
350
|
-
{ type: "string" },
|
|
351
|
-
{ $ref: "#/definitions/ingredient" },
|
|
352
|
-
{ $ref: "#/definitions/ingredientSubsection" }
|
|
353
|
-
]
|
|
354
|
-
}
|
|
280
|
+
description: "Ingredients payload; content is validated by profiles/modules"
|
|
355
281
|
},
|
|
356
282
|
instructions: {
|
|
357
283
|
type: "array",
|
|
358
|
-
|
|
359
|
-
anyOf: [
|
|
360
|
-
{ type: "string" },
|
|
361
|
-
{ $ref: "#/definitions/instruction" },
|
|
362
|
-
{ $ref: "#/definitions/instructionSubsection" }
|
|
363
|
-
]
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
storage: {
|
|
367
|
-
$ref: "#/definitions/storage"
|
|
368
|
-
},
|
|
369
|
-
substitutions: {
|
|
370
|
-
type: "array",
|
|
371
|
-
items: { $ref: "#/definitions/substitution" }
|
|
284
|
+
description: "Instruction payload; content is validated by profiles/modules"
|
|
372
285
|
}
|
|
373
286
|
},
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
type: "object",
|
|
387
|
-
properties: {
|
|
388
|
-
prep: { type: "number" },
|
|
389
|
-
active: { type: "number" },
|
|
390
|
-
passive: { type: "number" },
|
|
391
|
-
total: { type: "number" },
|
|
392
|
-
prepTime: { type: "string", format: "duration" },
|
|
393
|
-
cookTime: { type: "string", format: "duration" }
|
|
394
|
-
},
|
|
395
|
-
minProperties: 1
|
|
396
|
-
},
|
|
397
|
-
quantity: {
|
|
398
|
-
type: "object",
|
|
399
|
-
required: ["amount"],
|
|
400
|
-
properties: {
|
|
401
|
-
amount: { type: "number" },
|
|
402
|
-
unit: { type: ["string", "null"] }
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
scaling: {
|
|
287
|
+
required: ["@type"]
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/schemas/recipe/profiles/core.schema.json
|
|
291
|
+
var core_schema_default = {
|
|
292
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
293
|
+
$id: "http://soustack.org/schema/recipe/profiles/core.schema.json",
|
|
294
|
+
title: "Soustack Recipe Core Profile",
|
|
295
|
+
description: "Core profile that builds on the minimal profile and is intended to be combined with recipe modules.",
|
|
296
|
+
allOf: [
|
|
297
|
+
{ $ref: "http://soustack.org/schema/recipe/base.schema.json" },
|
|
298
|
+
{
|
|
406
299
|
type: "object",
|
|
407
|
-
required: ["type"],
|
|
408
300
|
properties: {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
301
|
+
profile: { const: "core" },
|
|
302
|
+
modules: {
|
|
303
|
+
type: "array",
|
|
304
|
+
items: { type: "string" },
|
|
305
|
+
uniqueItems: true,
|
|
306
|
+
default: []
|
|
412
307
|
},
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
min: { type: "number" },
|
|
417
|
-
max: { type: "number" }
|
|
418
|
-
},
|
|
419
|
-
if: {
|
|
420
|
-
properties: { type: { const: "bakers_percentage" } }
|
|
308
|
+
name: { type: "string", minLength: 1 },
|
|
309
|
+
ingredients: { type: "array", minItems: 1 },
|
|
310
|
+
instructions: { type: "array", minItems: 1 }
|
|
421
311
|
},
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
prepTime: { type: "number" },
|
|
438
|
-
destination: { type: "string" },
|
|
439
|
-
scaling: { $ref: "#/definitions/scaling" },
|
|
440
|
-
critical: { type: "boolean" },
|
|
441
|
-
optional: { type: "boolean" },
|
|
442
|
-
notes: { type: "string" }
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
ingredientSubsection: {
|
|
446
|
-
type: "object",
|
|
447
|
-
required: ["subsection", "items"],
|
|
448
|
-
properties: {
|
|
449
|
-
subsection: { type: "string" },
|
|
450
|
-
items: {
|
|
451
|
-
type: "array",
|
|
452
|
-
items: { $ref: "#/definitions/ingredient" }
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
equipment: {
|
|
457
|
-
type: "object",
|
|
458
|
-
required: ["name"],
|
|
459
|
-
properties: {
|
|
460
|
-
id: { type: "string" },
|
|
461
|
-
name: { type: "string" },
|
|
462
|
-
required: { type: "boolean" },
|
|
463
|
-
label: { type: "string" },
|
|
464
|
-
capacity: { $ref: "#/definitions/quantity" },
|
|
465
|
-
scalingLimit: { type: "number" },
|
|
466
|
-
alternatives: {
|
|
467
|
-
type: "array",
|
|
468
|
-
items: { type: "string" }
|
|
469
|
-
}
|
|
470
|
-
}
|
|
312
|
+
required: ["profile", "name", "ingredients", "instructions"],
|
|
313
|
+
additionalProperties: true
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// src/schemas/recipe/profiles/minimal.schema.json
|
|
319
|
+
var minimal_schema_default = {
|
|
320
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
321
|
+
$id: "http://soustack.org/schema/recipe/profiles/minimal.schema.json",
|
|
322
|
+
title: "Soustack Recipe Minimal Profile",
|
|
323
|
+
description: "Minimal profile that ensures the basic Recipe structure is present while allowing modules to extend it.",
|
|
324
|
+
allOf: [
|
|
325
|
+
{
|
|
326
|
+
$ref: "http://soustack.org/schema/recipe/base.schema.json"
|
|
471
327
|
},
|
|
472
|
-
|
|
328
|
+
{
|
|
473
329
|
type: "object",
|
|
474
|
-
required: ["text"],
|
|
475
330
|
properties: {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
image: {
|
|
479
|
-
type: "string",
|
|
480
|
-
format: "uri",
|
|
481
|
-
description: "Optional image that illustrates this instruction"
|
|
482
|
-
},
|
|
483
|
-
destination: { type: "string" },
|
|
484
|
-
dependsOn: {
|
|
485
|
-
type: "array",
|
|
486
|
-
items: { type: "string" }
|
|
331
|
+
profile: {
|
|
332
|
+
const: "minimal"
|
|
487
333
|
},
|
|
488
|
-
|
|
489
|
-
type: "array",
|
|
490
|
-
items: { type: "string" }
|
|
491
|
-
},
|
|
492
|
-
timing: {
|
|
493
|
-
type: "object",
|
|
494
|
-
required: ["duration", "type"],
|
|
495
|
-
properties: {
|
|
496
|
-
duration: {
|
|
497
|
-
anyOf: [
|
|
498
|
-
{ type: "number" },
|
|
499
|
-
{ type: "string", pattern: "^P" }
|
|
500
|
-
],
|
|
501
|
-
description: "Minutes as a number or ISO8601 duration string"
|
|
502
|
-
},
|
|
503
|
-
type: { type: "string", enum: ["active", "passive"] },
|
|
504
|
-
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
instructionSubsection: {
|
|
510
|
-
type: "object",
|
|
511
|
-
required: ["subsection", "items"],
|
|
512
|
-
properties: {
|
|
513
|
-
subsection: { type: "string" },
|
|
514
|
-
items: {
|
|
334
|
+
modules: {
|
|
515
335
|
type: "array",
|
|
516
336
|
items: {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
337
|
+
type: "string",
|
|
338
|
+
enum: [
|
|
339
|
+
"attribution@1",
|
|
340
|
+
"taxonomy@1",
|
|
341
|
+
"media@1",
|
|
342
|
+
"nutrition@1",
|
|
343
|
+
"times@1"
|
|
520
344
|
]
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
storage: {
|
|
526
|
-
type: "object",
|
|
527
|
-
properties: {
|
|
528
|
-
roomTemp: { $ref: "#/definitions/storageMethod" },
|
|
529
|
-
refrigerated: { $ref: "#/definitions/storageMethod" },
|
|
530
|
-
frozen: {
|
|
531
|
-
allOf: [
|
|
532
|
-
{ $ref: "#/definitions/storageMethod" },
|
|
533
|
-
{
|
|
534
|
-
type: "object",
|
|
535
|
-
properties: { thawing: { type: "string" } }
|
|
536
|
-
}
|
|
537
|
-
]
|
|
345
|
+
},
|
|
346
|
+
default: []
|
|
538
347
|
},
|
|
539
|
-
|
|
540
|
-
|
|
348
|
+
name: {
|
|
349
|
+
type: "string",
|
|
350
|
+
minLength: 1
|
|
351
|
+
},
|
|
352
|
+
ingredients: {
|
|
541
353
|
type: "array",
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
{
|
|
546
|
-
type: "object",
|
|
547
|
-
required: ["component", "storage"],
|
|
548
|
-
properties: {
|
|
549
|
-
component: { type: "string" },
|
|
550
|
-
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
]
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
},
|
|
558
|
-
storageMethod: {
|
|
559
|
-
type: "object",
|
|
560
|
-
required: ["duration"],
|
|
561
|
-
properties: {
|
|
562
|
-
duration: { type: "string", pattern: "^P" },
|
|
563
|
-
method: { type: "string" },
|
|
564
|
-
notes: { type: "string" }
|
|
565
|
-
}
|
|
566
|
-
},
|
|
567
|
-
substitution: {
|
|
568
|
-
type: "object",
|
|
569
|
-
required: ["ingredient"],
|
|
570
|
-
properties: {
|
|
571
|
-
ingredient: { type: "string" },
|
|
572
|
-
critical: { type: "boolean" },
|
|
573
|
-
notes: { type: "string" },
|
|
574
|
-
alternatives: {
|
|
354
|
+
minItems: 1
|
|
355
|
+
},
|
|
356
|
+
instructions: {
|
|
575
357
|
type: "array",
|
|
576
|
-
|
|
577
|
-
type: "object",
|
|
578
|
-
required: ["name", "ratio"],
|
|
579
|
-
properties: {
|
|
580
|
-
name: { type: "string" },
|
|
581
|
-
ratio: { type: "string" },
|
|
582
|
-
notes: { type: "string" },
|
|
583
|
-
impact: { type: "string" },
|
|
584
|
-
dietary: {
|
|
585
|
-
type: "array",
|
|
586
|
-
items: { type: "string" }
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
358
|
+
minItems: 1
|
|
590
359
|
}
|
|
591
|
-
}
|
|
360
|
+
},
|
|
361
|
+
required: [
|
|
362
|
+
"profile",
|
|
363
|
+
"name",
|
|
364
|
+
"ingredients",
|
|
365
|
+
"instructions"
|
|
366
|
+
],
|
|
367
|
+
additionalProperties: true
|
|
592
368
|
}
|
|
593
|
-
|
|
369
|
+
]
|
|
594
370
|
};
|
|
595
371
|
|
|
596
|
-
// src/
|
|
597
|
-
var
|
|
372
|
+
// src/schemas/recipe/modules/schedule/1.schema.json
|
|
373
|
+
var schema_default = {
|
|
598
374
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
599
|
-
$id: "
|
|
600
|
-
title: "Soustack Recipe
|
|
601
|
-
description: "
|
|
375
|
+
$id: "https://soustack.org/schemas/recipe/modules/schedule/1.schema.json",
|
|
376
|
+
title: "Soustack Recipe Module: schedule v1",
|
|
377
|
+
description: "Schema for the schedule module. Enforces bidirectional module gating and restricts usage to the core profile.",
|
|
602
378
|
type: "object",
|
|
603
|
-
required: ["name", "ingredients", "instructions"],
|
|
604
|
-
additionalProperties: false,
|
|
605
|
-
patternProperties: {
|
|
606
|
-
"^x-": {}
|
|
607
|
-
},
|
|
608
379
|
properties: {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
format: "uri",
|
|
612
|
-
description: "Optional schema hint for tooling compatibility"
|
|
613
|
-
},
|
|
614
|
-
id: {
|
|
615
|
-
type: "string",
|
|
616
|
-
description: "Unique identifier (slug or UUID)"
|
|
617
|
-
},
|
|
618
|
-
name: {
|
|
619
|
-
type: "string",
|
|
620
|
-
description: "The title of the recipe"
|
|
621
|
-
},
|
|
622
|
-
title: {
|
|
623
|
-
type: "string",
|
|
624
|
-
description: "Optional display title; alias for name"
|
|
625
|
-
},
|
|
626
|
-
version: {
|
|
627
|
-
type: "string",
|
|
628
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
629
|
-
description: "DEPRECATED: use recipeVersion for authoring revisions"
|
|
630
|
-
},
|
|
631
|
-
recipeVersion: {
|
|
632
|
-
type: "string",
|
|
633
|
-
pattern: "^\\d+\\.\\d+\\.\\d+$",
|
|
634
|
-
description: "Recipe content revision (semantic versioning, e.g., 1.0.0)"
|
|
635
|
-
},
|
|
636
|
-
description: {
|
|
637
|
-
type: "string"
|
|
638
|
-
},
|
|
639
|
-
category: {
|
|
640
|
-
type: "string",
|
|
641
|
-
examples: ["Main Course", "Dessert"]
|
|
642
|
-
},
|
|
643
|
-
tags: {
|
|
380
|
+
profile: { type: "string" },
|
|
381
|
+
modules: {
|
|
644
382
|
type: "array",
|
|
645
383
|
items: { type: "string" }
|
|
646
384
|
},
|
|
647
|
-
|
|
648
|
-
description: "Recipe-level hero image(s)",
|
|
649
|
-
anyOf: [
|
|
650
|
-
{
|
|
651
|
-
type: "string",
|
|
652
|
-
format: "uri"
|
|
653
|
-
},
|
|
654
|
-
{
|
|
655
|
-
type: "array",
|
|
656
|
-
minItems: 1,
|
|
657
|
-
items: {
|
|
658
|
-
type: "string",
|
|
659
|
-
format: "uri"
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
]
|
|
663
|
-
},
|
|
664
|
-
dateAdded: {
|
|
665
|
-
type: "string",
|
|
666
|
-
format: "date-time"
|
|
667
|
-
},
|
|
668
|
-
metadata: {
|
|
669
|
-
type: "object",
|
|
670
|
-
additionalProperties: true,
|
|
671
|
-
description: "Free-form vendor metadata"
|
|
672
|
-
},
|
|
673
|
-
source: {
|
|
385
|
+
schedule: {
|
|
674
386
|
type: "object",
|
|
675
387
|
properties: {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
adapted: { type: "boolean" }
|
|
680
|
-
}
|
|
681
|
-
},
|
|
682
|
-
yield: {
|
|
683
|
-
$ref: "#/definitions/yield"
|
|
684
|
-
},
|
|
685
|
-
time: {
|
|
686
|
-
$ref: "#/definitions/time"
|
|
687
|
-
},
|
|
688
|
-
equipment: {
|
|
689
|
-
type: "array",
|
|
690
|
-
items: { $ref: "#/definitions/equipment" }
|
|
691
|
-
},
|
|
692
|
-
ingredients: {
|
|
693
|
-
type: "array",
|
|
694
|
-
items: {
|
|
695
|
-
anyOf: [
|
|
696
|
-
{ type: "string" },
|
|
697
|
-
{ $ref: "#/definitions/ingredient" },
|
|
698
|
-
{ $ref: "#/definitions/ingredientSubsection" }
|
|
699
|
-
]
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
instructions: {
|
|
703
|
-
type: "array",
|
|
704
|
-
items: {
|
|
705
|
-
anyOf: [
|
|
706
|
-
{ type: "string" },
|
|
707
|
-
{ $ref: "#/definitions/instruction" },
|
|
708
|
-
{ $ref: "#/definitions/instructionSubsection" }
|
|
709
|
-
]
|
|
710
|
-
}
|
|
711
|
-
},
|
|
712
|
-
storage: {
|
|
713
|
-
$ref: "#/definitions/storage"
|
|
714
|
-
},
|
|
715
|
-
substitutions: {
|
|
716
|
-
type: "array",
|
|
717
|
-
items: { $ref: "#/definitions/substitution" }
|
|
388
|
+
tasks: { type: "array" }
|
|
389
|
+
},
|
|
390
|
+
additionalProperties: false
|
|
718
391
|
}
|
|
719
392
|
},
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
type: "object",
|
|
723
|
-
required: ["amount", "unit"],
|
|
724
|
-
properties: {
|
|
725
|
-
amount: { type: "number" },
|
|
726
|
-
unit: { type: "string" },
|
|
727
|
-
servings: { type: "number" },
|
|
728
|
-
description: { type: "string" }
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
time: {
|
|
732
|
-
type: "object",
|
|
733
|
-
properties: {
|
|
734
|
-
prep: { type: "number" },
|
|
735
|
-
active: { type: "number" },
|
|
736
|
-
passive: { type: "number" },
|
|
737
|
-
total: { type: "number" },
|
|
738
|
-
prepTime: { type: "string", format: "duration" },
|
|
739
|
-
cookTime: { type: "string", format: "duration" }
|
|
740
|
-
},
|
|
741
|
-
minProperties: 1
|
|
742
|
-
},
|
|
743
|
-
quantity: {
|
|
744
|
-
type: "object",
|
|
745
|
-
required: ["amount"],
|
|
746
|
-
properties: {
|
|
747
|
-
amount: { type: "number" },
|
|
748
|
-
unit: { type: ["string", "null"] }
|
|
749
|
-
}
|
|
750
|
-
},
|
|
751
|
-
scaling: {
|
|
752
|
-
type: "object",
|
|
753
|
-
required: ["type"],
|
|
754
|
-
properties: {
|
|
755
|
-
type: {
|
|
756
|
-
type: "string",
|
|
757
|
-
enum: ["linear", "discrete", "proportional", "fixed", "bakers_percentage"]
|
|
758
|
-
},
|
|
759
|
-
factor: { type: "number" },
|
|
760
|
-
referenceId: { type: "string" },
|
|
761
|
-
roundTo: { type: "number" },
|
|
762
|
-
min: { type: "number" },
|
|
763
|
-
max: { type: "number" }
|
|
764
|
-
},
|
|
393
|
+
allOf: [
|
|
394
|
+
{
|
|
765
395
|
if: {
|
|
766
|
-
properties: {
|
|
396
|
+
properties: {
|
|
397
|
+
modules: {
|
|
398
|
+
type: "array",
|
|
399
|
+
contains: { const: "schedule@1" }
|
|
400
|
+
}
|
|
401
|
+
}
|
|
767
402
|
},
|
|
768
403
|
then: {
|
|
769
|
-
required: ["
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
ingredient: {
|
|
773
|
-
type: "object",
|
|
774
|
-
required: ["item"],
|
|
775
|
-
properties: {
|
|
776
|
-
id: { type: "string" },
|
|
777
|
-
item: { type: "string" },
|
|
778
|
-
quantity: { $ref: "#/definitions/quantity" },
|
|
779
|
-
name: { type: "string" },
|
|
780
|
-
aisle: { type: "string" },
|
|
781
|
-
prep: { type: "string" },
|
|
782
|
-
prepAction: { type: "string" },
|
|
783
|
-
prepTime: { type: "number" },
|
|
784
|
-
destination: { type: "string" },
|
|
785
|
-
scaling: { $ref: "#/definitions/scaling" },
|
|
786
|
-
critical: { type: "boolean" },
|
|
787
|
-
optional: { type: "boolean" },
|
|
788
|
-
notes: { type: "string" }
|
|
789
|
-
}
|
|
790
|
-
},
|
|
791
|
-
ingredientSubsection: {
|
|
792
|
-
type: "object",
|
|
793
|
-
required: ["subsection", "items"],
|
|
794
|
-
properties: {
|
|
795
|
-
subsection: { type: "string" },
|
|
796
|
-
items: {
|
|
797
|
-
type: "array",
|
|
798
|
-
items: { $ref: "#/definitions/ingredient" }
|
|
404
|
+
required: ["schedule", "profile"],
|
|
405
|
+
properties: {
|
|
406
|
+
profile: { const: "core" }
|
|
799
407
|
}
|
|
800
408
|
}
|
|
801
409
|
},
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
410
|
+
{
|
|
411
|
+
if: {
|
|
412
|
+
required: ["schedule"]
|
|
413
|
+
},
|
|
414
|
+
then: {
|
|
415
|
+
required: ["modules", "profile"],
|
|
416
|
+
properties: {
|
|
417
|
+
modules: {
|
|
418
|
+
type: "array",
|
|
419
|
+
items: { type: "string" },
|
|
420
|
+
contains: { const: "schedule@1" }
|
|
421
|
+
},
|
|
422
|
+
profile: { const: "core" }
|
|
815
423
|
}
|
|
816
424
|
}
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
additionalProperties: true
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/schemas/recipe/modules/nutrition/1.schema.json
|
|
431
|
+
var schema_default2 = {
|
|
432
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
433
|
+
$id: "https://soustack.org/schemas/recipe/modules/nutrition/1.schema.json",
|
|
434
|
+
title: "Soustack Recipe Module: nutrition v1",
|
|
435
|
+
description: "Schema for the nutrition module. Keeps nutrition data aligned with module declarations and vice versa.",
|
|
436
|
+
type: "object",
|
|
437
|
+
properties: {
|
|
438
|
+
modules: {
|
|
439
|
+
type: "array",
|
|
440
|
+
items: { type: "string" }
|
|
817
441
|
},
|
|
818
|
-
|
|
442
|
+
nutrition: {
|
|
819
443
|
type: "object",
|
|
820
|
-
required: ["text"],
|
|
821
444
|
properties: {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
type: "array",
|
|
836
|
-
items: { type: "string" }
|
|
837
|
-
},
|
|
838
|
-
timing: {
|
|
839
|
-
type: "object",
|
|
840
|
-
required: ["duration", "type"],
|
|
841
|
-
properties: {
|
|
842
|
-
duration: {
|
|
843
|
-
anyOf: [
|
|
844
|
-
{ type: "number" },
|
|
845
|
-
{ type: "string", pattern: "^P" }
|
|
846
|
-
],
|
|
847
|
-
description: "Minutes as a number or ISO8601 duration string"
|
|
848
|
-
},
|
|
849
|
-
type: { type: "string", enum: ["active", "passive"] },
|
|
850
|
-
scaling: { type: "string", enum: ["linear", "fixed", "sqrt"] }
|
|
445
|
+
calories: { type: "number" },
|
|
446
|
+
protein_g: { type: "number" }
|
|
447
|
+
},
|
|
448
|
+
additionalProperties: false
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
allOf: [
|
|
452
|
+
{
|
|
453
|
+
if: {
|
|
454
|
+
properties: {
|
|
455
|
+
modules: {
|
|
456
|
+
type: "array",
|
|
457
|
+
contains: { const: "nutrition@1" }
|
|
851
458
|
}
|
|
852
459
|
}
|
|
460
|
+
},
|
|
461
|
+
then: {
|
|
462
|
+
required: ["nutrition"]
|
|
853
463
|
}
|
|
854
464
|
},
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
]
|
|
465
|
+
{
|
|
466
|
+
if: {
|
|
467
|
+
required: ["nutrition"]
|
|
468
|
+
},
|
|
469
|
+
then: {
|
|
470
|
+
required: ["modules"],
|
|
471
|
+
properties: {
|
|
472
|
+
modules: {
|
|
473
|
+
type: "array",
|
|
474
|
+
items: { type: "string" },
|
|
475
|
+
contains: { const: "nutrition@1" }
|
|
867
476
|
}
|
|
868
477
|
}
|
|
869
478
|
}
|
|
479
|
+
}
|
|
480
|
+
],
|
|
481
|
+
additionalProperties: true
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/schemas/recipe/modules/attribution/1.schema.json
|
|
485
|
+
var schema_default3 = {
|
|
486
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
487
|
+
$id: "https://soustack.org/schemas/recipe/modules/attribution/1.schema.json",
|
|
488
|
+
title: "Soustack Recipe Module: attribution v1",
|
|
489
|
+
description: "Schema for the attribution module. Ensures namespace data is present when the module is enabled and vice versa.",
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {
|
|
492
|
+
modules: {
|
|
493
|
+
type: "array",
|
|
494
|
+
items: { type: "string" }
|
|
870
495
|
},
|
|
871
|
-
|
|
496
|
+
attribution: {
|
|
872
497
|
type: "object",
|
|
873
498
|
properties: {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
items: {
|
|
889
|
-
allOf: [
|
|
890
|
-
{ $ref: "#/definitions/storageMethod" },
|
|
891
|
-
{
|
|
892
|
-
type: "object",
|
|
893
|
-
required: ["component", "storage"],
|
|
894
|
-
properties: {
|
|
895
|
-
component: { type: "string" },
|
|
896
|
-
storage: { type: "string", enum: ["roomTemp", "refrigerated", "frozen"] }
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
]
|
|
499
|
+
url: { type: "string" },
|
|
500
|
+
author: { type: "string" },
|
|
501
|
+
datePublished: { type: "string" }
|
|
502
|
+
},
|
|
503
|
+
additionalProperties: false
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
allOf: [
|
|
507
|
+
{
|
|
508
|
+
if: {
|
|
509
|
+
properties: {
|
|
510
|
+
modules: {
|
|
511
|
+
type: "array",
|
|
512
|
+
contains: { const: "attribution@1" }
|
|
900
513
|
}
|
|
901
514
|
}
|
|
515
|
+
},
|
|
516
|
+
then: {
|
|
517
|
+
required: ["attribution"]
|
|
902
518
|
}
|
|
903
519
|
},
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
required: ["ingredient"],
|
|
916
|
-
properties: {
|
|
917
|
-
ingredient: { type: "string" },
|
|
918
|
-
critical: { type: "boolean" },
|
|
919
|
-
notes: { type: "string" },
|
|
920
|
-
alternatives: {
|
|
921
|
-
type: "array",
|
|
922
|
-
items: {
|
|
923
|
-
type: "object",
|
|
924
|
-
required: ["name", "ratio"],
|
|
925
|
-
properties: {
|
|
926
|
-
name: { type: "string" },
|
|
927
|
-
ratio: { type: "string" },
|
|
928
|
-
notes: { type: "string" },
|
|
929
|
-
impact: { type: "string" },
|
|
930
|
-
dietary: {
|
|
931
|
-
type: "array",
|
|
932
|
-
items: { type: "string" }
|
|
933
|
-
}
|
|
934
|
-
}
|
|
520
|
+
{
|
|
521
|
+
if: {
|
|
522
|
+
required: ["attribution"]
|
|
523
|
+
},
|
|
524
|
+
then: {
|
|
525
|
+
required: ["modules"],
|
|
526
|
+
properties: {
|
|
527
|
+
modules: {
|
|
528
|
+
type: "array",
|
|
529
|
+
items: { type: "string" },
|
|
530
|
+
contains: { const: "attribution@1" }
|
|
935
531
|
}
|
|
936
532
|
}
|
|
937
533
|
}
|
|
938
534
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
// src/profiles/base.schema.json
|
|
943
|
-
var base_schema_default = {
|
|
944
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
945
|
-
$id: "http://soustack.org/schema/v0.2.1/profiles/base",
|
|
946
|
-
title: "Soustack Base Profile Schema",
|
|
947
|
-
description: "Wrapper schema that exposes the unmodified Soustack base schema.",
|
|
948
|
-
allOf: [
|
|
949
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" }
|
|
950
|
-
]
|
|
535
|
+
],
|
|
536
|
+
additionalProperties: true
|
|
951
537
|
};
|
|
952
538
|
|
|
953
|
-
// src/
|
|
954
|
-
var
|
|
539
|
+
// src/schemas/recipe/modules/taxonomy/1.schema.json
|
|
540
|
+
var schema_default4 = {
|
|
955
541
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
956
|
-
$id: "
|
|
957
|
-
title: "Soustack
|
|
958
|
-
description: "
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
{
|
|
962
|
-
|
|
542
|
+
$id: "https://soustack.org/schemas/recipe/modules/taxonomy/1.schema.json",
|
|
543
|
+
title: "Soustack Recipe Module: taxonomy v1",
|
|
544
|
+
description: "Schema for the taxonomy module. Enforces keyword and categorization data when enabled and ensures module declaration accompanies the namespace block.",
|
|
545
|
+
type: "object",
|
|
546
|
+
properties: {
|
|
547
|
+
modules: {
|
|
548
|
+
type: "array",
|
|
549
|
+
items: { type: "string" }
|
|
550
|
+
},
|
|
551
|
+
taxonomy: {
|
|
552
|
+
type: "object",
|
|
963
553
|
properties: {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
554
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
555
|
+
category: { type: "string" },
|
|
556
|
+
cuisine: { type: "string" }
|
|
557
|
+
},
|
|
558
|
+
additionalProperties: false
|
|
969
559
|
}
|
|
970
|
-
|
|
971
|
-
};
|
|
972
|
-
|
|
973
|
-
// src/profiles/quantified.schema.json
|
|
974
|
-
var quantified_schema_default = {
|
|
975
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
976
|
-
$id: "http://soustack.org/schema/v0.2.1/profiles/quantified",
|
|
977
|
-
title: "Soustack Quantified Profile Schema",
|
|
978
|
-
description: "Extends the base schema to require quantified ingredient entries.",
|
|
560
|
+
},
|
|
979
561
|
allOf: [
|
|
980
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
981
562
|
{
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
{ $ref: "#/definitions/quantifiedIngredient" },
|
|
988
|
-
{ $ref: "#/definitions/quantifiedIngredientSubsection" }
|
|
989
|
-
]
|
|
563
|
+
if: {
|
|
564
|
+
properties: {
|
|
565
|
+
modules: {
|
|
566
|
+
type: "array",
|
|
567
|
+
contains: { const: "taxonomy@1" }
|
|
990
568
|
}
|
|
991
569
|
}
|
|
570
|
+
},
|
|
571
|
+
then: {
|
|
572
|
+
required: ["taxonomy"]
|
|
992
573
|
}
|
|
993
|
-
}
|
|
994
|
-
],
|
|
995
|
-
definitions: {
|
|
996
|
-
quantifiedIngredient: {
|
|
997
|
-
allOf: [
|
|
998
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/ingredient" },
|
|
999
|
-
{ required: ["item", "quantity"] }
|
|
1000
|
-
]
|
|
1001
574
|
},
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
575
|
+
{
|
|
576
|
+
if: {
|
|
577
|
+
required: ["taxonomy"]
|
|
578
|
+
},
|
|
579
|
+
then: {
|
|
580
|
+
required: ["modules"],
|
|
581
|
+
properties: {
|
|
582
|
+
modules: {
|
|
583
|
+
type: "array",
|
|
584
|
+
items: { type: "string" },
|
|
585
|
+
contains: { const: "taxonomy@1" }
|
|
1011
586
|
}
|
|
1012
587
|
}
|
|
1013
|
-
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
],
|
|
591
|
+
additionalProperties: true
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/schemas/recipe/modules/media/1.schema.json
|
|
595
|
+
var schema_default5 = {
|
|
596
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
597
|
+
$id: "https://soustack.org/schemas/recipe/modules/media/1.schema.json",
|
|
598
|
+
title: "Soustack Recipe Module: media v1",
|
|
599
|
+
description: "Schema for the media module. Guards media blocks based on module activation and ensures declarations accompany payloads.",
|
|
600
|
+
type: "object",
|
|
601
|
+
properties: {
|
|
602
|
+
modules: {
|
|
603
|
+
type: "array",
|
|
604
|
+
items: { type: "string" }
|
|
605
|
+
},
|
|
606
|
+
media: {
|
|
607
|
+
type: "object",
|
|
608
|
+
properties: {
|
|
609
|
+
images: { type: "array", items: { type: "string" } },
|
|
610
|
+
videos: { type: "array", items: { type: "string" } }
|
|
611
|
+
},
|
|
612
|
+
additionalProperties: false
|
|
1014
613
|
}
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
// src/profiles/illustrated.schema.json
|
|
1019
|
-
var illustrated_schema_default = {
|
|
1020
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1021
|
-
$id: "http://soustack.org/schema/v0.2.1/profiles/illustrated",
|
|
1022
|
-
title: "Soustack Illustrated Profile Schema",
|
|
1023
|
-
description: "Extends the base schema to guarantee at least one illustrative image.",
|
|
614
|
+
},
|
|
1024
615
|
allOf: [
|
|
1025
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
1026
616
|
{
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
type: "array",
|
|
1033
|
-
contains: {
|
|
1034
|
-
anyOf: [
|
|
1035
|
-
{ $ref: "#/definitions/imageInstruction" },
|
|
1036
|
-
{ $ref: "#/definitions/instructionSubsectionWithImage" }
|
|
1037
|
-
]
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
617
|
+
if: {
|
|
618
|
+
properties: {
|
|
619
|
+
modules: {
|
|
620
|
+
type: "array",
|
|
621
|
+
contains: { const: "media@1" }
|
|
1040
622
|
}
|
|
1041
623
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
imageInstruction: {
|
|
1047
|
-
allOf: [
|
|
1048
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
1049
|
-
{ required: ["image"] }
|
|
1050
|
-
]
|
|
624
|
+
},
|
|
625
|
+
then: {
|
|
626
|
+
required: ["media"]
|
|
627
|
+
}
|
|
1051
628
|
},
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
629
|
+
{
|
|
630
|
+
if: {
|
|
631
|
+
required: ["media"]
|
|
632
|
+
},
|
|
633
|
+
then: {
|
|
634
|
+
required: ["modules"],
|
|
635
|
+
properties: {
|
|
636
|
+
modules: {
|
|
637
|
+
type: "array",
|
|
638
|
+
items: { type: "string" },
|
|
639
|
+
contains: { const: "media@1" }
|
|
1061
640
|
}
|
|
1062
641
|
}
|
|
1063
|
-
|
|
642
|
+
}
|
|
1064
643
|
}
|
|
1065
|
-
|
|
644
|
+
],
|
|
645
|
+
additionalProperties: true
|
|
1066
646
|
};
|
|
1067
647
|
|
|
1068
|
-
// src/
|
|
1069
|
-
var
|
|
648
|
+
// src/schemas/recipe/modules/times/1.schema.json
|
|
649
|
+
var schema_default6 = {
|
|
1070
650
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1071
|
-
$id: "
|
|
1072
|
-
title: "Soustack
|
|
1073
|
-
description: "
|
|
651
|
+
$id: "https://soustack.org/schemas/recipe/modules/times/1.schema.json",
|
|
652
|
+
title: "Soustack Recipe Module: times v1",
|
|
653
|
+
description: "Schema for the times module. Maintains alignment between module declarations and timing payloads.",
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
modules: {
|
|
657
|
+
type: "array",
|
|
658
|
+
items: { type: "string" }
|
|
659
|
+
},
|
|
660
|
+
times: {
|
|
661
|
+
type: "object",
|
|
662
|
+
properties: {
|
|
663
|
+
prepMinutes: { type: "number" },
|
|
664
|
+
cookMinutes: { type: "number" },
|
|
665
|
+
totalMinutes: { type: "number" }
|
|
666
|
+
},
|
|
667
|
+
additionalProperties: false
|
|
668
|
+
}
|
|
669
|
+
},
|
|
1074
670
|
allOf: [
|
|
1075
|
-
{ $ref: "http://soustack.org/schema/v0.2.1" },
|
|
1076
671
|
{
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
{ $ref: "#/definitions/schedulableInstruction" },
|
|
1083
|
-
{ $ref: "#/definitions/schedulableInstructionSubsection" }
|
|
1084
|
-
]
|
|
672
|
+
if: {
|
|
673
|
+
properties: {
|
|
674
|
+
modules: {
|
|
675
|
+
type: "array",
|
|
676
|
+
contains: { const: "times@1" }
|
|
1085
677
|
}
|
|
1086
678
|
}
|
|
679
|
+
},
|
|
680
|
+
then: {
|
|
681
|
+
required: ["times"]
|
|
1087
682
|
}
|
|
1088
|
-
}
|
|
1089
|
-
],
|
|
1090
|
-
definitions: {
|
|
1091
|
-
schedulableInstruction: {
|
|
1092
|
-
allOf: [
|
|
1093
|
-
{ $ref: "http://soustack.org/schema/v0.2.1#/definitions/instruction" },
|
|
1094
|
-
{ required: ["id", "timing"] }
|
|
1095
|
-
]
|
|
1096
683
|
},
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
684
|
+
{
|
|
685
|
+
if: {
|
|
686
|
+
required: ["times"]
|
|
687
|
+
},
|
|
688
|
+
then: {
|
|
689
|
+
required: ["modules"],
|
|
690
|
+
properties: {
|
|
691
|
+
modules: {
|
|
692
|
+
type: "array",
|
|
693
|
+
items: { type: "string" },
|
|
694
|
+
contains: { const: "times@1" }
|
|
1106
695
|
}
|
|
1107
696
|
}
|
|
1108
|
-
|
|
697
|
+
}
|
|
1109
698
|
}
|
|
1110
|
-
|
|
699
|
+
],
|
|
700
|
+
additionalProperties: true
|
|
1111
701
|
};
|
|
1112
702
|
|
|
1113
703
|
// src/validator.ts
|
|
704
|
+
var CANONICAL_BASE_SCHEMA_ID = base_schema_default.$id || "http://soustack.org/schema/recipe/base.schema.json";
|
|
705
|
+
var canonicalProfileId = (profile) => {
|
|
706
|
+
if (profile === "minimal") {
|
|
707
|
+
return minimal_schema_default.$id;
|
|
708
|
+
}
|
|
709
|
+
if (profile === "core") {
|
|
710
|
+
return core_schema_default.$id;
|
|
711
|
+
}
|
|
712
|
+
throw new Error(`Unknown profile: ${profile}`);
|
|
713
|
+
};
|
|
714
|
+
var moduleIdToSchemaRef = (moduleId) => {
|
|
715
|
+
const match = moduleId.match(/^([a-z0-9_-]+)@(\d+(?:\.\d+)*)$/i);
|
|
716
|
+
if (!match) {
|
|
717
|
+
throw new Error(`Invalid module identifier '${moduleId}'. Expected <name>@<version>.`);
|
|
718
|
+
}
|
|
719
|
+
const [, name, version] = match;
|
|
720
|
+
const moduleSchemas2 = {
|
|
721
|
+
"schedule@1": schema_default,
|
|
722
|
+
"nutrition@1": schema_default2,
|
|
723
|
+
"attribution@1": schema_default3,
|
|
724
|
+
"taxonomy@1": schema_default4,
|
|
725
|
+
"media@1": schema_default5,
|
|
726
|
+
"times@1": schema_default6
|
|
727
|
+
};
|
|
728
|
+
const schema = moduleSchemas2[moduleId];
|
|
729
|
+
if (schema && schema.$id) {
|
|
730
|
+
return schema.$id;
|
|
731
|
+
}
|
|
732
|
+
return `https://soustack.org/schemas/recipe/modules/${name}/${version}.schema.json`;
|
|
733
|
+
};
|
|
1114
734
|
var profileSchemas = {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
735
|
+
minimal: minimal_schema_default,
|
|
736
|
+
core: core_schema_default
|
|
737
|
+
};
|
|
738
|
+
var moduleSchemas = {
|
|
739
|
+
"schedule@1": schema_default,
|
|
740
|
+
"nutrition@1": schema_default2,
|
|
741
|
+
"attribution@1": schema_default3,
|
|
742
|
+
"taxonomy@1": schema_default4,
|
|
743
|
+
"media@1": schema_default5,
|
|
744
|
+
"times@1": schema_default6
|
|
1121
745
|
};
|
|
1122
746
|
var validationContexts = /* @__PURE__ */ new Map();
|
|
1123
747
|
function createContext(collectAllErrors) {
|
|
1124
748
|
const ajv = new Ajv__default.default({ strict: false, allErrors: collectAllErrors });
|
|
1125
749
|
addFormats__default.default(ajv);
|
|
1126
|
-
const
|
|
1127
|
-
const addSchemaIfNew = (schema) => {
|
|
750
|
+
const addSchemaWithAlias = (schema, alias) => {
|
|
1128
751
|
if (!schema) return;
|
|
1129
|
-
const schemaId = schema
|
|
1130
|
-
if (schemaId
|
|
1131
|
-
|
|
1132
|
-
|
|
752
|
+
const schemaId = schema.$id || alias;
|
|
753
|
+
if (schemaId) {
|
|
754
|
+
ajv.addSchema(schema, schemaId);
|
|
755
|
+
} else {
|
|
756
|
+
ajv.addSchema(schema);
|
|
757
|
+
}
|
|
1133
758
|
};
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
759
|
+
addSchemaWithAlias(base_schema_default, CANONICAL_BASE_SCHEMA_ID);
|
|
760
|
+
Object.entries(profileSchemas).forEach(([name, schema]) => {
|
|
761
|
+
addSchemaWithAlias(schema, canonicalProfileId(name));
|
|
762
|
+
});
|
|
763
|
+
Object.entries(moduleSchemas).forEach(([moduleId, schema]) => {
|
|
764
|
+
addSchemaWithAlias(schema, moduleIdToSchemaRef(moduleId));
|
|
765
|
+
});
|
|
766
|
+
return { ajv, validators: /* @__PURE__ */ new Map() };
|
|
1138
767
|
}
|
|
1139
768
|
function getContext(collectAllErrors) {
|
|
1140
769
|
if (!validationContexts.has(collectAllErrors)) {
|
|
@@ -1157,14 +786,58 @@ function detectProfileFromSchema(schemaRef) {
|
|
|
1157
786
|
}
|
|
1158
787
|
return void 0;
|
|
1159
788
|
}
|
|
1160
|
-
function
|
|
789
|
+
function resolveSchemaRef(inputSchema, requestedSchema) {
|
|
790
|
+
if (typeof requestedSchema === "string") return requestedSchema;
|
|
791
|
+
if (typeof inputSchema !== "string") return void 0;
|
|
792
|
+
return detectProfileFromSchema(inputSchema) ? inputSchema : void 0;
|
|
793
|
+
}
|
|
794
|
+
function inferModulesFromPayload(recipe) {
|
|
795
|
+
const inferred = [];
|
|
796
|
+
const payloadToModule = {
|
|
797
|
+
attribution: "attribution@1",
|
|
798
|
+
taxonomy: "taxonomy@1",
|
|
799
|
+
media: "media@1",
|
|
800
|
+
times: "times@1",
|
|
801
|
+
nutrition: "nutrition@1",
|
|
802
|
+
schedule: "schedule@1"
|
|
803
|
+
};
|
|
804
|
+
for (const [field, moduleId] of Object.entries(payloadToModule)) {
|
|
805
|
+
if (recipe && typeof recipe === "object" && field in recipe && recipe[field] != null) {
|
|
806
|
+
const payload = recipe[field];
|
|
807
|
+
if (typeof payload === "object" && !Array.isArray(payload)) {
|
|
808
|
+
if (Object.keys(payload).length > 0) {
|
|
809
|
+
inferred.push(moduleId);
|
|
810
|
+
}
|
|
811
|
+
} else if (Array.isArray(payload) && payload.length > 0) {
|
|
812
|
+
inferred.push(moduleId);
|
|
813
|
+
} else if (payload !== null && payload !== void 0) {
|
|
814
|
+
inferred.push(moduleId);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return inferred;
|
|
819
|
+
}
|
|
820
|
+
function getCombinedValidator(profile, modules, recipe, context) {
|
|
821
|
+
const inferredModules = inferModulesFromPayload(recipe);
|
|
822
|
+
const allModules = /* @__PURE__ */ new Set([...modules, ...inferredModules]);
|
|
823
|
+
const sortedModules = Array.from(allModules).sort();
|
|
824
|
+
const cacheKey = `${profile}::${sortedModules.join(",")}`;
|
|
825
|
+
const cached = context.validators.get(cacheKey);
|
|
826
|
+
if (cached) return cached;
|
|
1161
827
|
if (!profileSchemas[profile]) {
|
|
1162
828
|
throw new Error(`Unknown Soustack profile: ${profile}`);
|
|
1163
829
|
}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
830
|
+
const schema = {
|
|
831
|
+
$id: `urn:soustack:recipe:${cacheKey}`,
|
|
832
|
+
allOf: [
|
|
833
|
+
{ $ref: CANONICAL_BASE_SCHEMA_ID },
|
|
834
|
+
{ $ref: canonicalProfileId(profile) },
|
|
835
|
+
...sortedModules.map((moduleId) => ({ $ref: moduleIdToSchemaRef(moduleId) }))
|
|
836
|
+
]
|
|
837
|
+
};
|
|
838
|
+
const validateFn = context.ajv.compile(schema);
|
|
839
|
+
context.validators.set(cacheKey, validateFn);
|
|
840
|
+
return validateFn;
|
|
1168
841
|
}
|
|
1169
842
|
function normalizeRecipe(recipe) {
|
|
1170
843
|
const normalized = cloneRecipe(recipe);
|
|
@@ -1196,9 +869,33 @@ function normalizeTime(recipe) {
|
|
|
1196
869
|
}
|
|
1197
870
|
var _a, _b;
|
|
1198
871
|
var allowedTopLevelProps = /* @__PURE__ */ new Set([
|
|
1199
|
-
...Object.keys((_b = (_a =
|
|
1200
|
-
"
|
|
1201
|
-
|
|
872
|
+
...Object.keys((_b = (_a = base_schema_default) == null ? void 0 : _a.properties) != null ? _b : {}),
|
|
873
|
+
"$schema",
|
|
874
|
+
// Module fields (validated by module schemas)
|
|
875
|
+
"attribution",
|
|
876
|
+
"taxonomy",
|
|
877
|
+
"media",
|
|
878
|
+
"times",
|
|
879
|
+
"nutrition",
|
|
880
|
+
"schedule",
|
|
881
|
+
// Common recipe fields (allowed by base schema's additionalProperties: true)
|
|
882
|
+
"description",
|
|
883
|
+
"image",
|
|
884
|
+
"category",
|
|
885
|
+
"tags",
|
|
886
|
+
"source",
|
|
887
|
+
"dateAdded",
|
|
888
|
+
"dateModified",
|
|
889
|
+
"yield",
|
|
890
|
+
"time",
|
|
891
|
+
"id",
|
|
892
|
+
"title",
|
|
893
|
+
"recipeVersion",
|
|
894
|
+
"version",
|
|
895
|
+
// deprecated but allowed
|
|
896
|
+
"equipment",
|
|
897
|
+
"storage",
|
|
898
|
+
"substitutions"
|
|
1202
899
|
]);
|
|
1203
900
|
function detectUnknownTopLevelKeys(recipe) {
|
|
1204
901
|
if (!recipe || typeof recipe !== "object") return [];
|
|
@@ -1224,11 +921,19 @@ function formatAjvError(error) {
|
|
|
1224
921
|
message: error.message || "Validation error"
|
|
1225
922
|
};
|
|
1226
923
|
}
|
|
1227
|
-
function runAjvValidation(data, profile,
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
924
|
+
function runAjvValidation(data, profile, modules, context) {
|
|
925
|
+
try {
|
|
926
|
+
const validateFn = getCombinedValidator(profile, modules, data, context);
|
|
927
|
+
const isValid = validateFn(data);
|
|
928
|
+
return !isValid && validateFn.errors ? validateFn.errors.map(formatAjvError) : [];
|
|
929
|
+
} catch (error) {
|
|
930
|
+
return [
|
|
931
|
+
{
|
|
932
|
+
path: "/",
|
|
933
|
+
message: error instanceof Error ? error.message : "Validation failed to initialize"
|
|
934
|
+
}
|
|
935
|
+
];
|
|
936
|
+
}
|
|
1232
937
|
}
|
|
1233
938
|
function isInstruction(item) {
|
|
1234
939
|
return item && typeof item === "object" && !Array.isArray(item) && "text" in item;
|
|
@@ -1312,12 +1017,25 @@ function validateRecipe(input, options = {}) {
|
|
|
1312
1017
|
var _a2, _b2, _c, _d;
|
|
1313
1018
|
const collectAllErrors = (_a2 = options.collectAllErrors) != null ? _a2 : true;
|
|
1314
1019
|
const context = getContext(collectAllErrors);
|
|
1315
|
-
const schemaRef = (
|
|
1316
|
-
const
|
|
1020
|
+
const schemaRef = resolveSchemaRef(input == null ? void 0 : input.$schema, options.schema);
|
|
1021
|
+
const profileFromDocument = typeof (input == null ? void 0 : input.profile) === "string" ? input.profile : void 0;
|
|
1022
|
+
const profile = (_d = (_c = (_b2 = options.profile) != null ? _b2 : profileFromDocument) != null ? _c : detectProfileFromSchema(schemaRef)) != null ? _d : "core";
|
|
1023
|
+
const modulesFromDocument = Array.isArray(input == null ? void 0 : input.modules) ? input.modules.filter((value) => typeof value === "string") : [];
|
|
1024
|
+
const modules = modulesFromDocument.length > 0 ? [...modulesFromDocument].sort() : [];
|
|
1317
1025
|
const { normalized, warnings } = normalizeRecipe(input);
|
|
1026
|
+
if (!profileFromDocument) {
|
|
1027
|
+
normalized.profile = profile;
|
|
1028
|
+
} else {
|
|
1029
|
+
normalized.profile = profileFromDocument;
|
|
1030
|
+
}
|
|
1031
|
+
if (!("modules" in normalized) || normalized.modules === void 0 || normalized.modules === null) {
|
|
1032
|
+
normalized.modules = [];
|
|
1033
|
+
} else if (modulesFromDocument.length > 0) {
|
|
1034
|
+
normalized.modules = modules;
|
|
1035
|
+
}
|
|
1318
1036
|
const unknownKeyErrors = detectUnknownTopLevelKeys(normalized);
|
|
1319
|
-
const validationErrors = runAjvValidation(normalized, profile,
|
|
1320
|
-
const graphErrors =
|
|
1037
|
+
const validationErrors = runAjvValidation(normalized, profile, modules, context);
|
|
1038
|
+
const graphErrors = modules.includes("schedule@1") && validationErrors.length === 0 ? checkInstructionGraph(normalized) : [];
|
|
1321
1039
|
const errors = [...unknownKeyErrors, ...validationErrors, ...graphErrors];
|
|
1322
1040
|
return {
|
|
1323
1041
|
valid: errors.length === 0,
|
|
@@ -1328,15 +1046,14 @@ function validateRecipe(input, options = {}) {
|
|
|
1328
1046
|
}
|
|
1329
1047
|
function detectProfiles(recipe) {
|
|
1330
1048
|
var _a2;
|
|
1331
|
-
const result = validateRecipe(recipe, { profile: "
|
|
1049
|
+
const result = validateRecipe(recipe, { profile: "core", collectAllErrors: false });
|
|
1332
1050
|
if (!result.valid) return [];
|
|
1333
1051
|
const normalizedRecipe = (_a2 = result.normalized) != null ? _a2 : recipe;
|
|
1334
|
-
const profiles = [
|
|
1052
|
+
const profiles = [];
|
|
1335
1053
|
const context = getContext(false);
|
|
1336
1054
|
Object.keys(profileSchemas).forEach((profile) => {
|
|
1337
|
-
if (profile === "base") return;
|
|
1338
1055
|
if (!profileSchemas[profile]) return;
|
|
1339
|
-
const errors = runAjvValidation(normalizedRecipe, profile, context);
|
|
1056
|
+
const errors = runAjvValidation(normalizedRecipe, profile, [], context);
|
|
1340
1057
|
if (errors.length === 0) {
|
|
1341
1058
|
profiles.push(profile);
|
|
1342
1059
|
}
|
|
@@ -1442,8 +1159,22 @@ function fromSchemaOrg(input) {
|
|
|
1442
1159
|
const tags = collectTags(recipeNode.recipeCuisine, recipeNode.keywords);
|
|
1443
1160
|
const category = extractFirst(recipeNode.recipeCategory);
|
|
1444
1161
|
const source = convertSource(recipeNode);
|
|
1445
|
-
const
|
|
1162
|
+
const dateModified = recipeNode.dateModified || void 0;
|
|
1163
|
+
const nutrition = convertNutrition(recipeNode.nutrition);
|
|
1164
|
+
const attribution = convertAttribution(recipeNode);
|
|
1165
|
+
const taxonomy = convertTaxonomy(tags, category, extractFirst(recipeNode.recipeCuisine));
|
|
1166
|
+
const media = convertMedia(recipeNode.image, recipeNode.video);
|
|
1167
|
+
const times = convertTimes(time);
|
|
1168
|
+
const modules = [];
|
|
1169
|
+
if (attribution) modules.push("attribution@1");
|
|
1170
|
+
if (taxonomy) modules.push("taxonomy@1");
|
|
1171
|
+
if (media) modules.push("media@1");
|
|
1172
|
+
if (nutrition) modules.push("nutrition@1");
|
|
1173
|
+
if (times) modules.push("times@1");
|
|
1446
1174
|
return {
|
|
1175
|
+
"@type": "Recipe",
|
|
1176
|
+
profile: "minimal",
|
|
1177
|
+
modules: modules.sort(),
|
|
1447
1178
|
name: recipeNode.name.trim(),
|
|
1448
1179
|
description: ((_a2 = recipeNode.description) == null ? void 0 : _a2.trim()) || void 0,
|
|
1449
1180
|
image: normalizeImage(recipeNode.image),
|
|
@@ -1451,12 +1182,16 @@ function fromSchemaOrg(input) {
|
|
|
1451
1182
|
tags: tags.length ? tags : void 0,
|
|
1452
1183
|
source,
|
|
1453
1184
|
dateAdded: recipeNode.datePublished || void 0,
|
|
1454
|
-
dateModified: recipeNode.dateModified || void 0,
|
|
1455
1185
|
yield: recipeYield,
|
|
1456
1186
|
time,
|
|
1457
1187
|
ingredients,
|
|
1458
1188
|
instructions,
|
|
1459
|
-
|
|
1189
|
+
...dateModified ? { dateModified } : {},
|
|
1190
|
+
...nutrition ? { nutrition } : {},
|
|
1191
|
+
...attribution ? { attribution } : {},
|
|
1192
|
+
...taxonomy ? { taxonomy } : {},
|
|
1193
|
+
...media ? { media } : {},
|
|
1194
|
+
...times ? { times } : {}
|
|
1460
1195
|
};
|
|
1461
1196
|
}
|
|
1462
1197
|
function extractRecipeNode(input) {
|
|
@@ -1672,6 +1407,175 @@ function extractEntityName(value) {
|
|
|
1672
1407
|
}
|
|
1673
1408
|
return void 0;
|
|
1674
1409
|
}
|
|
1410
|
+
function convertAttribution(recipe) {
|
|
1411
|
+
var _a2, _b2;
|
|
1412
|
+
const attribution = {};
|
|
1413
|
+
const url = (_a2 = recipe.url || recipe.mainEntityOfPage) == null ? void 0 : _a2.trim();
|
|
1414
|
+
const author = extractEntityName(recipe.author);
|
|
1415
|
+
const datePublished = (_b2 = recipe.datePublished) == null ? void 0 : _b2.trim();
|
|
1416
|
+
if (url) attribution.url = url;
|
|
1417
|
+
if (author) attribution.author = author;
|
|
1418
|
+
if (datePublished) attribution.datePublished = datePublished;
|
|
1419
|
+
return Object.keys(attribution).length ? attribution : void 0;
|
|
1420
|
+
}
|
|
1421
|
+
function convertTaxonomy(keywords, category, cuisine) {
|
|
1422
|
+
const taxonomy = {};
|
|
1423
|
+
if (keywords.length) taxonomy.keywords = keywords;
|
|
1424
|
+
if (category) taxonomy.category = category;
|
|
1425
|
+
if (cuisine) taxonomy.cuisine = cuisine;
|
|
1426
|
+
return Object.keys(taxonomy).length ? taxonomy : void 0;
|
|
1427
|
+
}
|
|
1428
|
+
function normalizeMediaList(value) {
|
|
1429
|
+
if (!value) return [];
|
|
1430
|
+
if (typeof value === "string") return [value.trim()].filter(Boolean);
|
|
1431
|
+
if (Array.isArray(value)) {
|
|
1432
|
+
return value.map((item) => typeof item === "string" ? item.trim() : extractMediaUrl(item)).filter((entry) => Boolean(entry == null ? void 0 : entry.length));
|
|
1433
|
+
}
|
|
1434
|
+
const url = extractMediaUrl(value);
|
|
1435
|
+
return url ? [url] : [];
|
|
1436
|
+
}
|
|
1437
|
+
function extractMediaUrl(value) {
|
|
1438
|
+
if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
|
|
1439
|
+
const trimmed = value.url.trim();
|
|
1440
|
+
return trimmed || void 0;
|
|
1441
|
+
}
|
|
1442
|
+
return void 0;
|
|
1443
|
+
}
|
|
1444
|
+
function convertMedia(image, video) {
|
|
1445
|
+
const normalizedImage = normalizeImage(image);
|
|
1446
|
+
const images = normalizedImage ? Array.isArray(normalizedImage) ? normalizedImage : [normalizedImage] : [];
|
|
1447
|
+
const videos = normalizeMediaList(video);
|
|
1448
|
+
const media = {};
|
|
1449
|
+
if (images.length) media.images = images;
|
|
1450
|
+
if (videos.length) media.videos = videos;
|
|
1451
|
+
return Object.keys(media).length ? media : void 0;
|
|
1452
|
+
}
|
|
1453
|
+
function convertTimes(time) {
|
|
1454
|
+
if (!time) return void 0;
|
|
1455
|
+
const times = {};
|
|
1456
|
+
if (typeof time.prep === "number") times.prepMinutes = time.prep;
|
|
1457
|
+
if (typeof time.active === "number") times.cookMinutes = time.active;
|
|
1458
|
+
if (typeof time.total === "number") times.totalMinutes = time.total;
|
|
1459
|
+
return Object.keys(times).length ? times : void 0;
|
|
1460
|
+
}
|
|
1461
|
+
function convertNutrition(nutrition) {
|
|
1462
|
+
if (!nutrition || typeof nutrition !== "object") {
|
|
1463
|
+
return void 0;
|
|
1464
|
+
}
|
|
1465
|
+
const result = {};
|
|
1466
|
+
let hasData = false;
|
|
1467
|
+
if ("calories" in nutrition) {
|
|
1468
|
+
const calories = nutrition.calories;
|
|
1469
|
+
if (typeof calories === "number") {
|
|
1470
|
+
result.calories = calories;
|
|
1471
|
+
hasData = true;
|
|
1472
|
+
} else if (typeof calories === "string") {
|
|
1473
|
+
const parsed = parseFloat(calories.replace(/[^\d.-]/g, ""));
|
|
1474
|
+
if (!isNaN(parsed)) {
|
|
1475
|
+
result.calories = parsed;
|
|
1476
|
+
hasData = true;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if ("proteinContent" in nutrition || "protein_g" in nutrition) {
|
|
1481
|
+
const protein = nutrition.proteinContent || nutrition.protein_g;
|
|
1482
|
+
if (typeof protein === "number") {
|
|
1483
|
+
result.protein_g = protein;
|
|
1484
|
+
hasData = true;
|
|
1485
|
+
} else if (typeof protein === "string") {
|
|
1486
|
+
const parsed = parseFloat(protein.replace(/[^\d.-]/g, ""));
|
|
1487
|
+
if (!isNaN(parsed)) {
|
|
1488
|
+
result.protein_g = parsed;
|
|
1489
|
+
hasData = true;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return hasData ? result : void 0;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/schemas/registry/modules.json
|
|
1497
|
+
var modules_default = {
|
|
1498
|
+
modules: [
|
|
1499
|
+
{
|
|
1500
|
+
id: "attribution",
|
|
1501
|
+
versions: [
|
|
1502
|
+
1
|
|
1503
|
+
],
|
|
1504
|
+
latest: 1,
|
|
1505
|
+
namespace: "https://soustack.org/schemas/recipe/modules/attribution",
|
|
1506
|
+
schema: "https://soustack.org/schemas/recipe/modules/attribution/1.schema.json",
|
|
1507
|
+
schemaOrgMappable: true,
|
|
1508
|
+
schemaOrgConfidence: "medium",
|
|
1509
|
+
minProfile: "minimal",
|
|
1510
|
+
allowedOnMinimal: true
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
id: "taxonomy",
|
|
1514
|
+
versions: [
|
|
1515
|
+
1
|
|
1516
|
+
],
|
|
1517
|
+
latest: 1,
|
|
1518
|
+
namespace: "https://soustack.org/schemas/recipe/modules/taxonomy",
|
|
1519
|
+
schema: "https://soustack.org/schemas/recipe/modules/taxonomy/1.schema.json",
|
|
1520
|
+
schemaOrgMappable: true,
|
|
1521
|
+
schemaOrgConfidence: "high",
|
|
1522
|
+
minProfile: "minimal",
|
|
1523
|
+
allowedOnMinimal: true
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
id: "media",
|
|
1527
|
+
versions: [
|
|
1528
|
+
1
|
|
1529
|
+
],
|
|
1530
|
+
latest: 1,
|
|
1531
|
+
namespace: "https://soustack.org/schemas/recipe/modules/media",
|
|
1532
|
+
schema: "https://soustack.org/schemas/recipe/modules/media/1.schema.json",
|
|
1533
|
+
schemaOrgMappable: true,
|
|
1534
|
+
schemaOrgConfidence: "medium",
|
|
1535
|
+
minProfile: "minimal",
|
|
1536
|
+
allowedOnMinimal: true
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
id: "nutrition",
|
|
1540
|
+
versions: [
|
|
1541
|
+
1
|
|
1542
|
+
],
|
|
1543
|
+
latest: 1,
|
|
1544
|
+
namespace: "https://soustack.org/schemas/recipe/modules/nutrition",
|
|
1545
|
+
schema: "https://soustack.org/schemas/recipe/modules/nutrition/1.schema.json",
|
|
1546
|
+
schemaOrgMappable: false,
|
|
1547
|
+
schemaOrgConfidence: "low",
|
|
1548
|
+
minProfile: "minimal",
|
|
1549
|
+
allowedOnMinimal: true
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
id: "times",
|
|
1553
|
+
versions: [
|
|
1554
|
+
1
|
|
1555
|
+
],
|
|
1556
|
+
latest: 1,
|
|
1557
|
+
namespace: "https://soustack.org/schemas/recipe/modules/times",
|
|
1558
|
+
schema: "https://soustack.org/schemas/recipe/modules/times/1.schema.json",
|
|
1559
|
+
schemaOrgMappable: true,
|
|
1560
|
+
schemaOrgConfidence: "medium",
|
|
1561
|
+
minProfile: "minimal",
|
|
1562
|
+
allowedOnMinimal: true
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
id: "schedule",
|
|
1566
|
+
versions: [
|
|
1567
|
+
1
|
|
1568
|
+
],
|
|
1569
|
+
latest: 1,
|
|
1570
|
+
namespace: "https://soustack.org/schemas/recipe/modules/schedule",
|
|
1571
|
+
schema: "https://soustack.org/schemas/recipe/modules/schedule/1.schema.json",
|
|
1572
|
+
schemaOrgMappable: false,
|
|
1573
|
+
schemaOrgConfidence: "low",
|
|
1574
|
+
minProfile: "core",
|
|
1575
|
+
allowedOnMinimal: false
|
|
1576
|
+
}
|
|
1577
|
+
]
|
|
1578
|
+
};
|
|
1675
1579
|
|
|
1676
1580
|
// src/converters/toSchemaOrg.ts
|
|
1677
1581
|
function convertBasicMetadata(recipe) {
|
|
@@ -1813,6 +1717,22 @@ function convertTime2(time) {
|
|
|
1813
1717
|
}
|
|
1814
1718
|
return result;
|
|
1815
1719
|
}
|
|
1720
|
+
function convertTimesModule(times) {
|
|
1721
|
+
if (!times) {
|
|
1722
|
+
return {};
|
|
1723
|
+
}
|
|
1724
|
+
const result = {};
|
|
1725
|
+
if (times.prepMinutes !== void 0) {
|
|
1726
|
+
result.prepTime = formatDuration(times.prepMinutes);
|
|
1727
|
+
}
|
|
1728
|
+
if (times.cookMinutes !== void 0) {
|
|
1729
|
+
result.cookTime = formatDuration(times.cookMinutes);
|
|
1730
|
+
}
|
|
1731
|
+
if (times.totalMinutes !== void 0) {
|
|
1732
|
+
result.totalTime = formatDuration(times.totalMinutes);
|
|
1733
|
+
}
|
|
1734
|
+
return result;
|
|
1735
|
+
}
|
|
1816
1736
|
function convertYield(yld) {
|
|
1817
1737
|
if (!yld) {
|
|
1818
1738
|
return void 0;
|
|
@@ -1851,33 +1771,58 @@ function convertCategoryTags(category, tags) {
|
|
|
1851
1771
|
}
|
|
1852
1772
|
return result;
|
|
1853
1773
|
}
|
|
1854
|
-
function
|
|
1774
|
+
function convertNutrition2(nutrition) {
|
|
1855
1775
|
if (!nutrition) {
|
|
1856
1776
|
return void 0;
|
|
1857
1777
|
}
|
|
1858
|
-
|
|
1859
|
-
...nutrition,
|
|
1778
|
+
const result = {
|
|
1860
1779
|
"@type": "NutritionInformation"
|
|
1861
1780
|
};
|
|
1781
|
+
if (nutrition.calories !== void 0) {
|
|
1782
|
+
if (typeof nutrition.calories === "number") {
|
|
1783
|
+
result.calories = `${nutrition.calories} calories`;
|
|
1784
|
+
} else {
|
|
1785
|
+
result.calories = nutrition.calories;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
Object.keys(nutrition).forEach((key) => {
|
|
1789
|
+
if (key !== "calories" && key !== "@type") {
|
|
1790
|
+
result[key] = nutrition[key];
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
return result;
|
|
1862
1794
|
}
|
|
1863
1795
|
function cleanOutput(obj) {
|
|
1864
1796
|
return Object.fromEntries(
|
|
1865
1797
|
Object.entries(obj).filter(([, value]) => value !== void 0)
|
|
1866
1798
|
);
|
|
1867
1799
|
}
|
|
1800
|
+
function getSchemaOrgMappableModules(modules = []) {
|
|
1801
|
+
const mappableModules = modules_default.modules.filter((m) => m.schemaOrgMappable).map((m) => `${m.id}@${m.latest}`);
|
|
1802
|
+
return modules.filter((moduleId) => mappableModules.includes(moduleId));
|
|
1803
|
+
}
|
|
1868
1804
|
function toSchemaOrg(recipe) {
|
|
1869
1805
|
const base = convertBasicMetadata(recipe);
|
|
1870
1806
|
const ingredients = convertIngredients2(recipe.ingredients);
|
|
1871
1807
|
const instructions = convertInstructions2(recipe.instructions);
|
|
1872
|
-
const
|
|
1808
|
+
const recipeModules = Array.isArray(recipe.modules) ? recipe.modules : [];
|
|
1809
|
+
const mappableModules = getSchemaOrgMappableModules(recipeModules);
|
|
1810
|
+
const hasMappableNutrition = mappableModules.includes("nutrition@1");
|
|
1811
|
+
const nutrition = hasMappableNutrition ? convertNutrition2(recipe.nutrition) : void 0;
|
|
1812
|
+
const hasMappableTimes = mappableModules.includes("times@1");
|
|
1813
|
+
const timeData = hasMappableTimes ? recipe.times ? convertTimesModule(recipe.times) : convertTime2(recipe.time) : {};
|
|
1814
|
+
const hasMappableAttribution = mappableModules.includes("attribution@1");
|
|
1815
|
+
const attributionData = hasMappableAttribution ? convertAuthor(recipe.source) : {};
|
|
1816
|
+
const hasMappableTaxonomy = mappableModules.includes("taxonomy@1");
|
|
1817
|
+
const taxonomyData = hasMappableTaxonomy ? convertCategoryTags(recipe.category, recipe.tags) : {};
|
|
1873
1818
|
return cleanOutput({
|
|
1874
1819
|
...base,
|
|
1875
1820
|
recipeIngredient: ingredients.length ? ingredients : void 0,
|
|
1876
1821
|
recipeInstructions: instructions.length ? instructions : void 0,
|
|
1877
1822
|
recipeYield: convertYield(recipe.yield),
|
|
1878
|
-
...
|
|
1879
|
-
...
|
|
1880
|
-
...
|
|
1823
|
+
...timeData,
|
|
1824
|
+
...attributionData,
|
|
1825
|
+
...taxonomyData,
|
|
1881
1826
|
nutrition
|
|
1882
1827
|
});
|
|
1883
1828
|
}
|
|
@@ -2026,12 +1971,530 @@ function extractSchemaOrgRecipeFromHTML(html) {
|
|
|
2026
1971
|
}
|
|
2027
1972
|
|
|
2028
1973
|
// src/specVersion.ts
|
|
2029
|
-
var SOUSTACK_SPEC_VERSION = "0.
|
|
1974
|
+
var SOUSTACK_SPEC_VERSION = "0.3.0";
|
|
1975
|
+
|
|
1976
|
+
// src/conversion/units.ts
|
|
1977
|
+
var MASS_UNITS = {
|
|
1978
|
+
g: {
|
|
1979
|
+
dimension: "mass",
|
|
1980
|
+
toMetricBase: 1,
|
|
1981
|
+
metricBaseUnit: "g",
|
|
1982
|
+
isMetric: true
|
|
1983
|
+
},
|
|
1984
|
+
kg: {
|
|
1985
|
+
dimension: "mass",
|
|
1986
|
+
toMetricBase: 1e3,
|
|
1987
|
+
metricBaseUnit: "g",
|
|
1988
|
+
isMetric: true
|
|
1989
|
+
},
|
|
1990
|
+
oz: {
|
|
1991
|
+
dimension: "mass",
|
|
1992
|
+
toMetricBase: 28.349523125,
|
|
1993
|
+
metricBaseUnit: "g",
|
|
1994
|
+
isMetric: false
|
|
1995
|
+
},
|
|
1996
|
+
lb: {
|
|
1997
|
+
dimension: "mass",
|
|
1998
|
+
toMetricBase: 453.59237,
|
|
1999
|
+
metricBaseUnit: "g",
|
|
2000
|
+
isMetric: false
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
var VOLUME_UNITS = {
|
|
2004
|
+
ml: {
|
|
2005
|
+
dimension: "volume",
|
|
2006
|
+
toMetricBase: 1,
|
|
2007
|
+
metricBaseUnit: "ml",
|
|
2008
|
+
isMetric: true
|
|
2009
|
+
},
|
|
2010
|
+
l: {
|
|
2011
|
+
dimension: "volume",
|
|
2012
|
+
toMetricBase: 1e3,
|
|
2013
|
+
metricBaseUnit: "ml",
|
|
2014
|
+
isMetric: true
|
|
2015
|
+
},
|
|
2016
|
+
tsp: {
|
|
2017
|
+
dimension: "volume",
|
|
2018
|
+
toMetricBase: 4.92892159375,
|
|
2019
|
+
metricBaseUnit: "ml",
|
|
2020
|
+
isMetric: false
|
|
2021
|
+
},
|
|
2022
|
+
tbsp: {
|
|
2023
|
+
dimension: "volume",
|
|
2024
|
+
toMetricBase: 14.78676478125,
|
|
2025
|
+
metricBaseUnit: "ml",
|
|
2026
|
+
isMetric: false
|
|
2027
|
+
},
|
|
2028
|
+
fl_oz: {
|
|
2029
|
+
dimension: "volume",
|
|
2030
|
+
toMetricBase: 29.5735295625,
|
|
2031
|
+
metricBaseUnit: "ml",
|
|
2032
|
+
isMetric: false
|
|
2033
|
+
},
|
|
2034
|
+
cup: {
|
|
2035
|
+
dimension: "volume",
|
|
2036
|
+
toMetricBase: 236.5882365,
|
|
2037
|
+
metricBaseUnit: "ml",
|
|
2038
|
+
isMetric: false
|
|
2039
|
+
},
|
|
2040
|
+
pint: {
|
|
2041
|
+
dimension: "volume",
|
|
2042
|
+
toMetricBase: 473.176473,
|
|
2043
|
+
metricBaseUnit: "ml",
|
|
2044
|
+
isMetric: false
|
|
2045
|
+
},
|
|
2046
|
+
quart: {
|
|
2047
|
+
dimension: "volume",
|
|
2048
|
+
toMetricBase: 946.352946,
|
|
2049
|
+
metricBaseUnit: "ml",
|
|
2050
|
+
isMetric: false
|
|
2051
|
+
},
|
|
2052
|
+
gallon: {
|
|
2053
|
+
dimension: "volume",
|
|
2054
|
+
toMetricBase: 3785.411784,
|
|
2055
|
+
metricBaseUnit: "ml",
|
|
2056
|
+
isMetric: false
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
var COUNT_UNITS = {
|
|
2060
|
+
clove: {
|
|
2061
|
+
dimension: "count",
|
|
2062
|
+
toMetricBase: 1,
|
|
2063
|
+
metricBaseUnit: "count",
|
|
2064
|
+
isMetric: true
|
|
2065
|
+
},
|
|
2066
|
+
sprig: {
|
|
2067
|
+
dimension: "count",
|
|
2068
|
+
toMetricBase: 1,
|
|
2069
|
+
metricBaseUnit: "count",
|
|
2070
|
+
isMetric: true
|
|
2071
|
+
},
|
|
2072
|
+
leaf: {
|
|
2073
|
+
dimension: "count",
|
|
2074
|
+
toMetricBase: 1,
|
|
2075
|
+
metricBaseUnit: "count",
|
|
2076
|
+
isMetric: true
|
|
2077
|
+
},
|
|
2078
|
+
pinch: {
|
|
2079
|
+
dimension: "count",
|
|
2080
|
+
toMetricBase: 1,
|
|
2081
|
+
metricBaseUnit: "count",
|
|
2082
|
+
isMetric: true
|
|
2083
|
+
},
|
|
2084
|
+
bottle: {
|
|
2085
|
+
dimension: "count",
|
|
2086
|
+
toMetricBase: 1,
|
|
2087
|
+
metricBaseUnit: "count",
|
|
2088
|
+
isMetric: true
|
|
2089
|
+
},
|
|
2090
|
+
count: {
|
|
2091
|
+
dimension: "count",
|
|
2092
|
+
toMetricBase: 1,
|
|
2093
|
+
metricBaseUnit: "count",
|
|
2094
|
+
isMetric: true
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
var UNIT_DEFINITIONS = {
|
|
2098
|
+
...MASS_UNITS,
|
|
2099
|
+
...VOLUME_UNITS,
|
|
2100
|
+
...COUNT_UNITS
|
|
2101
|
+
};
|
|
2102
|
+
function normalizeUnitToken(unit) {
|
|
2103
|
+
var _a2;
|
|
2104
|
+
if (!unit) {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
const token = unit.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
2108
|
+
const canonical = (_a2 = UNIT_SYNONYMS[token]) != null ? _a2 : token;
|
|
2109
|
+
return canonical in UNIT_DEFINITIONS ? canonical : null;
|
|
2110
|
+
}
|
|
2111
|
+
var UNIT_SYNONYMS = {
|
|
2112
|
+
teaspoons: "tsp",
|
|
2113
|
+
teaspoon: "tsp",
|
|
2114
|
+
tsps: "tsp",
|
|
2115
|
+
tbsp: "tbsp",
|
|
2116
|
+
tbsps: "tbsp",
|
|
2117
|
+
tablespoon: "tbsp",
|
|
2118
|
+
tablespoons: "tbsp",
|
|
2119
|
+
cup: "cup",
|
|
2120
|
+
cups: "cup",
|
|
2121
|
+
pint: "pint",
|
|
2122
|
+
pints: "pint",
|
|
2123
|
+
quart: "quart",
|
|
2124
|
+
quarts: "quart",
|
|
2125
|
+
gallon: "gallon",
|
|
2126
|
+
gallons: "gallon",
|
|
2127
|
+
ml: "ml",
|
|
2128
|
+
milliliter: "ml",
|
|
2129
|
+
milliliters: "ml",
|
|
2130
|
+
millilitre: "ml",
|
|
2131
|
+
millilitres: "ml",
|
|
2132
|
+
l: "l",
|
|
2133
|
+
liter: "l",
|
|
2134
|
+
liters: "l",
|
|
2135
|
+
litre: "l",
|
|
2136
|
+
litres: "l",
|
|
2137
|
+
fl_oz: "fl_oz",
|
|
2138
|
+
"fl.oz": "fl_oz",
|
|
2139
|
+
"fl.oz.": "fl_oz",
|
|
2140
|
+
"fl_oz.": "fl_oz",
|
|
2141
|
+
"fl oz": "fl_oz",
|
|
2142
|
+
"fl oz.": "fl_oz",
|
|
2143
|
+
fluid_ounce: "fl_oz",
|
|
2144
|
+
fluid_ounces: "fl_oz",
|
|
2145
|
+
oz: "oz",
|
|
2146
|
+
ounce: "oz",
|
|
2147
|
+
ounces: "oz",
|
|
2148
|
+
lb: "lb",
|
|
2149
|
+
lbs: "lb",
|
|
2150
|
+
pound: "lb",
|
|
2151
|
+
pounds: "lb",
|
|
2152
|
+
g: "g",
|
|
2153
|
+
gram: "g",
|
|
2154
|
+
grams: "g",
|
|
2155
|
+
kg: "kg",
|
|
2156
|
+
kilogram: "kg",
|
|
2157
|
+
kilograms: "kg",
|
|
2158
|
+
clove: "clove",
|
|
2159
|
+
cloves: "clove",
|
|
2160
|
+
sprig: "sprig",
|
|
2161
|
+
sprigs: "sprig",
|
|
2162
|
+
leaf: "leaf",
|
|
2163
|
+
leaves: "leaf",
|
|
2164
|
+
pinch: "pinch",
|
|
2165
|
+
pinches: "pinch",
|
|
2166
|
+
bottle: "bottle",
|
|
2167
|
+
bottles: "bottle",
|
|
2168
|
+
count: "count",
|
|
2169
|
+
counts: "count"
|
|
2170
|
+
};
|
|
2171
|
+
function convertToMetricBase(quantity, unit) {
|
|
2172
|
+
const definition = UNIT_DEFINITIONS[unit];
|
|
2173
|
+
const quantityInMetricBase = quantity * definition.toMetricBase;
|
|
2174
|
+
return {
|
|
2175
|
+
quantity: quantityInMetricBase,
|
|
2176
|
+
baseUnit: definition.metricBaseUnit,
|
|
2177
|
+
definition
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/conversion/convertLineItem.ts
|
|
2182
|
+
var UnknownUnitError = class extends Error {
|
|
2183
|
+
constructor(unit) {
|
|
2184
|
+
super(`Unknown unit "${unit}".`);
|
|
2185
|
+
this.unit = unit;
|
|
2186
|
+
this.name = "UnknownUnitError";
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
var UnsupportedConversionError = class extends Error {
|
|
2190
|
+
constructor(unit, mode) {
|
|
2191
|
+
super(`Cannot convert unit "${unit}" in ${mode} mode.`);
|
|
2192
|
+
this.unit = unit;
|
|
2193
|
+
this.mode = mode;
|
|
2194
|
+
this.name = "UnsupportedConversionError";
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
var MissingEquivalencyError = class extends Error {
|
|
2198
|
+
constructor(ingredient, unit) {
|
|
2199
|
+
super(
|
|
2200
|
+
`No volume to mass equivalency for "${ingredient}" (${unit}).`
|
|
2201
|
+
);
|
|
2202
|
+
this.ingredient = ingredient;
|
|
2203
|
+
this.unit = unit;
|
|
2204
|
+
this.name = "MissingEquivalencyError";
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
var VOLUME_TO_MASS_EQUIV_G_PER_UNIT = {
|
|
2208
|
+
flour: {
|
|
2209
|
+
cup: 120
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
var DEFAULT_ROUND_MODE = "sane";
|
|
2213
|
+
function convertLineItemToMetric(item, mode, opts) {
|
|
2214
|
+
var _a2, _b2, _c, _d;
|
|
2215
|
+
const roundMode = (_a2 = opts == null ? void 0 : opts.round) != null ? _a2 : DEFAULT_ROUND_MODE;
|
|
2216
|
+
const normalizedUnit = normalizeUnitToken(item.unit);
|
|
2217
|
+
if (!normalizedUnit) {
|
|
2218
|
+
if (!item.unit || item.unit.trim() === "") {
|
|
2219
|
+
return item;
|
|
2220
|
+
}
|
|
2221
|
+
throw new UnknownUnitError(item.unit);
|
|
2222
|
+
}
|
|
2223
|
+
const definition = UNIT_DEFINITIONS[normalizedUnit];
|
|
2224
|
+
if (definition.dimension === "count") {
|
|
2225
|
+
return item;
|
|
2226
|
+
}
|
|
2227
|
+
if (mode === "volume") {
|
|
2228
|
+
if (definition.dimension !== "volume") {
|
|
2229
|
+
throw new UnsupportedConversionError((_b2 = item.unit) != null ? _b2 : "", mode);
|
|
2230
|
+
}
|
|
2231
|
+
const { quantity, unit } = finalizeMetricVolume(
|
|
2232
|
+
convertToMetricBase(item.quantity, normalizedUnit).quantity,
|
|
2233
|
+
roundMode
|
|
2234
|
+
);
|
|
2235
|
+
return {
|
|
2236
|
+
...item,
|
|
2237
|
+
quantity,
|
|
2238
|
+
unit
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
if (definition.dimension === "mass") {
|
|
2242
|
+
const { quantity, unit } = finalizeMetricMass(
|
|
2243
|
+
convertToMetricBase(item.quantity, normalizedUnit).quantity,
|
|
2244
|
+
roundMode
|
|
2245
|
+
);
|
|
2246
|
+
return {
|
|
2247
|
+
...item,
|
|
2248
|
+
quantity,
|
|
2249
|
+
unit
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
if (definition.dimension !== "volume") {
|
|
2253
|
+
throw new UnsupportedConversionError((_c = item.unit) != null ? _c : "", mode);
|
|
2254
|
+
}
|
|
2255
|
+
const gramsPerUnit = lookupEquivalency(
|
|
2256
|
+
item.ingredient,
|
|
2257
|
+
normalizedUnit
|
|
2258
|
+
);
|
|
2259
|
+
if (!gramsPerUnit) {
|
|
2260
|
+
throw new MissingEquivalencyError(item.ingredient, (_d = item.unit) != null ? _d : "");
|
|
2261
|
+
}
|
|
2262
|
+
const grams = item.quantity * gramsPerUnit;
|
|
2263
|
+
const massResult = finalizeMetricMass(grams, roundMode);
|
|
2264
|
+
return {
|
|
2265
|
+
...item,
|
|
2266
|
+
quantity: massResult.quantity,
|
|
2267
|
+
unit: massResult.unit,
|
|
2268
|
+
notes: `Converted using ${gramsPerUnit}g per ${normalizedUnit} for ${item.ingredient}.`
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function finalizeMetricVolume(milliliters, roundMode) {
|
|
2272
|
+
if (roundMode === "none") {
|
|
2273
|
+
return milliliters >= 1e3 ? { quantity: milliliters / 1e3, unit: "l" } : { quantity: milliliters, unit: "ml" };
|
|
2274
|
+
}
|
|
2275
|
+
const roundedMl = roundMilliliters(milliliters);
|
|
2276
|
+
if (roundedMl >= 1e3) {
|
|
2277
|
+
const liters = roundedMl / 1e3;
|
|
2278
|
+
return {
|
|
2279
|
+
quantity: roundLargeMetric(liters),
|
|
2280
|
+
unit: "l"
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
return { quantity: roundedMl, unit: "ml" };
|
|
2284
|
+
}
|
|
2285
|
+
function finalizeMetricMass(grams, roundMode) {
|
|
2286
|
+
if (roundMode === "none") {
|
|
2287
|
+
return grams >= 1e3 ? { quantity: grams / 1e3, unit: "kg" } : { quantity: grams, unit: "g" };
|
|
2288
|
+
}
|
|
2289
|
+
const roundedGrams = roundGrams(grams);
|
|
2290
|
+
if (roundedGrams >= 1e3) {
|
|
2291
|
+
const kilograms = roundedGrams / 1e3;
|
|
2292
|
+
return {
|
|
2293
|
+
quantity: roundLargeMetric(kilograms),
|
|
2294
|
+
unit: "kg"
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
return { quantity: roundedGrams, unit: "g" };
|
|
2298
|
+
}
|
|
2299
|
+
function roundGrams(value) {
|
|
2300
|
+
if (value < 1e3) {
|
|
2301
|
+
return Math.round(value);
|
|
2302
|
+
}
|
|
2303
|
+
return Math.round(value / 5) * 5;
|
|
2304
|
+
}
|
|
2305
|
+
function roundMilliliters(value) {
|
|
2306
|
+
if (value < 1e3) {
|
|
2307
|
+
return Math.round(value);
|
|
2308
|
+
}
|
|
2309
|
+
return Math.round(value / 10) * 10;
|
|
2310
|
+
}
|
|
2311
|
+
function roundLargeMetric(value) {
|
|
2312
|
+
return Math.round(value * 100) / 100;
|
|
2313
|
+
}
|
|
2314
|
+
function lookupEquivalency(ingredient, unit) {
|
|
2315
|
+
var _a2;
|
|
2316
|
+
const key = ingredient.trim().toLowerCase();
|
|
2317
|
+
return (_a2 = VOLUME_TO_MASS_EQUIV_G_PER_UNIT[key]) == null ? void 0 : _a2[unit];
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// src/mise-en-place/index.ts
|
|
2321
|
+
function miseEnPlace(ingredients) {
|
|
2322
|
+
const list = Array.isArray(ingredients) ? ingredients : [];
|
|
2323
|
+
const prepGroups = /* @__PURE__ */ new Map();
|
|
2324
|
+
const stateGroups = /* @__PURE__ */ new Map();
|
|
2325
|
+
let measureTask;
|
|
2326
|
+
let otherTask;
|
|
2327
|
+
const ungrouped = [];
|
|
2328
|
+
for (const ingredient of list) {
|
|
2329
|
+
if (!ingredient || typeof ingredient !== "object") continue;
|
|
2330
|
+
const label = deriveIngredientLabel(ingredient);
|
|
2331
|
+
const quantity = normalizeQuantity(ingredient.quantity);
|
|
2332
|
+
const baseNotes = toDisplayString(ingredient.notes);
|
|
2333
|
+
const prepNotes = toDisplayString(ingredient.prep);
|
|
2334
|
+
const isOptional = typeof ingredient.optional === "boolean" ? ingredient.optional : void 0;
|
|
2335
|
+
const buildItem = (extraNotes) => {
|
|
2336
|
+
const item = {
|
|
2337
|
+
ingredient: label
|
|
2338
|
+
};
|
|
2339
|
+
if (quantity) {
|
|
2340
|
+
item.quantity = { ...quantity };
|
|
2341
|
+
}
|
|
2342
|
+
if (typeof isOptional === "boolean") {
|
|
2343
|
+
item.optional = isOptional;
|
|
2344
|
+
}
|
|
2345
|
+
const notes = combineNotes(extraNotes, baseNotes);
|
|
2346
|
+
if (notes) {
|
|
2347
|
+
item.notes = notes;
|
|
2348
|
+
}
|
|
2349
|
+
return item;
|
|
2350
|
+
};
|
|
2351
|
+
let addedToTask = false;
|
|
2352
|
+
let hasPrepGrouping = false;
|
|
2353
|
+
const prepActionKeys = extractNormalizedList(ingredient.prepActions);
|
|
2354
|
+
if (prepActionKeys.length > 0) {
|
|
2355
|
+
hasPrepGrouping = true;
|
|
2356
|
+
for (const actionKey of prepActionKeys) {
|
|
2357
|
+
const task = ensureGroup(prepGroups, actionKey, () => ({
|
|
2358
|
+
category: "prep",
|
|
2359
|
+
action: actionKey,
|
|
2360
|
+
items: []
|
|
2361
|
+
}));
|
|
2362
|
+
task.items.push(buildItem());
|
|
2363
|
+
addedToTask = true;
|
|
2364
|
+
}
|
|
2365
|
+
} else {
|
|
2366
|
+
const singleActionKey = normalizeKey(ingredient.prepAction);
|
|
2367
|
+
if (singleActionKey) {
|
|
2368
|
+
hasPrepGrouping = true;
|
|
2369
|
+
const task = ensureGroup(prepGroups, singleActionKey, () => ({
|
|
2370
|
+
category: "prep",
|
|
2371
|
+
action: singleActionKey,
|
|
2372
|
+
items: []
|
|
2373
|
+
}));
|
|
2374
|
+
task.items.push(buildItem());
|
|
2375
|
+
addedToTask = true;
|
|
2376
|
+
} else if (prepNotes) {
|
|
2377
|
+
otherTask = otherTask != null ? otherTask : { category: "other", items: [] };
|
|
2378
|
+
otherTask.items.push(buildItem(prepNotes));
|
|
2379
|
+
addedToTask = true;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
const formKey = normalizeKey(ingredient.form);
|
|
2383
|
+
const hasStateGrouping = Boolean(formKey);
|
|
2384
|
+
if (formKey) {
|
|
2385
|
+
const task = ensureGroup(stateGroups, formKey, () => ({
|
|
2386
|
+
category: "state",
|
|
2387
|
+
form: formKey,
|
|
2388
|
+
items: []
|
|
2389
|
+
}));
|
|
2390
|
+
task.items.push(buildItem());
|
|
2391
|
+
addedToTask = true;
|
|
2392
|
+
}
|
|
2393
|
+
const shouldMeasure = Boolean(quantity) && !hasPrepGrouping && !hasStateGrouping;
|
|
2394
|
+
if (shouldMeasure) {
|
|
2395
|
+
measureTask = measureTask != null ? measureTask : { category: "measure", items: [] };
|
|
2396
|
+
measureTask.items.push(buildItem());
|
|
2397
|
+
addedToTask = true;
|
|
2398
|
+
}
|
|
2399
|
+
if (!addedToTask) {
|
|
2400
|
+
ungrouped.push(ingredient);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
const tasks = [
|
|
2404
|
+
...Array.from(prepGroups.values()).sort((a, b) => localeCompare(a.action, b.action)),
|
|
2405
|
+
...Array.from(stateGroups.values()).sort((a, b) => localeCompare(a.form, b.form))
|
|
2406
|
+
];
|
|
2407
|
+
if (measureTask) {
|
|
2408
|
+
tasks.push(measureTask);
|
|
2409
|
+
}
|
|
2410
|
+
if (otherTask) {
|
|
2411
|
+
tasks.push(otherTask);
|
|
2412
|
+
}
|
|
2413
|
+
return { tasks, ungrouped };
|
|
2414
|
+
}
|
|
2415
|
+
function deriveIngredientLabel(ingredient) {
|
|
2416
|
+
var _a2, _b2, _c;
|
|
2417
|
+
return (_c = (_b2 = (_a2 = toDisplayString(ingredient.name)) != null ? _a2 : toDisplayString(ingredient.item)) != null ? _b2 : toDisplayString(ingredient.id)) != null ? _c : "ingredient";
|
|
2418
|
+
}
|
|
2419
|
+
function extractNormalizedList(values) {
|
|
2420
|
+
if (!Array.isArray(values)) {
|
|
2421
|
+
return [];
|
|
2422
|
+
}
|
|
2423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2424
|
+
const result = [];
|
|
2425
|
+
for (const value of values) {
|
|
2426
|
+
const key = normalizeKey(value);
|
|
2427
|
+
if (key && !seen.has(key)) {
|
|
2428
|
+
seen.add(key);
|
|
2429
|
+
result.push(key);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return result;
|
|
2433
|
+
}
|
|
2434
|
+
function normalizeKey(value) {
|
|
2435
|
+
if (typeof value !== "string") {
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2438
|
+
const trimmed = value.trim().toLowerCase();
|
|
2439
|
+
return trimmed || null;
|
|
2440
|
+
}
|
|
2441
|
+
function toDisplayString(value) {
|
|
2442
|
+
if (typeof value !== "string") {
|
|
2443
|
+
return void 0;
|
|
2444
|
+
}
|
|
2445
|
+
const trimmed = value.trim();
|
|
2446
|
+
return trimmed || void 0;
|
|
2447
|
+
}
|
|
2448
|
+
function combineNotes(...notes) {
|
|
2449
|
+
const cleaned = notes.map((note) => toDisplayString(note != null ? note : void 0)).filter(Boolean);
|
|
2450
|
+
if (cleaned.length === 0) {
|
|
2451
|
+
return void 0;
|
|
2452
|
+
}
|
|
2453
|
+
return cleaned.join(" | ");
|
|
2454
|
+
}
|
|
2455
|
+
function normalizeQuantity(quantity) {
|
|
2456
|
+
if (!quantity || typeof quantity !== "object") {
|
|
2457
|
+
return void 0;
|
|
2458
|
+
}
|
|
2459
|
+
const amount = quantity.amount;
|
|
2460
|
+
if (typeof amount !== "number" || Number.isNaN(amount)) {
|
|
2461
|
+
return void 0;
|
|
2462
|
+
}
|
|
2463
|
+
const normalized = { amount };
|
|
2464
|
+
if ("unit" in quantity) {
|
|
2465
|
+
const unit = quantity.unit;
|
|
2466
|
+
if (typeof unit === "string") {
|
|
2467
|
+
const trimmed = unit.trim();
|
|
2468
|
+
if (trimmed) {
|
|
2469
|
+
normalized.unit = trimmed;
|
|
2470
|
+
}
|
|
2471
|
+
} else if (unit === null) {
|
|
2472
|
+
normalized.unit = null;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return normalized;
|
|
2476
|
+
}
|
|
2477
|
+
function ensureGroup(map, key, factory) {
|
|
2478
|
+
let task = map.get(key);
|
|
2479
|
+
if (!task) {
|
|
2480
|
+
task = factory();
|
|
2481
|
+
map.set(key, task);
|
|
2482
|
+
}
|
|
2483
|
+
return task;
|
|
2484
|
+
}
|
|
2485
|
+
function localeCompare(left, right) {
|
|
2486
|
+
return (left != null ? left : "").localeCompare(right != null ? right : "");
|
|
2487
|
+
}
|
|
2030
2488
|
|
|
2489
|
+
exports.MissingEquivalencyError = MissingEquivalencyError;
|
|
2031
2490
|
exports.SOUSTACK_SPEC_VERSION = SOUSTACK_SPEC_VERSION;
|
|
2491
|
+
exports.UnknownUnitError = UnknownUnitError;
|
|
2492
|
+
exports.UnsupportedConversionError = UnsupportedConversionError;
|
|
2493
|
+
exports.convertLineItemToMetric = convertLineItemToMetric;
|
|
2032
2494
|
exports.detectProfiles = detectProfiles;
|
|
2033
2495
|
exports.extractSchemaOrgRecipeFromHTML = extractSchemaOrgRecipeFromHTML;
|
|
2034
2496
|
exports.fromSchemaOrg = fromSchemaOrg;
|
|
2497
|
+
exports.miseEnPlace = miseEnPlace;
|
|
2035
2498
|
exports.scaleRecipe = scaleRecipe;
|
|
2036
2499
|
exports.toSchemaOrg = toSchemaOrg;
|
|
2037
2500
|
exports.validateRecipe = validateRecipe;
|