soustack 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Soustack Recipe Schema v0.1
3
3
  * A portable, scalable, interoperable recipe format.
4
4
  */
5
- interface Recipe {
5
+ interface SoustackRecipe {
6
6
  /** Unique identifier (slug or UUID) */
7
7
  id?: string;
8
8
  /** The title of the recipe */
@@ -14,8 +14,8 @@ interface Recipe {
14
14
  category?: string;
15
15
  /** Additional tags for filtering */
16
16
  tags?: string[];
17
- /** URL to recipe image */
18
- image?: string;
17
+ /** URL(s) to recipe image(s) */
18
+ image?: string | string[];
19
19
  /** ISO 8601 date string */
20
20
  dateAdded?: string;
21
21
  /** Last updated timestamp */
@@ -30,6 +30,7 @@ interface Recipe {
30
30
  substitutions?: Substitution[];
31
31
  nutrition?: NutritionFacts;
32
32
  }
33
+ type Recipe = SoustackRecipe;
33
34
  interface Source {
34
35
  author?: string;
35
36
  url?: string;
@@ -147,7 +148,7 @@ interface InstructionSubsection {
147
148
  subsection: string;
148
149
  items: (string | Instruction)[];
149
150
  }
150
- interface Instruction {
151
+ interface SoustackInstruction {
151
152
  id?: string;
152
153
  text: string;
153
154
  destination?: string;
@@ -156,7 +157,10 @@ interface Instruction {
156
157
  /** IDs of ingredients used in this step */
157
158
  inputs?: string[];
158
159
  timing?: StepTiming;
160
+ /** Optional image URL for this instruction */
161
+ image?: string;
159
162
  }
163
+ type Instruction = SoustackInstruction;
160
164
  interface StepTiming {
161
165
  duration: number;
162
166
  type: "active" | "passive";
@@ -275,6 +279,9 @@ interface SchemaOrgImageObject {
275
279
  '@type'?: string;
276
280
  url?: string;
277
281
  contentUrl?: string;
282
+ width?: number;
283
+ height?: number;
284
+ [key: string]: unknown;
278
285
  }
279
286
  type SchemaOrgImage = string | SchemaOrgImageObject | Array<string | SchemaOrgImageObject>;
280
287
  interface SchemaOrgYield {
@@ -287,7 +294,7 @@ interface HowToStep$1 {
287
294
  text?: string;
288
295
  name?: string;
289
296
  url?: string;
290
- image?: string;
297
+ image?: SchemaOrgImage;
291
298
  }
292
299
  interface HowToSection {
293
300
  '@type': 'HowToSection';
@@ -425,4 +432,12 @@ declare function normalizeYield(text: string): string;
425
432
  declare function parseYield(text: string): ParsedYield | null;
426
433
  declare function formatYield(value: ParsedYield): string;
427
434
 
428
- export { type Alternative, type ComputedIngredient, type ComputedInstruction, type ComputedRecipe, type Equipment, type FrozenStorageMethod, type Ingredient, type IngredientItem, type IngredientSubsection, type Instruction, type InstructionItem, type InstructionSubsection, type MakeAheadComponent, type NutritionFacts, type ParsedIngredient, type ParsedYield, type Quantity, type Recipe, type Scaling, type ScalingBakersPercentage, type ScalingBase, type ScalingDiscrete, type ScalingFixed, type ScalingLinear, type ScalingProportional, type SchemaOrgRecipe, type SimpleTime, type Source, type StepTiming, type Storage, type StorageMethod, type StructuredTime, type Substitution, type Time, type Yield, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, formatDuration, formatYield, fromSchemaOrg, normalizeIngredientInput, normalizeYield, parseDuration, parseHumanDuration, parseIngredient, parseIngredientLine, parseIngredients, parseYield, scaleRecipe, scrapeRecipe, smartParseDuration, toSchemaOrg, validateRecipe };
435
+ /**
436
+ * Normalize Schema.org image formats to Soustack format.
437
+ * - String values pass through
438
+ * - Arrays collapse to string or string[] after URL extraction
439
+ * - ImageObjects extract their url/contentUrl
440
+ */
441
+ declare function normalizeImage(image: SchemaOrgImage | undefined | null): string | string[] | undefined;
442
+
443
+ export { type Alternative, type ComputedIngredient, type ComputedInstruction, type ComputedRecipe, type Equipment, type FrozenStorageMethod, type Ingredient, type IngredientItem, type IngredientSubsection, type Instruction, type InstructionItem, type InstructionSubsection, type MakeAheadComponent, type NutritionFacts, type ParsedIngredient, type ParsedYield, type Quantity, type Recipe, type Scaling, type ScalingBakersPercentage, type ScalingBase, type ScalingDiscrete, type ScalingFixed, type ScalingLinear, type ScalingProportional, type SchemaOrgRecipe, type SimpleTime, type Source, type SoustackInstruction, type SoustackRecipe, type StepTiming, type Storage, type StorageMethod, type StructuredTime, type Substitution, type Time, type Yield, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, formatDuration, formatYield, fromSchemaOrg, normalizeImage, normalizeIngredientInput, normalizeYield, parseDuration, parseHumanDuration, parseIngredient, parseIngredientLine, parseIngredients, parseYield, scaleRecipe, scrapeRecipe, smartParseDuration, toSchemaOrg, validateRecipe };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Soustack Recipe Schema v0.1
3
3
  * A portable, scalable, interoperable recipe format.
4
4
  */
5
- interface Recipe {
5
+ interface SoustackRecipe {
6
6
  /** Unique identifier (slug or UUID) */
7
7
  id?: string;
8
8
  /** The title of the recipe */
@@ -14,8 +14,8 @@ interface Recipe {
14
14
  category?: string;
15
15
  /** Additional tags for filtering */
16
16
  tags?: string[];
17
- /** URL to recipe image */
18
- image?: string;
17
+ /** URL(s) to recipe image(s) */
18
+ image?: string | string[];
19
19
  /** ISO 8601 date string */
20
20
  dateAdded?: string;
21
21
  /** Last updated timestamp */
@@ -30,6 +30,7 @@ interface Recipe {
30
30
  substitutions?: Substitution[];
31
31
  nutrition?: NutritionFacts;
32
32
  }
33
+ type Recipe = SoustackRecipe;
33
34
  interface Source {
34
35
  author?: string;
35
36
  url?: string;
@@ -147,7 +148,7 @@ interface InstructionSubsection {
147
148
  subsection: string;
148
149
  items: (string | Instruction)[];
149
150
  }
150
- interface Instruction {
151
+ interface SoustackInstruction {
151
152
  id?: string;
152
153
  text: string;
153
154
  destination?: string;
@@ -156,7 +157,10 @@ interface Instruction {
156
157
  /** IDs of ingredients used in this step */
157
158
  inputs?: string[];
158
159
  timing?: StepTiming;
160
+ /** Optional image URL for this instruction */
161
+ image?: string;
159
162
  }
163
+ type Instruction = SoustackInstruction;
160
164
  interface StepTiming {
161
165
  duration: number;
162
166
  type: "active" | "passive";
@@ -275,6 +279,9 @@ interface SchemaOrgImageObject {
275
279
  '@type'?: string;
276
280
  url?: string;
277
281
  contentUrl?: string;
282
+ width?: number;
283
+ height?: number;
284
+ [key: string]: unknown;
278
285
  }
279
286
  type SchemaOrgImage = string | SchemaOrgImageObject | Array<string | SchemaOrgImageObject>;
280
287
  interface SchemaOrgYield {
@@ -287,7 +294,7 @@ interface HowToStep$1 {
287
294
  text?: string;
288
295
  name?: string;
289
296
  url?: string;
290
- image?: string;
297
+ image?: SchemaOrgImage;
291
298
  }
292
299
  interface HowToSection {
293
300
  '@type': 'HowToSection';
@@ -425,4 +432,12 @@ declare function normalizeYield(text: string): string;
425
432
  declare function parseYield(text: string): ParsedYield | null;
426
433
  declare function formatYield(value: ParsedYield): string;
427
434
 
428
- export { type Alternative, type ComputedIngredient, type ComputedInstruction, type ComputedRecipe, type Equipment, type FrozenStorageMethod, type Ingredient, type IngredientItem, type IngredientSubsection, type Instruction, type InstructionItem, type InstructionSubsection, type MakeAheadComponent, type NutritionFacts, type ParsedIngredient, type ParsedYield, type Quantity, type Recipe, type Scaling, type ScalingBakersPercentage, type ScalingBase, type ScalingDiscrete, type ScalingFixed, type ScalingLinear, type ScalingProportional, type SchemaOrgRecipe, type SimpleTime, type Source, type StepTiming, type Storage, type StorageMethod, type StructuredTime, type Substitution, type Time, type Yield, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, formatDuration, formatYield, fromSchemaOrg, normalizeIngredientInput, normalizeYield, parseDuration, parseHumanDuration, parseIngredient, parseIngredientLine, parseIngredients, parseYield, scaleRecipe, scrapeRecipe, smartParseDuration, toSchemaOrg, validateRecipe };
435
+ /**
436
+ * Normalize Schema.org image formats to Soustack format.
437
+ * - String values pass through
438
+ * - Arrays collapse to string or string[] after URL extraction
439
+ * - ImageObjects extract their url/contentUrl
440
+ */
441
+ declare function normalizeImage(image: SchemaOrgImage | undefined | null): string | string[] | undefined;
442
+
443
+ export { type Alternative, type ComputedIngredient, type ComputedInstruction, type ComputedRecipe, type Equipment, type FrozenStorageMethod, type Ingredient, type IngredientItem, type IngredientSubsection, type Instruction, type InstructionItem, type InstructionSubsection, type MakeAheadComponent, type NutritionFacts, type ParsedIngredient, type ParsedYield, type Quantity, type Recipe, type Scaling, type ScalingBakersPercentage, type ScalingBase, type ScalingDiscrete, type ScalingFixed, type ScalingLinear, type ScalingProportional, type SchemaOrgRecipe, type SimpleTime, type Source, type SoustackInstruction, type SoustackRecipe, type StepTiming, type Storage, type StorageMethod, type StructuredTime, type Substitution, type Time, type Yield, extractRecipeFromHTML, extractSchemaOrgRecipeFromHTML, formatDuration, formatYield, fromSchemaOrg, normalizeImage, normalizeIngredientInput, normalizeYield, parseDuration, parseHumanDuration, parseIngredient, parseIngredientLine, parseIngredients, parseYield, scaleRecipe, scrapeRecipe, smartParseDuration, toSchemaOrg, validateRecipe };
package/dist/index.js CHANGED
@@ -140,8 +140,8 @@ function flattenInstructions(items) {
140
140
  // src/schema.json
141
141
  var schema_default = {
142
142
  $schema: "http://json-schema.org/draft-07/schema#",
143
- $id: "http://soustack.org/schema/v0.1",
144
- title: "Soustack Recipe Schema v0.1",
143
+ $id: "http://soustack.org/schema/v0.2",
144
+ title: "Soustack Recipe Schema v0.2",
145
145
  description: "A portable, scalable, interoperable recipe format.",
146
146
  type: "object",
147
147
  required: ["name", "ingredients", "instructions"],
@@ -171,8 +171,21 @@ var schema_default = {
171
171
  items: { type: "string" }
172
172
  },
173
173
  image: {
174
- type: "string",
175
- format: "uri"
174
+ description: "Recipe-level hero image(s)",
175
+ anyOf: [
176
+ {
177
+ type: "string",
178
+ format: "uri"
179
+ },
180
+ {
181
+ type: "array",
182
+ minItems: 1,
183
+ items: {
184
+ type: "string",
185
+ format: "uri"
186
+ }
187
+ }
188
+ ]
176
189
  },
177
190
  dateAdded: {
178
191
  type: "string",
@@ -337,6 +350,11 @@ var schema_default = {
337
350
  properties: {
338
351
  id: { type: "string" },
339
352
  text: { type: "string" },
353
+ image: {
354
+ type: "string",
355
+ format: "uri",
356
+ description: "Optional image that illustrates this instruction"
357
+ },
340
358
  destination: { type: "string" },
341
359
  dependsOn: {
342
360
  type: "array",
@@ -1234,6 +1252,40 @@ function smartParseDuration(input) {
1234
1252
  return parseHumanDuration(input);
1235
1253
  }
1236
1254
 
1255
+ // src/utils/image.ts
1256
+ function normalizeImage(image) {
1257
+ if (!image) {
1258
+ return void 0;
1259
+ }
1260
+ if (typeof image === "string") {
1261
+ const trimmed = image.trim();
1262
+ return trimmed || void 0;
1263
+ }
1264
+ if (Array.isArray(image)) {
1265
+ const urls = image.map((entry) => typeof entry === "string" ? entry.trim() : extractUrl(entry)).filter((url) => typeof url === "string" && Boolean(url));
1266
+ if (urls.length === 0) {
1267
+ return void 0;
1268
+ }
1269
+ if (urls.length === 1) {
1270
+ return urls[0];
1271
+ }
1272
+ return urls;
1273
+ }
1274
+ return extractUrl(image);
1275
+ }
1276
+ function extractUrl(value) {
1277
+ if (!value || typeof value !== "object") {
1278
+ return void 0;
1279
+ }
1280
+ const record = value;
1281
+ const candidate = typeof record.url === "string" ? record.url : typeof record.contentUrl === "string" ? record.contentUrl : void 0;
1282
+ if (!candidate) {
1283
+ return void 0;
1284
+ }
1285
+ const trimmed = candidate.trim();
1286
+ return trimmed || void 0;
1287
+ }
1288
+
1237
1289
  // src/fromSchemaOrg.ts
1238
1290
  function fromSchemaOrg(input) {
1239
1291
  const recipeNode = extractRecipeNode(input);
@@ -1246,13 +1298,12 @@ function fromSchemaOrg(input) {
1246
1298
  const recipeYield = parseYield(recipeNode.recipeYield);
1247
1299
  const tags = collectTags(recipeNode.recipeCuisine, recipeNode.keywords);
1248
1300
  const category = extractFirst(recipeNode.recipeCategory);
1249
- const image = convertImage(recipeNode.image);
1250
1301
  const source = convertSource(recipeNode);
1251
1302
  const nutrition = recipeNode.nutrition && typeof recipeNode.nutrition === "object" ? recipeNode.nutrition : void 0;
1252
1303
  return {
1253
1304
  name: recipeNode.name.trim(),
1254
1305
  description: recipeNode.description?.trim() || void 0,
1255
- image,
1306
+ image: normalizeImage(recipeNode.image),
1256
1307
  category,
1257
1308
  tags: tags.length ? tags : void 0,
1258
1309
  source,
@@ -1335,9 +1386,9 @@ function convertInstructions(value) {
1335
1386
  continue;
1336
1387
  }
1337
1388
  if (isHowToStep(entry)) {
1338
- const text = extractInstructionText(entry);
1339
- if (text) {
1340
- result.push(text);
1389
+ const parsed = convertHowToStep(entry);
1390
+ if (parsed) {
1391
+ result.push(parsed);
1341
1392
  }
1342
1393
  }
1343
1394
  }
@@ -1355,9 +1406,9 @@ function extractSectionItems(items = []) {
1355
1406
  continue;
1356
1407
  }
1357
1408
  if (isHowToStep(item)) {
1358
- const text = extractInstructionText(item);
1359
- if (text) {
1360
- result.push(text);
1409
+ const parsed = convertHowToStep(item);
1410
+ if (parsed) {
1411
+ result.push(parsed);
1361
1412
  }
1362
1413
  continue;
1363
1414
  }
@@ -1371,6 +1422,17 @@ function extractInstructionText(value) {
1371
1422
  const text = typeof value.text === "string" ? value.text : value.name;
1372
1423
  return typeof text === "string" ? text.trim() || void 0 : void 0;
1373
1424
  }
1425
+ function convertHowToStep(step) {
1426
+ const text = extractInstructionText(step);
1427
+ if (!text) {
1428
+ return void 0;
1429
+ }
1430
+ const normalizedImage = normalizeImage(step.image);
1431
+ if (typeof normalizedImage === "string") {
1432
+ return { text, image: normalizedImage };
1433
+ }
1434
+ return text;
1435
+ }
1374
1436
  function isHowToStep(value) {
1375
1437
  return Boolean(value) && typeof value === "object" && value["@type"] === "HowToStep";
1376
1438
  }
@@ -1412,26 +1474,6 @@ function extractFirst(value) {
1412
1474
  const arr = flattenStrings(value);
1413
1475
  return arr.length ? arr[0] : void 0;
1414
1476
  }
1415
- function convertImage(value) {
1416
- if (!value) return void 0;
1417
- if (typeof value === "string") {
1418
- return value;
1419
- }
1420
- if (Array.isArray(value)) {
1421
- for (const item of value) {
1422
- const url = typeof item === "string" ? item : extractImageUrl(item);
1423
- if (url) return url;
1424
- }
1425
- return void 0;
1426
- }
1427
- return extractImageUrl(value);
1428
- }
1429
- function extractImageUrl(value) {
1430
- if (!value || typeof value !== "object") return void 0;
1431
- const record = value;
1432
- const candidate = typeof record.url === "string" ? record.url : typeof record.contentUrl === "string" ? record.contentUrl : void 0;
1433
- return candidate?.trim() || void 0;
1434
- }
1435
1477
  function convertSource(recipe) {
1436
1478
  const author = extractEntityName(recipe.author);
1437
1479
  const publisher = extractEntityName(recipe.publisher);
@@ -1527,7 +1569,7 @@ function convertInstruction(entry) {
1527
1569
  return createHowToStep(entry);
1528
1570
  }
1529
1571
  if ("subsection" in entry) {
1530
- const steps = entry.items.map((item) => typeof item === "string" ? createHowToStep(item) : createHowToStep(item.text)).filter((step) => Boolean(step));
1572
+ const steps = entry.items.map((item) => createHowToStep(item)).filter((step) => Boolean(step));
1531
1573
  if (!steps.length) {
1532
1574
  return null;
1533
1575
  }
@@ -1538,18 +1580,34 @@ function convertInstruction(entry) {
1538
1580
  };
1539
1581
  }
1540
1582
  if ("text" in entry) {
1541
- return createHowToStep(entry.text);
1583
+ return createHowToStep(entry);
1542
1584
  }
1543
1585
  return createHowToStep(String(entry));
1544
1586
  }
1545
- function createHowToStep(text) {
1546
- if (!text) return null;
1547
- const trimmed = text.trim();
1548
- if (!trimmed) return null;
1549
- return {
1587
+ function createHowToStep(entry) {
1588
+ if (!entry) return null;
1589
+ if (typeof entry === "string") {
1590
+ const trimmed2 = entry.trim();
1591
+ if (!trimmed2) {
1592
+ return null;
1593
+ }
1594
+ return {
1595
+ "@type": "HowToStep",
1596
+ text: trimmed2
1597
+ };
1598
+ }
1599
+ const trimmed = entry.text?.trim();
1600
+ if (!trimmed) {
1601
+ return null;
1602
+ }
1603
+ const step = {
1550
1604
  "@type": "HowToStep",
1551
1605
  text: trimmed
1552
1606
  };
1607
+ if (entry.image) {
1608
+ step.image = entry.image;
1609
+ }
1610
+ return step;
1553
1611
  }
1554
1612
  function convertTime2(time) {
1555
1613
  if (!time) {
@@ -1712,7 +1770,7 @@ async function fetchPage(url, options = {}) {
1712
1770
  };
1713
1771
  const response = await resolvedFetch(url, requestInit);
1714
1772
  clearTimeout(timeoutId);
1715
- if (response && (typeof process === "undefined" || process.env.NODE_ENV !== "test")) {
1773
+ if (response && typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
1716
1774
  try {
1717
1775
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
1718
1776
  if (globalFetch) {
@@ -1730,7 +1788,7 @@ async function fetchPage(url, options = {}) {
1730
1788
  throw error;
1731
1789
  }
1732
1790
  const html = await response.text();
1733
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
1791
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
1734
1792
  try {
1735
1793
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
1736
1794
  if (globalFetch) {
@@ -1990,7 +2048,7 @@ function extractRecipe(html) {
1990
2048
  return extractRecipeBrowser(html);
1991
2049
  }
1992
2050
  const jsonLdRecipe = extractJsonLd(html);
1993
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2051
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
1994
2052
  try {
1995
2053
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
1996
2054
  if (globalFetch) {
@@ -2004,7 +2062,7 @@ function extractRecipe(html) {
2004
2062
  return { recipe: jsonLdRecipe, source: "jsonld" };
2005
2063
  }
2006
2064
  const microdataRecipe = extractMicrodata(html);
2007
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2065
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
2008
2066
  try {
2009
2067
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
2010
2068
  if (globalFetch) {
@@ -2022,7 +2080,7 @@ function extractRecipe(html) {
2022
2080
 
2023
2081
  // src/scraper/index.ts
2024
2082
  async function scrapeRecipe(url, options = {}) {
2025
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2083
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
2026
2084
  try {
2027
2085
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
2028
2086
  if (globalFetch) {
@@ -2033,7 +2091,7 @@ async function scrapeRecipe(url, options = {}) {
2033
2091
  }
2034
2092
  }
2035
2093
  const html = await fetchPage(url, options);
2036
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2094
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
2037
2095
  try {
2038
2096
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
2039
2097
  if (globalFetch) {
@@ -2044,7 +2102,7 @@ async function scrapeRecipe(url, options = {}) {
2044
2102
  }
2045
2103
  }
2046
2104
  const { recipe } = extractRecipe(html);
2047
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2105
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
2048
2106
  try {
2049
2107
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
2050
2108
  if (globalFetch) {
@@ -2058,7 +2116,7 @@ async function scrapeRecipe(url, options = {}) {
2058
2116
  throw new Error("No Schema.org recipe data found in page");
2059
2117
  }
2060
2118
  const soustackRecipe = fromSchemaOrg(recipe);
2061
- if (typeof process === "undefined" || process.env.NODE_ENV !== "test") {
2119
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
2062
2120
  try {
2063
2121
  const globalFetch = typeof globalThis !== "undefined" && typeof globalThis.fetch !== "undefined" ? globalThis.fetch : null;
2064
2122
  if (globalFetch) {
@@ -2336,6 +2394,7 @@ exports.extractSchemaOrgRecipeFromHTML = extractSchemaOrgRecipeFromHTML;
2336
2394
  exports.formatDuration = formatDuration;
2337
2395
  exports.formatYield = formatYield2;
2338
2396
  exports.fromSchemaOrg = fromSchemaOrg;
2397
+ exports.normalizeImage = normalizeImage;
2339
2398
  exports.normalizeIngredientInput = normalizeIngredientInput;
2340
2399
  exports.normalizeYield = normalizeYield;
2341
2400
  exports.parseDuration = parseDuration;