@recipe-book/core 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/LICENSE +21 -0
- package/README.md +50 -7
- package/dist/chunk-D5CV3UF7.cjs +2754 -0
- package/dist/chunk-D5CV3UF7.cjs.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/errors.js.map +1 -0
- package/dist/format/duration.js +36 -0
- package/dist/format/duration.js.map +1 -0
- package/dist/format/ingredient.js +49 -0
- package/dist/format/ingredient.js.map +1 -0
- package/dist/format/number.js +120 -0
- package/dist/format/number.js.map +1 -0
- package/dist/format/options.js +31 -0
- package/dist/format/options.js.map +1 -0
- package/dist/format/quantity.js +144 -0
- package/dist/format/quantity.js.map +1 -0
- package/dist/format/servings.js +68 -0
- package/dist/format/servings.js.map +1 -0
- package/dist/format/temperature.js +35 -0
- package/dist/format/temperature.js.map +1 -0
- package/dist/index.cjs +476 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +994 -0
- package/dist/index.d.ts +994 -5
- package/dist/index.js +48 -1
- package/dist/index.js.map +1 -0
- package/dist/ingredient/parse.js +412 -0
- package/dist/ingredient/parse.js.map +1 -0
- package/dist/issues.js +26 -0
- package/dist/issues.js.map +1 -0
- package/dist/quantity/amount.js +78 -0
- package/dist/quantity/amount.js.map +1 -0
- package/dist/quantity/parse.js +255 -0
- package/dist/quantity/parse.js.map +1 -0
- package/dist/quantity/scale.js +23 -0
- package/dist/quantity/scale.js.map +1 -0
- package/dist/recipe/duration.js +28 -0
- package/dist/recipe/duration.js.map +1 -0
- package/dist/recipe/ids.js +68 -0
- package/dist/recipe/ids.js.map +1 -0
- package/dist/recipe/infer.js +24 -0
- package/dist/recipe/infer.js.map +1 -0
- package/dist/recipe/nutrient-units.js +18 -0
- package/dist/recipe/nutrient-units.js.map +1 -0
- package/dist/recipe/nutrition.js +26 -0
- package/dist/recipe/nutrition.js.map +1 -0
- package/dist/recipe/parse.js +854 -0
- package/dist/recipe/parse.js.map +1 -0
- package/dist/recipe/schema.js +32 -0
- package/dist/recipe/schema.js.map +1 -0
- package/dist/recipe/url.js +9 -0
- package/dist/recipe/url.js.map +1 -0
- package/dist/resolve.js +120 -0
- package/dist/resolve.js.map +1 -0
- package/dist/schema-org/find.js +158 -0
- package/dist/schema-org/find.js.map +1 -0
- package/dist/schema-org/from-json-ld.js +647 -0
- package/dist/schema-org/from-json-ld.js.map +1 -0
- package/dist/schema-org/nutrients.js +19 -0
- package/dist/schema-org/nutrients.js.map +1 -0
- package/dist/schema-org/text.js +125 -0
- package/dist/schema-org/text.js.map +1 -0
- package/dist/schema-org/to-json-ld.js +151 -0
- package/dist/schema-org/to-json-ld.js.map +1 -0
- package/dist/schema-org.cjs +1062 -0
- package/dist/schema-org.cjs.map +1 -0
- package/dist/schema-org.d.cts +223 -0
- package/dist/schema-org.d.ts +223 -0
- package/dist/schema-org.js +10 -0
- package/dist/schema-org.js.map +1 -0
- package/dist/steps/segment.js +45 -0
- package/dist/steps/segment.js.map +1 -0
- package/dist/steps/temperature.js +174 -0
- package/dist/steps/temperature.js.map +1 -0
- package/dist/suggest.js +50 -0
- package/dist/suggest.js.map +1 -0
- package/dist/types-BknM8KOd.d.cts +628 -0
- package/dist/types-BknM8KOd.d.ts +628 -0
- package/dist/units/convert.js +79 -0
- package/dist/units/convert.js.map +1 -0
- package/dist/units/lookup.js +86 -0
- package/dist/units/lookup.js.map +1 -0
- package/dist/units/registry.js +183 -0
- package/dist/units/registry.js.map +1 -0
- package/dist/units/system.js +91 -0
- package/dist/units/system.js.map +1 -0
- package/dist/units/temperature.js +40 -0
- package/dist/units/temperature.js.map +1 -0
- package/dist/validation.js +90 -0
- package/dist/validation.js.map +1 -0
- package/package.json +53 -8
- package/schema-org/package.json +5 -0
- package/dist/chunk-22RBQQWH.mjs +0 -1
- package/dist/chunk-3CSP4UZG.mjs +0 -1
- package/dist/chunk-3FFMHRB7.mjs +0 -26
- package/dist/chunk-4EB3HG6B.mjs +0 -50
- package/dist/chunk-4EHNJLGI.mjs +0 -50
- package/dist/chunk-4IA5EEKI.mjs +0 -81
- package/dist/chunk-4TATADPT.mjs +0 -51
- package/dist/chunk-6GI5KLI4.mjs +0 -22
- package/dist/chunk-6QJZOZ6W.mjs +0 -49
- package/dist/chunk-6YYQ4RYZ.mjs +0 -26
- package/dist/chunk-7BBUDI2L.mjs +0 -0
- package/dist/chunk-7KEMNCV7.mjs +0 -26
- package/dist/chunk-7MMJZZ7Z.mjs +0 -75
- package/dist/chunk-BES4V7DM.mjs +0 -26
- package/dist/chunk-BRDXRIAH.mjs +0 -50
- package/dist/chunk-C45UD45A.mjs +0 -1
- package/dist/chunk-CBLJXA6P.mjs +0 -1
- package/dist/chunk-CFR2ZBPB.mjs +0 -77
- package/dist/chunk-DYMNVFL2.mjs +0 -78
- package/dist/chunk-F6AQKXJU.mjs +0 -51
- package/dist/chunk-FW4M2R43.mjs +0 -51
- package/dist/chunk-GCCEDUST.mjs +0 -75
- package/dist/chunk-GE7KBYZR.mjs +0 -26
- package/dist/chunk-GN7ZDGYC.mjs +0 -51
- package/dist/chunk-GOO2SCQX.mjs +0 -75
- package/dist/chunk-HAVKBQLH.mjs +0 -51
- package/dist/chunk-HCBIPANU.mjs +0 -50
- package/dist/chunk-IEBAGBS7.mjs +0 -51
- package/dist/chunk-IQ6WN2V4.mjs +0 -75
- package/dist/chunk-JB4Z2FR7.mjs +0 -77
- package/dist/chunk-JMTQOTBM.mjs +0 -75
- package/dist/chunk-JRO6JUES.mjs +0 -51
- package/dist/chunk-KH3SRXET.mjs +0 -52
- package/dist/chunk-KNZMCAQZ.mjs +0 -51
- package/dist/chunk-KXFUCO3R.mjs +0 -1
- package/dist/chunk-LEBGCVCJ.mjs +0 -75
- package/dist/chunk-MAHWFCIL.mjs +0 -51
- package/dist/chunk-MPH76SLC.mjs +0 -51
- package/dist/chunk-MQGMFBJQ.mjs +0 -26
- package/dist/chunk-NMIVG7T4.mjs +0 -6
- package/dist/chunk-Q3TT3AZ4.mjs +0 -26
- package/dist/chunk-QY5HJQIL.mjs +0 -51
- package/dist/chunk-RCPLSNKM.mjs +0 -1
- package/dist/chunk-SR2KFBT3.mjs +0 -1
- package/dist/chunk-ULDLBAEG.mjs +0 -1
- package/dist/chunk-UQ3EUYFR.mjs +0 -1
- package/dist/chunk-V26OCCTL.mjs +0 -82
- package/dist/chunk-WBQAMGXK.mjs +0 -0
- package/dist/chunk-WSW6FMUL.mjs +0 -51
- package/dist/chunk-WU55SBIV.mjs +0 -75
- package/dist/chunk-XINK45P3.mjs +0 -51
- package/dist/chunk-XNOHIWI3.mjs +0 -1
- package/dist/chunk-XV4QN4LA.mjs +0 -6
- package/dist/chunk-YHBXEVUX.mjs +0 -51
- package/dist/chunk-YSZSCEI7.mjs +0 -75
- package/dist/chunk-YY3SUQ7P.mjs +0 -16
- package/dist/chunk-YZB3IG47.mjs +0 -80
- package/dist/chunk-ZPZT4LMQ.mjs +0 -25
- package/dist/const/conversion.d.mts +0 -7
- package/dist/const/conversion.d.ts +0 -7
- package/dist/const/conversion.js +0 -1
- package/dist/const/conversion.mjs +0 -1
- package/dist/const/unit.d.mts +0 -119
- package/dist/const/unit.d.ts +0 -119
- package/dist/const/unit.js +0 -1
- package/dist/const/unit.mjs +0 -1
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -1
- package/dist/lib/assert.d.mts +0 -18
- package/dist/lib/assert.d.ts +0 -18
- package/dist/lib/assert.js +0 -1
- package/dist/lib/assert.mjs +0 -1
- package/dist/lib/ingredient.d.mts +0 -9
- package/dist/lib/ingredient.d.ts +0 -9
- package/dist/lib/ingredient.js +0 -1
- package/dist/lib/ingredient.mjs +0 -1
- package/dist/lib/recipe.d.mts +0 -12
- package/dist/lib/recipe.d.ts +0 -12
- package/dist/lib/recipe.js +0 -1
- package/dist/lib/recipe.mjs +0 -1
- package/dist/lib/runtime.d.mts +0 -3
- package/dist/lib/runtime.d.ts +0 -3
- package/dist/lib/runtime.js +0 -1
- package/dist/lib/runtime.mjs +0 -1
- package/dist/lib/unit.d.mts +0 -19
- package/dist/lib/unit.d.ts +0 -19
- package/dist/lib/unit.js +0 -1
- package/dist/lib/unit.mjs +0 -1
- package/dist/types.d.mts +0 -36
- package/dist/types.d.ts +0 -36
- package/dist/types.js +0 -1
- package/dist/types.mjs +0 -1
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
// src/schema-org/from-json-ld.ts
|
|
2
|
+
import {
|
|
3
|
+
describeValue,
|
|
4
|
+
quote
|
|
5
|
+
} from "../errors.js";
|
|
6
|
+
import { parseIngredientLine } from "../ingredient/parse.js";
|
|
7
|
+
import { issue, warning } from "../issues.js";
|
|
8
|
+
import { matchAmount } from "../quantity/parse.js";
|
|
9
|
+
import { parseIsoDuration } from "../recipe/duration.js";
|
|
10
|
+
import { parseRecipeWith } from "../recipe/parse.js";
|
|
11
|
+
import { disallowedScheme } from "../recipe/url.js";
|
|
12
|
+
import { asRecord, toResult } from "../validation.js";
|
|
13
|
+
import { entriesOf, locate, MAX_DEPTH } from "./find.js";
|
|
14
|
+
import { NUTRIENTS } from "./nutrients.js";
|
|
15
|
+
import { cleanLines, cleanText } from "./text.js";
|
|
16
|
+
var READ_FIELDS = [
|
|
17
|
+
"@context",
|
|
18
|
+
"@type",
|
|
19
|
+
"name",
|
|
20
|
+
"description",
|
|
21
|
+
"image",
|
|
22
|
+
"recipeYield",
|
|
23
|
+
"prepTime",
|
|
24
|
+
"cookTime",
|
|
25
|
+
"totalTime",
|
|
26
|
+
"recipeIngredient",
|
|
27
|
+
"recipeInstructions",
|
|
28
|
+
"tool",
|
|
29
|
+
"nutrition"
|
|
30
|
+
];
|
|
31
|
+
var TIMES = [
|
|
32
|
+
["prep", "prepTime"],
|
|
33
|
+
["cook", "cookTime"],
|
|
34
|
+
["total", "totalTime"]
|
|
35
|
+
];
|
|
36
|
+
var MAX_URL_LENGTH = 4096;
|
|
37
|
+
var SERVINGS_WORD = /^(?:servings?|portions?|(?:serving|portion)\(s\)|people|persons?)\.?$/i;
|
|
38
|
+
function fromJsonLd(input) {
|
|
39
|
+
const { recipes, ids } = locate([input]);
|
|
40
|
+
const [found] = recipes;
|
|
41
|
+
if (!found) {
|
|
42
|
+
return {
|
|
43
|
+
...toResult("invalid_recipe", void 0, [
|
|
44
|
+
issue(
|
|
45
|
+
"error",
|
|
46
|
+
"invalid_type",
|
|
47
|
+
[],
|
|
48
|
+
`Expected a schema.org Recipe, or an array, @graph, mainEntity, or ItemList that holds one, received ${describeValue(input)}.`
|
|
49
|
+
)
|
|
50
|
+
]),
|
|
51
|
+
extra: {}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const node = asRecord(found.node);
|
|
55
|
+
const reader = {
|
|
56
|
+
recipe: found.node,
|
|
57
|
+
ids,
|
|
58
|
+
warnings: [],
|
|
59
|
+
sources: /* @__PURE__ */ new Map(),
|
|
60
|
+
droppable: /* @__PURE__ */ new Set(),
|
|
61
|
+
images: [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()],
|
|
62
|
+
repeated: 0,
|
|
63
|
+
deepest: 0
|
|
64
|
+
};
|
|
65
|
+
const name = text(node.name);
|
|
66
|
+
if (recipes.length > 1) {
|
|
67
|
+
warning(
|
|
68
|
+
reader.warnings,
|
|
69
|
+
"ignored_value",
|
|
70
|
+
[],
|
|
71
|
+
`Found ${recipes.length} recipes. Only the first${typeof name === "string" ? `, ${quote(name)},` : ""} is read.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const model = readRecipe(reader, node, found.path);
|
|
75
|
+
const parse = () => parseRecipeWith(model, parseIngredientLine);
|
|
76
|
+
let result = parse();
|
|
77
|
+
const dropped = [];
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
const targets = [];
|
|
80
|
+
for (const error of result.error.issues) {
|
|
81
|
+
const target = error.severity === "error" ? droppableAt(reader, error.path) : void 0;
|
|
82
|
+
if (target) {
|
|
83
|
+
targets.push(target);
|
|
84
|
+
dropped.push({
|
|
85
|
+
...error,
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: `${error.message} It is ignored.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
targets.sort((a, b) => b.length - a.length).forEach((target) => remove(model, target));
|
|
92
|
+
if (dropped.length > 0) {
|
|
93
|
+
result = parse();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const order = ({ code, path }) => [
|
|
97
|
+
path.length > found.path.length ? READ_FIELDS.indexOf(String(path[found.path.length])) : -1,
|
|
98
|
+
...path.slice(found.path.length + 1).filter((key) => typeof key === "number"),
|
|
99
|
+
...code === "unknown_field" ? [Infinity] : []
|
|
100
|
+
];
|
|
101
|
+
const issues = [
|
|
102
|
+
...reader.warnings,
|
|
103
|
+
...[
|
|
104
|
+
...dropped,
|
|
105
|
+
...result.success ? result.warnings : result.error.issues
|
|
106
|
+
].map((issue2) => ({ ...issue2, path: sourceOf(reader, issue2.path) }))
|
|
107
|
+
].sort((a, b) => compareOrder(order(a), order(b)));
|
|
108
|
+
const checked = /* @__PURE__ */ new Map();
|
|
109
|
+
const extra = Object.fromEntries(
|
|
110
|
+
Object.entries(node).filter(([key, value]) => {
|
|
111
|
+
if (READ_FIELDS.includes(key)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const path = [...found.path, key];
|
|
115
|
+
if (tooDeep(value, path.length, checked)) {
|
|
116
|
+
warning(
|
|
117
|
+
issues,
|
|
118
|
+
"invalid_type",
|
|
119
|
+
path,
|
|
120
|
+
`Expected a value to keep in extra, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep. It is ignored.`
|
|
121
|
+
);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
return {
|
|
128
|
+
...toResult(
|
|
129
|
+
"invalid_recipe",
|
|
130
|
+
result.success ? result.data : void 0,
|
|
131
|
+
issues,
|
|
132
|
+
typeof name === "string" ? quote(name) : void 0
|
|
133
|
+
),
|
|
134
|
+
extra
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function tooDeep(value, length, checked) {
|
|
138
|
+
if (length > MAX_DEPTH) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (typeof value !== "object" || value === null || (checked.get(value) ?? 0) >= length) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const deep = Object.values(value).some(
|
|
145
|
+
(child) => tooDeep(child, length + 1, checked)
|
|
146
|
+
);
|
|
147
|
+
if (!deep) {
|
|
148
|
+
checked.set(value, length);
|
|
149
|
+
}
|
|
150
|
+
return deep;
|
|
151
|
+
}
|
|
152
|
+
function readRecipe(reader, node, base) {
|
|
153
|
+
const at = (...path) => [...base, ...path];
|
|
154
|
+
const model = {
|
|
155
|
+
name: text(node.name),
|
|
156
|
+
description: text(node.description)
|
|
157
|
+
};
|
|
158
|
+
link(reader, [], base);
|
|
159
|
+
link(reader, ["name"], at("name"));
|
|
160
|
+
link(reader, ["description"], at("description"), true);
|
|
161
|
+
const image = readImage(reader, node.image, at("image"));
|
|
162
|
+
if (image) {
|
|
163
|
+
model.image = image.url;
|
|
164
|
+
link(reader, ["image"], image.source, true);
|
|
165
|
+
}
|
|
166
|
+
readYield(reader, model, node.recipeYield, at("recipeYield"));
|
|
167
|
+
const times = {};
|
|
168
|
+
for (const [key, field] of TIMES) {
|
|
169
|
+
const minutes = readTime(reader, node[field], at(field));
|
|
170
|
+
if (minutes !== void 0) {
|
|
171
|
+
times[key] = minutes;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
model.times = times;
|
|
175
|
+
model.ingredients = readList(
|
|
176
|
+
reader,
|
|
177
|
+
"ingredients",
|
|
178
|
+
node.recipeIngredient,
|
|
179
|
+
at("recipeIngredient"),
|
|
180
|
+
readLine,
|
|
181
|
+
'an ingredient line such as "2 cups flour"'
|
|
182
|
+
);
|
|
183
|
+
model.steps = readList(
|
|
184
|
+
reader,
|
|
185
|
+
"steps",
|
|
186
|
+
node.recipeInstructions,
|
|
187
|
+
at("recipeInstructions"),
|
|
188
|
+
readStep,
|
|
189
|
+
'a step such as "Whisk the eggs." or a HowToStep'
|
|
190
|
+
);
|
|
191
|
+
model.equipment = readTools(reader, node.tool, at("tool"));
|
|
192
|
+
model.nutrition = readNutrition(reader, node.nutrition, at("nutrition"));
|
|
193
|
+
return model;
|
|
194
|
+
}
|
|
195
|
+
function readImage(reader, value, source) {
|
|
196
|
+
if (isAbsent(value)) {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
const warnings = reader.warnings.length + reader.repeated;
|
|
200
|
+
const image = findImage(reader, value, source);
|
|
201
|
+
if (!image && reader.warnings.length + reader.repeated === warnings) {
|
|
202
|
+
ignore(
|
|
203
|
+
reader,
|
|
204
|
+
"invalid_type",
|
|
205
|
+
source,
|
|
206
|
+
`Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)}.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return image;
|
|
210
|
+
}
|
|
211
|
+
function findImage(reader, value, source, referenced = false) {
|
|
212
|
+
reader.deepest = Math.max(reader.deepest, source.length);
|
|
213
|
+
if (source.length > MAX_DEPTH) {
|
|
214
|
+
if (!isAbsent(value)) {
|
|
215
|
+
ignore(
|
|
216
|
+
reader,
|
|
217
|
+
"invalid_type",
|
|
218
|
+
source,
|
|
219
|
+
`Expected an image URL, an ImageObject with a url, or a list of them, received ${describeValue(value)} nested more than ${MAX_DEPTH} levels deep.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
if (typeof value === "string") {
|
|
225
|
+
const url = cleanText(value);
|
|
226
|
+
const scheme = disallowedScheme(url);
|
|
227
|
+
if (scheme) {
|
|
228
|
+
ignore(
|
|
229
|
+
reader,
|
|
230
|
+
"invalid_url",
|
|
231
|
+
source,
|
|
232
|
+
`Expected an http or https URL, or a relative one, received ${quote(`${scheme}\u2026`)}.`
|
|
233
|
+
);
|
|
234
|
+
return void 0;
|
|
235
|
+
}
|
|
236
|
+
if (url.length > MAX_URL_LENGTH) {
|
|
237
|
+
ignore(
|
|
238
|
+
reader,
|
|
239
|
+
"invalid_url",
|
|
240
|
+
source,
|
|
241
|
+
`Expected a URL of at most ${MAX_URL_LENGTH} UTF-16 code units, received ${url.length}.`
|
|
242
|
+
);
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
return url ? { url, source } : void 0;
|
|
246
|
+
}
|
|
247
|
+
if (Array.isArray(value)) {
|
|
248
|
+
for (const [entry, entrySource] of entriesOf(value, source)) {
|
|
249
|
+
const image2 = findImage(reader, entry, entrySource, referenced);
|
|
250
|
+
if (image2) {
|
|
251
|
+
return image2;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
const images = reader.images[referenced ? 1 : 0];
|
|
257
|
+
const reads = images.get(value) ?? [];
|
|
258
|
+
const read = reads.find(
|
|
259
|
+
([, , depth, length]) => length === source.length || Math.max(length, source.length) + depth <= MAX_DEPTH
|
|
260
|
+
);
|
|
261
|
+
if (read) {
|
|
262
|
+
reader.repeated += read[1];
|
|
263
|
+
reader.deepest = Math.max(reader.deepest, source.length + read[2]);
|
|
264
|
+
return read[0];
|
|
265
|
+
}
|
|
266
|
+
const record = asRecord(value);
|
|
267
|
+
if (!record) {
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
images.set(value, reads);
|
|
271
|
+
const warnings = reader.warnings.length + reader.repeated;
|
|
272
|
+
const deepest = reader.deepest;
|
|
273
|
+
reader.deepest = source.length;
|
|
274
|
+
const id = record["@id"];
|
|
275
|
+
const target = typeof id === "string" && !referenced ? reader.ids.get(id) : void 0;
|
|
276
|
+
let image = findImage(reader, record.url, [...source, "url"], referenced) ?? findImage(reader, record.contentUrl, [...source, "contentUrl"], referenced);
|
|
277
|
+
if (!image && target?.node === reader.recipe) {
|
|
278
|
+
ignore(
|
|
279
|
+
reader,
|
|
280
|
+
"invalid_type",
|
|
281
|
+
source,
|
|
282
|
+
"Expected an image URL, an ImageObject with a url, or a list of them, received a reference to the recipe itself."
|
|
283
|
+
);
|
|
284
|
+
} else if (!image && target) {
|
|
285
|
+
image = findImage(reader, target.node, target.path, true);
|
|
286
|
+
}
|
|
287
|
+
reads.push([
|
|
288
|
+
image,
|
|
289
|
+
reader.warnings.length + reader.repeated - warnings,
|
|
290
|
+
reader.deepest - source.length,
|
|
291
|
+
source.length
|
|
292
|
+
]);
|
|
293
|
+
reader.deepest = Math.max(reader.deepest, deepest);
|
|
294
|
+
return image;
|
|
295
|
+
}
|
|
296
|
+
function readYield(reader, model, value, source) {
|
|
297
|
+
const batches = entriesOf(value, source).flatMap(
|
|
298
|
+
([entry, entrySource]) => (typeof entry === "string" ? entry.split(/,\s+/) : [entry]).map(
|
|
299
|
+
(part) => readBatch(reader, part, entrySource)
|
|
300
|
+
)
|
|
301
|
+
).filter((batch) => batch !== void 0);
|
|
302
|
+
const yields = batches.filter((batch) => batch.yield !== void 0);
|
|
303
|
+
const servings = batches.filter((batch) => batch.servings !== void 0);
|
|
304
|
+
const kept = yields.find((batch) => !batch.article) ?? yields.find((batch) => batch.article);
|
|
305
|
+
const keptServings = servings.find((batch) => batch.worded) ?? servings.find((batch) => batch.amount !== kept?.amount);
|
|
306
|
+
for (const batch of servings) {
|
|
307
|
+
if (keptServings && batch.amount !== keptServings.amount && (batch.worded || batch.amount !== kept?.amount)) {
|
|
308
|
+
warning(
|
|
309
|
+
reader.warnings,
|
|
310
|
+
"ignored_value",
|
|
311
|
+
batch.source,
|
|
312
|
+
`Only one number of servings is read, ${quote(keptServings.label)}, so this one is ignored.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const batch of yields) {
|
|
317
|
+
if (batch !== kept) {
|
|
318
|
+
warning(
|
|
319
|
+
reader.warnings,
|
|
320
|
+
"ignored_value",
|
|
321
|
+
batch.source,
|
|
322
|
+
`Only one yield is read, ${quote(kept.label)}, so this one is ignored.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (keptServings) {
|
|
327
|
+
model.servings = keptServings.servings;
|
|
328
|
+
link(reader, ["servings"], keptServings.source, true);
|
|
329
|
+
}
|
|
330
|
+
if (kept) {
|
|
331
|
+
model.yield = kept.yield;
|
|
332
|
+
link(reader, ["yield"], kept.source, true);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function readBatch(reader, value, source) {
|
|
336
|
+
if (typeof value === "number") {
|
|
337
|
+
return { source, label: String(value), amount: value, servings: value };
|
|
338
|
+
}
|
|
339
|
+
if (typeof value === "string") {
|
|
340
|
+
const label = cleanText(value);
|
|
341
|
+
if (!label) {
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
const text2 = label.replace(
|
|
345
|
+
/^(?:(?:serves|servings?|yields?|makes)\s*:?\s*)?(?:about\s+)?/i,
|
|
346
|
+
""
|
|
347
|
+
);
|
|
348
|
+
const match = matchAmount(text2);
|
|
349
|
+
if (!match || "code" in match) {
|
|
350
|
+
ignore(
|
|
351
|
+
reader,
|
|
352
|
+
match?.code ?? "invalid_quantity",
|
|
353
|
+
source,
|
|
354
|
+
match?.message ?? `Expected a batch such as "24 cookies" or "3\u20134 loaves", received ${describeValue(label)}.`
|
|
355
|
+
);
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
const amount2 = typeof match.amount === "number" ? match.amount : match.amount.min;
|
|
359
|
+
const rest = match.rest.trim();
|
|
360
|
+
return !rest || SERVINGS_WORD.test(rest) ? { source, label, amount: amount2, servings: match.amountText, worded: !!rest } : { source, label, amount: amount2, yield: text2, article: /^an?\s/i.test(text2) };
|
|
361
|
+
}
|
|
362
|
+
const record = asRecord(value);
|
|
363
|
+
const amount = toNumber(record?.value ?? record?.minValue);
|
|
364
|
+
if (record && amount !== void 0) {
|
|
365
|
+
const max = toNumber(record.maxValue);
|
|
366
|
+
const unit = typeof record.unitText === "string" ? cleanText(record.unitText) : "";
|
|
367
|
+
const range = {
|
|
368
|
+
amount,
|
|
369
|
+
...max !== void 0 && max !== amount && { max }
|
|
370
|
+
};
|
|
371
|
+
const label = `${amount}${max !== amount && max ? `\u2013${max}` : ""}${unit && ` ${unit}`}`;
|
|
372
|
+
return !unit || SERVINGS_WORD.test(unit) ? { source, label, amount, servings: range, worded: !!unit } : { source, label, amount, yield: { ...range, unit } };
|
|
373
|
+
}
|
|
374
|
+
if (!isAbsent(value)) {
|
|
375
|
+
ignore(
|
|
376
|
+
reader,
|
|
377
|
+
"invalid_type",
|
|
378
|
+
source,
|
|
379
|
+
`Expected a yield such as "4 servings" or "24 cookies", received ${describeValue(value)}.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return void 0;
|
|
383
|
+
}
|
|
384
|
+
function readTime(reader, value, source) {
|
|
385
|
+
if (isAbsent(value)) {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
const minutes = typeof value === "string" ? parseIsoDuration(cleanText(value)) : void 0;
|
|
389
|
+
if (minutes === void 0 || minutes > Number.MAX_SAFE_INTEGER) {
|
|
390
|
+
ignore(
|
|
391
|
+
reader,
|
|
392
|
+
"invalid_duration",
|
|
393
|
+
source,
|
|
394
|
+
`Expected an ISO 8601 duration such as "PT30M", received ${describeValue(value)}${minutes === void 0 ? "" : ", which is too long"}.`
|
|
395
|
+
);
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
return minutes > 0 ? minutes : void 0;
|
|
399
|
+
}
|
|
400
|
+
function readList(reader, key, value, source, readItem, expected) {
|
|
401
|
+
const list = [];
|
|
402
|
+
link(reader, [key], source);
|
|
403
|
+
const add = (items, path, entry, entrySource, inSection) => {
|
|
404
|
+
if (isAbsent(entry)) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (entrySource.length > MAX_DEPTH) {
|
|
408
|
+
ignore(
|
|
409
|
+
reader,
|
|
410
|
+
"invalid_type",
|
|
411
|
+
entrySource,
|
|
412
|
+
`Expected ${expected}, received ${describeValue(entry)} nested more than ${MAX_DEPTH} levels deep.`
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (Array.isArray(entry)) {
|
|
417
|
+
const elements2 = entriesOf(entry, entrySource);
|
|
418
|
+
const positions = elements2.map(
|
|
419
|
+
([element]) => toNumber(asRecord(element)?.position)
|
|
420
|
+
);
|
|
421
|
+
const order = [...elements2.keys()];
|
|
422
|
+
if (!positions.includes(void 0)) {
|
|
423
|
+
order.sort((a, b) => positions[a] - positions[b]);
|
|
424
|
+
}
|
|
425
|
+
for (const i of order) {
|
|
426
|
+
const [element, elementSource] = elements2[i];
|
|
427
|
+
add(items, path, element, elementSource, inSection);
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const record = asRecord(entry);
|
|
432
|
+
const elements = [...entrySource, "itemListElement"];
|
|
433
|
+
if (record?.itemListElement !== void 0 && record.text === void 0) {
|
|
434
|
+
const title = typeof record.name === "string" ? cleanText(record.name) : "";
|
|
435
|
+
if (title && !inSection) {
|
|
436
|
+
const section = { title, items: [] };
|
|
437
|
+
const sectionPath = [key, list.length];
|
|
438
|
+
list.push(section);
|
|
439
|
+
link(reader, sectionPath, entrySource);
|
|
440
|
+
link(reader, [...sectionPath, "title"], [...entrySource, "name"]);
|
|
441
|
+
add(
|
|
442
|
+
section.items,
|
|
443
|
+
[...sectionPath, "items"],
|
|
444
|
+
record.itemListElement,
|
|
445
|
+
elements,
|
|
446
|
+
true
|
|
447
|
+
);
|
|
448
|
+
} else {
|
|
449
|
+
add(items, path, record.itemListElement, elements, inSection);
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (record && !isAbsent(record.item)) {
|
|
454
|
+
add(items, path, record.item, [...entrySource, "item"], inSection);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const read = readItem(reader, entry, entrySource);
|
|
458
|
+
if (!read) {
|
|
459
|
+
ignore(
|
|
460
|
+
reader,
|
|
461
|
+
"invalid_type",
|
|
462
|
+
entrySource,
|
|
463
|
+
`Expected ${expected}, received ${describeValue(entry)}.`
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const item of read) {
|
|
468
|
+
const itemPath = [...path, items.length];
|
|
469
|
+
items.push(item.value);
|
|
470
|
+
link(reader, itemPath, item.source, true);
|
|
471
|
+
for (const [field, fieldSource, droppable] of item.fields) {
|
|
472
|
+
link(reader, [...itemPath, field], fieldSource, droppable);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
add(list, [key], value, source, false);
|
|
477
|
+
return list;
|
|
478
|
+
}
|
|
479
|
+
function readLine(reader, value, source) {
|
|
480
|
+
if (typeof value === "string") {
|
|
481
|
+
return cleanLines(value).map((line) => ({
|
|
482
|
+
value: line,
|
|
483
|
+
source,
|
|
484
|
+
fields: []
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
const record = asRecord(value);
|
|
488
|
+
const parts = [record?.value, record?.unitText, record?.name ?? record?.text].filter((part) => typeof part === "string" || typeof part === "number").join(" ");
|
|
489
|
+
return parts ? cleanLines(parts).map((line) => ({ value: line, source, fields: [] })) : void 0;
|
|
490
|
+
}
|
|
491
|
+
function readStep(reader, value, source) {
|
|
492
|
+
if (typeof value === "string") {
|
|
493
|
+
return cleanLines(value).map((line) => ({
|
|
494
|
+
value: line,
|
|
495
|
+
source,
|
|
496
|
+
fields: []
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
const record = asRecord(value);
|
|
500
|
+
if (!record) {
|
|
501
|
+
return void 0;
|
|
502
|
+
}
|
|
503
|
+
const name = typeof record.name === "string" ? cleanText(record.name) : "";
|
|
504
|
+
const body = text(record.text) ?? name;
|
|
505
|
+
const image = readImage(reader, record.image, [...source, "image"]);
|
|
506
|
+
const lead = name.replace(/(?:…|\.\.\.)$/, "").trim();
|
|
507
|
+
const hasName = typeof body === "string" && !body.startsWith(lead) && !/^step\s*\d*[.:]?$/i.test(name);
|
|
508
|
+
const fields = [
|
|
509
|
+
["text", [...source, name && body === name ? "name" : "text"], false]
|
|
510
|
+
];
|
|
511
|
+
if (image) {
|
|
512
|
+
fields.push(["image", image.source, true]);
|
|
513
|
+
}
|
|
514
|
+
return [
|
|
515
|
+
{
|
|
516
|
+
value: {
|
|
517
|
+
text: body,
|
|
518
|
+
...hasName && { name },
|
|
519
|
+
...image && { image: image.url }
|
|
520
|
+
},
|
|
521
|
+
source,
|
|
522
|
+
fields
|
|
523
|
+
}
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
function readTools(reader, value, source) {
|
|
527
|
+
const equipment = [];
|
|
528
|
+
link(reader, ["equipment"], source);
|
|
529
|
+
for (const [entry, entrySource] of entriesOf(value, source)) {
|
|
530
|
+
const record = asRecord(entry);
|
|
531
|
+
const item = record ? {
|
|
532
|
+
name: text(record.name),
|
|
533
|
+
...record.requiredQuantity !== void 0 && {
|
|
534
|
+
quantity: toNumber(
|
|
535
|
+
asRecord(record.requiredQuantity)?.value ?? record.requiredQuantity
|
|
536
|
+
) ?? record.requiredQuantity
|
|
537
|
+
},
|
|
538
|
+
...record.url !== void 0 && { url: text(record.url) }
|
|
539
|
+
} : text(entry);
|
|
540
|
+
if (item === void 0) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const path = ["equipment", equipment.length];
|
|
544
|
+
equipment.push(item);
|
|
545
|
+
link(reader, path, entrySource, true);
|
|
546
|
+
link(reader, [...path, "name"], [...entrySource, "name"]);
|
|
547
|
+
link(
|
|
548
|
+
reader,
|
|
549
|
+
[...path, "quantity"],
|
|
550
|
+
[...entrySource, "requiredQuantity"],
|
|
551
|
+
true
|
|
552
|
+
);
|
|
553
|
+
link(reader, [...path, "url"], [...entrySource, "url"], true);
|
|
554
|
+
}
|
|
555
|
+
return equipment;
|
|
556
|
+
}
|
|
557
|
+
function readNutrition(reader, value, source) {
|
|
558
|
+
link(reader, ["nutrition"], source);
|
|
559
|
+
if (isAbsent(value)) {
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
const record = asRecord(value);
|
|
563
|
+
if (!record) {
|
|
564
|
+
ignore(
|
|
565
|
+
reader,
|
|
566
|
+
"invalid_type",
|
|
567
|
+
source,
|
|
568
|
+
`Expected NutritionInformation, such as { "calories": "250 kcal" }, received ${describeValue(value)}.`
|
|
569
|
+
);
|
|
570
|
+
return void 0;
|
|
571
|
+
}
|
|
572
|
+
const nutrition = {};
|
|
573
|
+
const keys = Object.keys(NUTRIENTS);
|
|
574
|
+
for (const [field, amount] of Object.entries(record)) {
|
|
575
|
+
const key = keys.find((key2) => NUTRIENTS[key2] === field);
|
|
576
|
+
if (key) {
|
|
577
|
+
nutrition[key] = text(amount);
|
|
578
|
+
link(reader, ["nutrition", key], [...source, field], true);
|
|
579
|
+
} else if (!field.startsWith("@")) {
|
|
580
|
+
ignore(reader, "unknown_field", [...source, field], "Unknown field.");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return nutrition;
|
|
584
|
+
}
|
|
585
|
+
function text(value) {
|
|
586
|
+
return typeof value === "string" ? cleanText(value) || void 0 : value ?? void 0;
|
|
587
|
+
}
|
|
588
|
+
function toNumber(value) {
|
|
589
|
+
const number = typeof value === "string" && value.trim() ? Number(value) : value;
|
|
590
|
+
return typeof number === "number" && Number.isFinite(number) ? number : void 0;
|
|
591
|
+
}
|
|
592
|
+
function isAbsent(value) {
|
|
593
|
+
return value === void 0 || value === null || typeof value === "string" && !value.trim();
|
|
594
|
+
}
|
|
595
|
+
function ignore(reader, code, path, message) {
|
|
596
|
+
warning(reader.warnings, code, path, `${message} It is ignored.`);
|
|
597
|
+
}
|
|
598
|
+
function link(reader, path, source, droppable = false) {
|
|
599
|
+
const key = JSON.stringify(path);
|
|
600
|
+
reader.sources.set(key, source);
|
|
601
|
+
if (droppable) {
|
|
602
|
+
reader.droppable.add(key);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function sourceOf(reader, path) {
|
|
606
|
+
for (let n = path.length; n >= 0; n--) {
|
|
607
|
+
const source = reader.sources.get(JSON.stringify(path.slice(0, n)));
|
|
608
|
+
if (source) {
|
|
609
|
+
return source;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
function compareOrder(a, b) {
|
|
615
|
+
for (const [i, n] of a.entries()) {
|
|
616
|
+
if (i === b.length) {
|
|
617
|
+
return 1;
|
|
618
|
+
}
|
|
619
|
+
const m = b[i];
|
|
620
|
+
if (n !== m) {
|
|
621
|
+
return n - m;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return a.length - b.length;
|
|
625
|
+
}
|
|
626
|
+
function droppableAt(reader, path) {
|
|
627
|
+
for (let n = path.length; n > 0; n--) {
|
|
628
|
+
const at = path.slice(0, n);
|
|
629
|
+
if (reader.droppable.has(JSON.stringify(at))) {
|
|
630
|
+
return at;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return void 0;
|
|
634
|
+
}
|
|
635
|
+
function remove(model, path) {
|
|
636
|
+
const parent = path.slice(0, -1).reduce(
|
|
637
|
+
(value, key) => value?.[key],
|
|
638
|
+
model
|
|
639
|
+
);
|
|
640
|
+
if (parent) {
|
|
641
|
+
delete parent[path[path.length - 1]];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
export {
|
|
645
|
+
fromJsonLd
|
|
646
|
+
};
|
|
647
|
+
//# sourceMappingURL=from-json-ld.js.map
|