@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.
- package/CHANGELOG.md +138 -0
- package/README.md +1 -1
- package/dist/{chunk-VWVIBCTG.cjs → chunk-ISRDJR2O.cjs} +227 -179
- package/dist/chunk-ISRDJR2O.cjs.map +1 -0
- package/dist/errors.js +29 -1
- package/dist/errors.js.map +1 -1
- package/dist/format/duration.js.map +1 -1
- package/dist/format/ingredient.js.map +1 -1
- package/dist/format/number.js +1 -1
- package/dist/format/number.js.map +1 -1
- package/dist/format/options.js.map +1 -1
- package/dist/format/quantity.js.map +1 -1
- package/dist/format/servings.js.map +1 -1
- package/dist/format/temperature.js.map +1 -1
- package/dist/index.cjs +87 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -28
- package/dist/index.d.ts +71 -28
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/ingredient/parse.js +27 -22
- package/dist/ingredient/parse.js.map +1 -1
- package/dist/issues.js.map +1 -1
- package/dist/quantity/amount.js +11 -0
- package/dist/quantity/amount.js.map +1 -1
- package/dist/quantity/parse.js +58 -81
- package/dist/quantity/parse.js.map +1 -1
- package/dist/quantity/scale.js +5 -2
- package/dist/quantity/scale.js.map +1 -1
- package/dist/recipe/ids.js.map +1 -1
- package/dist/recipe/parse.js +80 -60
- package/dist/recipe/parse.js.map +1 -1
- package/dist/recipe/schema.js +8 -6
- package/dist/recipe/schema.js.map +1 -1
- package/dist/resolve.js +6 -1
- package/dist/resolve.js.map +1 -1
- package/dist/schema-org/find.js +46 -13
- package/dist/schema-org/find.js.map +1 -1
- package/dist/schema-org/from-json-ld.js +134 -71
- package/dist/schema-org/from-json-ld.js.map +1 -1
- package/dist/schema-org/text.js.map +1 -1
- package/dist/schema-org/to-json-ld.js +12 -15
- package/dist/schema-org/to-json-ld.js.map +1 -1
- package/dist/schema-org.cjs +239 -147
- package/dist/schema-org.cjs.map +1 -1
- package/dist/schema-org.d.cts +35 -21
- package/dist/schema-org.d.ts +35 -21
- package/dist/steps/segment.js.map +1 -1
- package/dist/steps/temperature.js +13 -13
- package/dist/steps/temperature.js.map +1 -1
- package/dist/{types-DSMf45H5.d.cts → types-CKIoWsz8.d.cts} +25 -7
- package/dist/{types-DSMf45H5.d.ts → types-CKIoWsz8.d.ts} +25 -7
- package/dist/units/convert.js +16 -8
- package/dist/units/convert.js.map +1 -1
- package/dist/units/temperature.js +7 -1
- package/dist/units/temperature.js.map +1 -1
- package/dist/validation.js +1 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- 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 {
|
|
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
|
|
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
|
|
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 = () =>
|
|
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
|
-
|
|
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
|
|
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 [
|
|
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 &&
|
|
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 [
|
|
195
|
-
const image2 = findImage(reader, entry,
|
|
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
|
-
|
|
209
|
-
if (image
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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}${
|
|
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
|
|
355
|
-
|
|
415
|
+
const elements2 = entriesOf(entry, entrySource);
|
|
416
|
+
const positions = elements2.map(
|
|
417
|
+
([element]) => toNumber(asRecord(element)?.position)
|
|
356
418
|
);
|
|
357
|
-
const order = [...
|
|
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
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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) => ({
|
|
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
|
|
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
|
-
|
|
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 `<b>` 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 `<b>` 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// `é` 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 `<b>`, 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,
|
|
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// `é` 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 `<b>`, 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":[]}
|