@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,854 @@
|
|
|
1
|
+
// src/recipe/parse.ts
|
|
2
|
+
import {
|
|
3
|
+
describeValue,
|
|
4
|
+
quote,
|
|
5
|
+
RecipeBookError
|
|
6
|
+
} from "../errors.js";
|
|
7
|
+
import { hasOwn, isRecord, warning } from "../issues.js";
|
|
8
|
+
import { isPositive } from "../quantity/amount.js";
|
|
9
|
+
import { matchAmount, readPositive, readQuantity } from "../quantity/parse.js";
|
|
10
|
+
import {
|
|
11
|
+
asRecord,
|
|
12
|
+
checkFields,
|
|
13
|
+
error,
|
|
14
|
+
toResult
|
|
15
|
+
} from "../validation.js";
|
|
16
|
+
import { parseIsoDuration } from "./duration.js";
|
|
17
|
+
import {
|
|
18
|
+
assignIds,
|
|
19
|
+
insertPending,
|
|
20
|
+
slugify
|
|
21
|
+
} from "./ids.js";
|
|
22
|
+
import { inferSystem } from "./infer.js";
|
|
23
|
+
import { NUTRIENT_UNITS } from "./nutrient-units.js";
|
|
24
|
+
import { parseNutrient } from "./nutrition.js";
|
|
25
|
+
import { disallowedScheme } from "./url.js";
|
|
26
|
+
var RECIPE_FIELDS = {
|
|
27
|
+
id: true,
|
|
28
|
+
name: true,
|
|
29
|
+
description: true,
|
|
30
|
+
image: true,
|
|
31
|
+
servings: true,
|
|
32
|
+
yield: true,
|
|
33
|
+
system: true,
|
|
34
|
+
times: true,
|
|
35
|
+
ingredients: true,
|
|
36
|
+
steps: true,
|
|
37
|
+
notes: true,
|
|
38
|
+
equipment: true,
|
|
39
|
+
nutrition: true
|
|
40
|
+
};
|
|
41
|
+
var INGREDIENT_FIELDS = {
|
|
42
|
+
id: true,
|
|
43
|
+
name: true,
|
|
44
|
+
plural: true,
|
|
45
|
+
quantity: true,
|
|
46
|
+
equivalents: true,
|
|
47
|
+
note: true,
|
|
48
|
+
url: true,
|
|
49
|
+
optional: true
|
|
50
|
+
};
|
|
51
|
+
var SECTION_FIELDS = {
|
|
52
|
+
id: true,
|
|
53
|
+
title: true,
|
|
54
|
+
items: true
|
|
55
|
+
};
|
|
56
|
+
var STEP_FIELDS = {
|
|
57
|
+
id: true,
|
|
58
|
+
text: true,
|
|
59
|
+
name: true,
|
|
60
|
+
image: true
|
|
61
|
+
};
|
|
62
|
+
var SERVINGS_FIELDS = {
|
|
63
|
+
amount: true,
|
|
64
|
+
max: true
|
|
65
|
+
};
|
|
66
|
+
var YIELD_FIELDS = {
|
|
67
|
+
amount: true,
|
|
68
|
+
max: true,
|
|
69
|
+
unit: true,
|
|
70
|
+
plural: true
|
|
71
|
+
};
|
|
72
|
+
var TIMES_FIELDS = {
|
|
73
|
+
prep: true,
|
|
74
|
+
cook: true,
|
|
75
|
+
total: true,
|
|
76
|
+
custom: true
|
|
77
|
+
};
|
|
78
|
+
var CUSTOM_TIME_FIELDS = {
|
|
79
|
+
id: true,
|
|
80
|
+
label: true,
|
|
81
|
+
minutes: true
|
|
82
|
+
};
|
|
83
|
+
var NOTE_FIELDS = {
|
|
84
|
+
id: true,
|
|
85
|
+
title: true,
|
|
86
|
+
text: true
|
|
87
|
+
};
|
|
88
|
+
var EQUIPMENT_FIELDS = {
|
|
89
|
+
id: true,
|
|
90
|
+
name: true,
|
|
91
|
+
plural: true,
|
|
92
|
+
quantity: true,
|
|
93
|
+
url: true,
|
|
94
|
+
note: true
|
|
95
|
+
};
|
|
96
|
+
var NUTRIENT_EXAMPLES = {
|
|
97
|
+
kcal: 'kcal, e.g. 250, "250 kcal", or "1046 kJ"',
|
|
98
|
+
g: 'grams, e.g. 12 or "12 g"',
|
|
99
|
+
mg: 'milligrams, e.g. 300, "300 mg", or "0.3 g"'
|
|
100
|
+
};
|
|
101
|
+
function parseRecipe(input, options) {
|
|
102
|
+
return parseRecipeWith(
|
|
103
|
+
input,
|
|
104
|
+
readLineOption("parseRecipe", options) ?? "pass parseIngredientLine: parseRecipe(input, { parseIngredientLine })"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function parseRecipeWith(input, lines) {
|
|
108
|
+
const issues = [];
|
|
109
|
+
const recipe = readRecipe(issues, input, lines);
|
|
110
|
+
const label = recipe && quote(recipe.name);
|
|
111
|
+
return toResult("invalid_recipe", recipe, issues, label);
|
|
112
|
+
}
|
|
113
|
+
function readLineOption(fn, options) {
|
|
114
|
+
const reader = asRecord(options)?.parseIngredientLine;
|
|
115
|
+
if (reader !== void 0 && typeof reader !== "function") {
|
|
116
|
+
throw new RecipeBookError(
|
|
117
|
+
"invalid_argument",
|
|
118
|
+
`${fn}: parseIngredientLine must be a function such as parseIngredientLine, received ${describeValue(reader)}.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return reader;
|
|
122
|
+
}
|
|
123
|
+
function readRecipe(issues, value, lines) {
|
|
124
|
+
const input = asRecord(value);
|
|
125
|
+
if (!input) {
|
|
126
|
+
return error(
|
|
127
|
+
issues,
|
|
128
|
+
"invalid_type",
|
|
129
|
+
[],
|
|
130
|
+
`Expected a recipe object such as { name, ingredients, steps }, received ${describeValue(value)}.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const pending = [];
|
|
134
|
+
const id = readText(issues, input.id, ["id"]);
|
|
135
|
+
const name = readText(
|
|
136
|
+
issues,
|
|
137
|
+
input.name,
|
|
138
|
+
["name"],
|
|
139
|
+
"Add a name for the recipe."
|
|
140
|
+
);
|
|
141
|
+
const description = readText(issues, input.description, ["description"]);
|
|
142
|
+
const image = readUrl(issues, input.image, ["image"]);
|
|
143
|
+
const servings = readServings(issues, input.servings, ["servings"]);
|
|
144
|
+
const recipeYield = readYield(issues, input.yield, ["yield"]);
|
|
145
|
+
const authoredSystem = readSystem(issues, input.system, ["system"]);
|
|
146
|
+
const times = readTimes(issues, pending, input.times, ["times"]);
|
|
147
|
+
const ingredients = readList(
|
|
148
|
+
issues,
|
|
149
|
+
pending,
|
|
150
|
+
input.ingredients,
|
|
151
|
+
["ingredients"],
|
|
152
|
+
"ingredients",
|
|
153
|
+
(issues2, value2, path) => readIngredient(issues2, value2, path, lines)
|
|
154
|
+
);
|
|
155
|
+
const steps = readList(
|
|
156
|
+
issues,
|
|
157
|
+
pending,
|
|
158
|
+
input.steps,
|
|
159
|
+
["steps"],
|
|
160
|
+
"steps",
|
|
161
|
+
readStep
|
|
162
|
+
);
|
|
163
|
+
const notes = readItems(
|
|
164
|
+
issues,
|
|
165
|
+
pending,
|
|
166
|
+
input.notes,
|
|
167
|
+
["notes"],
|
|
168
|
+
"notes",
|
|
169
|
+
readNote
|
|
170
|
+
);
|
|
171
|
+
const equipment = readItems(
|
|
172
|
+
issues,
|
|
173
|
+
pending,
|
|
174
|
+
input.equipment,
|
|
175
|
+
["equipment"],
|
|
176
|
+
"equipment",
|
|
177
|
+
readEquipment
|
|
178
|
+
);
|
|
179
|
+
const nutrition = readNutrition(issues, input.nutrition, ["nutrition"]);
|
|
180
|
+
checkFields(issues, input, RECIPE_FIELDS, []);
|
|
181
|
+
insertPending(issues, pending);
|
|
182
|
+
if (name === void 0) {
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
const system = authoredSystem ?? inferSystem(ingredients);
|
|
186
|
+
return defined({
|
|
187
|
+
id: id ?? (slugify(name) || "recipe"),
|
|
188
|
+
name,
|
|
189
|
+
description,
|
|
190
|
+
image,
|
|
191
|
+
servings,
|
|
192
|
+
yield: recipeYield,
|
|
193
|
+
system,
|
|
194
|
+
times,
|
|
195
|
+
ingredients,
|
|
196
|
+
steps,
|
|
197
|
+
notes,
|
|
198
|
+
equipment,
|
|
199
|
+
nutrition
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function readList(issues, pending, value, path, noun, readItem) {
|
|
203
|
+
const sections = [];
|
|
204
|
+
let loose;
|
|
205
|
+
let count = 0;
|
|
206
|
+
const read = (entry, entryPath) => readItem(issues, entry, entryPath, ++count);
|
|
207
|
+
const visit = (entry, i) => {
|
|
208
|
+
const entryPath = [...path, i];
|
|
209
|
+
const section = asSection(entry);
|
|
210
|
+
if (section) {
|
|
211
|
+
loose = void 0;
|
|
212
|
+
sections.push(readSection(issues, section, entryPath, noun, read));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!loose) {
|
|
216
|
+
loose = {
|
|
217
|
+
authored: void 0,
|
|
218
|
+
derived: noun,
|
|
219
|
+
path: entryPath,
|
|
220
|
+
position: issues.length,
|
|
221
|
+
title: void 0,
|
|
222
|
+
items: []
|
|
223
|
+
};
|
|
224
|
+
sections.push(loose);
|
|
225
|
+
}
|
|
226
|
+
const item = read(entry, entryPath);
|
|
227
|
+
if (item) {
|
|
228
|
+
loose.items.push(item);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
if (value !== void 0 && !readArray(issues, value, path, noun, visit)) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
if (sections.length === 0) {
|
|
235
|
+
warning(issues, "empty_list", path, `Recipe has no ${noun}.`);
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
const sectionIds = assignIds(sections, pending);
|
|
239
|
+
const itemIds = assignIds(
|
|
240
|
+
sections.flatMap((section) => section.items),
|
|
241
|
+
pending
|
|
242
|
+
);
|
|
243
|
+
let next = 0;
|
|
244
|
+
return sections.map(
|
|
245
|
+
(section, i) => defined({
|
|
246
|
+
id: sectionIds[i],
|
|
247
|
+
title: section.title,
|
|
248
|
+
items: section.items.map((item) => item.build(itemIds[next++]))
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function asSection(entry) {
|
|
253
|
+
const record = asRecord(entry);
|
|
254
|
+
return record && "items" in record ? record : void 0;
|
|
255
|
+
}
|
|
256
|
+
function readSection(issues, record, path, noun, read) {
|
|
257
|
+
const position = issues.length;
|
|
258
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
259
|
+
const title = readText(issues, record.title, [...path, "title"]);
|
|
260
|
+
const items = [];
|
|
261
|
+
const itemsPath = [...path, "items"];
|
|
262
|
+
let visited = 0;
|
|
263
|
+
const entries = readArray(
|
|
264
|
+
issues,
|
|
265
|
+
record.items,
|
|
266
|
+
itemsPath,
|
|
267
|
+
noun,
|
|
268
|
+
(entry, j) => {
|
|
269
|
+
visited++;
|
|
270
|
+
const itemPath = [...itemsPath, j];
|
|
271
|
+
if (asSection(entry)) {
|
|
272
|
+
error(
|
|
273
|
+
issues,
|
|
274
|
+
"invalid_type",
|
|
275
|
+
itemPath,
|
|
276
|
+
`Sections can't be nested. Make this a top-level section in ${noun}.`
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const item = read(entry, itemPath);
|
|
281
|
+
if (item) {
|
|
282
|
+
items.push(item);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
if (entries && visited === 0) {
|
|
287
|
+
warning(issues, "empty_list", itemsPath, `Section has no ${noun}.`);
|
|
288
|
+
}
|
|
289
|
+
checkFields(issues, record, SECTION_FIELDS, path);
|
|
290
|
+
return {
|
|
291
|
+
authored,
|
|
292
|
+
derived: title && slugify(title) || noun,
|
|
293
|
+
path,
|
|
294
|
+
position,
|
|
295
|
+
title,
|
|
296
|
+
items
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function readItems(issues, pending, value, path, noun, readItem) {
|
|
300
|
+
let n = 0;
|
|
301
|
+
const drafts = value === void 0 ? [] : readArray(
|
|
302
|
+
issues,
|
|
303
|
+
value,
|
|
304
|
+
path,
|
|
305
|
+
noun,
|
|
306
|
+
(entry, i) => readItem(issues, entry, [...path, i], ++n)
|
|
307
|
+
);
|
|
308
|
+
if (!drafts) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const ids = assignIds(drafts, pending);
|
|
312
|
+
return drafts.map((draft, i) => draft.build(ids[i]));
|
|
313
|
+
}
|
|
314
|
+
function readArray(issues, value, path, noun, read) {
|
|
315
|
+
if (!Array.isArray(value)) {
|
|
316
|
+
return error(
|
|
317
|
+
issues,
|
|
318
|
+
"invalid_type",
|
|
319
|
+
path,
|
|
320
|
+
`Expected a list of ${noun}, received ${describeValue(value)}.`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const kept = [];
|
|
324
|
+
for (let i = 0, n = value.length; i < n; i++) {
|
|
325
|
+
if (i in value) {
|
|
326
|
+
const entry = value[i];
|
|
327
|
+
const result = hasOwn(value, i) && read(entry, i);
|
|
328
|
+
if (result) {
|
|
329
|
+
kept.push(result);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return kept;
|
|
334
|
+
}
|
|
335
|
+
function readIngredient(issues, value, path, lines) {
|
|
336
|
+
if (typeof value === "string" && typeof lines === "function") {
|
|
337
|
+
const result = lines(value);
|
|
338
|
+
for (const issue of result.success ? result.warnings : result.error.issues) {
|
|
339
|
+
issues.push({ ...issue, path: [...path, ...issue.path] });
|
|
340
|
+
}
|
|
341
|
+
return result.success ? readIngredient(issues, result.data, path) : void 0;
|
|
342
|
+
}
|
|
343
|
+
const record = asRecord(value);
|
|
344
|
+
if (!record) {
|
|
345
|
+
return error(
|
|
346
|
+
issues,
|
|
347
|
+
"invalid_type",
|
|
348
|
+
path,
|
|
349
|
+
`Expected an ingredient such as ${typeof lines === "function" ? '"2 cups flour" or ' : ""}{ name: "flour", quantity: "2 cups" }, received ${describeValue(value)}.${typeof value === "string" && typeof lines === "string" ? ` To read ingredient lines, ${lines}.` : ""}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const position = issues.length;
|
|
353
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
354
|
+
const name = readText(
|
|
355
|
+
issues,
|
|
356
|
+
record.name,
|
|
357
|
+
[...path, "name"],
|
|
358
|
+
'Add a name, e.g. name: "flour".'
|
|
359
|
+
);
|
|
360
|
+
const plural = readText(issues, record.plural, [...path, "plural"]);
|
|
361
|
+
const quantity = record.quantity === void 0 ? void 0 : readQuantity(issues, record.quantity, [...path, "quantity"]);
|
|
362
|
+
const equivalents = readEquivalents(issues, record.equivalents, [
|
|
363
|
+
...path,
|
|
364
|
+
"equivalents"
|
|
365
|
+
]);
|
|
366
|
+
const note = readText(issues, record.note, [...path, "note"]);
|
|
367
|
+
const url = readUrl(issues, record.url, [...path, "url"]);
|
|
368
|
+
const optional = readBoolean(issues, record.optional, [...path, "optional"]);
|
|
369
|
+
if (equivalents.length > 0 && record.quantity === void 0) {
|
|
370
|
+
error(
|
|
371
|
+
issues,
|
|
372
|
+
"missing_field",
|
|
373
|
+
[...path, "quantity"],
|
|
374
|
+
"Add a quantity. Equivalents are alternatives to it, so they need one."
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
checkFields(issues, record, INGREDIENT_FIELDS, path);
|
|
378
|
+
if (name === void 0) {
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
authored,
|
|
383
|
+
derived: slugify(name) || "ingredient",
|
|
384
|
+
path,
|
|
385
|
+
position,
|
|
386
|
+
build: (id) => defined({ id, name, plural, quantity, equivalents, note, url, optional })
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function readEquivalents(issues, value, path) {
|
|
390
|
+
return value !== void 0 && readArray(
|
|
391
|
+
issues,
|
|
392
|
+
value,
|
|
393
|
+
path,
|
|
394
|
+
'quantities, e.g. ["190 g"]',
|
|
395
|
+
(entry, i) => readQuantity(issues, entry, [...path, i])
|
|
396
|
+
) || [];
|
|
397
|
+
}
|
|
398
|
+
function readStep(issues, value, path, n) {
|
|
399
|
+
const isText = typeof value === "string";
|
|
400
|
+
const record = asRecord(isText ? { text: value } : value);
|
|
401
|
+
if (!record) {
|
|
402
|
+
return error(
|
|
403
|
+
issues,
|
|
404
|
+
"invalid_type",
|
|
405
|
+
path,
|
|
406
|
+
`Expected a string or { text }, received ${describeValue(value)}.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const position = issues.length;
|
|
410
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
411
|
+
const text = readText(
|
|
412
|
+
issues,
|
|
413
|
+
record.text,
|
|
414
|
+
isText ? path : [...path, "text"],
|
|
415
|
+
"Add the step text."
|
|
416
|
+
);
|
|
417
|
+
const name = readText(issues, record.name, [...path, "name"]);
|
|
418
|
+
const image = readUrl(issues, record.image, [...path, "image"]);
|
|
419
|
+
checkFields(issues, record, STEP_FIELDS, path);
|
|
420
|
+
if (text === void 0) {
|
|
421
|
+
return void 0;
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
authored,
|
|
425
|
+
derived: name && slugify(name) || `step-${n}`,
|
|
426
|
+
path,
|
|
427
|
+
position,
|
|
428
|
+
build: (id) => defined({ id, text, name, image })
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function readNote(issues, value, path, n) {
|
|
432
|
+
const isText = typeof value === "string";
|
|
433
|
+
const record = asRecord(isText ? { text: value } : value);
|
|
434
|
+
if (!record) {
|
|
435
|
+
return error(
|
|
436
|
+
issues,
|
|
437
|
+
"invalid_type",
|
|
438
|
+
path,
|
|
439
|
+
`Expected a string or { text }, received ${describeValue(value)}.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const position = issues.length;
|
|
443
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
444
|
+
const title = readText(issues, record.title, [...path, "title"]);
|
|
445
|
+
const text = readText(
|
|
446
|
+
issues,
|
|
447
|
+
record.text,
|
|
448
|
+
isText ? path : [...path, "text"],
|
|
449
|
+
"Add the note text."
|
|
450
|
+
);
|
|
451
|
+
checkFields(issues, record, NOTE_FIELDS, path);
|
|
452
|
+
if (text === void 0) {
|
|
453
|
+
return void 0;
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
authored,
|
|
457
|
+
derived: title && slugify(title) || `note-${n}`,
|
|
458
|
+
path,
|
|
459
|
+
position,
|
|
460
|
+
build: (id) => defined({ id, title, text })
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function readEquipment(issues, value, path) {
|
|
464
|
+
const isText = typeof value === "string";
|
|
465
|
+
const record = asRecord(isText ? { name: value } : value);
|
|
466
|
+
if (!record) {
|
|
467
|
+
return error(
|
|
468
|
+
issues,
|
|
469
|
+
"invalid_type",
|
|
470
|
+
path,
|
|
471
|
+
`Expected a string or { name }, received ${describeValue(value)}.`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const position = issues.length;
|
|
475
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
476
|
+
const name = readText(
|
|
477
|
+
issues,
|
|
478
|
+
record.name,
|
|
479
|
+
isText ? path : [...path, "name"],
|
|
480
|
+
`Add a name, e.g. ${isText ? "" : "name: "}"whisk".`
|
|
481
|
+
);
|
|
482
|
+
const plural = readText(issues, record.plural, [...path, "plural"]);
|
|
483
|
+
const quantity = record.quantity === void 0 ? void 0 : readCount(issues, record.quantity, [...path, "quantity"]);
|
|
484
|
+
const url = readUrl(issues, record.url, [...path, "url"]);
|
|
485
|
+
const note = readText(issues, record.note, [...path, "note"]);
|
|
486
|
+
checkFields(issues, record, EQUIPMENT_FIELDS, path);
|
|
487
|
+
if (name === void 0) {
|
|
488
|
+
return void 0;
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
authored,
|
|
492
|
+
derived: slugify(name) || "equipment",
|
|
493
|
+
path,
|
|
494
|
+
position,
|
|
495
|
+
build: (id) => defined({ id, name, plural, quantity, url, note })
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function defined(fields) {
|
|
499
|
+
const copy = {};
|
|
500
|
+
for (const key in fields) {
|
|
501
|
+
if (hasOwn(fields, key) && fields[key] !== void 0) {
|
|
502
|
+
copy[key] = fields[key];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return copy;
|
|
506
|
+
}
|
|
507
|
+
function readCount(issues, value, path) {
|
|
508
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
return error(
|
|
512
|
+
issues,
|
|
513
|
+
typeof value === "number" ? "invalid_amount" : "invalid_type",
|
|
514
|
+
path,
|
|
515
|
+
`Expected a whole number greater than 0, received ${describeValue(value)}.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
function readServings(issues, value, path) {
|
|
519
|
+
if (value === void 0) {
|
|
520
|
+
return void 0;
|
|
521
|
+
}
|
|
522
|
+
if (typeof value === "number") {
|
|
523
|
+
const amount = readPositive(issues, value, path);
|
|
524
|
+
return amount === void 0 ? void 0 : { amount };
|
|
525
|
+
}
|
|
526
|
+
const expected = `Expected a number of servings, e.g. 4 or "4\u20136", received ${describeValue(value)}.`;
|
|
527
|
+
if (typeof value === "string") {
|
|
528
|
+
const match = readRangeText(issues, value, path, expected);
|
|
529
|
+
if (match?.rest) {
|
|
530
|
+
return error(issues, "invalid_quantity", path, expected);
|
|
531
|
+
}
|
|
532
|
+
return match?.range;
|
|
533
|
+
}
|
|
534
|
+
const record = asRecord(value);
|
|
535
|
+
if (!record) {
|
|
536
|
+
return error(issues, "invalid_type", path, expected);
|
|
537
|
+
}
|
|
538
|
+
const range = readRange(
|
|
539
|
+
issues,
|
|
540
|
+
record,
|
|
541
|
+
path,
|
|
542
|
+
"Add an amount, e.g. { amount: 4, max: 6 }."
|
|
543
|
+
);
|
|
544
|
+
checkFields(issues, record, SERVINGS_FIELDS, path);
|
|
545
|
+
return range;
|
|
546
|
+
}
|
|
547
|
+
function readYield(issues, value, path) {
|
|
548
|
+
if (value === void 0) {
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
const isText = typeof value === "string";
|
|
552
|
+
const expected = `Expected a batch such as "24 cookies" or ${isText ? '"3\u20134 loaves"' : "{ amount, unit }"}, received ${describeValue(value)}.`;
|
|
553
|
+
if (isText) {
|
|
554
|
+
const match = readRangeText(issues, value, path, expected);
|
|
555
|
+
if (!match) {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
const { range: range2, rest } = match;
|
|
559
|
+
if (!rest) {
|
|
560
|
+
return error(
|
|
561
|
+
issues,
|
|
562
|
+
"missing_field",
|
|
563
|
+
path,
|
|
564
|
+
`Add what the recipe makes, e.g. "24 cookies".${servingsHint(range2)}`
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
if (!/\p{L}/u.test(rest) || /^(?:[-–—&]|(?:[\d.,/\p{No}]+|or|to|x|and)(?:\s|$))/iu.test(rest)) {
|
|
568
|
+
const suggestion = suggestJoinedYield(value, range2, rest);
|
|
569
|
+
return error(
|
|
570
|
+
issues,
|
|
571
|
+
"invalid_quantity",
|
|
572
|
+
path,
|
|
573
|
+
suggestion ? `${expected} Did you mean "${suggestion}"?` : expected,
|
|
574
|
+
suggestion
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
return { ...range2, unit: rest };
|
|
578
|
+
}
|
|
579
|
+
const record = asRecord(value);
|
|
580
|
+
if (!record) {
|
|
581
|
+
return error(
|
|
582
|
+
issues,
|
|
583
|
+
"invalid_type",
|
|
584
|
+
path,
|
|
585
|
+
`${expected}${isPositive(value) ? servingsHint({ amount: value }) : ""}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const range = readRange(
|
|
589
|
+
issues,
|
|
590
|
+
record,
|
|
591
|
+
path,
|
|
592
|
+
'Add an amount, e.g. { amount: 24, unit: "cookie" }.'
|
|
593
|
+
);
|
|
594
|
+
const unit = readText(
|
|
595
|
+
issues,
|
|
596
|
+
record.unit,
|
|
597
|
+
[...path, "unit"],
|
|
598
|
+
`Add a unit, e.g. { amount: 24, unit: "cookie" }.${servingsHint(range)}`
|
|
599
|
+
);
|
|
600
|
+
const plural = readText(issues, record.plural, [...path, "plural"]);
|
|
601
|
+
checkFields(issues, record, YIELD_FIELDS, path);
|
|
602
|
+
if (range === void 0 || unit === void 0) {
|
|
603
|
+
return void 0;
|
|
604
|
+
}
|
|
605
|
+
return defined({ ...range, unit, plural });
|
|
606
|
+
}
|
|
607
|
+
function suggestJoinedYield(text, { amount }, rest) {
|
|
608
|
+
const joiner = /^(?:and|&)\s*/i.exec(rest);
|
|
609
|
+
const next = joiner && matchAmount(rest.slice(joiner[0].length));
|
|
610
|
+
if (!next || "code" in next || typeof next.amount !== "number") {
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
const isFraction = next.amount < 1;
|
|
614
|
+
if (!isFraction && !Number.isInteger(next.amount)) {
|
|
615
|
+
return void 0;
|
|
616
|
+
}
|
|
617
|
+
const head = text.replace(/\s+/g, " ").trim().slice(0, -rest.length);
|
|
618
|
+
const suggestion = `${head.trim()}${isFraction ? " " : "-"}${next.amountText} ${next.rest}`;
|
|
619
|
+
const parsed = readYield([], suggestion, []);
|
|
620
|
+
return parsed && (isFraction ? parsed.amount === amount + next.amount && parsed.max === void 0 : parsed.amount === amount && parsed.max === next.amount) ? suggestion : void 0;
|
|
621
|
+
}
|
|
622
|
+
function readRangeText(issues, text, path, expected) {
|
|
623
|
+
const match = matchAmount(text.replace(/\s+/g, " ").trim());
|
|
624
|
+
if (!match) {
|
|
625
|
+
return error(issues, "invalid_quantity", path, expected);
|
|
626
|
+
}
|
|
627
|
+
if ("code" in match) {
|
|
628
|
+
return error(issues, match.code, path, match.message);
|
|
629
|
+
}
|
|
630
|
+
const { amount, rest } = match;
|
|
631
|
+
const range = typeof amount === "number" ? { amount } : { amount: amount.min, max: amount.max };
|
|
632
|
+
if (readPositive(issues, range.amount, path) === void 0 || range.max !== void 0 && readPositive(issues, range.max, path) === void 0) {
|
|
633
|
+
return void 0;
|
|
634
|
+
}
|
|
635
|
+
return { range, rest };
|
|
636
|
+
}
|
|
637
|
+
function readRange(issues, record, path, missingAmount) {
|
|
638
|
+
const value = record.amount;
|
|
639
|
+
const amount = value === void 0 ? error(issues, "missing_field", [...path, "amount"], missingAmount) : isRecord(value) && hasOwn(value, "min") && hasOwn(value, "max") ? error(
|
|
640
|
+
issues,
|
|
641
|
+
"invalid_type",
|
|
642
|
+
[...path, "amount"],
|
|
643
|
+
"Expected a positive number, received an object. Write a range as { amount: 4, max: 6 }."
|
|
644
|
+
) : readPositive(issues, value, [...path, "amount"]);
|
|
645
|
+
const max = record.max === void 0 ? void 0 : readPositive(issues, record.max, [...path, "max"]);
|
|
646
|
+
if (amount === void 0) {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
if (max !== void 0 && max < amount) {
|
|
650
|
+
return error(
|
|
651
|
+
issues,
|
|
652
|
+
"invalid_range",
|
|
653
|
+
[...path, "max"],
|
|
654
|
+
`Expected a maximum greater than the amount (${amount}), received ${max}.`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return max === void 0 || max === amount ? { amount } : { amount, max };
|
|
658
|
+
}
|
|
659
|
+
function servingsHint(servings) {
|
|
660
|
+
if (!servings) {
|
|
661
|
+
return "";
|
|
662
|
+
}
|
|
663
|
+
const { amount, max } = servings;
|
|
664
|
+
return ` For a number of servings, use servings: ${max === void 0 ? amount : quote(`${amount}\u2013${max}`)}.`;
|
|
665
|
+
}
|
|
666
|
+
function readSystem(issues, value, path) {
|
|
667
|
+
if (value === void 0 || value === "metric" || value === "us") {
|
|
668
|
+
return value;
|
|
669
|
+
}
|
|
670
|
+
const suggestion = value === "imperial" ? "us" : void 0;
|
|
671
|
+
return error(
|
|
672
|
+
issues,
|
|
673
|
+
"invalid_type",
|
|
674
|
+
path,
|
|
675
|
+
`Expected "metric" or "us", received ${describeValue(value)}.${suggestion ? ` Did you mean "${suggestion}"?` : ""}`,
|
|
676
|
+
suggestion
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
function readTimes(issues, pending, value, path) {
|
|
680
|
+
if (value === void 0) {
|
|
681
|
+
return { custom: [] };
|
|
682
|
+
}
|
|
683
|
+
const record = asRecord(value);
|
|
684
|
+
if (!record) {
|
|
685
|
+
error(
|
|
686
|
+
issues,
|
|
687
|
+
"invalid_type",
|
|
688
|
+
path,
|
|
689
|
+
`Expected { prep, cook, total, custom }, received ${describeValue(value)}.`
|
|
690
|
+
);
|
|
691
|
+
return { custom: [] };
|
|
692
|
+
}
|
|
693
|
+
const prep = readDuration(issues, record.prep, [...path, "prep"]);
|
|
694
|
+
const cook = readDuration(issues, record.cook, [...path, "cook"]);
|
|
695
|
+
const total = readDuration(issues, record.total, [...path, "total"]);
|
|
696
|
+
const custom = readItems(
|
|
697
|
+
issues,
|
|
698
|
+
pending,
|
|
699
|
+
record.custom,
|
|
700
|
+
[...path, "custom"],
|
|
701
|
+
"custom times",
|
|
702
|
+
readCustomTime
|
|
703
|
+
);
|
|
704
|
+
checkFields(issues, record, TIMES_FIELDS, path);
|
|
705
|
+
return defined({ prep, cook, total, custom });
|
|
706
|
+
}
|
|
707
|
+
function readCustomTime(issues, value, path) {
|
|
708
|
+
const record = asRecord(value);
|
|
709
|
+
if (!record) {
|
|
710
|
+
return error(
|
|
711
|
+
issues,
|
|
712
|
+
"invalid_type",
|
|
713
|
+
path,
|
|
714
|
+
`Expected { label, minutes }, e.g. { label: "Chill", minutes: 60 }, received ${describeValue(value)}.`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const position = issues.length;
|
|
718
|
+
const authored = readText(issues, record.id, [...path, "id"]);
|
|
719
|
+
const label = readText(
|
|
720
|
+
issues,
|
|
721
|
+
record.label,
|
|
722
|
+
[...path, "label"],
|
|
723
|
+
'Add a label, e.g. label: "Chill".'
|
|
724
|
+
);
|
|
725
|
+
const minutes = record.minutes === void 0 ? error(
|
|
726
|
+
issues,
|
|
727
|
+
"missing_field",
|
|
728
|
+
[...path, "minutes"],
|
|
729
|
+
'Add the time, e.g. minutes: 60 or minutes: "PT1H".'
|
|
730
|
+
) : readDuration(issues, record.minutes, [...path, "minutes"]);
|
|
731
|
+
checkFields(issues, record, CUSTOM_TIME_FIELDS, path);
|
|
732
|
+
if (label === void 0 || minutes === void 0) {
|
|
733
|
+
return void 0;
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
authored,
|
|
737
|
+
derived: slugify(label) || "time",
|
|
738
|
+
path,
|
|
739
|
+
position,
|
|
740
|
+
build: (id) => ({ id, label, minutes })
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function readDuration(issues, value, path) {
|
|
744
|
+
if (value === void 0) {
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
747
|
+
const minutes = typeof value === "number" ? nonNegative(value) : typeof value === "string" ? parseIsoDuration(value) : void 0;
|
|
748
|
+
if (minutes === void 0) {
|
|
749
|
+
return error(
|
|
750
|
+
issues,
|
|
751
|
+
"invalid_duration",
|
|
752
|
+
path,
|
|
753
|
+
`Expected minutes or an ISO 8601 duration like "PT30M", received ${describeValue(value)}.`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return minutes;
|
|
757
|
+
}
|
|
758
|
+
function readNutrition(issues, value, path) {
|
|
759
|
+
if (value === void 0) {
|
|
760
|
+
return void 0;
|
|
761
|
+
}
|
|
762
|
+
const record = asRecord(value);
|
|
763
|
+
if (!record) {
|
|
764
|
+
return error(
|
|
765
|
+
issues,
|
|
766
|
+
"invalid_type",
|
|
767
|
+
path,
|
|
768
|
+
`Expected nutrition for one serving, e.g. { calories: 250, fat: "12 g" }, received ${describeValue(value)}.`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const nutrition = {};
|
|
772
|
+
const servingSize = readText(issues, record.servingSize, [
|
|
773
|
+
...path,
|
|
774
|
+
"servingSize"
|
|
775
|
+
]);
|
|
776
|
+
if (servingSize !== void 0) {
|
|
777
|
+
nutrition.servingSize = servingSize;
|
|
778
|
+
}
|
|
779
|
+
for (const key of Object.keys(NUTRIENT_UNITS)) {
|
|
780
|
+
const amount = readNutrient(
|
|
781
|
+
issues,
|
|
782
|
+
record[key],
|
|
783
|
+
[...path, key],
|
|
784
|
+
NUTRIENT_UNITS[key]
|
|
785
|
+
);
|
|
786
|
+
if (amount !== void 0) {
|
|
787
|
+
nutrition[key] = amount;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
checkFields(issues, record, { servingSize: true, ...NUTRIENT_UNITS }, path);
|
|
791
|
+
return Object.keys(nutrition).length > 0 ? nutrition : void 0;
|
|
792
|
+
}
|
|
793
|
+
function readNutrient(issues, value, path, unit) {
|
|
794
|
+
const text = typeof value === "string" ? value.trim() : void 0;
|
|
795
|
+
if (value === void 0 || text === "") {
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
const amount = typeof value === "number" ? nonNegative(value) : text === void 0 ? void 0 : parseNutrient(text, unit);
|
|
799
|
+
if (typeof amount === "number") {
|
|
800
|
+
return amount;
|
|
801
|
+
}
|
|
802
|
+
return error(
|
|
803
|
+
issues,
|
|
804
|
+
typeof value === "number" ? "invalid_amount" : text === void 0 ? "invalid_type" : "invalid_quantity",
|
|
805
|
+
path,
|
|
806
|
+
amount?.message ?? `Expected ${NUTRIENT_EXAMPLES[unit]}, received ${describeValue(value)}.`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function nonNegative(value) {
|
|
810
|
+
return Number.isFinite(value) && value >= 0 ? Math.abs(value) : void 0;
|
|
811
|
+
}
|
|
812
|
+
function readText(issues, value, path, requiredMessage) {
|
|
813
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
814
|
+
return error(
|
|
815
|
+
issues,
|
|
816
|
+
"invalid_type",
|
|
817
|
+
path,
|
|
818
|
+
`Expected text, received ${describeValue(value)}.`
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
const text = value?.trim();
|
|
822
|
+
if (!text) {
|
|
823
|
+
return requiredMessage === void 0 ? void 0 : error(issues, "missing_field", path, requiredMessage);
|
|
824
|
+
}
|
|
825
|
+
return text;
|
|
826
|
+
}
|
|
827
|
+
function readUrl(issues, value, path) {
|
|
828
|
+
const url = readText(issues, value, path);
|
|
829
|
+
const scheme = url === void 0 ? void 0 : disallowedScheme(url);
|
|
830
|
+
return scheme === void 0 ? url : error(
|
|
831
|
+
issues,
|
|
832
|
+
"invalid_url",
|
|
833
|
+
path,
|
|
834
|
+
`Expected an http or https URL, or a relative one, received ${quote(`${scheme}\u2026`)}.`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
function readBoolean(issues, value, path) {
|
|
838
|
+
if (value === void 0 || typeof value === "boolean") {
|
|
839
|
+
return value ?? false;
|
|
840
|
+
}
|
|
841
|
+
error(
|
|
842
|
+
issues,
|
|
843
|
+
"invalid_type",
|
|
844
|
+
path,
|
|
845
|
+
`Expected true or false, received ${describeValue(value)}.`
|
|
846
|
+
);
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
export {
|
|
850
|
+
parseRecipe,
|
|
851
|
+
parseRecipeWith,
|
|
852
|
+
readLineOption
|
|
853
|
+
};
|
|
854
|
+
//# sourceMappingURL=parse.js.map
|