@recipe-book/core 0.1.0 → 0.1.2

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