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