@recipe-book/core 0.1.0 → 0.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VWVIBCTG.cjs → chunk-D5CV3UF7.cjs} +188 -173
  4. package/dist/chunk-D5CV3UF7.cjs.map +1 -0
  5. package/dist/errors.js.map +1 -1
  6. package/dist/format/number.js +1 -1
  7. package/dist/format/number.js.map +1 -1
  8. package/dist/format/options.js.map +1 -1
  9. package/dist/index.cjs +83 -74
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +35 -19
  12. package/dist/index.d.ts +35 -19
  13. package/dist/ingredient/parse.js +27 -22
  14. package/dist/ingredient/parse.js.map +1 -1
  15. package/dist/issues.js.map +1 -1
  16. package/dist/quantity/amount.js +11 -0
  17. package/dist/quantity/amount.js.map +1 -1
  18. package/dist/quantity/parse.js +58 -81
  19. package/dist/quantity/parse.js.map +1 -1
  20. package/dist/quantity/scale.js +5 -2
  21. package/dist/quantity/scale.js.map +1 -1
  22. package/dist/recipe/parse.js +70 -55
  23. package/dist/recipe/parse.js.map +1 -1
  24. package/dist/recipe/schema.js +8 -6
  25. package/dist/recipe/schema.js.map +1 -1
  26. package/dist/resolve.js +6 -1
  27. package/dist/resolve.js.map +1 -1
  28. package/dist/schema-org/find.js +46 -13
  29. package/dist/schema-org/find.js.map +1 -1
  30. package/dist/schema-org/from-json-ld.js +127 -57
  31. package/dist/schema-org/from-json-ld.js.map +1 -1
  32. package/dist/schema-org/text.js.map +1 -1
  33. package/dist/schema-org/to-json-ld.js +12 -15
  34. package/dist/schema-org/to-json-ld.js.map +1 -1
  35. package/dist/schema-org.cjs +231 -132
  36. package/dist/schema-org.cjs.map +1 -1
  37. package/dist/schema-org.d.cts +32 -19
  38. package/dist/schema-org.d.ts +32 -19
  39. package/dist/steps/temperature.js +13 -13
  40. package/dist/steps/temperature.js.map +1 -1
  41. package/dist/{types-DSMf45H5.d.cts → types-BknM8KOd.d.cts} +4 -3
  42. package/dist/{types-DSMf45H5.d.ts → types-BknM8KOd.d.ts} +4 -3
  43. package/dist/units/convert.js +16 -8
  44. package/dist/units/convert.js.map +1 -1
  45. package/dist/units/temperature.js +7 -1
  46. package/dist/units/temperature.js.map +1 -1
  47. package/dist/validation.js +1 -1
  48. package/dist/validation.js.map +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-VWVIBCTG.cjs.map +0 -1
@@ -1,32 +1,39 @@
1
1
  'use strict';
2
2
 
3
- var chunkVWVIBCTG_cjs = require('./chunk-VWVIBCTG.cjs');
3
+ var chunkD5CV3UF7_cjs = require('./chunk-D5CV3UF7.cjs');
4
4
 
5
5
  // src/schema-org/find.ts
6
6
  var MAX_DEPTH = 32;
7
7
  function locate(documents) {
8
+ const direct = scan(documents, false);
9
+ return direct.recipes.length > 0 ? direct : scan(documents, true);
10
+ }
11
+ function scan(documents, lists) {
8
12
  const recipes = [];
9
13
  const ids = /* @__PURE__ */ new Map();
10
14
  const recipeIds = /* @__PURE__ */ new Map();
11
- const visit = (value, path, mainEntity = false) => {
15
+ const visit = (value, path, reach = "any") => {
12
16
  if (path.length > MAX_DEPTH) {
13
17
  return;
14
18
  }
15
19
  if (Array.isArray(value)) {
16
- value.forEach(
17
- (entry, i) => visit(entry, [...path, i], mainEntity)
18
- );
20
+ for (const [entry, entryPath] of entriesOf(value, path)) {
21
+ visit(entry, entryPath, reach);
22
+ }
19
23
  return;
20
24
  }
21
- const fields = chunkVWVIBCTG_cjs.asRecord(value);
25
+ const fields = chunkD5CV3UF7_cjs.asRecord(value);
22
26
  if (!fields) {
23
27
  return;
24
28
  }
25
- const type = fields["@type"];
26
- const isRecipe = [type].flat().some(
27
- (name) => typeof name === "string" && /(?:^|[/:#])Recipe$/.test(name)
28
- );
29
- if (mainEntity && !isRecipe) {
29
+ const isRecipe = hasType(fields, "Recipe");
30
+ const isList = lists && hasType(fields, "ItemList");
31
+ if (reach !== "any" && !isRecipe) {
32
+ if (isList && reach === "mainEntity") {
33
+ visit(fields.itemListElement, [...path, "itemListElement"], "entry");
34
+ } else if (lists && reach === "entry" && hasType(fields, "ListItem")) {
35
+ visit(fields.item, [...path, "item"], "item");
36
+ }
30
37
  return;
31
38
  }
32
39
  const located = { node: value, path };
@@ -47,7 +54,10 @@ function locate(documents) {
47
54
  }
48
55
  } else {
49
56
  visit(fields["@graph"], [...path, "@graph"]);
50
- visit(fields.mainEntity, [...path, "mainEntity"], true);
57
+ visit(fields.mainEntity, [...path, "mainEntity"], "mainEntity");
58
+ if (isList) {
59
+ visit(fields.itemListElement, [...path, "itemListElement"], "entry");
60
+ }
51
61
  }
52
62
  };
53
63
  for (const document of documents) {
@@ -55,14 +65,36 @@ function locate(documents) {
55
65
  }
56
66
  return { recipes, ids };
57
67
  }
68
+ var TYPES = {
69
+ Recipe: /(?:^|[/:#])Recipe$/,
70
+ ItemList: /(?:^|[/:#])ItemList$/,
71
+ ListItem: /(?:^|[/:#])ListItem$/
72
+ };
73
+ function hasType(node, name) {
74
+ return entriesOf(node["@type"], []).some(
75
+ ([type]) => typeof type === "string" && TYPES[name].test(type)
76
+ );
77
+ }
58
78
  function hasContent(node) {
59
- return Object.keys(node).some((key) => key[0] !== "@");
79
+ return Object.keys(node).some((key) => !key.startsWith("@"));
80
+ }
81
+ function entriesOf(value, path) {
82
+ if (!Array.isArray(value)) {
83
+ return [[value, path]];
84
+ }
85
+ const entries = [];
86
+ for (let i = 0; i < value.length; i++) {
87
+ if (chunkD5CV3UF7_cjs.hasOwn(value, i)) {
88
+ entries.push([value[i], [...path, i]]);
89
+ }
90
+ }
91
+ return entries;
60
92
  }
61
93
  function findRecipeJsonLd(html) {
62
94
  if (typeof html !== "string") {
63
- throw new chunkVWVIBCTG_cjs.RecipeBookError(
95
+ throw new chunkD5CV3UF7_cjs.RecipeBookError(
64
96
  "invalid_argument",
65
- `findRecipeJsonLd: html must be a string, such as await response.text(), received ${chunkVWVIBCTG_cjs.describeValue(html)}.`
97
+ `findRecipeJsonLd: html must be a string, such as await response.text(), received ${chunkD5CV3UF7_cjs.describeValue(html)}.`
66
98
  );
67
99
  }
68
100
  const documents = [];
@@ -85,7 +117,7 @@ function findRecipeJsonLd(html) {
85
117
  }
86
118
  function withImages(node, ids) {
87
119
  const follow = (value) => {
88
- const id = chunkVWVIBCTG_cjs.isRecord(value) && Object.keys(value).length === 1 && value["@id"];
120
+ const id = chunkD5CV3UF7_cjs.isRecord(value) && Object.keys(value).join() === "@id" && value["@id"];
89
121
  const target = typeof id === "string" && ids.get(id)?.node;
90
122
  return target && target !== node ? target : value;
91
123
  };
@@ -97,7 +129,7 @@ function withImages(node, ids) {
97
129
  if (Array.isArray(value)) {
98
130
  return value.map((entry) => copy(entry, depth + 1, keys));
99
131
  }
100
- const fields = chunkVWVIBCTG_cjs.asRecord(value);
132
+ const fields = chunkD5CV3UF7_cjs.asRecord(value);
101
133
  if (!fields) {
102
134
  return value;
103
135
  }
@@ -231,11 +263,11 @@ function cleanLines(text2) {
231
263
  const code = decimal === void 0 ? parseInt(hex, 16) : +decimal;
232
264
  return code > 0 && code <= 1114111 && (code < 55296 || code > 57343) ? String.fromCodePoint(code) : entity;
233
265
  }
234
- if (chunkVWVIBCTG_cjs.hasOwn(NAMED_ENTITIES, name)) {
266
+ if (chunkD5CV3UF7_cjs.hasOwn(NAMED_ENTITIES, name)) {
235
267
  return NAMED_ENTITIES[name];
236
268
  }
237
269
  const accent = /^([aceinouyACEINOUY])([a-z]+)$/.exec(name);
238
- return accent && chunkVWVIBCTG_cjs.hasOwn(MARKS, accent[2]) ? `${accent[1]}${MARKS[accent[2]]}`.normalize("NFC") : entity;
270
+ return accent && chunkD5CV3UF7_cjs.hasOwn(MARKS, accent[2]) ? `${accent[1]}${MARKS[accent[2]]}`.normalize("NFC") : entity;
239
271
  }
240
272
  ).split(/[\n\r\u2028\u2029]/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
241
273
  }
@@ -276,50 +308,54 @@ var TIMES = [
276
308
  ["cook", "cookTime"],
277
309
  ["total", "totalTime"]
278
310
  ];
311
+ var MAX_URL_LENGTH = 4096;
279
312
  var SERVINGS_WORD = /^(?:servings?|portions?|(?:serving|portion)\(s\)|people|persons?)\.?$/i;
280
313
  function fromJsonLd(input) {
281
314
  const { recipes, ids } = locate([input]);
282
315
  const [found] = recipes;
283
316
  if (!found) {
284
317
  return {
285
- ...chunkVWVIBCTG_cjs.toResult("invalid_recipe", void 0, [
286
- chunkVWVIBCTG_cjs.issue(
318
+ ...chunkD5CV3UF7_cjs.toResult("invalid_recipe", void 0, [
319
+ chunkD5CV3UF7_cjs.issue(
287
320
  "error",
288
321
  "invalid_type",
289
322
  [],
290
- `Expected a schema.org Recipe, or an array, @graph, or mainEntity that holds one, received ${chunkVWVIBCTG_cjs.describeValue(input)}.`
323
+ `Expected a schema.org Recipe, or an array, @graph, mainEntity, or ItemList that holds one, received ${chunkD5CV3UF7_cjs.describeValue(input)}.`
291
324
  )
292
325
  ]),
293
326
  extra: {}
294
327
  };
295
328
  }
296
- const node = chunkVWVIBCTG_cjs.asRecord(found.node);
329
+ const node = chunkD5CV3UF7_cjs.asRecord(found.node);
297
330
  const reader = {
298
331
  recipe: found.node,
299
332
  ids,
300
333
  warnings: [],
301
334
  sources: /* @__PURE__ */ new Map(),
302
335
  droppable: /* @__PURE__ */ new Set(),
303
- images: /* @__PURE__ */ new Map()
336
+ images: [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()],
337
+ repeated: 0,
338
+ deepest: 0
304
339
  };
305
340
  const name = text(node.name);
306
341
  if (recipes.length > 1) {
307
- chunkVWVIBCTG_cjs.warning(
342
+ chunkD5CV3UF7_cjs.warning(
308
343
  reader.warnings,
309
344
  "ignored_value",
310
345
  [],
311
- `Found ${recipes.length} recipes. Only the first${typeof name === "string" ? `, ${chunkVWVIBCTG_cjs.quote(name)},` : ""} is read.`
346
+ `Found ${recipes.length} recipes. Only the first${typeof name === "string" ? `, ${chunkD5CV3UF7_cjs.quote(name)},` : ""} is read.`
312
347
  );
313
348
  }
314
349
  const model = readRecipe(reader, node, found.path);
315
- const parse = () => chunkVWVIBCTG_cjs.parseRecipe(model, { parseIngredientLine: chunkVWVIBCTG_cjs.parseIngredientLine });
350
+ const parse = () => chunkD5CV3UF7_cjs.parseRecipeWith(model, chunkD5CV3UF7_cjs.parseIngredientLine);
316
351
  let result = parse();
317
352
  const dropped = [];
318
353
  if (!result.success) {
354
+ const targets = [];
319
355
  for (const error of result.error.issues) {
320
356
  const target = error.severity === "error" ? droppableAt(reader, error.path) : void 0;
321
357
  if (target) {
322
- remove(model, target);
358
+ targets.push(target);
323
359
  dropped.push({
324
360
  ...error,
325
361
  severity: "warning",
@@ -327,12 +363,13 @@ function fromJsonLd(input) {
327
363
  });
328
364
  }
329
365
  }
366
+ targets.sort((a, b) => b.length - a.length).forEach((target) => remove(model, target));
330
367
  if (dropped.length > 0) {
331
368
  result = parse();
332
369
  }
333
370
  }
334
371
  const order = ({ code, path }) => [
335
- READ_FIELDS.indexOf(String(path[found.path.length])),
372
+ path.length > found.path.length ? READ_FIELDS.indexOf(String(path[found.path.length])) : -1,
336
373
  ...path.slice(found.path.length + 1).filter((key) => typeof key === "number"),
337
374
  ...code === "unknown_field" ? [Infinity] : []
338
375
  ];
@@ -343,18 +380,50 @@ function fromJsonLd(input) {
343
380
  ...result.success ? result.warnings : result.error.issues
344
381
  ].map((issue2) => ({ ...issue2, path: sourceOf(reader, issue2.path) }))
345
382
  ].sort((a, b) => compareOrder(order(a), order(b)));
383
+ const checked = /* @__PURE__ */ new Map();
384
+ const extra = Object.fromEntries(
385
+ Object.entries(node).filter(([key, value]) => {
386
+ if (READ_FIELDS.includes(key)) {
387
+ return false;
388
+ }
389
+ const path = [...found.path, key];
390
+ if (tooDeep(value, path.length, checked)) {
391
+ chunkD5CV3UF7_cjs.warning(
392
+ issues,
393
+ "invalid_type",
394
+ path,
395
+ `Expected a value to keep in extra, received ${chunkD5CV3UF7_cjs.describeValue(value)} nested more than ${MAX_DEPTH} levels deep. It is ignored.`
396
+ );
397
+ return false;
398
+ }
399
+ return true;
400
+ })
401
+ );
346
402
  return {
347
- ...chunkVWVIBCTG_cjs.toResult(
403
+ ...chunkD5CV3UF7_cjs.toResult(
348
404
  "invalid_recipe",
349
405
  result.success ? result.data : void 0,
350
406
  issues,
351
- typeof name === "string" ? chunkVWVIBCTG_cjs.quote(name) : void 0
407
+ typeof name === "string" ? chunkD5CV3UF7_cjs.quote(name) : void 0
352
408
  ),
353
- extra: Object.fromEntries(
354
- Object.entries(node).filter(([key]) => !READ_FIELDS.includes(key))
355
- )
409
+ extra
356
410
  };
357
411
  }
412
+ function tooDeep(value, length, checked) {
413
+ if (length > MAX_DEPTH) {
414
+ return true;
415
+ }
416
+ if (typeof value !== "object" || value === null || (checked.get(value) ?? 0) >= length) {
417
+ return false;
418
+ }
419
+ const deep = Object.values(value).some(
420
+ (child) => tooDeep(child, length + 1, checked)
421
+ );
422
+ if (!deep) {
423
+ checked.set(value, length);
424
+ }
425
+ return deep;
426
+ }
358
427
  function readRecipe(reader, node, base) {
359
428
  const at = (...path) => [...base, ...path];
360
429
  const model = {
@@ -402,104 +471,130 @@ function readImage(reader, value, source) {
402
471
  if (isAbsent(value)) {
403
472
  return void 0;
404
473
  }
405
- const warnings = reader.warnings.length;
406
- reader.repeated = false;
474
+ const warnings = reader.warnings.length + reader.repeated;
407
475
  const image = findImage(reader, value, source);
408
- if (!image && !reader.repeated && reader.warnings.length === warnings) {
476
+ if (!image && reader.warnings.length + reader.repeated === warnings) {
409
477
  ignore(
410
478
  reader,
411
479
  "invalid_type",
412
480
  source,
413
- `Expected an image URL, an ImageObject with a url, or a list of them, received ${chunkVWVIBCTG_cjs.describeValue(value)}.`
481
+ `Expected an image URL, an ImageObject with a url, or a list of them, received ${chunkD5CV3UF7_cjs.describeValue(value)}.`
414
482
  );
415
483
  }
416
484
  return image;
417
485
  }
418
486
  function findImage(reader, value, source, referenced = false) {
487
+ reader.deepest = Math.max(reader.deepest, source.length);
488
+ if (source.length > MAX_DEPTH) {
489
+ if (!isAbsent(value)) {
490
+ ignore(
491
+ reader,
492
+ "invalid_type",
493
+ source,
494
+ `Expected an image URL, an ImageObject with a url, or a list of them, received ${chunkD5CV3UF7_cjs.describeValue(value)} nested more than ${MAX_DEPTH} levels deep.`
495
+ );
496
+ }
497
+ return void 0;
498
+ }
419
499
  if (typeof value === "string") {
420
500
  const url = cleanText(value);
421
- const scheme = chunkVWVIBCTG_cjs.disallowedScheme(url);
501
+ const scheme = chunkD5CV3UF7_cjs.disallowedScheme(url);
422
502
  if (scheme) {
423
503
  ignore(
424
504
  reader,
425
505
  "invalid_url",
426
506
  source,
427
- `Expected an http or https URL, or a relative one, received ${chunkVWVIBCTG_cjs.quote(`${scheme}\u2026`)}.`
507
+ `Expected an http or https URL, or a relative one, received ${chunkD5CV3UF7_cjs.quote(`${scheme}\u2026`)}.`
508
+ );
509
+ return void 0;
510
+ }
511
+ if (url.length > MAX_URL_LENGTH) {
512
+ ignore(
513
+ reader,
514
+ "invalid_url",
515
+ source,
516
+ `Expected a URL of at most ${MAX_URL_LENGTH} UTF-16 code units, received ${url.length}.`
428
517
  );
429
518
  return void 0;
430
519
  }
431
520
  return url ? { url, source } : void 0;
432
521
  }
433
- if (source.length > MAX_DEPTH) {
434
- return void 0;
435
- }
436
522
  if (Array.isArray(value)) {
437
- for (const [i, entry] of value.entries()) {
438
- const image2 = findImage(reader, entry, [...source, i], referenced);
523
+ for (const [entry, entrySource] of entriesOf(value, source)) {
524
+ const image2 = findImage(reader, entry, entrySource, referenced);
439
525
  if (image2) {
440
526
  return image2;
441
527
  }
442
528
  }
443
529
  return void 0;
444
530
  }
445
- const record = chunkVWVIBCTG_cjs.asRecord(value);
531
+ const images = reader.images[referenced ? 1 : 0];
532
+ const reads = images.get(value) ?? [];
533
+ const read = reads.find(
534
+ ([, , depth, length]) => length === source.length || Math.max(length, source.length) + depth <= MAX_DEPTH
535
+ );
536
+ if (read) {
537
+ reader.repeated += read[1];
538
+ reader.deepest = Math.max(reader.deepest, source.length + read[2]);
539
+ return read[0];
540
+ }
541
+ const record = chunkD5CV3UF7_cjs.asRecord(value);
446
542
  if (!record) {
447
543
  return void 0;
448
544
  }
545
+ images.set(value, reads);
546
+ const warnings = reader.warnings.length + reader.repeated;
547
+ const deepest = reader.deepest;
548
+ reader.deepest = source.length;
449
549
  const id = record["@id"];
450
550
  const target = typeof id === "string" && !referenced ? reader.ids.get(id) : void 0;
451
- const image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
452
- if (image || !target) {
453
- return image;
454
- }
455
- if (target.node === reader.recipe) {
551
+ let image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
552
+ if (!image && target?.node === reader.recipe) {
456
553
  ignore(
457
554
  reader,
458
555
  "invalid_type",
459
556
  source,
460
557
  "Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself."
461
558
  );
462
- return void 0;
559
+ } else if (!image && target) {
560
+ image = findImage(reader, target.node, target.path, true);
463
561
  }
464
- let read = reader.images.get(target);
465
- if (!read) {
466
- const warnings = reader.warnings.length;
467
- const found = findImage(reader, target.node, target.path, true);
468
- read = [found, reader.warnings.length > warnings];
469
- reader.images.set(target, read);
470
- } else if (read[1]) {
471
- reader.repeated = true;
472
- }
473
- return read[0];
562
+ reads.push([
563
+ image,
564
+ reader.warnings.length + reader.repeated - warnings,
565
+ reader.deepest - source.length,
566
+ source.length
567
+ ]);
568
+ reader.deepest = Math.max(reader.deepest, deepest);
569
+ return image;
474
570
  }
475
571
  function readYield(reader, model, value, source) {
476
- const entries = Array.isArray(value) ? value.map((entry, i) => [entry, [...source, i]]) : [[value, source]];
477
- const batches = entries.flatMap(
572
+ const batches = entriesOf(value, source).flatMap(
478
573
  ([entry, entrySource]) => (typeof entry === "string" ? entry.split(/,\s+/) : [entry]).map(
479
574
  (part) => readBatch(reader, part, entrySource)
480
575
  )
481
576
  ).filter((batch) => batch !== void 0);
482
577
  const yields = batches.filter((batch) => batch.yield !== void 0);
483
578
  const servings = batches.filter((batch) => batch.servings !== void 0);
484
- const kept = yields.find((batch) => !batch.article) ?? yields[0];
579
+ const kept = yields.find((batch) => !batch.article) ?? yields.find((batch) => batch.article);
485
580
  const keptServings = servings.find((batch) => batch.worded) ?? servings.find((batch) => batch.amount !== kept?.amount);
486
581
  for (const batch of servings) {
487
582
  if (keptServings && batch.amount !== keptServings.amount && (batch.worded || batch.amount !== kept?.amount)) {
488
- chunkVWVIBCTG_cjs.warning(
583
+ chunkD5CV3UF7_cjs.warning(
489
584
  reader.warnings,
490
585
  "ignored_value",
491
586
  batch.source,
492
- `Only one number of servings is read, ${chunkVWVIBCTG_cjs.quote(keptServings.label)}, so this one is ignored.`
587
+ `Only one number of servings is read, ${chunkD5CV3UF7_cjs.quote(keptServings.label)}, so this one is ignored.`
493
588
  );
494
589
  }
495
590
  }
496
591
  for (const batch of yields) {
497
592
  if (batch !== kept) {
498
- chunkVWVIBCTG_cjs.warning(
593
+ chunkD5CV3UF7_cjs.warning(
499
594
  reader.warnings,
500
595
  "ignored_value",
501
596
  batch.source,
502
- `Only one yield is read, ${chunkVWVIBCTG_cjs.quote(kept.label)}, so this one is ignored.`
597
+ `Only one yield is read, ${chunkD5CV3UF7_cjs.quote(kept.label)}, so this one is ignored.`
503
598
  );
504
599
  }
505
600
  }
@@ -525,13 +620,13 @@ function readBatch(reader, value, source) {
525
620
  /^(?:(?:serves|servings?|yields?|makes)\s*:?\s*)?(?:about\s+)?/i,
526
621
  ""
527
622
  );
528
- const match = chunkVWVIBCTG_cjs.matchAmount(text2);
623
+ const match = chunkD5CV3UF7_cjs.matchAmount(text2);
529
624
  if (!match || "code" in match) {
530
625
  ignore(
531
626
  reader,
532
627
  match?.code ?? "invalid_quantity",
533
628
  source,
534
- match?.message ?? `Expected a batch such as "24 cookies" or "3\u20134 loaves", received ${chunkVWVIBCTG_cjs.describeValue(label)}.`
629
+ match?.message ?? `Expected a batch such as "24 cookies" or "3\u20134 loaves", received ${chunkD5CV3UF7_cjs.describeValue(label)}.`
535
630
  );
536
631
  return void 0;
537
632
  }
@@ -539,7 +634,7 @@ function readBatch(reader, value, source) {
539
634
  const rest = match.rest.trim();
540
635
  return !rest || SERVINGS_WORD.test(rest) ? { source, label, amount: amount2, servings: match.amountText, worded: !!rest } : { source, label, amount: amount2, yield: text2, article: /^an?\s/i.test(text2) };
541
636
  }
542
- const record = chunkVWVIBCTG_cjs.asRecord(value);
637
+ const record = chunkD5CV3UF7_cjs.asRecord(value);
543
638
  const amount = toNumber(record?.value ?? record?.minValue);
544
639
  if (record && amount !== void 0) {
545
640
  const max = toNumber(record.maxValue);
@@ -548,7 +643,7 @@ function readBatch(reader, value, source) {
548
643
  amount,
549
644
  ...max !== void 0 && max !== amount && { max }
550
645
  };
551
- const label = `${amount}${range.max ? `\u2013${range.max}` : ""}${unit && ` ${unit}`}`;
646
+ const label = `${amount}${max !== amount && max ? `\u2013${max}` : ""}${unit && ` ${unit}`}`;
552
647
  return !unit || SERVINGS_WORD.test(unit) ? { source, label, amount, servings: range, worded: !!unit } : { source, label, amount, yield: { ...range, unit } };
553
648
  }
554
649
  if (!isAbsent(value)) {
@@ -556,7 +651,7 @@ function readBatch(reader, value, source) {
556
651
  reader,
557
652
  "invalid_type",
558
653
  source,
559
- `Expected a yield such as "4 servings" or "24 cookies", received ${chunkVWVIBCTG_cjs.describeValue(value)}.`
654
+ `Expected a yield such as "4 servings" or "24 cookies", received ${chunkD5CV3UF7_cjs.describeValue(value)}.`
560
655
  );
561
656
  }
562
657
  return void 0;
@@ -565,13 +660,13 @@ function readTime(reader, value, source) {
565
660
  if (isAbsent(value)) {
566
661
  return void 0;
567
662
  }
568
- const minutes = typeof value === "string" ? chunkVWVIBCTG_cjs.parseIsoDuration(cleanText(value)) : void 0;
663
+ const minutes = typeof value === "string" ? chunkD5CV3UF7_cjs.parseIsoDuration(cleanText(value)) : void 0;
569
664
  if (minutes === void 0 || minutes > Number.MAX_SAFE_INTEGER) {
570
665
  ignore(
571
666
  reader,
572
667
  "invalid_duration",
573
668
  source,
574
- `Expected an ISO 8601 duration such as "PT30M", received ${chunkVWVIBCTG_cjs.describeValue(value)}${minutes === void 0 ? "" : ", which is too long"}.`
669
+ `Expected an ISO 8601 duration such as "PT30M", received ${chunkD5CV3UF7_cjs.describeValue(value)}${minutes === void 0 ? "" : ", which is too long"}.`
575
670
  );
576
671
  return void 0;
577
672
  }
@@ -589,24 +684,26 @@ function readList(reader, key, value, source, readItem, expected) {
589
684
  reader,
590
685
  "invalid_type",
591
686
  entrySource,
592
- `Expected ${expected}, received ${chunkVWVIBCTG_cjs.describeValue(entry)} nested more than ${MAX_DEPTH} levels deep.`
687
+ `Expected ${expected}, received ${chunkD5CV3UF7_cjs.describeValue(entry)} nested more than ${MAX_DEPTH} levels deep.`
593
688
  );
594
689
  return;
595
690
  }
596
691
  if (Array.isArray(entry)) {
597
- const positions = entry.map(
598
- (element) => toNumber(chunkVWVIBCTG_cjs.asRecord(element)?.position)
692
+ const elements2 = entriesOf(entry, entrySource);
693
+ const positions = elements2.map(
694
+ ([element]) => toNumber(chunkD5CV3UF7_cjs.asRecord(element)?.position)
599
695
  );
600
- const order = [...entry.keys()];
696
+ const order = [...elements2.keys()];
601
697
  if (!positions.includes(void 0)) {
602
698
  order.sort((a, b) => positions[a] - positions[b]);
603
699
  }
604
700
  for (const i of order) {
605
- add(items, path, entry[i], [...entrySource, i], inSection);
701
+ const [element, elementSource] = elements2[i];
702
+ add(items, path, element, elementSource, inSection);
606
703
  }
607
704
  return;
608
705
  }
609
- const record = chunkVWVIBCTG_cjs.asRecord(entry);
706
+ const record = chunkD5CV3UF7_cjs.asRecord(entry);
610
707
  const elements = [...entrySource, "itemListElement"];
611
708
  if (record?.itemListElement !== void 0 && record.text === void 0) {
612
709
  const title = typeof record.name === "string" ? cleanText(record.name) : "";
@@ -628,13 +725,17 @@ function readList(reader, key, value, source, readItem, expected) {
628
725
  }
629
726
  return;
630
727
  }
728
+ if (record && !isAbsent(record.item)) {
729
+ add(items, path, record.item, [...entrySource, "item"], inSection);
730
+ return;
731
+ }
631
732
  const read = readItem(reader, entry, entrySource);
632
733
  if (!read) {
633
734
  ignore(
634
735
  reader,
635
736
  "invalid_type",
636
737
  entrySource,
637
- `Expected ${expected}, received ${chunkVWVIBCTG_cjs.describeValue(entry)}.`
738
+ `Expected ${expected}, received ${chunkD5CV3UF7_cjs.describeValue(entry)}.`
638
739
  );
639
740
  return;
640
741
  }
@@ -642,7 +743,7 @@ function readList(reader, key, value, source, readItem, expected) {
642
743
  const itemPath = [...path, items.length];
643
744
  items.push(item.value);
644
745
  link(reader, itemPath, item.source, true);
645
- for (const [field, fieldSource, droppable] of item.fields ?? []) {
746
+ for (const [field, fieldSource, droppable] of item.fields) {
646
747
  link(reader, [...itemPath, field], fieldSource, droppable);
647
748
  }
648
749
  }
@@ -652,26 +753,28 @@ function readList(reader, key, value, source, readItem, expected) {
652
753
  }
653
754
  function readLine(reader, value, source) {
654
755
  if (typeof value === "string") {
655
- return cleanLines(value).map((line) => ({ value: line, source }));
656
- }
657
- const record = chunkVWVIBCTG_cjs.asRecord(value);
658
- if (record?.item !== void 0 && source.length < MAX_DEPTH) {
659
- return readLine(reader, record.item, [...source, "item"]);
756
+ return cleanLines(value).map((line) => ({
757
+ value: line,
758
+ source,
759
+ fields: []
760
+ }));
660
761
  }
762
+ const record = chunkD5CV3UF7_cjs.asRecord(value);
661
763
  const parts = [record?.value, record?.unitText, record?.name ?? record?.text].filter((part) => typeof part === "string" || typeof part === "number").join(" ");
662
- return parts ? cleanLines(parts).map((line) => ({ value: line, source })) : void 0;
764
+ return parts ? cleanLines(parts).map((line) => ({ value: line, source, fields: [] })) : void 0;
663
765
  }
664
766
  function readStep(reader, value, source) {
665
767
  if (typeof value === "string") {
666
- return cleanLines(value).map((line) => ({ value: line, source }));
768
+ return cleanLines(value).map((line) => ({
769
+ value: line,
770
+ source,
771
+ fields: []
772
+ }));
667
773
  }
668
- const record = chunkVWVIBCTG_cjs.asRecord(value);
774
+ const record = chunkD5CV3UF7_cjs.asRecord(value);
669
775
  if (!record) {
670
776
  return void 0;
671
777
  }
672
- if (record.item !== void 0 && source.length < MAX_DEPTH) {
673
- return readStep(reader, record.item, [...source, "item"]);
674
- }
675
778
  const name = typeof record.name === "string" ? cleanText(record.name) : "";
676
779
  const body = text(record.text) ?? name;
677
780
  const image = readImage(reader, record.image, [...source, "image"]);
@@ -696,16 +799,15 @@ function readStep(reader, value, source) {
696
799
  ];
697
800
  }
698
801
  function readTools(reader, value, source) {
699
- const entries = Array.isArray(value) ? value.map((entry, i) => [entry, [...source, i]]) : [[value, source]];
700
802
  const equipment = [];
701
803
  link(reader, ["equipment"], source);
702
- for (const [entry, entrySource] of entries) {
703
- const record = chunkVWVIBCTG_cjs.asRecord(entry);
804
+ for (const [entry, entrySource] of entriesOf(value, source)) {
805
+ const record = chunkD5CV3UF7_cjs.asRecord(entry);
704
806
  const item = record ? {
705
807
  name: text(record.name),
706
808
  ...record.requiredQuantity !== void 0 && {
707
809
  quantity: toNumber(
708
- chunkVWVIBCTG_cjs.asRecord(record.requiredQuantity)?.value ?? record.requiredQuantity
810
+ chunkD5CV3UF7_cjs.asRecord(record.requiredQuantity)?.value ?? record.requiredQuantity
709
811
  ) ?? record.requiredQuantity
710
812
  },
711
813
  ...record.url !== void 0 && { url: text(record.url) }
@@ -732,13 +834,13 @@ function readNutrition(reader, value, source) {
732
834
  if (isAbsent(value)) {
733
835
  return void 0;
734
836
  }
735
- const record = chunkVWVIBCTG_cjs.asRecord(value);
837
+ const record = chunkD5CV3UF7_cjs.asRecord(value);
736
838
  if (!record) {
737
839
  ignore(
738
840
  reader,
739
841
  "invalid_type",
740
842
  source,
741
- `Expected NutritionInformation, such as { "calories": "250 kcal" }, received ${chunkVWVIBCTG_cjs.describeValue(value)}.`
843
+ `Expected NutritionInformation, such as { "calories": "250 kcal" }, received ${chunkD5CV3UF7_cjs.describeValue(value)}.`
742
844
  );
743
845
  return void 0;
744
846
  }
@@ -766,7 +868,7 @@ function isAbsent(value) {
766
868
  return value === void 0 || value === null || typeof value === "string" && !value.trim();
767
869
  }
768
870
  function ignore(reader, code, path, message) {
769
- chunkVWVIBCTG_cjs.warning(reader.warnings, code, path, `${message} It is ignored.`);
871
+ chunkD5CV3UF7_cjs.warning(reader.warnings, code, path, `${message} It is ignored.`);
770
872
  }
771
873
  function link(reader, path, source, droppable = false) {
772
874
  const key = JSON.stringify(path);
@@ -786,10 +888,10 @@ function sourceOf(reader, path) {
786
888
  }
787
889
  function compareOrder(a, b) {
788
890
  for (const [i, n] of a.entries()) {
789
- const m = b[i];
790
- if (m === void 0) {
891
+ if (i === b.length) {
791
892
  return 1;
792
893
  }
894
+ const m = b[i];
793
895
  if (n !== m) {
794
896
  return n - m;
795
897
  }
@@ -818,18 +920,28 @@ function remove(model, path) {
818
920
  // src/schema-org/to-json-ld.ts
819
921
  function toJsonLd(recipe, options) {
820
922
  const { servings, system, url, extra } = options ?? {};
821
- const resolved = chunkVWVIBCTG_cjs.resolveRecipe(recipe, { servings, system });
822
- const page = typeof url === "string" ? /^(?:[\s\S]*[\n\r\u2028\u2029])?[^#\n\r\u2028\u2029]*/.exec(url)[0] : void 0;
923
+ const resolved = chunkD5CV3UF7_cjs.resolveRecipe(recipe, { servings, system });
924
+ const page = typeof url === "string" ? url.replace(/#.*/s, "") : void 0;
823
925
  const yields = [
824
926
  ...resolved.servings ? servingsText(resolved.servings) : [],
825
- ...resolved.yield ? [chunkVWVIBCTG_cjs.formatYield(resolved.yield, { fractions: false })] : []
927
+ ...resolved.yield ? [chunkD5CV3UF7_cjs.formatYield(resolved.yield, { fractions: false })] : []
826
928
  ];
827
929
  const ingredients = resolved.ingredients.flatMap(
828
930
  ({ items }) => items.map((item) => ingredientLine(item, resolved.scale, resolved.system))
829
931
  );
830
932
  const steps = resolved.steps.flatMap(
831
933
  ({ title, items }) => {
832
- const howToSteps = items.map((step) => howToStep(step, page));
934
+ const howToSteps = items.map((step) => ({
935
+ "@type": "HowToStep",
936
+ ...step.name !== void 0 && { name: step.name },
937
+ text: step.text,
938
+ ...step.image !== void 0 && { image: step.image },
939
+ ...page !== void 0 && {
940
+ // encodeURIComponent throws on a lone surrogate. With the u flag,
941
+ // \p{Cs} matches only those, as a pair reads as one code point.
942
+ url: `${page}#${encodeURIComponent(step.id.replace(/\p{Cs}/gu, "\uFFFD"))}`
943
+ }
944
+ }));
833
945
  return title === void 0 || howToSteps.length === 0 ? howToSteps : [
834
946
  {
835
947
  "@type": "HowToSection",
@@ -861,13 +973,13 @@ function toJsonLd(recipe, options) {
861
973
  const data = {
862
974
  ...written,
863
975
  ...Object.fromEntries(
864
- Object.entries(chunkVWVIBCTG_cjs.isRecord(extra) ? extra : {}).filter(
865
- ([field, value]) => value !== void 0 && !chunkVWVIBCTG_cjs.hasOwn(written, field)
976
+ Object.entries(chunkD5CV3UF7_cjs.isRecord(extra) ? extra : {}).filter(
977
+ ([field, value]) => value !== void 0 && !chunkD5CV3UF7_cjs.hasOwn(written, field)
866
978
  )
867
979
  )
868
980
  };
869
981
  if (data.image === void 0) {
870
- chunkVWVIBCTG_cjs.warning(
982
+ chunkD5CV3UF7_cjs.warning(
871
983
  warnings,
872
984
  "missing_field",
873
985
  ["image"],
@@ -875,7 +987,7 @@ function toJsonLd(recipe, options) {
875
987
  );
876
988
  }
877
989
  if (data.nutrition !== void 0 && data.recipeYield === void 0) {
878
- chunkVWVIBCTG_cjs.warning(
990
+ chunkD5CV3UF7_cjs.warning(
879
991
  warnings,
880
992
  "missing_field",
881
993
  ["recipeYield"],
@@ -885,45 +997,32 @@ function toJsonLd(recipe, options) {
885
997
  return { data, warnings };
886
998
  }
887
999
  function servingsText(servings) {
888
- const amount = chunkVWVIBCTG_cjs.formatServings(servings, { fractions: false });
1000
+ const amount = chunkD5CV3UF7_cjs.formatServings(servings, { fractions: false });
889
1001
  const word = servings.amount === 1 && servings.max === void 0 ? "serving" : "servings";
890
1002
  return [amount, `${amount} ${word}`];
891
1003
  }
892
1004
  function ingredientLine(item, scale, system) {
893
1005
  const { ingredient, origin } = item;
894
- const { quantity, name, note } = chunkVWVIBCTG_cjs.formatIngredient(item);
1006
+ const { quantity, name, note } = chunkD5CV3UF7_cjs.formatIngredient(item);
895
1007
  const authored = ingredient.quantity ? [ingredient.quantity, ...ingredient.equivalents] : [];
896
- const shown = origin === "authored" ? 0 : origin === "equivalent" ? 1 + ingredient.equivalents.findIndex((q) => chunkVWVIBCTG_cjs.isNative(q, system)) : -1;
897
- const alternatives = authored.filter((_, i) => i !== shown).map((q) => chunkVWVIBCTG_cjs.formatQuantity(scale === 1 ? q : chunkVWVIBCTG_cjs.multiply(q, scale))).join("; ");
1008
+ const shown = origin === "authored" ? 0 : origin === "equivalent" ? 1 + ingredient.equivalents.findIndex((q) => chunkD5CV3UF7_cjs.isNative(q, system)) : -1;
1009
+ const alternatives = authored.filter((_, i) => i !== shown).map((q) => chunkD5CV3UF7_cjs.formatQuantity(scale === 1 ? q : chunkD5CV3UF7_cjs.multiply(q, scale))).join("; ");
898
1010
  const inParentheses = alternatives && ` (${alternatives})`;
899
1011
  const head = quantity === void 0 ? name : item.quantity?.unit === void 0 ? `${quantity} ${name}${inParentheses}` : `${quantity}${inParentheses} ${name}`;
900
1012
  return plainSpaces(
901
1013
  `${head}${note ? `, ${note}` : ""}${ingredient.optional ? " (optional)" : ""}`
902
1014
  );
903
1015
  }
904
- function howToStep(step, page) {
905
- return {
906
- "@type": "HowToStep",
907
- ...step.name !== void 0 && { name: step.name },
908
- text: step.text,
909
- ...step.image !== void 0 && { image: step.image },
910
- ...page !== void 0 && {
911
- // encodeURIComponent throws on a lone surrogate. With the u flag,
912
- // \p{Cs} matches only those, as a pair reads as one code point.
913
- url: `${page}#${encodeURIComponent(step.id.replace(/\p{Cs}/gu, "\uFFFD"))}`
914
- }
915
- };
916
- }
917
1016
  function duration(warnings, key, minutes) {
918
1017
  if (minutes === void 0) {
919
1018
  return {};
920
1019
  }
921
1020
  if (Number.isNaN(minutes) || minutes > Number.MAX_SAFE_INTEGER) {
922
- chunkVWVIBCTG_cjs.warning(
1021
+ chunkD5CV3UF7_cjs.warning(
923
1022
  warnings,
924
1023
  "invalid_duration",
925
1024
  ["times", key],
926
- `Expected minutes such as 30, received ${chunkVWVIBCTG_cjs.describeValue(minutes)}${Number.isNaN(minutes) ? "" : ", which is too long"}. It is ignored.`
1025
+ `Expected minutes such as 30, received ${chunkD5CV3UF7_cjs.describeValue(minutes)}${Number.isNaN(minutes) ? "" : ", which is too long"}. It is ignored.`
927
1026
  );
928
1027
  return {};
929
1028
  }
@@ -947,7 +1046,7 @@ function nutritionInformation(nutrition) {
947
1046
  for (const key of Object.keys(NUTRIENTS)) {
948
1047
  const value = nutrition[key];
949
1048
  if (value !== void 0) {
950
- information[NUTRIENTS[key]] = typeof value === "string" ? value : `${chunkVWVIBCTG_cjs.round(value, 3)} ${chunkVWVIBCTG_cjs.NUTRIENT_UNITS[key]}`;
1049
+ information[NUTRIENTS[key]] = typeof value === "string" ? value : `${chunkD5CV3UF7_cjs.round(value, 3)} ${chunkD5CV3UF7_cjs.NUTRIENT_UNITS[key]}`;
951
1050
  }
952
1051
  }
953
1052
  return information;