@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
@@ -7,10 +7,10 @@ import { parseIngredientLine } from "../ingredient/parse.js";
7
7
  import { issue, warning } from "../issues.js";
8
8
  import { matchAmount } from "../quantity/parse.js";
9
9
  import { parseIsoDuration } from "../recipe/duration.js";
10
- import { parseRecipe } from "../recipe/parse.js";
10
+ import { parseRecipeWith } from "../recipe/parse.js";
11
11
  import { disallowedScheme } from "../recipe/url.js";
12
12
  import { asRecord, toResult } from "../validation.js";
13
- import { locate, MAX_DEPTH } from "./find.js";
13
+ import { entriesOf, locate, MAX_DEPTH } from "./find.js";
14
14
  import { NUTRIENTS } from "./nutrients.js";
15
15
  import { cleanLines, cleanText } from "./text.js";
16
16
  var READ_FIELDS = [
@@ -28,11 +28,7 @@ var READ_FIELDS = [
28
28
  "tool",
29
29
  "nutrition"
30
30
  ];
31
- var TIMES = [
32
- ["prep", "prepTime"],
33
- ["cook", "cookTime"],
34
- ["total", "totalTime"]
35
- ];
31
+ var MAX_URL_LENGTH = 4096;
36
32
  var SERVINGS_WORD = /^(?:servings?|portions?|(?:serving|portion)\(s\)|people|persons?)\.?$/i;
37
33
  function fromJsonLd(input) {
38
34
  const { recipes, ids } = locate([input]);
@@ -44,7 +40,7 @@ function fromJsonLd(input) {
44
40
  "error",
45
41
  "invalid_type",
46
42
  [],
47
- `Expected a schema.org Recipe, or an array, @graph, or mainEntity that holds one, received ${describeValue(input)}.`
43
+ `Expected a schema.org Recipe, or an array, @graph, mainEntity, or ItemList that holds one, received ${describeValue(input)}.`
48
44
  )
49
45
  ]),
50
46
  extra: {}
@@ -57,7 +53,9 @@ function fromJsonLd(input) {
57
53
  warnings: [],
58
54
  sources: /* @__PURE__ */ new Map(),
59
55
  droppable: /* @__PURE__ */ new Set(),
60
- images: /* @__PURE__ */ new Map()
56
+ images: [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()],
57
+ repeated: 0,
58
+ deepest: 0
61
59
  };
62
60
  const name = text(node.name);
63
61
  if (recipes.length > 1) {
@@ -69,14 +67,15 @@ function fromJsonLd(input) {
69
67
  );
70
68
  }
71
69
  const model = readRecipe(reader, node, found.path);
72
- const parse = () => parseRecipe(model, { parseIngredientLine });
70
+ const parse = () => parseRecipeWith(model, parseIngredientLine);
73
71
  let result = parse();
74
72
  const dropped = [];
75
73
  if (!result.success) {
74
+ const targets = [];
76
75
  for (const error of result.error.issues) {
77
76
  const target = error.severity === "error" ? droppableAt(reader, error.path) : void 0;
78
77
  if (target) {
79
- remove(model, target);
78
+ targets.push(target);
80
79
  dropped.push({
81
80
  ...error,
82
81
  severity: "warning",
@@ -84,12 +83,13 @@ function fromJsonLd(input) {
84
83
  });
85
84
  }
86
85
  }
86
+ targets.sort((a, b) => b.length - a.length).forEach((target) => remove(model, target));
87
87
  if (dropped.length > 0) {
88
88
  result = parse();
89
89
  }
90
90
  }
91
91
  const order = ({ code, path }) => [
92
- READ_FIELDS.indexOf(String(path[found.path.length])),
92
+ path.length > found.path.length ? READ_FIELDS.indexOf(String(path[found.path.length])) : -1,
93
93
  ...path.slice(found.path.length + 1).filter((key) => typeof key === "number"),
94
94
  ...code === "unknown_field" ? [Infinity] : []
95
95
  ];
@@ -100,6 +100,25 @@ function fromJsonLd(input) {
100
100
  ...result.success ? result.warnings : result.error.issues
101
101
  ].map((issue2) => ({ ...issue2, path: sourceOf(reader, issue2.path) }))
102
102
  ].sort((a, b) => compareOrder(order(a), order(b)));
103
+ const checked = /* @__PURE__ */ new Map();
104
+ const extra = Object.fromEntries(
105
+ Object.entries(node).filter(([key, value]) => {
106
+ if (READ_FIELDS.includes(key)) {
107
+ return false;
108
+ }
109
+ const path = [...found.path, key];
110
+ if (tooDeep(value, path.length, checked)) {
111
+ warning(
112
+ issues,
113
+ "invalid_type",
114
+ path,
115
+ `Expected a value to keep in extra, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep. It is ignored.`
116
+ );
117
+ return false;
118
+ }
119
+ return true;
120
+ })
121
+ );
103
122
  return {
104
123
  ...toResult(
105
124
  "invalid_recipe",
@@ -107,11 +126,24 @@ function fromJsonLd(input) {
107
126
  issues,
108
127
  typeof name === "string" ? quote(name) : void 0
109
128
  ),
110
- extra: Object.fromEntries(
111
- Object.entries(node).filter(([key]) => !READ_FIELDS.includes(key))
112
- )
129
+ extra
113
130
  };
114
131
  }
132
+ function tooDeep(value, length, checked) {
133
+ if (length > MAX_DEPTH) {
134
+ return true;
135
+ }
136
+ if (typeof value !== "object" || value === null || (checked.get(value) ?? 0) >= length) {
137
+ return false;
138
+ }
139
+ const deep = Object.values(value).some(
140
+ (child) => tooDeep(child, length + 1, checked)
141
+ );
142
+ if (!deep) {
143
+ checked.set(value, length);
144
+ }
145
+ return deep;
146
+ }
115
147
  function readRecipe(reader, node, base) {
116
148
  const at = (...path) => [...base, ...path];
117
149
  const model = {
@@ -128,7 +160,8 @@ function readRecipe(reader, node, base) {
128
160
  }
129
161
  readYield(reader, model, node.recipeYield, at("recipeYield"));
130
162
  const times = {};
131
- for (const [key, field] of TIMES) {
163
+ for (const key of ["prep", "cook", "total"]) {
164
+ const field = `${key}Time`;
132
165
  const minutes = readTime(reader, node[field], at(field));
133
166
  if (minutes !== void 0) {
134
167
  times[key] = minutes;
@@ -159,10 +192,9 @@ function readImage(reader, value, source) {
159
192
  if (isAbsent(value)) {
160
193
  return void 0;
161
194
  }
162
- const warnings = reader.warnings.length;
163
- reader.repeated = false;
195
+ const warnings = reader.warnings.length + reader.repeated;
164
196
  const image = findImage(reader, value, source);
165
- if (!image && !reader.repeated && reader.warnings.length === warnings) {
197
+ if (!image && reader.warnings.length + reader.repeated === warnings) {
166
198
  ignore(
167
199
  reader,
168
200
  "invalid_type",
@@ -173,6 +205,18 @@ function readImage(reader, value, source) {
173
205
  return image;
174
206
  }
175
207
  function findImage(reader, value, source, referenced = false) {
208
+ reader.deepest = Math.max(reader.deepest, source.length);
209
+ if (source.length > MAX_DEPTH) {
210
+ if (!isAbsent(value)) {
211
+ ignore(
212
+ reader,
213
+ "invalid_type",
214
+ source,
215
+ `Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep.`
216
+ );
217
+ }
218
+ return void 0;
219
+ }
176
220
  if (typeof value === "string") {
177
221
  const url = cleanText(value);
178
222
  const scheme = disallowedScheme(url);
@@ -185,60 +229,77 @@ function findImage(reader, value, source, referenced = false) {
185
229
  );
186
230
  return void 0;
187
231
  }
232
+ if (url.length > MAX_URL_LENGTH) {
233
+ ignore(
234
+ reader,
235
+ "invalid_url",
236
+ source,
237
+ `Expected a URL of at most ${MAX_URL_LENGTH} UTF-16 code units, received ${url.length}.`
238
+ );
239
+ return void 0;
240
+ }
188
241
  return url ? { url, source } : void 0;
189
242
  }
190
- if (source.length > MAX_DEPTH) {
191
- return void 0;
192
- }
193
243
  if (Array.isArray(value)) {
194
- for (const [i, entry] of value.entries()) {
195
- const image2 = findImage(reader, entry, [...source, i], referenced);
244
+ for (const [entry, entrySource] of entriesOf(value, source)) {
245
+ const image2 = findImage(reader, entry, entrySource, referenced);
196
246
  if (image2) {
197
247
  return image2;
198
248
  }
199
249
  }
200
250
  return void 0;
201
251
  }
252
+ const images = reader.images[referenced ? 1 : 0];
253
+ const reads = images.get(value) ?? [];
254
+ const read = reads.find(
255
+ ([, , depth, length]) => length === source.length || Math.max(length, source.length) + depth <= MAX_DEPTH
256
+ );
257
+ if (read) {
258
+ reader.repeated += read[1];
259
+ reader.deepest = Math.max(reader.deepest, source.length + read[2]);
260
+ return read[0];
261
+ }
202
262
  const record = asRecord(value);
203
263
  if (!record) {
204
264
  return void 0;
205
265
  }
266
+ images.set(value, reads);
267
+ const warnings = reader.warnings.length + reader.repeated;
268
+ const deepest = reader.deepest;
269
+ reader.deepest = source.length;
206
270
  const id = record["@id"];
207
271
  const target = typeof id === "string" && !referenced ? reader.ids.get(id) : void 0;
208
- const image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
209
- if (image || !target) {
210
- return image;
211
- }
212
- if (target.node === reader.recipe) {
213
- ignore(
214
- reader,
215
- "invalid_type",
216
- source,
217
- "Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself."
218
- );
219
- return void 0;
220
- }
221
- let read = reader.images.get(target);
222
- if (!read) {
223
- const warnings = reader.warnings.length;
224
- const found = findImage(reader, target.node, target.path, true);
225
- read = [found, reader.warnings.length > warnings];
226
- reader.images.set(target, read);
227
- } else if (read[1]) {
228
- reader.repeated = true;
272
+ let image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
273
+ if (!image && target) {
274
+ if (target.node === reader.recipe) {
275
+ ignore(
276
+ reader,
277
+ "invalid_type",
278
+ source,
279
+ "Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself."
280
+ );
281
+ } else {
282
+ image = findImage(reader, target.node, target.path, true);
283
+ }
229
284
  }
230
- return read[0];
285
+ reads.push([
286
+ image,
287
+ reader.warnings.length + reader.repeated - warnings,
288
+ reader.deepest - source.length,
289
+ source.length
290
+ ]);
291
+ reader.deepest = Math.max(reader.deepest, deepest);
292
+ return image;
231
293
  }
232
294
  function readYield(reader, model, value, source) {
233
- const entries = Array.isArray(value) ? value.map((entry, i) => [entry, [...source, i]]) : [[value, source]];
234
- const batches = entries.flatMap(
295
+ const batches = entriesOf(value, source).flatMap(
235
296
  ([entry, entrySource]) => (typeof entry === "string" ? entry.split(/,\s+/) : [entry]).map(
236
297
  (part) => readBatch(reader, part, entrySource)
237
298
  )
238
299
  ).filter((batch) => batch !== void 0);
239
300
  const yields = batches.filter((batch) => batch.yield !== void 0);
240
301
  const servings = batches.filter((batch) => batch.servings !== void 0);
241
- const kept = yields.find((batch) => !batch.article) ?? yields[0];
302
+ const kept = yields.find((batch) => !batch.article) ?? yields.find((batch) => batch.article);
242
303
  const keptServings = servings.find((batch) => batch.worded) ?? servings.find((batch) => batch.amount !== kept?.amount);
243
304
  for (const batch of servings) {
244
305
  if (keptServings && batch.amount !== keptServings.amount && (batch.worded || batch.amount !== kept?.amount)) {
@@ -305,7 +366,7 @@ function readBatch(reader, value, source) {
305
366
  amount,
306
367
  ...max !== void 0 && max !== amount && { max }
307
368
  };
308
- const label = `${amount}${range.max ? `\u2013${range.max}` : ""}${unit && ` ${unit}`}`;
369
+ const label = `${amount}${max !== amount && max ? `\u2013${max}` : ""}${unit && ` ${unit}`}`;
309
370
  return !unit || SERVINGS_WORD.test(unit) ? { source, label, amount, servings: range, worded: !!unit } : { source, label, amount, yield: { ...range, unit } };
310
371
  }
311
372
  if (!isAbsent(value)) {
@@ -351,15 +412,17 @@ function readList(reader, key, value, source, readItem, expected) {
351
412
  return;
352
413
  }
353
414
  if (Array.isArray(entry)) {
354
- const positions = entry.map(
355
- (element) => toNumber(asRecord(element)?.position)
415
+ const elements2 = entriesOf(entry, entrySource);
416
+ const positions = elements2.map(
417
+ ([element]) => toNumber(asRecord(element)?.position)
356
418
  );
357
- const order = [...entry.keys()];
419
+ const order = [...elements2.keys()];
358
420
  if (!positions.includes(void 0)) {
359
421
  order.sort((a, b) => positions[a] - positions[b]);
360
422
  }
361
423
  for (const i of order) {
362
- add(items, path, entry[i], [...entrySource, i], inSection);
424
+ const [element, elementSource] = elements2[i];
425
+ add(items, path, element, elementSource, inSection);
363
426
  }
364
427
  return;
365
428
  }
@@ -385,6 +448,10 @@ function readList(reader, key, value, source, readItem, expected) {
385
448
  }
386
449
  return;
387
450
  }
451
+ if (record && !isAbsent(record.item)) {
452
+ add(items, path, record.item, [...entrySource, "item"], inSection);
453
+ return;
454
+ }
388
455
  const read = readItem(reader, entry, entrySource);
389
456
  if (!read) {
390
457
  ignore(
@@ -399,7 +466,7 @@ function readList(reader, key, value, source, readItem, expected) {
399
466
  const itemPath = [...path, items.length];
400
467
  items.push(item.value);
401
468
  link(reader, itemPath, item.source, true);
402
- for (const [field, fieldSource, droppable] of item.fields ?? []) {
469
+ for (const [field, fieldSource, droppable] of item.fields) {
403
470
  link(reader, [...itemPath, field], fieldSource, droppable);
404
471
  }
405
472
  }
@@ -408,27 +475,24 @@ function readList(reader, key, value, source, readItem, expected) {
408
475
  return list;
409
476
  }
410
477
  function readLine(reader, value, source) {
411
- if (typeof value === "string") {
412
- return cleanLines(value).map((line) => ({ value: line, source }));
413
- }
414
478
  const record = asRecord(value);
415
- if (record?.item !== void 0 && source.length < MAX_DEPTH) {
416
- return readLine(reader, record.item, [...source, "item"]);
417
- }
418
- const parts = [record?.value, record?.unitText, record?.name ?? record?.text].filter((part) => typeof part === "string" || typeof part === "number").join(" ");
419
- return parts ? cleanLines(parts).map((line) => ({ value: line, source })) : void 0;
479
+ const parts = typeof value === "string" ? value : [record?.value, record?.unitText, record?.name ?? record?.text].filter(
480
+ (part) => typeof part === "string" || typeof part === "number"
481
+ ).join(" ");
482
+ return parts ? cleanLines(parts).map((line) => ({ value: line, source, fields: [] })) : void 0;
420
483
  }
421
484
  function readStep(reader, value, source) {
422
485
  if (typeof value === "string") {
423
- return cleanLines(value).map((line) => ({ value: line, source }));
486
+ return cleanLines(value).map((line) => ({
487
+ value: line,
488
+ source,
489
+ fields: []
490
+ }));
424
491
  }
425
492
  const record = asRecord(value);
426
493
  if (!record) {
427
494
  return void 0;
428
495
  }
429
- if (record.item !== void 0 && source.length < MAX_DEPTH) {
430
- return readStep(reader, record.item, [...source, "item"]);
431
- }
432
496
  const name = typeof record.name === "string" ? cleanText(record.name) : "";
433
497
  const body = text(record.text) ?? name;
434
498
  const image = readImage(reader, record.image, [...source, "image"]);
@@ -453,10 +517,9 @@ function readStep(reader, value, source) {
453
517
  ];
454
518
  }
455
519
  function readTools(reader, value, source) {
456
- const entries = Array.isArray(value) ? value.map((entry, i) => [entry, [...source, i]]) : [[value, source]];
457
520
  const equipment = [];
458
521
  link(reader, ["equipment"], source);
459
- for (const [entry, entrySource] of entries) {
522
+ for (const [entry, entrySource] of entriesOf(value, source)) {
460
523
  const record = asRecord(entry);
461
524
  const item = record ? {
462
525
  name: text(record.name),
@@ -543,10 +606,10 @@ function sourceOf(reader, path) {
543
606
  }
544
607
  function compareOrder(a, b) {
545
608
  for (const [i, n] of a.entries()) {
546
- const m = b[i];
547
- if (m === void 0) {
609
+ if (i === b.length) {
548
610
  return 1;
549
611
  }
612
+ const m = b[i];
550
613
  if (n !== m) {
551
614
  return n - m;
552
615
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema-org/from-json-ld.ts"],"sourcesContent":["import {\n describeValue,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport { parseIngredientLine } from '../ingredient/parse'\nimport { issue, type Path, warning } from '../issues'\nimport { matchAmount } from '../quantity/parse'\nimport { parseIsoDuration } from '../recipe/duration'\nimport { parseRecipe } from '../recipe/parse'\nimport type { Nutrition, Recipe } from '../recipe/types'\nimport { disallowedScheme } from '../recipe/url'\nimport { asRecord, toResult } from '../validation'\nimport { type Located, locate, MAX_DEPTH } from './find'\nimport { NUTRIENTS } from './nutrients'\nimport { cleanLines, cleanText } from './text'\nimport type { FromJsonLdResult } from './types'\n\n/**\n * The node's fields that `fromJsonLd` reads or that core owns, so `extra`\n * leaves them out, in the order issues are reported.\n */\nconst READ_FIELDS: readonly string[] = [\n '@context',\n '@type',\n 'name',\n 'description',\n 'image',\n 'recipeYield',\n 'prepTime',\n 'cookTime',\n 'totalTime',\n 'recipeIngredient',\n 'recipeInstructions',\n 'tool',\n 'nutrition',\n]\n\nconst TIMES = [\n ['prep', 'prepTime'],\n ['cook', 'cookTime'],\n ['total', 'totalTime'],\n] as const\n\nconst SERVINGS_WORD =\n /^(?:servings?|portions?|(?:serving|portion)\\(s\\)|people|persons?)\\.?$/i\n\ntype Reader = {\n /** The Recipe node, as passed in. */\n recipe: Record<string, unknown>\n ids: Map<string, Located>\n warnings: RecipeIssue[]\n /** The JSON-LD path of each value in the recipe input, keyed by its path there. */\n sources: Map<string, Path>\n /** The recipe input paths of the values a failed parse drops. */\n droppable: Set<string>\n /** The image each node that an image references holds, and whether reading it warned. */\n images: Map<Located, [image: Image | undefined, warned: boolean]>\n /** Set when an image reaches a node whose warnings were given earlier. */\n repeated?: boolean\n}\n\ntype Image = { url: string; source: Path }\n\n/** A value read from `recipeYield`. */\ntype Batch = {\n source: Path\n /** The value as text, to name it in a message. */\n label: string\n /** The amount, or the low end of its range. */\n amount: number\n servings?: unknown\n /** Servings with a word, such as \"4 servings\", not a bare number. */\n worded?: boolean\n yield?: unknown\n /** The yield's amount is \"a\" or \"an\", as in \"a few\", not a number. */\n article?: boolean\n}\n\n/** A list item read from JSON-LD, and where its fields came from. */\ntype Item = {\n value: unknown\n source: Path\n fields?: [field: string, source: Path, droppable: boolean][]\n}\n\n/**\n * Reads a schema.org Recipe from JSON-LD, as recipe sites publish it for\n * search engines: a Recipe node, or an array, `@graph`, or `mainEntity` that\n * holds one, such as a node from `findRecipeJsonLd` or `JSON.parse` of a\n * script tag. Never throws for bad data; an error thrown by a getter or proxy\n * in the input passes through.\n *\n * Every string has its tags removed, then its HTML entities decoded, so an\n * encoded tag such as `&lt;b&gt;` stays as text. Ingredient lines are read\n * with `parseIngredientLine`. Issue paths point into the JSON-LD, such as\n * `recipeIngredient[3]`.\n *\n * Reading is forgiving. A value the recipe can't hold, such as a `data:`\n * image or nutrition of \"N/A\", is left out with a warning that keeps its usual\n * code and ends \"It is ignored.\" A second Recipe node, a second number of\n * servings, or a second yield is an `ignored_value` warning. The result fails\n * only when there is no Recipe node, the recipe's name is missing or isn't\n * text, or a problem remains that leaving a value out can't fix.\n *\n * @returns The recipe or the error, and `extra`: the node's fields the recipe\n * doesn't model, such as `author` and `keywords`, as they were. Pass `extra`\n * to `toJsonLd` to write them back. It is `{}` when no Recipe node is found.\n *\n * @example\n * const result = fromJsonLd(node)\n * if (!result.success) {\n * console.error(result.error.message)\n * // Recipe has 1 problem:\n * // ✖ name Add a name for the recipe.\n * // See https://recipe-book.siraj.page/errors/invalid_recipe\n * return\n * }\n * result.data.ingredients[0].items[0] // { id: 'flour', name: 'flour', quantity: { amount: 2, unit: 'cup' }, … }\n * result.extra.author // { '@type': 'Person', name: 'Ada' }\n */\nexport function fromJsonLd(input: unknown): FromJsonLdResult {\n const { recipes, ids } = locate([input])\n const [found] = recipes\n\n if (!found) {\n return {\n ...toResult<Recipe>('invalid_recipe', undefined, [\n issue(\n 'error',\n 'invalid_type',\n [],\n `Expected a schema.org Recipe, or an array, @graph, or mainEntity that holds one, received ${describeValue(input)}.`,\n ),\n ]),\n extra: {},\n }\n }\n\n const node = asRecord(found.node)!\n const reader: Reader = {\n recipe: found.node,\n ids,\n warnings: [],\n sources: new Map(),\n droppable: new Set(),\n images: new Map(),\n }\n const name = text(node.name)\n\n if (recipes.length > 1) {\n warning(\n reader.warnings,\n 'ignored_value',\n [],\n `Found ${recipes.length} recipes. Only the first${typeof name === 'string' ? `, ${quote(name)},` : ''} is read.`,\n )\n }\n\n const model = readRecipe(reader, node, found.path)\n const parse = () => parseRecipe(model, { parseIngredientLine })\n let result = parse()\n const dropped: RecipeIssue[] = []\n\n if (!result.success) {\n for (const error of result.error.issues) {\n const target =\n error.severity === 'error' ? droppableAt(reader, error.path) : undefined\n\n if (target) {\n remove(model, target)\n dropped.push({\n ...error,\n severity: 'warning',\n message: `${error.message} It is ignored.`,\n })\n }\n }\n if (dropped.length > 0) {\n result = parse()\n }\n }\n\n // An object's unknown fields sort after its other issues, as in parseRecipe.\n const order = ({ code, path }: RecipeIssue): number[] => [\n READ_FIELDS.indexOf(String(path[found.path.length])),\n ...path\n .slice(found.path.length + 1)\n .filter((key) => typeof key === 'number'),\n ...(code === 'unknown_field' ? [Infinity] : []),\n ]\n const issues = [\n ...reader.warnings,\n ...[\n ...dropped,\n ...(result.success ? result.warnings : result.error.issues),\n ].map((issue) => ({ ...issue, path: sourceOf(reader, issue.path) })),\n ].sort((a, b) => compareOrder(order(a), order(b)))\n\n return {\n ...toResult(\n 'invalid_recipe',\n result.success ? result.data : undefined,\n issues,\n typeof name === 'string' ? quote(name) : undefined,\n ),\n extra: Object.fromEntries(\n Object.entries(node).filter(([key]) => !READ_FIELDS.includes(key)),\n ),\n }\n}\n\n/** Builds `parseRecipe` input from a Recipe node, and links each value to its JSON-LD path. */\nfunction readRecipe(\n reader: Reader,\n node: Record<string, unknown>,\n base: Path,\n): Record<string, unknown> {\n const at = (...path: Path): Path => [...base, ...path]\n const model: Record<string, unknown> = {\n name: text(node.name),\n description: text(node.description),\n }\n link(reader, [], base)\n link(reader, ['name'], at('name'))\n link(reader, ['description'], at('description'), true)\n\n const image = readImage(reader, node.image, at('image'))\n if (image) {\n model.image = image.url\n link(reader, ['image'], image.source, true)\n }\n\n readYield(reader, model, node.recipeYield, at('recipeYield'))\n\n const times: Record<string, number> = {}\n for (const [key, field] of TIMES) {\n const minutes = readTime(reader, node[field], at(field))\n if (minutes !== undefined) {\n times[key] = minutes\n }\n }\n model.times = times\n\n model.ingredients = readList(\n reader,\n 'ingredients',\n node.recipeIngredient,\n at('recipeIngredient'),\n readLine,\n 'an ingredient line such as \"2 cups flour\"',\n )\n model.steps = readList(\n reader,\n 'steps',\n node.recipeInstructions,\n at('recipeInstructions'),\n readStep,\n 'a step such as \"Whisk the eggs.\" or a HowToStep',\n )\n model.equipment = readTools(reader, node.tool, at('tool'))\n model.nutrition = readNutrition(reader, node.nutrition, at('nutrition'))\n return model\n}\n\nfunction readImage(\n reader: Reader,\n value: unknown,\n source: Path,\n): Image | undefined {\n if (isAbsent(value)) {\n return undefined\n }\n\n const warnings = reader.warnings.length\n reader.repeated = false\n const image = findImage(reader, value, source)\n if (!image && !reader.repeated && reader.warnings.length === warnings) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)}.`,\n )\n }\n return image\n}\n\n/**\n * The first URL a recipe can hold in an image, a list of them, an\n * ImageObject, or a reference to one. Each URL before it with another scheme,\n * such as `data:`, and a reference to the recipe itself get a warning. Follows\n * one reference, so references that cycle end, and reads each node it reaches\n * once, so many references to one node cost one read and warn once.\n */\nfunction findImage(\n reader: Reader,\n value: unknown,\n source: Path,\n referenced = false,\n): Image | undefined {\n if (typeof value === 'string') {\n const url = cleanText(value)\n const scheme = disallowedScheme(url)\n if (scheme) {\n ignore(\n reader,\n 'invalid_url',\n source,\n `Expected an http or https URL, or a relative one, received ${quote(`${scheme}…`)}.`,\n )\n return undefined\n }\n return url ? { url, source } : undefined\n }\n\n if (source.length > MAX_DEPTH) {\n return undefined\n }\n\n if (Array.isArray(value)) {\n for (const [i, entry] of value.entries()) {\n const image = findImage(reader, entry, [...source, i], referenced)\n if (image) {\n return image\n }\n }\n return undefined\n }\n\n const record = asRecord(value)\n if (!record) {\n return undefined\n }\n\n const id = record['@id']\n const target =\n typeof id === 'string' && !referenced ? reader.ids.get(id) : undefined\n const image =\n findImage(reader, record.url, [...source, 'url'], referenced) ??\n findImage(reader, record.contentUrl, [...source, 'contentUrl'], referenced)\n\n if (image || !target) {\n return image\n }\n if (target.node === reader.recipe) {\n ignore(\n reader,\n 'invalid_type',\n source,\n 'Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself.',\n )\n return undefined\n }\n\n let read = reader.images.get(target)\n if (!read) {\n const warnings = reader.warnings.length\n const found = findImage(reader, target.node, target.path, true)\n read = [found, reader.warnings.length > warnings]\n reader.images.set(target, read)\n } else if (read[1]) {\n reader.repeated = true\n }\n return read[0]\n}\n\n/**\n * Reads servings and the yield from `recipeYield`'s values, and from the\n * comma-separated values in a string, as in \"4 servings, 24 cookies\". A bare\n * number equal to the yield's amount, as in WP Recipe Maker's `[\"28\", \"28-32\n * cookies\"]`, is the yield's count, not servings. The yield is the first one\n * with an amount, so \"One 9-inch pie, 8 slices\" yields 8 slices, but one whose\n * amount is a number comes before one whose amount is \"a\" or \"an\", so\n * `[\"a few\", \"24 cookies\"]` yields 24 cookies.\n */\nfunction readYield(\n reader: Reader,\n model: Record<string, unknown>,\n value: unknown,\n source: Path,\n): void {\n const entries: [unknown, Path][] = Array.isArray(value)\n ? value.map((entry: unknown, i) => [entry, [...source, i]])\n : [[value, source]]\n const batches = entries\n .flatMap(([entry, entrySource]) =>\n (typeof entry === 'string' ? entry.split(/,\\s+/) : [entry]).map((part) =>\n readBatch(reader, part, entrySource),\n ),\n )\n .filter((batch) => batch !== undefined)\n const yields = batches.filter((batch) => batch.yield !== undefined)\n const servings = batches.filter((batch) => batch.servings !== undefined)\n const kept = yields.find((batch) => !batch.article) ?? yields[0]\n const keptServings =\n servings.find((batch) => batch.worded) ??\n servings.find((batch) => batch.amount !== kept?.amount)\n\n for (const batch of servings) {\n if (\n keptServings &&\n batch.amount !== keptServings.amount &&\n (batch.worded || batch.amount !== kept?.amount)\n ) {\n warning(\n reader.warnings,\n 'ignored_value',\n batch.source,\n `Only one number of servings is read, ${quote(keptServings.label)}, so this one is ignored.`,\n )\n }\n }\n\n for (const batch of yields) {\n if (batch !== kept) {\n warning(\n reader.warnings,\n 'ignored_value',\n batch.source,\n `Only one yield is read, ${quote(kept!.label)}, so this one is ignored.`,\n )\n }\n }\n\n if (keptServings) {\n model.servings = keptServings.servings\n link(reader, ['servings'], keptServings.source, true)\n }\n if (kept) {\n model.yield = kept.yield\n link(reader, ['yield'], kept.source, true)\n }\n}\n\nfunction readBatch(\n reader: Reader,\n value: unknown,\n source: Path,\n): Batch | undefined {\n if (typeof value === 'number') {\n return { source, label: String(value), amount: value, servings: value }\n }\n\n if (typeof value === 'string') {\n const label = cleanText(value)\n\n if (!label) {\n return undefined\n }\n\n const text = label.replace(\n /^(?:(?:serves|servings?|yields?|makes)\\s*:?\\s*)?(?:about\\s+)?/i,\n '',\n )\n const match = matchAmount(text)\n\n if (!match || 'code' in match) {\n ignore(\n reader,\n match?.code ?? 'invalid_quantity',\n source,\n match?.message ??\n `Expected a batch such as \"24 cookies\" or \"3–4 loaves\", received ${describeValue(label)}.`,\n )\n return undefined\n }\n\n const amount =\n typeof match.amount === 'number' ? match.amount : match.amount.min\n const rest = match.rest.trim()\n\n return !rest || SERVINGS_WORD.test(rest)\n ? { source, label, amount, servings: match.amountText, worded: !!rest }\n : { source, label, amount, yield: text, article: /^an?\\s/i.test(text) }\n }\n\n const record = asRecord(value)\n const amount = toNumber(record?.value ?? record?.minValue)\n\n if (record && amount !== undefined) {\n const max = toNumber(record.maxValue)\n const unit =\n typeof record.unitText === 'string' ? cleanText(record.unitText) : ''\n const range = {\n amount,\n ...(max !== undefined && max !== amount && { max }),\n }\n const label = `${amount}${range.max ? `–${range.max}` : ''}${unit && ` ${unit}`}`\n\n return !unit || SERVINGS_WORD.test(unit)\n ? { source, label, amount, servings: range, worded: !!unit }\n : { source, label, amount, yield: { ...range, unit } }\n }\n\n if (!isAbsent(value)) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected a yield such as \"4 servings\" or \"24 cookies\", received ${describeValue(value)}.`,\n )\n }\n return undefined\n}\n\n/**\n * Reads an ISO 8601 duration. Zero, as in `PT0S`, counts as absent, and a time\n * longer than `Number.MAX_SAFE_INTEGER` minutes is ignored, as `toJsonLd`\n * ignores it.\n */\nfunction readTime(\n reader: Reader,\n value: unknown,\n source: Path,\n): number | undefined {\n if (isAbsent(value)) {\n return undefined\n }\n\n const minutes =\n typeof value === 'string' ? parseIsoDuration(cleanText(value)) : undefined\n\n if (minutes === undefined || minutes > Number.MAX_SAFE_INTEGER) {\n ignore(\n reader,\n 'invalid_duration',\n source,\n `Expected an ISO 8601 duration such as \"PT30M\", received ${describeValue(value)}${minutes === undefined ? '' : ', which is too long'}.`,\n )\n return undefined\n }\n\n return minutes > 0 ? minutes : undefined\n}\n\n/**\n * Reads `recipeIngredient` or `recipeInstructions`. A named `HowToSection` or\n * `ItemList` becomes a titled section, an unnamed or nested one adds its items\n * where it is, and a string adds one item per line.\n */\nfunction readList(\n reader: Reader,\n key: 'ingredients' | 'steps',\n value: unknown,\n source: Path,\n readItem: (\n reader: Reader,\n value: unknown,\n source: Path,\n ) => Item[] | undefined,\n expected: string,\n): unknown[] {\n const list: unknown[] = []\n link(reader, [key], source)\n\n const add = (\n items: unknown[],\n path: Path,\n entry: unknown,\n entrySource: Path,\n inSection: boolean,\n ): void => {\n if (isAbsent(entry)) {\n return\n }\n\n if (entrySource.length > MAX_DEPTH) {\n ignore(\n reader,\n 'invalid_type',\n entrySource,\n `Expected ${expected}, received ${describeValue(entry)} nested more than ${MAX_DEPTH} levels deep.`,\n )\n return\n }\n\n if (Array.isArray(entry)) {\n const positions = entry.map((element: unknown) =>\n toNumber(asRecord(element)?.position),\n )\n const order = [...entry.keys()]\n if (!positions.includes(undefined)) {\n order.sort((a, b) => positions[a]! - positions[b]!)\n }\n for (const i of order) {\n add(items, path, entry[i], [...entrySource, i], inSection)\n }\n return\n }\n\n const record = asRecord(entry)\n const elements = [...entrySource, 'itemListElement']\n\n if (record?.itemListElement !== undefined && record.text === undefined) {\n const title =\n typeof record.name === 'string' ? cleanText(record.name) : ''\n\n if (title && !inSection) {\n const section = { title, items: [] as unknown[] }\n const sectionPath = [key, list.length]\n list.push(section)\n link(reader, sectionPath, entrySource)\n link(reader, [...sectionPath, 'title'], [...entrySource, 'name'])\n add(\n section.items,\n [...sectionPath, 'items'],\n record.itemListElement,\n elements,\n true,\n )\n } else {\n add(items, path, record.itemListElement, elements, inSection)\n }\n return\n }\n\n const read = readItem(reader, entry, entrySource)\n\n if (!read) {\n ignore(\n reader,\n 'invalid_type',\n entrySource,\n `Expected ${expected}, received ${describeValue(entry)}.`,\n )\n return\n }\n\n for (const item of read) {\n const itemPath = [...path, items.length]\n items.push(item.value)\n link(reader, itemPath, item.source, true)\n\n for (const [field, fieldSource, droppable] of item.fields ?? []) {\n link(reader, [...itemPath, field], fieldSource, droppable)\n }\n }\n }\n\n add(list, [key], value, source, false)\n return list\n}\n\n/** Reads an ingredient line: text, a `PropertyValue`, or a `ListItem`. */\nfunction readLine(\n reader: Reader,\n value: unknown,\n source: Path,\n): Item[] | undefined {\n if (typeof value === 'string') {\n return cleanLines(value).map((line) => ({ value: line, source }))\n }\n\n const record = asRecord(value)\n\n if (record?.item !== undefined && source.length < MAX_DEPTH) {\n return readLine(reader, record.item, [...source, 'item'])\n }\n\n const parts = [record?.value, record?.unitText, record?.name ?? record?.text]\n .filter((part) => typeof part === 'string' || typeof part === 'number')\n .join(' ')\n\n return parts\n ? cleanLines(parts).map((line) => ({ value: line, source }))\n : undefined\n}\n\n/**\n * Reads a step: text, a `HowToStep`, or a `ListItem`. A step's name is kept\n * only when it says something its text doesn't, unlike \"Step 1\" or the start\n * of the text.\n */\nfunction readStep(\n reader: Reader,\n value: unknown,\n source: Path,\n): Item[] | undefined {\n if (typeof value === 'string') {\n return cleanLines(value).map((line) => ({ value: line, source }))\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return undefined\n }\n\n if (record.item !== undefined && source.length < MAX_DEPTH) {\n return readStep(reader, record.item, [...source, 'item'])\n }\n\n const name = typeof record.name === 'string' ? cleanText(record.name) : ''\n const body = text(record.text) ?? name\n const image = readImage(reader, record.image, [...source, 'image'])\n const lead = name.replace(/(?:…|\\.\\.\\.)$/, '').trim()\n const hasName =\n typeof body === 'string' &&\n !body.startsWith(lead) &&\n !/^step\\s*\\d*[.:]?$/i.test(name)\n const fields: NonNullable<Item['fields']> = [\n ['text', [...source, name && body === name ? 'name' : 'text'], false],\n ]\n\n if (image) {\n fields.push(['image', image.source, true])\n }\n\n return [\n {\n value: {\n text: body,\n ...(hasName && { name }),\n ...(image && { image: image.url }),\n },\n source,\n fields,\n },\n ]\n}\n\nfunction readTools(reader: Reader, value: unknown, source: Path): unknown[] {\n const entries: [unknown, Path][] = Array.isArray(value)\n ? value.map((entry: unknown, i) => [entry, [...source, i]])\n : [[value, source]]\n const equipment: unknown[] = []\n link(reader, ['equipment'], source)\n\n for (const [entry, entrySource] of entries) {\n const record = asRecord(entry)\n const item = record\n ? {\n name: text(record.name),\n ...(record.requiredQuantity !== undefined && {\n quantity:\n toNumber(\n asRecord(record.requiredQuantity)?.value ??\n record.requiredQuantity,\n ) ?? record.requiredQuantity,\n }),\n ...(record.url !== undefined && { url: text(record.url) }),\n }\n : text(entry)\n\n if (item === undefined) {\n continue\n }\n\n const path = ['equipment', equipment.length]\n equipment.push(item)\n link(reader, path, entrySource, true)\n link(reader, [...path, 'name'], [...entrySource, 'name'])\n link(\n reader,\n [...path, 'quantity'],\n [...entrySource, 'requiredQuantity'],\n true,\n )\n link(reader, [...path, 'url'], [...entrySource, 'url'], true)\n }\n\n return equipment\n}\n\nfunction readNutrition(\n reader: Reader,\n value: unknown,\n source: Path,\n): Record<string, unknown> | undefined {\n link(reader, ['nutrition'], source)\n\n if (isAbsent(value)) {\n return undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected NutritionInformation, such as { \"calories\": \"250 kcal\" }, received ${describeValue(value)}.`,\n )\n return undefined\n }\n\n const nutrition: Record<string, unknown> = {}\n const keys = Object.keys(NUTRIENTS) as (keyof Nutrition)[]\n\n for (const [field, amount] of Object.entries(record)) {\n const key = keys.find((key) => NUTRIENTS[key] === field)\n\n if (key) {\n nutrition[key] = text(amount)\n link(reader, ['nutrition', key], [...source, field], true)\n } else if (!field.startsWith('@')) {\n ignore(reader, 'unknown_field', [...source, field], 'Unknown field.')\n }\n }\n\n return nutrition\n}\n\n/** Cleaned text, with empty text and `null` as absent. Other values pass through for `parseRecipe` to report. */\nfunction text(value: unknown): unknown {\n return typeof value === 'string'\n ? cleanText(value) || undefined\n : (value ?? undefined)\n}\n\nfunction toNumber(value: unknown): number | undefined {\n const number =\n typeof value === 'string' && value.trim() ? Number(value) : value\n return typeof number === 'number' && Number.isFinite(number)\n ? number\n : undefined\n}\n\nfunction isAbsent(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n (typeof value === 'string' && !value.trim())\n )\n}\n\nfunction ignore(\n reader: Reader,\n code: RecipeIssueCode,\n path: Path,\n message: string,\n): void {\n warning(reader.warnings, code, path, `${message} It is ignored.`)\n}\n\nfunction link(\n reader: Reader,\n path: Path,\n source: Path,\n droppable = false,\n): void {\n const key = JSON.stringify(path)\n reader.sources.set(key, source)\n if (droppable) {\n reader.droppable.add(key)\n }\n}\n\n/** The JSON-LD path of a path in the recipe input, through the longest linked prefix. */\nfunction sourceOf(reader: Reader, path: Path): Path {\n for (let n = path.length; n >= 0; n--) {\n const source = reader.sources.get(JSON.stringify(path.slice(0, n)))\n if (source) {\n return source\n }\n }\n return []\n}\n\nfunction compareOrder(a: number[], b: number[]): number {\n for (const [i, n] of a.entries()) {\n const m = b[i]\n if (m === undefined) {\n return 1\n }\n if (n !== m) {\n return n - m\n }\n }\n return a.length - b.length\n}\n\nfunction droppableAt(reader: Reader, path: Path): Path | undefined {\n for (let n = path.length; n > 0; n--) {\n const at = path.slice(0, n)\n if (reader.droppable.has(JSON.stringify(at))) {\n return at\n }\n }\n return undefined\n}\n\n/**\n * Deletes a value from the recipe input, unless a value holding it is already\n * gone. A list keeps a hole, so later items keep their paths.\n */\nfunction remove(model: Record<string, unknown>, path: Path): void {\n const parent = path\n .slice(0, -1)\n .reduce<unknown>(\n (value, key) =>\n (value as Record<string | number, unknown> | undefined)?.[key],\n model,\n ) as Record<string | number, unknown> | undefined\n\n if (parent) {\n delete parent[path[path.length - 1]!]\n }\n}\n"],"mappings":"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","names":["issue","image","text","amount","key"]}
1
+ {"version":3,"sources":["../../src/schema-org/from-json-ld.ts"],"sourcesContent":["import {\n describeValue,\n quote,\n type RecipeIssue,\n type RecipeIssueCode,\n} from '../errors'\nimport { parseIngredientLine } from '../ingredient/parse'\nimport { issue, type Path, warning } from '../issues'\nimport { matchAmount } from '../quantity/parse'\nimport { parseIsoDuration } from '../recipe/duration'\nimport { parseRecipeWith } from '../recipe/parse'\nimport type { Nutrition, Recipe } from '../recipe/types'\nimport { disallowedScheme } from '../recipe/url'\nimport { asRecord, toResult } from '../validation'\nimport { entriesOf, type Located, locate, MAX_DEPTH } from './find'\nimport { NUTRIENTS } from './nutrients'\nimport { cleanLines, cleanText } from './text'\nimport type { FromJsonLdResult } from './types'\n\n/**\n * The node's fields that `fromJsonLd` reads or that core owns, so `extra`\n * leaves them out, in the order issues are reported.\n */\nconst READ_FIELDS: readonly string[] = [\n '@context',\n '@type',\n 'name',\n 'description',\n 'image',\n 'recipeYield',\n 'prepTime',\n 'cookTime',\n 'totalTime',\n 'recipeIngredient',\n 'recipeInstructions',\n 'tool',\n 'nutrition',\n]\n\n/**\n * The longest image URL kept. Each step that refers to an image node holds\n * its own copy of the URL, so the bound keeps the recipe's JSON from growing\n * with the URL's length times the number of steps.\n */\nconst MAX_URL_LENGTH = 4096\n\nconst SERVINGS_WORD =\n /^(?:servings?|portions?|(?:serving|portion)\\(s\\)|people|persons?)\\.?$/i\n\ntype Reader = {\n /** The Recipe node, as passed in. */\n recipe: Record<string, unknown>\n ids: Map<string, Located>\n warnings: RecipeIssue[]\n /** The JSON-LD path of each value in the recipe input, keyed by its path there. */\n sources: Map<string, Path>\n /** The recipe input paths of the values a failed parse drops. */\n droppable: Set<string>\n /**\n * Each node's reads as an image: the image it holds, how many warnings\n * reading it gave, how far below the node the read went, and the path length\n * it was read at. Read directly, and read through a reference, which doesn't\n * follow another.\n */\n images: [direct: Map<unknown, Read[]>, referenced: Map<unknown, Read[]>]\n /** How many warnings reading those nodes again would have repeated. */\n repeated: number\n /**\n * The longest path the current read has gone to. Only the path's length\n * decides where `MAX_DEPTH` cuts a read off, so a read is reused at the\n * length it was read at, and one that stayed within the limit also anywhere\n * reading the node again would, so reusing a read never changes the result.\n */\n deepest: number\n}\n\ntype Image = { url: string; source: Path }\n\ntype Read = [\n image: Image | undefined,\n warnings: number,\n depth: number,\n length: number,\n]\n\n/** A value read from `recipeYield`. */\ntype Batch = {\n source: Path\n /** The value as text, to name it in a message. */\n label: string\n /** The amount, or the low end of its range. */\n amount: number\n servings?: unknown\n /** Servings with a word, such as \"4 servings\", not a bare number. */\n worded?: boolean\n yield?: unknown\n /** The yield's amount is \"a\" or \"an\", as in \"a few\", not a number. */\n article?: boolean\n}\n\n/** A list item read from JSON-LD, and where its fields came from. */\ntype Item = {\n value: unknown\n source: Path\n fields: [field: string, source: Path, droppable: boolean][]\n}\n\n/**\n * Reads a schema.org Recipe from JSON-LD, as recipe sites publish it for\n * search engines: a Recipe node, or an array, `@graph`, `mainEntity`, or\n * `ItemList` that holds one, such as a node from `findRecipeJsonLd` or\n * `JSON.parse` of a script tag. An `ItemList` is read only when no other\n * Recipe is found, as in `findRecipeJsonLd`. Never throws for bad data; an\n * error thrown by a getter or proxy in the input passes through.\n *\n * Every string has its tags removed, then its HTML entities decoded, so an\n * encoded tag such as `&lt;b&gt;` stays as text. Ingredient lines are read\n * with `parseIngredientLine`. Issue paths point into the JSON-LD, such as\n * `recipeIngredient[3]`.\n *\n * Reading is forgiving. A value the recipe can't hold, such as a `data:`\n * image or nutrition of \"N/A\", is left out with a warning that keeps its usual\n * code and ends \"It is ignored.\" A second Recipe node, a second number of\n * servings, or a second yield is an `ignored_value` warning. The result fails\n * only when there is no Recipe node, the recipe's name is missing or isn't\n * text, or a problem remains that leaving a value out can't fix.\n *\n * @returns The recipe or the error, and `extra`: the node's fields the recipe\n * doesn't model, such as `author` and `keywords`, as they were. A field that\n * nests more than 32 levels deep, counted from the top of `input`, is left\n * out, with a warning. Pass `extra` to `toJsonLd` to write them back. It is\n * `{}` when no Recipe node is found.\n *\n * @example\n * const result = fromJsonLd(node)\n * if (!result.success) {\n * console.error(result.error.message)\n * // Recipe has 1 problem:\n * // ✖ name Add a name for the recipe.\n * // See https://recipe-book.siraj.page/errors/invalid_recipe/\n * return\n * }\n * result.data.ingredients[0].items[0] // { id: 'flour', name: 'flour', quantity: { amount: 2, unit: 'cup' }, … }\n * result.extra.author // { '@type': 'Person', name: 'Ada' }\n */\nexport function fromJsonLd(input: unknown): FromJsonLdResult {\n const { recipes, ids } = locate([input])\n const [found] = recipes\n\n if (!found) {\n return {\n ...toResult<Recipe>('invalid_recipe', undefined, [\n issue(\n 'error',\n 'invalid_type',\n [],\n `Expected a schema.org Recipe, or an array, @graph, mainEntity, or ItemList that holds one, received ${describeValue(input)}.`,\n ),\n ]),\n extra: {},\n }\n }\n\n const node = asRecord(found.node)!\n const reader: Reader = {\n recipe: found.node,\n ids,\n warnings: [],\n sources: new Map(),\n droppable: new Set(),\n images: [new Map(), new Map()],\n repeated: 0,\n deepest: 0,\n }\n const name = text(node.name)\n\n if (recipes.length > 1) {\n warning(\n reader.warnings,\n 'ignored_value',\n [],\n `Found ${recipes.length} recipes. Only the first${typeof name === 'string' ? `, ${quote(name)},` : ''} is read.`,\n )\n }\n\n const model = readRecipe(reader, node, found.path)\n const parse = () => parseRecipeWith(model, parseIngredientLine)\n let result = parse()\n const dropped: RecipeIssue[] = []\n\n if (!result.success) {\n const targets: Path[] = []\n for (const error of result.error.issues) {\n const target =\n error.severity === 'error' ? droppableAt(reader, error.path) : undefined\n\n if (target) {\n targets.push(target)\n dropped.push({\n ...error,\n severity: 'warning',\n message: `${error.message} It is ignored.`,\n })\n }\n }\n // Deepest first, so no removal walks through a hole an earlier one left,\n // where a polluted `Object.prototype` would stand in for the missing item.\n targets\n .sort((a, b) => b.length - a.length)\n .forEach((target) => remove(model, target))\n if (dropped.length > 0) {\n result = parse()\n }\n }\n\n // An object's unknown fields sort after its other issues, as in parseRecipe.\n const order = ({ code, path }: RecipeIssue): number[] => [\n path.length > found.path.length\n ? READ_FIELDS.indexOf(String(path[found.path.length]))\n : -1,\n ...path\n .slice(found.path.length + 1)\n .filter((key) => typeof key === 'number'),\n ...(code === 'unknown_field' ? [Infinity] : []),\n ]\n const issues = [\n ...reader.warnings,\n ...[\n ...dropped,\n ...(result.success ? result.warnings : result.error.issues),\n ].map((issue) => ({ ...issue, path: sourceOf(reader, issue.path) })),\n ].sort((a, b) => compareOrder(order(a), order(b)))\n\n const checked = new Map<object, number>()\n const extra = Object.fromEntries(\n Object.entries(node).filter(([key, value]) => {\n if (READ_FIELDS.includes(key)) {\n return false\n }\n const path = [...found.path, key]\n if (tooDeep(value, path.length, checked)) {\n warning(\n issues,\n 'invalid_type',\n path,\n `Expected a value to keep in extra, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep. It is ignored.`,\n )\n return false\n }\n return true\n }),\n )\n\n return {\n ...toResult(\n 'invalid_recipe',\n result.success ? result.data : undefined,\n issues,\n typeof name === 'string' ? quote(name) : undefined,\n ),\n extra,\n }\n}\n\n/**\n * Whether a value at a JSON-LD path of `length` keys, or a value it holds, is\n * more than `MAX_DEPTH` keys deep, so `extra` stays shallow enough to\n * serialize. `checked` holds the longest path each object was found shallow\n * at, so objects shared in the input are read a bounded number of times.\n */\nfunction tooDeep(\n value: unknown,\n length: number,\n checked: Map<object, number>,\n): boolean {\n if (length > MAX_DEPTH) {\n return true\n }\n if (\n typeof value !== 'object' ||\n value === null ||\n (checked.get(value) ?? 0) >= length\n ) {\n return false\n }\n const deep = Object.values(value).some((child) =>\n tooDeep(child, length + 1, checked),\n )\n if (!deep) {\n checked.set(value, length)\n }\n return deep\n}\n\n/** Builds `parseRecipe` input from a Recipe node, and links each value to its JSON-LD path. */\nfunction readRecipe(\n reader: Reader,\n node: Record<string, unknown>,\n base: Path,\n): Record<string, unknown> {\n const at = (...path: Path): Path => [...base, ...path]\n const model: Record<string, unknown> = {\n name: text(node.name),\n description: text(node.description),\n }\n link(reader, [], base)\n link(reader, ['name'], at('name'))\n link(reader, ['description'], at('description'), true)\n\n const image = readImage(reader, node.image, at('image'))\n if (image) {\n model.image = image.url\n link(reader, ['image'], image.source, true)\n }\n\n readYield(reader, model, node.recipeYield, at('recipeYield'))\n\n const times: Record<string, number> = {}\n for (const key of ['prep', 'cook', 'total']) {\n const field = `${key}Time`\n const minutes = readTime(reader, node[field], at(field))\n if (minutes !== undefined) {\n times[key] = minutes\n }\n }\n model.times = times\n\n model.ingredients = readList(\n reader,\n 'ingredients',\n node.recipeIngredient,\n at('recipeIngredient'),\n readLine,\n 'an ingredient line such as \"2 cups flour\"',\n )\n model.steps = readList(\n reader,\n 'steps',\n node.recipeInstructions,\n at('recipeInstructions'),\n readStep,\n 'a step such as \"Whisk the eggs.\" or a HowToStep',\n )\n model.equipment = readTools(reader, node.tool, at('tool'))\n model.nutrition = readNutrition(reader, node.nutrition, at('nutrition'))\n return model\n}\n\nfunction readImage(\n reader: Reader,\n value: unknown,\n source: Path,\n): Image | undefined {\n if (isAbsent(value)) {\n return undefined\n }\n\n const warnings = reader.warnings.length + reader.repeated\n const image = findImage(reader, value, source)\n if (!image && reader.warnings.length + reader.repeated === warnings) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)}.`,\n )\n }\n return image\n}\n\n/**\n * The first URL a recipe can hold in an image, a list of them, an\n * ImageObject, or a reference to one. Each URL before it that's longer than\n * `MAX_URL_LENGTH` or has another scheme, such as `data:`, each value more\n * than `MAX_DEPTH` levels deep, and a reference to the recipe itself get a\n * warning. Follows one reference, so references that cycle end. A node that\n * several images reference or hold is read and warns once, where it's first\n * read, or near `MAX_DEPTH`, where how deep a read starts decides where it's\n * cut off, once at each depth.\n */\nfunction findImage(\n reader: Reader,\n value: unknown,\n source: Path,\n referenced = false,\n): Image | undefined {\n reader.deepest = Math.max(reader.deepest, source.length)\n if (source.length > MAX_DEPTH) {\n if (!isAbsent(value)) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep.`,\n )\n }\n return undefined\n }\n if (typeof value === 'string') {\n const url = cleanText(value)\n const scheme = disallowedScheme(url)\n if (scheme) {\n ignore(\n reader,\n 'invalid_url',\n source,\n `Expected an http or https URL, or a relative one, received ${quote(`${scheme}…`)}.`,\n )\n return undefined\n }\n if (url.length > MAX_URL_LENGTH) {\n ignore(\n reader,\n 'invalid_url',\n source,\n `Expected a URL of at most ${MAX_URL_LENGTH} UTF-16 code units, received ${url.length}.`,\n )\n return undefined\n }\n return url ? { url, source } : undefined\n }\n\n if (Array.isArray(value)) {\n for (const [entry, entrySource] of entriesOf(value, source)) {\n const image = findImage(reader, entry, entrySource, referenced)\n if (image) {\n return image\n }\n }\n return undefined\n }\n\n const images = reader.images[referenced ? 1 : 0]\n const reads = images.get(value) ?? []\n const read = reads.find(\n ([, , depth, length]) =>\n length === source.length ||\n Math.max(length, source.length) + depth <= MAX_DEPTH,\n )\n if (read) {\n reader.repeated += read[1]\n reader.deepest = Math.max(reader.deepest, source.length + read[2])\n return read[0]\n }\n\n const record = asRecord(value)\n if (!record) {\n return undefined\n }\n\n images.set(value, reads)\n const warnings = reader.warnings.length + reader.repeated\n const deepest = reader.deepest\n reader.deepest = source.length\n const id = record['@id']\n const target =\n typeof id === 'string' && !referenced ? reader.ids.get(id) : undefined\n let image =\n findImage(reader, record.url, [...source, 'url'], referenced) ??\n findImage(reader, record.contentUrl, [...source, 'contentUrl'], referenced)\n\n if (!image && target) {\n if (target.node === reader.recipe) {\n ignore(\n reader,\n 'invalid_type',\n source,\n 'Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself.',\n )\n } else {\n image = findImage(reader, target.node, target.path, true)\n }\n }\n reads.push([\n image,\n reader.warnings.length + reader.repeated - warnings,\n reader.deepest - source.length,\n source.length,\n ])\n reader.deepest = Math.max(reader.deepest, deepest)\n return image\n}\n\n/**\n * Reads servings and the yield from `recipeYield`'s values, and from the\n * comma-separated values in a string, as in \"4 servings, 24 cookies\". A bare\n * number equal to the yield's amount, as in WP Recipe Maker's `[\"28\", \"28-32\n * cookies\"]`, is the yield's count, not servings. The yield is the first one\n * with an amount, so \"One 9-inch pie, 8 slices\" yields 8 slices, but one whose\n * amount is a number comes before one whose amount is \"a\" or \"an\", so\n * `[\"a few\", \"24 cookies\"]` yields 24 cookies.\n */\nfunction readYield(\n reader: Reader,\n model: Record<string, unknown>,\n value: unknown,\n source: Path,\n): void {\n const batches = entriesOf(value, source)\n .flatMap(([entry, entrySource]) =>\n (typeof entry === 'string' ? entry.split(/,\\s+/) : [entry]).map((part) =>\n readBatch(reader, part, entrySource),\n ),\n )\n .filter((batch) => batch !== undefined)\n const yields = batches.filter((batch) => batch.yield !== undefined)\n const servings = batches.filter((batch) => batch.servings !== undefined)\n const kept =\n yields.find((batch) => !batch.article) ??\n yields.find((batch) => batch.article)\n const keptServings =\n servings.find((batch) => batch.worded) ??\n servings.find((batch) => batch.amount !== kept?.amount)\n\n for (const batch of servings) {\n if (\n keptServings &&\n batch.amount !== keptServings.amount &&\n (batch.worded || batch.amount !== kept?.amount)\n ) {\n warning(\n reader.warnings,\n 'ignored_value',\n batch.source,\n `Only one number of servings is read, ${quote(keptServings.label)}, so this one is ignored.`,\n )\n }\n }\n\n for (const batch of yields) {\n if (batch !== kept) {\n warning(\n reader.warnings,\n 'ignored_value',\n batch.source,\n `Only one yield is read, ${quote(kept!.label)}, so this one is ignored.`,\n )\n }\n }\n\n if (keptServings) {\n model.servings = keptServings.servings\n link(reader, ['servings'], keptServings.source, true)\n }\n if (kept) {\n model.yield = kept.yield\n link(reader, ['yield'], kept.source, true)\n }\n}\n\nfunction readBatch(\n reader: Reader,\n value: unknown,\n source: Path,\n): Batch | undefined {\n if (typeof value === 'number') {\n return { source, label: String(value), amount: value, servings: value }\n }\n\n if (typeof value === 'string') {\n const label = cleanText(value)\n\n if (!label) {\n return undefined\n }\n\n const text = label.replace(\n /^(?:(?:serves|servings?|yields?|makes)\\s*:?\\s*)?(?:about\\s+)?/i,\n '',\n )\n const match = matchAmount(text)\n\n if (!match || 'code' in match) {\n ignore(\n reader,\n match?.code ?? 'invalid_quantity',\n source,\n match?.message ??\n `Expected a batch such as \"24 cookies\" or \"3–4 loaves\", received ${describeValue(label)}.`,\n )\n return undefined\n }\n\n const amount =\n typeof match.amount === 'number' ? match.amount : match.amount.min\n const rest = match.rest.trim()\n\n return !rest || SERVINGS_WORD.test(rest)\n ? { source, label, amount, servings: match.amountText, worded: !!rest }\n : { source, label, amount, yield: text, article: /^an?\\s/i.test(text) }\n }\n\n const record = asRecord(value)\n const amount = toNumber(record?.value ?? record?.minValue)\n\n if (record && amount !== undefined) {\n const max = toNumber(record.maxValue)\n const unit =\n typeof record.unitText === 'string' ? cleanText(record.unitText) : ''\n const range = {\n amount,\n ...(max !== undefined && max !== amount && { max }),\n }\n const label = `${amount}${max !== amount && max ? `–${max}` : ''}${unit && ` ${unit}`}`\n\n return !unit || SERVINGS_WORD.test(unit)\n ? { source, label, amount, servings: range, worded: !!unit }\n : { source, label, amount, yield: { ...range, unit } }\n }\n\n if (!isAbsent(value)) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected a yield such as \"4 servings\" or \"24 cookies\", received ${describeValue(value)}.`,\n )\n }\n return undefined\n}\n\n/**\n * Reads an ISO 8601 duration. Zero, as in `PT0S`, counts as absent, and a time\n * longer than `Number.MAX_SAFE_INTEGER` minutes is ignored, as `toJsonLd`\n * ignores it.\n */\nfunction readTime(\n reader: Reader,\n value: unknown,\n source: Path,\n): number | undefined {\n if (isAbsent(value)) {\n return undefined\n }\n\n const minutes =\n typeof value === 'string' ? parseIsoDuration(cleanText(value)) : undefined\n\n if (minutes === undefined || minutes > Number.MAX_SAFE_INTEGER) {\n ignore(\n reader,\n 'invalid_duration',\n source,\n `Expected an ISO 8601 duration such as \"PT30M\", received ${describeValue(value)}${minutes === undefined ? '' : ', which is too long'}.`,\n )\n return undefined\n }\n\n return minutes > 0 ? minutes : undefined\n}\n\n/**\n * Reads `recipeIngredient` or `recipeInstructions`. A named `HowToSection` or\n * `ItemList` becomes a titled section, an unnamed or nested one adds its items\n * where it is, a `ListItem` stands for its `item`, and a string adds one item\n * per line.\n */\nfunction readList(\n reader: Reader,\n key: 'ingredients' | 'steps',\n value: unknown,\n source: Path,\n readItem: (\n reader: Reader,\n value: unknown,\n source: Path,\n ) => Item[] | undefined,\n expected: string,\n): unknown[] {\n const list: unknown[] = []\n link(reader, [key], source)\n\n const add = (\n items: unknown[],\n path: Path,\n entry: unknown,\n entrySource: Path,\n inSection: boolean,\n ): void => {\n if (isAbsent(entry)) {\n return\n }\n\n if (entrySource.length > MAX_DEPTH) {\n ignore(\n reader,\n 'invalid_type',\n entrySource,\n `Expected ${expected}, received ${describeValue(entry)} nested more than ${MAX_DEPTH} levels deep.`,\n )\n return\n }\n\n if (Array.isArray(entry)) {\n const elements = entriesOf(entry, entrySource)\n const positions = elements.map(([element]) =>\n toNumber(asRecord(element)?.position),\n )\n const order = [...elements.keys()]\n if (!positions.includes(undefined)) {\n order.sort((a, b) => positions[a]! - positions[b]!)\n }\n for (const i of order) {\n const [element, elementSource] = elements[i]!\n add(items, path, element, elementSource, inSection)\n }\n return\n }\n\n const record = asRecord(entry)\n const elements = [...entrySource, 'itemListElement']\n\n if (record?.itemListElement !== undefined && record.text === undefined) {\n const title =\n typeof record.name === 'string' ? cleanText(record.name) : ''\n\n if (title && !inSection) {\n const section = { title, items: [] as unknown[] }\n const sectionPath = [key, list.length]\n list.push(section)\n link(reader, sectionPath, entrySource)\n link(reader, [...sectionPath, 'title'], [...entrySource, 'name'])\n add(\n section.items,\n [...sectionPath, 'items'],\n record.itemListElement,\n elements,\n true,\n )\n } else {\n add(items, path, record.itemListElement, elements, inSection)\n }\n return\n }\n\n if (record && !isAbsent(record.item)) {\n add(items, path, record.item, [...entrySource, 'item'], inSection)\n return\n }\n\n const read = readItem(reader, entry, entrySource)\n\n if (!read) {\n ignore(\n reader,\n 'invalid_type',\n entrySource,\n `Expected ${expected}, received ${describeValue(entry)}.`,\n )\n return\n }\n\n for (const item of read) {\n const itemPath = [...path, items.length]\n items.push(item.value)\n link(reader, itemPath, item.source, true)\n\n for (const [field, fieldSource, droppable] of item.fields) {\n link(reader, [...itemPath, field], fieldSource, droppable)\n }\n }\n }\n\n add(list, [key], value, source, false)\n return list\n}\n\n/** Reads an ingredient line: text or a `PropertyValue`. */\nfunction readLine(\n reader: Reader,\n value: unknown,\n source: Path,\n): Item[] | undefined {\n const record = asRecord(value)\n const parts =\n typeof value === 'string'\n ? value\n : [record?.value, record?.unitText, record?.name ?? record?.text]\n .filter(\n (part) => typeof part === 'string' || typeof part === 'number',\n )\n .join(' ')\n\n return parts\n ? cleanLines(parts).map((line) => ({ value: line, source, fields: [] }))\n : undefined\n}\n\n/**\n * Reads a step: text or a `HowToStep`. A step's name is kept\n * only when it says something its text doesn't, unlike \"Step 1\" or the start\n * of the text.\n */\nfunction readStep(\n reader: Reader,\n value: unknown,\n source: Path,\n): Item[] | undefined {\n if (typeof value === 'string') {\n return cleanLines(value).map((line) => ({\n value: line,\n source,\n fields: [],\n }))\n }\n\n const record = asRecord(value)\n\n if (!record) {\n return undefined\n }\n\n const name = typeof record.name === 'string' ? cleanText(record.name) : ''\n const body = text(record.text) ?? name\n const image = readImage(reader, record.image, [...source, 'image'])\n const lead = name.replace(/(?:…|\\.\\.\\.)$/, '').trim()\n const hasName =\n typeof body === 'string' &&\n !body.startsWith(lead) &&\n !/^step\\s*\\d*[.:]?$/i.test(name)\n const fields: Item['fields'] = [\n ['text', [...source, name && body === name ? 'name' : 'text'], false],\n ]\n\n if (image) {\n fields.push(['image', image.source, true])\n }\n\n return [\n {\n value: {\n text: body,\n ...(hasName && { name }),\n ...(image && { image: image.url }),\n },\n source,\n fields,\n },\n ]\n}\n\nfunction readTools(reader: Reader, value: unknown, source: Path): unknown[] {\n const equipment: unknown[] = []\n link(reader, ['equipment'], source)\n\n for (const [entry, entrySource] of entriesOf(value, source)) {\n const record = asRecord(entry)\n const item = record\n ? {\n name: text(record.name),\n ...(record.requiredQuantity !== undefined && {\n quantity:\n toNumber(\n asRecord(record.requiredQuantity)?.value ??\n record.requiredQuantity,\n ) ?? record.requiredQuantity,\n }),\n ...(record.url !== undefined && { url: text(record.url) }),\n }\n : text(entry)\n\n if (item === undefined) {\n continue\n }\n\n const path = ['equipment', equipment.length]\n equipment.push(item)\n link(reader, path, entrySource, true)\n link(reader, [...path, 'name'], [...entrySource, 'name'])\n link(\n reader,\n [...path, 'quantity'],\n [...entrySource, 'requiredQuantity'],\n true,\n )\n link(reader, [...path, 'url'], [...entrySource, 'url'], true)\n }\n\n return equipment\n}\n\nfunction readNutrition(\n reader: Reader,\n value: unknown,\n source: Path,\n): Record<string, unknown> | undefined {\n link(reader, ['nutrition'], source)\n\n if (isAbsent(value)) {\n return undefined\n }\n\n const record = asRecord(value)\n\n if (!record) {\n ignore(\n reader,\n 'invalid_type',\n source,\n `Expected NutritionInformation, such as { \"calories\": \"250 kcal\" }, received ${describeValue(value)}.`,\n )\n return undefined\n }\n\n const nutrition: Record<string, unknown> = {}\n const keys = Object.keys(NUTRIENTS) as (keyof Nutrition)[]\n\n for (const [field, amount] of Object.entries(record)) {\n const key = keys.find((key) => NUTRIENTS[key] === field)\n\n if (key) {\n nutrition[key] = text(amount)\n link(reader, ['nutrition', key], [...source, field], true)\n } else if (!field.startsWith('@')) {\n ignore(reader, 'unknown_field', [...source, field], 'Unknown field.')\n }\n }\n\n return nutrition\n}\n\n/** Cleaned text, with empty text and `null` as absent. Other values pass through for `parseRecipe` to report. */\nfunction text(value: unknown): unknown {\n return typeof value === 'string'\n ? cleanText(value) || undefined\n : (value ?? undefined)\n}\n\nfunction toNumber(value: unknown): number | undefined {\n const number =\n typeof value === 'string' && value.trim() ? Number(value) : value\n return typeof number === 'number' && Number.isFinite(number)\n ? number\n : undefined\n}\n\nfunction isAbsent(value: unknown): boolean {\n return (\n value === undefined ||\n value === null ||\n (typeof value === 'string' && !value.trim())\n )\n}\n\nfunction ignore(\n reader: Reader,\n code: RecipeIssueCode,\n path: Path,\n message: string,\n): void {\n warning(reader.warnings, code, path, `${message} It is ignored.`)\n}\n\nfunction link(\n reader: Reader,\n path: Path,\n source: Path,\n droppable = false,\n): void {\n const key = JSON.stringify(path)\n reader.sources.set(key, source)\n if (droppable) {\n reader.droppable.add(key)\n }\n}\n\n/** The JSON-LD path of a path in the recipe input, through the longest linked prefix. */\nfunction sourceOf(reader: Reader, path: Path): Path {\n for (let n = path.length; n >= 0; n--) {\n const source = reader.sources.get(JSON.stringify(path.slice(0, n)))\n if (source) {\n return source\n }\n }\n return []\n}\n\nfunction compareOrder(a: number[], b: number[]): number {\n for (const [i, n] of a.entries()) {\n if (i === b.length) {\n return 1\n }\n const m = b[i]!\n if (n !== m) {\n return n - m\n }\n }\n return a.length - b.length\n}\n\nfunction droppableAt(reader: Reader, path: Path): Path | undefined {\n for (let n = path.length; n > 0; n--) {\n const at = path.slice(0, n)\n if (reader.droppable.has(JSON.stringify(at))) {\n return at\n }\n }\n return undefined\n}\n\n/**\n * Deletes a value from the recipe input, unless a value holding it is already\n * gone. A list keeps a hole, so later items keep their paths.\n */\nfunction remove(model: Record<string, unknown>, path: Path): void {\n const parent = path\n .slice(0, -1)\n .reduce<unknown>(\n (value, key) =>\n (value as Record<string | number, unknown> | undefined)?.[key],\n model,\n ) as Record<string | number, unknown> | undefined\n\n if (parent) {\n delete parent[path[path.length - 1]!]\n }\n}\n"],"mappings":"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","names":["issue","image","text","amount","elements","key"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/schema-org/text.ts"],"sourcesContent":["import { hasOwn } from '../issues'\n\nconst NAMED_ENTITIES: Readonly<Record<string, string>> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n ensp: ' ',\n emsp: ' ',\n thinsp: ' ',\n shy: '',\n zwj: '',\n zwnj: '',\n deg: '°',\n colon: ':',\n Tab: '\\t',\n NewLine: '\\n',\n euro: '€',\n pound: '£',\n yen: '¥',\n cent: '¢',\n half: '½',\n frac12: '½',\n frac14: '¼',\n frac34: '¾',\n frac13: '⅓',\n frac23: '⅔',\n frac15: '⅕',\n frac25: '⅖',\n frac35: '⅗',\n frac45: '⅘',\n frac16: '⅙',\n frac56: '⅚',\n frac18: '⅛',\n frac38: '⅜',\n frac58: '⅝',\n frac78: '⅞',\n frasl: '⁄',\n ndash: '–',\n mdash: '—',\n minus: '−',\n hellip: '…',\n lsquo: '‘',\n rsquo: '’',\n sbquo: '‚',\n ldquo: '“',\n rdquo: '”',\n bdquo: '„',\n laquo: '«',\n raquo: '»',\n times: '×',\n divide: '÷',\n middot: '·',\n bull: '•',\n micro: 'µ',\n sup1: '¹',\n sup2: '²',\n sup3: '³',\n ordm: 'º',\n ordf: 'ª',\n copy: '©',\n reg: '®',\n trade: '™',\n iexcl: '¡',\n iquest: '¿',\n szlig: 'ß',\n aelig: 'æ',\n AElig: 'Æ',\n oelig: 'œ',\n OElig: 'Œ',\n oslash: 'ø',\n Oslash: 'Ø',\n}\n\n// `&eacute;` and the like decode to the letter plus a combining mark, then NFC.\nconst MARKS: Readonly<Record<string, string>> = {\n grave: '\\u0300',\n acute: '\\u0301',\n circ: '\\u0302',\n tilde: '\\u0303',\n uml: '\\u0308',\n ring: '\\u030A',\n cedil: '\\u0327',\n}\n\n// A comment, a script or style element with its content, or a tag, with its\n// name captured if it's a block tag. As in a browser, a quote starts an\n// attribute value only right after `=`. A value can hold a `>`, and a `<`\n// that can't start a tag, as in `a < b`. A tag with no `>` is tried again at\n// each `<` in it, so the attributes end at any `<` that can start a tag, which\n// keeps each try short. They're read once, in a lookahead, then matched by\n// backreference, so a failed tag doesn't retry other splits of them, which\n// would be exponential. A literal, unlike `new RegExp`, lets bundlers drop it.\nconst MARKUP =\n /<!--[\\s\\S]*?(?:-->|$)|<(script|style)(?=[\\s/>])(?=((?:=\\s*(?:\"(?:[^\"<]|<(?![a-z/!?]))*\"|'(?:[^'<]|<(?![a-z/!?]))*')|[^<>])*))\\2>[\\s\\S]*?(?:<\\/\\1\\s*>|$)|<(?:(\\/?(?:br|p|li|div|h[1-6])\\b)|[/!?]?[a-z])(?=((?:=\\s*(?:\"(?:[^\"<]|<(?![a-z/!?]))*\"|'(?:[^'<]|<(?![a-z/!?]))*')|[^<>])*))\\4>/gi\n\n// Removing a tag can join the text around it into a new one, as in\n// `<scr<b></b>ipt>`, so stripping repeats. Real markup needs a pass or two;\n// the cap keeps hostile nesting linear.\nconst MAX_PASSES = 8\n\n/**\n * JSON-LD text with its tags removed, then its HTML entities decoded, on one\n * line with single spaces. An encoded tag, such as `&lt;b&gt;`, stays as text.\n */\nexport function cleanText(text: string): string {\n return cleanLines(text).join(' ')\n}\n\n/** {@link cleanText} for each line, split at line breaks and block tags. Empty lines are left out. */\nexport function cleanLines(text: string): string[] {\n return stripTags(text)\n .replace(\n /&(?:#(\\d{1,7})|#x([\\da-f]{1,6})|([a-z][a-z\\d]*));/gi,\n (entity, decimal?: string, hex?: string, name?: string) => {\n if (name === undefined) {\n const code = decimal === undefined ? parseInt(hex!, 16) : +decimal\n return code > 0 &&\n code <= 0x10ffff &&\n (code < 0xd800 || code > 0xdfff)\n ? String.fromCodePoint(code)\n : entity\n }\n\n if (hasOwn(NAMED_ENTITIES, name)) {\n return NAMED_ENTITIES[name]!\n }\n\n const accent = /^([aceinouyACEINOUY])([a-z]+)$/.exec(name)\n return accent && hasOwn(MARKS, accent[2]!)\n ? `${accent[1]}${MARKS[accent[2]!]}`.normalize('NFC')\n : entity\n },\n )\n .split(/[\\n\\r\\u2028\\u2029]/)\n .map((line) => line.replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n}\n\n/**\n * Removes comments, tags, and the content of `script` and `style`, and ends a\n * line at block tags. Strips again until nothing changes, then removes any `<`\n * that could still open a tag: one before a letter, `/`, `!`, or `?` with a\n * `>` after it, as in `<a <>`. A `<` with no `>` after it, as in `x<y`, stays.\n */\nfunction stripTags(text: string): string {\n let stripped = text\n\n for (let pass = 0; pass < MAX_PASSES; pass++) {\n const next = stripped.replace(\n MARKUP,\n (_, _element, _attributes, block?: string) => (block ? '\\n' : ''),\n )\n if (next === stripped) {\n break\n }\n stripped = next\n }\n\n const end = stripped.lastIndexOf('>') + 1\n return (\n stripped\n .slice(0, end)\n .replace(/<+([/!?a-z]?)/gi, (run, next: string) => next || run) +\n stripped.slice(end)\n )\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/schema-org/text.ts"],"sourcesContent":["import { hasOwn } from '../issues'\n\nconst NAMED_ENTITIES: Readonly<Record<string, string>> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n ensp: ' ',\n emsp: ' ',\n thinsp: ' ',\n shy: '',\n zwj: '',\n zwnj: '',\n deg: '°',\n colon: ':',\n Tab: '\\t',\n NewLine: '\\n',\n euro: '€',\n pound: '£',\n yen: '¥',\n cent: '¢',\n half: '½',\n frac12: '½',\n frac14: '¼',\n frac34: '¾',\n frac13: '⅓',\n frac23: '⅔',\n frac15: '⅕',\n frac25: '⅖',\n frac35: '⅗',\n frac45: '⅘',\n frac16: '⅙',\n frac56: '⅚',\n frac18: '⅛',\n frac38: '⅜',\n frac58: '⅝',\n frac78: '⅞',\n frasl: '⁄',\n ndash: '–',\n mdash: '—',\n minus: '−',\n hellip: '…',\n lsquo: '‘',\n rsquo: '’',\n sbquo: '‚',\n ldquo: '“',\n rdquo: '”',\n bdquo: '„',\n laquo: '«',\n raquo: '»',\n times: '×',\n divide: '÷',\n middot: '·',\n bull: '•',\n micro: 'µ',\n sup1: '¹',\n sup2: '²',\n sup3: '³',\n ordm: 'º',\n ordf: 'ª',\n copy: '©',\n reg: '®',\n trade: '™',\n iexcl: '¡',\n iquest: '¿',\n szlig: 'ß',\n aelig: 'æ',\n AElig: 'Æ',\n oelig: 'œ',\n OElig: 'Œ',\n oslash: 'ø',\n Oslash: 'Ø',\n}\n\n// `&eacute;` and the like decode to the letter plus a combining mark, then NFC.\nconst MARKS: Readonly<Record<string, string>> = {\n grave: '\\u0300',\n acute: '\\u0301',\n circ: '\\u0302',\n tilde: '\\u0303',\n uml: '\\u0308',\n ring: '\\u030A',\n cedil: '\\u0327',\n}\n\n// A comment, a script or style element with its content, or a tag, with its\n// name captured if it's a block tag. As in a browser, a quote starts an\n// attribute value only right after `=`. A value can hold a `>`, and a `<`\n// that can't start a tag, as in `a < b`. A tag with no `>` is tried again at\n// each `<` in it, so the attributes end at any `<` that can start a tag, which\n// keeps each try short. They're read once, in a lookahead, then matched by\n// backreference, so a failed tag doesn't retry other splits of them, which\n// would be exponential. A literal, unlike `new RegExp`, lets bundlers drop it.\nconst MARKUP =\n /<!--[\\s\\S]*?(?:-->|$)|<(script|style)(?=[\\s/>])(?=((?:=\\s*(?:\"(?:[^\"<]|<(?![a-z/!?]))*\"|'(?:[^'<]|<(?![a-z/!?]))*')|[^<>])*))\\2>[\\s\\S]*?(?:<\\/\\1\\s*>|$)|<(?:(\\/?(?:br|p|li|div|h[1-6])\\b)|[/!?]?[a-z])(?=((?:=\\s*(?:\"(?:[^\"<]|<(?![a-z/!?]))*\"|'(?:[^'<]|<(?![a-z/!?]))*')|[^<>])*))\\4>/gi\n\n// Removing a tag can join the text around it into a new one, as in\n// `<scr<b></b>ipt>`, so stripping repeats. Real markup needs a pass or two;\n// the cap keeps hostile nesting linear.\nconst MAX_PASSES = 8\n\n/**\n * JSON-LD text with its tags removed, then its HTML entities decoded, on one\n * line with single spaces. An encoded tag, such as `&lt;b&gt;`, stays as text.\n */\nexport function cleanText(text: string): string {\n return cleanLines(text).join(' ')\n}\n\n/** {@link cleanText} for each line, split at line breaks and block tags. Empty lines are left out. */\nexport function cleanLines(text: string): string[] {\n return stripTags(text)\n .replace(\n /&(?:#(\\d{1,7})|#x([\\da-f]{1,6})|([a-z][a-z\\d]*));/gi,\n (entity, decimal?: string, hex?: string, name?: string) => {\n if (name === undefined) {\n const code = decimal === undefined ? parseInt(hex!, 16) : +decimal\n return code > 0 &&\n code <= 0x10ffff &&\n (code < 0xd800 || code > 0xdfff)\n ? String.fromCodePoint(code)\n : entity\n }\n\n if (hasOwn(NAMED_ENTITIES, name)) {\n return NAMED_ENTITIES[name]!\n }\n\n const accent = /^([aceinouyACEINOUY])([a-z]+)$/.exec(name)\n return accent && hasOwn(MARKS, accent[2]!)\n ? `${accent[1]}${MARKS[accent[2]!]}`.normalize('NFC')\n : entity\n },\n )\n .split(/[\\n\\r\\u2028\\u2029]/)\n .map((line) => line.replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n}\n\n/**\n * Removes comments, tags, and the content of `script` and `style`, and ends a\n * line at block tags. Strips again until nothing changes, for up to\n * `MAX_PASSES` passes, so a ninth nested level stays as text such as `b>`.\n * Then removes any `<` that could still open a tag: one before a letter, `/`,\n * `!`, or `?` with a `>` after it, as in `<a <>`. A `<` with no `>` after it,\n * as in `x<y`, stays.\n */\nfunction stripTags(text: string): string {\n let stripped = text\n\n for (let pass = 0; pass < MAX_PASSES; pass++) {\n const next = stripped.replace(\n MARKUP,\n (_, _element, _attributes, block?: string) => (block ? '\\n' : ''),\n )\n if (next === stripped) {\n break\n }\n stripped = next\n }\n\n const end = stripped.lastIndexOf('>') + 1\n return (\n stripped\n .slice(0, end)\n .replace(/<+([/!?a-z]?)/gi, (run, next: string) => next || run) +\n stripped.slice(end)\n )\n}\n"],"mappings":"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","names":[]}