mcp-recipes 1.3.0 → 2.0.0
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 +21 -0
- package/README.md +41 -3
- package/dist/index.js +1298 -552
- package/dist/index.js.map +1 -1
- package/dist/recipe/scale.js +861 -326
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +13 -133
- package/dist/sources/client.js +217 -153
- package/dist/sources/client.js.map +1 -1
- package/package.json +18 -9
- package/server.json +78 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import process2 from "process";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
|
|
6
7
|
// src/server.ts
|
|
7
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
9
|
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import process from "process";
|
|
12
|
+
|
|
9
13
|
// src/version.ts
|
|
10
|
-
var PKG_VERSION = "
|
|
14
|
+
var PKG_VERSION = "2.0.0";
|
|
11
15
|
var REPO_URL = "https://github.com/smeet666/mcp-recipes";
|
|
12
16
|
var ISSUES_URL = "https://github.com/smeet666/mcp-recipes/issues";
|
|
13
17
|
|
|
@@ -20,7 +24,9 @@ var DEFAULT_USER_AGENT = `mcp-recipes/${PKG_VERSION} (+${REPO_URL})`;
|
|
|
20
24
|
function createLogger(level) {
|
|
21
25
|
const rank = LOG_LEVELS.indexOf(level);
|
|
22
26
|
const write = (at, message) => {
|
|
23
|
-
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank)
|
|
27
|
+
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
process.stderr.write(`[mcp-recipes] ${at}: ${message}
|
|
25
31
|
`);
|
|
26
32
|
};
|
|
@@ -35,9 +41,11 @@ function createLogger(level) {
|
|
|
35
41
|
}
|
|
36
42
|
function readInteger(env, name, fallback, min, max) {
|
|
37
43
|
const raw = env[name];
|
|
38
|
-
if (raw === void 0 || raw.trim() === "")
|
|
44
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
39
47
|
const value = Number(raw);
|
|
40
|
-
if (!Number.isFinite(value)
|
|
48
|
+
if (!(Number.isFinite(value) && Number.isInteger(value))) {
|
|
41
49
|
process.stderr.write(
|
|
42
50
|
`[mcp-recipes] error: ${name}="${raw}" is not a whole number; using ${fallback}.
|
|
43
51
|
`
|
|
@@ -96,14 +104,14 @@ var RecipesError = class extends Error {
|
|
|
96
104
|
code;
|
|
97
105
|
details;
|
|
98
106
|
constructor(code, message, details = {}) {
|
|
99
|
-
super(message);
|
|
107
|
+
super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
|
|
100
108
|
this.name = "RecipesError";
|
|
101
109
|
this.code = code;
|
|
102
110
|
this.details = details;
|
|
103
111
|
}
|
|
104
112
|
};
|
|
105
113
|
var notFound = (message, details) => new RecipesError("not_found", message, details ?? {});
|
|
106
|
-
var invalidInput = (message, hint) => new RecipesError("invalid_input", message, hint ? { hint } : {});
|
|
114
|
+
var invalidInput = (message, hint, cause) => new RecipesError("invalid_input", message, { ...hint ? { hint } : {}, cause });
|
|
107
115
|
var rateLimited = (message, details) => new RecipesError("rate_limited", message, {
|
|
108
116
|
hint: "Wait a moment and ask again. This says nothing about whether the recipe exists.",
|
|
109
117
|
...details
|
|
@@ -115,7 +123,9 @@ var parseFailure = (message, details) => new RecipesError("parse_failure", messa
|
|
|
115
123
|
var networkError = (message, details) => new RecipesError("network_error", message, details ?? {});
|
|
116
124
|
var timeout = (message, details) => new RecipesError("timeout", message, details ?? {});
|
|
117
125
|
function toRecipesError(error) {
|
|
118
|
-
if (error instanceof RecipesError)
|
|
126
|
+
if (error instanceof RecipesError) {
|
|
127
|
+
return error;
|
|
128
|
+
}
|
|
119
129
|
const message = error instanceof Error ? error.message : String(error);
|
|
120
130
|
const raw = error;
|
|
121
131
|
const code = typeof raw?.code === "string" ? raw.code : "";
|
|
@@ -124,14 +134,20 @@ function toRecipesError(error) {
|
|
|
124
134
|
return new RecipesError(known ?? "network_error", message, details);
|
|
125
135
|
}
|
|
126
136
|
function fromSource(error, sourceName) {
|
|
127
|
-
if (error instanceof RecipesError)
|
|
137
|
+
if (error instanceof RecipesError) {
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
128
140
|
const known = toRecipesError(error);
|
|
129
141
|
const reported = known.message.replace(/\s+/g, " ").trim();
|
|
130
142
|
const opening = known.code === "not_found" ? `${sourceName} holds nothing under this name.` : `${sourceName} could not be read.`;
|
|
131
143
|
const message = reported === "" ? opening : `${opening} It reports: "${reported}"`;
|
|
132
144
|
const details = {};
|
|
133
|
-
if (known.details.url !== void 0)
|
|
134
|
-
|
|
145
|
+
if (known.details.url !== void 0) {
|
|
146
|
+
details.url = known.details.url;
|
|
147
|
+
}
|
|
148
|
+
if (known.details.status !== void 0) {
|
|
149
|
+
details.status = known.details.status;
|
|
150
|
+
}
|
|
135
151
|
switch (known.code) {
|
|
136
152
|
case "not_found":
|
|
137
153
|
return notFound(message, details);
|
|
@@ -148,72 +164,6 @@ function fromSource(error, sourceName) {
|
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
|
|
151
|
-
// src/sources/adapter.ts
|
|
152
|
-
function text(value) {
|
|
153
|
-
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
154
|
-
}
|
|
155
|
-
function textList(value) {
|
|
156
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
157
|
-
}
|
|
158
|
-
function count(value) {
|
|
159
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
160
|
-
}
|
|
161
|
-
function required(value, field, source) {
|
|
162
|
-
const found = text(value);
|
|
163
|
-
if (found === null) {
|
|
164
|
-
throw parseFailure(
|
|
165
|
-
`${source.name} answered without a readable "${field}", so there is no recipe to return.`
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return found;
|
|
169
|
-
}
|
|
170
|
-
function rowsOf(value, source) {
|
|
171
|
-
if (!Array.isArray(value)) {
|
|
172
|
-
throw parseFailure(
|
|
173
|
-
`${source.name} answered in a shape this server could not read: the list of results was missing.`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return value;
|
|
177
|
-
}
|
|
178
|
-
function namespacedId(source, reference) {
|
|
179
|
-
return `${source}:${reference}`;
|
|
180
|
-
}
|
|
181
|
-
function publishedFigures(panel) {
|
|
182
|
-
if (typeof panel !== "object" || panel === null) return null;
|
|
183
|
-
const kept = {};
|
|
184
|
-
for (const [key, value] of Object.entries(panel)) {
|
|
185
|
-
if (typeof value === "string" && value.trim() !== "") kept[key] = value.trim();
|
|
186
|
-
}
|
|
187
|
-
return Object.keys(kept).length > 0 ? kept : null;
|
|
188
|
-
}
|
|
189
|
-
function readYieldSpan(published) {
|
|
190
|
-
if (published === null) return { count: null, max: null, unit: null };
|
|
191
|
-
const span = /^\s*(\d+(?:[.,]\d+)?)\s*(?:à|a|to|-|–|—|ou|or)\s*(\d+(?:[.,]\d+)?)\s*(.*)$/i.exec(
|
|
192
|
-
published
|
|
193
|
-
);
|
|
194
|
-
if (span) {
|
|
195
|
-
const low = Number(span[1].replace(",", "."));
|
|
196
|
-
const high = Number(span[2].replace(",", "."));
|
|
197
|
-
if (Number.isFinite(low) && Number.isFinite(high) && high > low) {
|
|
198
|
-
return { count: low, max: high, unit: text(span[3]) };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const single = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/.exec(published);
|
|
202
|
-
if (single) {
|
|
203
|
-
const value = Number(single[1].replace(",", "."));
|
|
204
|
-
if (Number.isFinite(value)) return { count: value, max: null, unit: text(single[2]) };
|
|
205
|
-
}
|
|
206
|
-
return { count: null, max: null, unit: null };
|
|
207
|
-
}
|
|
208
|
-
function detailBase(source) {
|
|
209
|
-
return {
|
|
210
|
-
source: source.id,
|
|
211
|
-
sourceName: source.name,
|
|
212
|
-
language: source.language,
|
|
213
|
-
attribution: source.attribution
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
167
|
// src/sources/ids.ts
|
|
218
168
|
var SEPARATOR = ":";
|
|
219
169
|
function resolveId(rawId, sources) {
|
|
@@ -226,7 +176,9 @@ function resolveId(rawId, sources) {
|
|
|
226
176
|
}
|
|
227
177
|
for (const source of sources) {
|
|
228
178
|
const prefix = `${source.id}${SEPARATOR}`;
|
|
229
|
-
if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
179
|
+
if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
230
182
|
const reference = trimmed.slice(prefix.length).trim();
|
|
231
183
|
if (reference === "") {
|
|
232
184
|
throw invalidInput(
|
|
@@ -260,8 +212,10 @@ function resolveId(rawId, sources) {
|
|
|
260
212
|
}
|
|
261
213
|
function names(sources) {
|
|
262
214
|
const all = sources.map((source) => source.name);
|
|
263
|
-
if (all.length <= 1)
|
|
264
|
-
|
|
215
|
+
if (all.length <= 1) {
|
|
216
|
+
return all.join("");
|
|
217
|
+
}
|
|
218
|
+
return `${all.slice(0, -1).join(", ")} and ${all.at(-1)}`;
|
|
265
219
|
}
|
|
266
220
|
function example(sources) {
|
|
267
221
|
const first = sources[0];
|
|
@@ -272,6 +226,82 @@ function example(sources) {
|
|
|
272
226
|
import { MarmitonClient } from "mcp-marmiton/client";
|
|
273
227
|
import { CookbookClient } from "mcp-wikibooks-cookbook/client";
|
|
274
228
|
|
|
229
|
+
// src/sources/adapter.ts
|
|
230
|
+
var NUMBER_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/;
|
|
231
|
+
var RANGE_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(?:à|a|to|-|–|—|ou|or)\s*(\d+(?:[.,]\d+)?)\s*(.*)$/i;
|
|
232
|
+
function text(value) {
|
|
233
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
234
|
+
}
|
|
235
|
+
function textList(value) {
|
|
236
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
237
|
+
}
|
|
238
|
+
function count(value) {
|
|
239
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
240
|
+
}
|
|
241
|
+
function required(value, field, source) {
|
|
242
|
+
const found = text(value);
|
|
243
|
+
if (found === null) {
|
|
244
|
+
throw parseFailure(
|
|
245
|
+
`${source.name} answered without a readable "${field}", so there is no recipe to return.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return found;
|
|
249
|
+
}
|
|
250
|
+
function rowsOf(value, source) {
|
|
251
|
+
if (!Array.isArray(value)) {
|
|
252
|
+
throw parseFailure(
|
|
253
|
+
`${source.name} answered in a shape this server could not read: the list of results was missing.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
function namespacedId(source, reference) {
|
|
259
|
+
return `${source}:${reference}`;
|
|
260
|
+
}
|
|
261
|
+
function publishedFigures(panel) {
|
|
262
|
+
if (typeof panel !== "object" || panel === null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const kept = {};
|
|
266
|
+
for (const [key, value] of Object.entries(panel)) {
|
|
267
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
268
|
+
kept[key] = value.trim();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return Object.keys(kept).length > 0 ? kept : null;
|
|
272
|
+
}
|
|
273
|
+
function readYieldSpan(published) {
|
|
274
|
+
if (published === null) {
|
|
275
|
+
return { count: null, max: null, unit: null };
|
|
276
|
+
}
|
|
277
|
+
const span = RANGE_AND_REST.exec(published);
|
|
278
|
+
if (span) {
|
|
279
|
+
const [lowText = "", highText = ""] = span.slice(1);
|
|
280
|
+
const low = Number(lowText.replace(",", "."));
|
|
281
|
+
const high = Number(highText.replace(",", "."));
|
|
282
|
+
if (Number.isFinite(low) && Number.isFinite(high) && high > low) {
|
|
283
|
+
return { count: low, max: high, unit: text(span[3]) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const single = NUMBER_AND_REST.exec(published);
|
|
287
|
+
if (single) {
|
|
288
|
+
const [figure = ""] = single.slice(1);
|
|
289
|
+
const value = Number(figure.replace(",", "."));
|
|
290
|
+
if (Number.isFinite(value)) {
|
|
291
|
+
return { count: value, max: null, unit: text(single[2]) };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { count: null, max: null, unit: null };
|
|
295
|
+
}
|
|
296
|
+
function detailBase(source) {
|
|
297
|
+
return {
|
|
298
|
+
source: source.id,
|
|
299
|
+
sourceName: source.name,
|
|
300
|
+
language: source.language,
|
|
301
|
+
attribution: source.attribution
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
275
305
|
// src/sources/cookbook.ts
|
|
276
306
|
var COOKBOOK_PROFILE = {
|
|
277
307
|
id: "cookbook",
|
|
@@ -295,10 +325,11 @@ function cookbookAdapter(reader) {
|
|
|
295
325
|
let key;
|
|
296
326
|
try {
|
|
297
327
|
key = decodeURIComponent(raw.replace(SITE_URL, "")).replace(/_/g, " ");
|
|
298
|
-
} catch {
|
|
328
|
+
} catch (cause) {
|
|
299
329
|
throw invalidInput(
|
|
300
330
|
`"${raw}" carries a percent sign that is not a valid escape, so the page name cannot be read.`,
|
|
301
|
-
"Pass the id search_recipes returned instead of the address."
|
|
331
|
+
"Pass the id search_recipes returned instead of the address.",
|
|
332
|
+
cause
|
|
302
333
|
);
|
|
303
334
|
}
|
|
304
335
|
return {
|
|
@@ -328,7 +359,7 @@ function cookbookAdapter(reader) {
|
|
|
328
359
|
const key = text(summary?.key);
|
|
329
360
|
const title = text(summary?.title) ?? key;
|
|
330
361
|
const url = text(summary?.sourceUrl);
|
|
331
|
-
if (!key
|
|
362
|
+
if (!(key && title && url)) {
|
|
332
363
|
skipped += 1;
|
|
333
364
|
continue;
|
|
334
365
|
}
|
|
@@ -401,6 +432,7 @@ function cookbookDetail(payload) {
|
|
|
401
432
|
}
|
|
402
433
|
|
|
403
434
|
// src/sources/marmiton.ts
|
|
435
|
+
var ABSOLUTE_URL = /^https?:\/\//i;
|
|
404
436
|
var MARMITON_PROFILE = {
|
|
405
437
|
id: "marmiton",
|
|
406
438
|
name: "Marmiton",
|
|
@@ -417,7 +449,9 @@ async function readSearch(reader, query) {
|
|
|
417
449
|
return await reader.search(query);
|
|
418
450
|
} catch (error) {
|
|
419
451
|
const code = error?.code;
|
|
420
|
-
if (code === "not_found")
|
|
452
|
+
if (code === "not_found") {
|
|
453
|
+
return { data: [], cached: false };
|
|
454
|
+
}
|
|
421
455
|
throw error;
|
|
422
456
|
}
|
|
423
457
|
}
|
|
@@ -446,7 +480,7 @@ function marmitonAdapter(reader) {
|
|
|
446
480
|
const id = text(summary?.id);
|
|
447
481
|
const title = text(summary?.title);
|
|
448
482
|
const url = text(summary?.url);
|
|
449
|
-
if (!id
|
|
483
|
+
if (!(id && title && url)) {
|
|
450
484
|
skipped += 1;
|
|
451
485
|
continue;
|
|
452
486
|
}
|
|
@@ -474,7 +508,7 @@ function marmitonAdapter(reader) {
|
|
|
474
508
|
};
|
|
475
509
|
},
|
|
476
510
|
async getRecipe(reference) {
|
|
477
|
-
const ref =
|
|
511
|
+
const ref = ABSOLUTE_URL.test(reference) ? { url: reference } : { id: reference };
|
|
478
512
|
const outcome = await reader.getRecipe(ref);
|
|
479
513
|
return { recipe: marmitonDetail(outcome.data), cached: outcome.cached };
|
|
480
514
|
}
|
|
@@ -537,7 +571,9 @@ function buildSources(config, readers = {}) {
|
|
|
537
571
|
];
|
|
538
572
|
}
|
|
539
573
|
function selectSources(sources, wanted) {
|
|
540
|
-
if (!wanted)
|
|
574
|
+
if (!wanted) {
|
|
575
|
+
return sources;
|
|
576
|
+
}
|
|
541
577
|
const unknown = wanted.filter((id) => !sources.some((source) => source.id === id));
|
|
542
578
|
if (unknown.length > 0) {
|
|
543
579
|
throw invalidInput(
|
|
@@ -556,6 +592,8 @@ function selectSources(sources, wanted) {
|
|
|
556
592
|
}
|
|
557
593
|
|
|
558
594
|
// src/sources/wordings.ts
|
|
595
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
596
|
+
var WORD_SEPARATORS = /[^\p{L}\p{N}-]+/u;
|
|
559
597
|
var MAX_WORDINGS_PER_SOURCE = 3;
|
|
560
598
|
var FRENCH_FRAME = /* @__PURE__ */ new Set([
|
|
561
599
|
"a",
|
|
@@ -855,18 +893,37 @@ var FREE_OF = /\b([\p{L}]{3,})[\s-]?free\b/iu;
|
|
|
855
893
|
function foldWord(word) {
|
|
856
894
|
return word.toLowerCase().normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").replace(/œ/gu, "oe");
|
|
857
895
|
}
|
|
896
|
+
function namesTheDish(word, key) {
|
|
897
|
+
if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
if (DIGITS_ONLY.test(word)) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
if (word.length < 2) {
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
return !CONDITION_UNITS.has(word);
|
|
910
|
+
}
|
|
858
911
|
function readQuestion(question) {
|
|
859
912
|
const words = tokenise(question);
|
|
860
913
|
const conditions = [];
|
|
861
914
|
const dish = [];
|
|
862
915
|
const state = (named, kind) => {
|
|
863
916
|
const trimmed = named === null ? null : named.trim();
|
|
864
|
-
if (trimmed === "")
|
|
865
|
-
|
|
917
|
+
if (trimmed === "") {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (conditions.some((held) => held.named === trimmed && held.kind === kind)) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
866
923
|
conditions.push({ named: trimmed, kind });
|
|
867
924
|
};
|
|
868
925
|
for (let index = 0; index < words.length; index += 1) {
|
|
869
|
-
const word = words[index];
|
|
926
|
+
const word = words[index] ?? "";
|
|
870
927
|
const key = foldWord(word);
|
|
871
928
|
const hyphen = word.indexOf("-");
|
|
872
929
|
if (hyphen > 0 && NEGATORS.has(foldWord(word.slice(0, hyphen)))) {
|
|
@@ -875,7 +932,7 @@ function readQuestion(question) {
|
|
|
875
932
|
}
|
|
876
933
|
const free = FREE_OF.exec(word);
|
|
877
934
|
if (free) {
|
|
878
|
-
state(free[1], "excluded");
|
|
935
|
+
state(free[1] ?? "", "excluded");
|
|
879
936
|
continue;
|
|
880
937
|
}
|
|
881
938
|
if (key === "free") {
|
|
@@ -902,12 +959,9 @@ function readQuestion(question) {
|
|
|
902
959
|
state(word, "diet");
|
|
903
960
|
continue;
|
|
904
961
|
}
|
|
905
|
-
if (
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
if (word.length < 2) continue;
|
|
909
|
-
if (CONDITION_UNITS.has(word)) continue;
|
|
910
|
-
if (!dish.includes(word)) dish.push(word);
|
|
962
|
+
if (namesTheDish(word, key) && !dish.includes(word)) {
|
|
963
|
+
dish.push(word);
|
|
964
|
+
}
|
|
911
965
|
}
|
|
912
966
|
return { dish, conditions };
|
|
913
967
|
}
|
|
@@ -928,15 +982,20 @@ function scopeOf(words, marker, dish) {
|
|
|
928
982
|
}
|
|
929
983
|
function takeFood(words, from) {
|
|
930
984
|
let index = from;
|
|
931
|
-
while (index < words.length && isFiller(words[index]))
|
|
985
|
+
while (index < words.length && isFiller(words[index] ?? "")) {
|
|
986
|
+
index += 1;
|
|
987
|
+
}
|
|
932
988
|
const named = [];
|
|
933
989
|
while (index < words.length && named.length < CONDITION_WORDS) {
|
|
934
|
-
|
|
935
|
-
|
|
990
|
+
const word = words[index] ?? "";
|
|
991
|
+
if (!namesFood(word)) {
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
named.push(word);
|
|
936
995
|
index += 1;
|
|
937
996
|
const beyond = words[index + 1];
|
|
938
997
|
if (named.length + 2 <= CONDITION_WORDS && FOOD_LINKERS.has(foldWord(words[index] ?? "")) && beyond !== void 0 && namesFood(beyond)) {
|
|
939
|
-
named.push(words[index]);
|
|
998
|
+
named.push(words[index] ?? "");
|
|
940
999
|
index += 1;
|
|
941
1000
|
continue;
|
|
942
1001
|
}
|
|
@@ -950,12 +1009,24 @@ function isFiller(word) {
|
|
|
950
1009
|
}
|
|
951
1010
|
function namesFood(word) {
|
|
952
1011
|
const key = foldWord(word);
|
|
953
|
-
if (key.length < 2)
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
1012
|
+
if (key.length < 2) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
if (DIGITS_ONLY.test(key)) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
if (key === "free") {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
if (CONDITION_ENDS.has(key)) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
if (NEGATORS.has(key) || HEALTH_MARKERS.has(key)) {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
if (CONDITION_UNITS.has(key)) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
959
1030
|
return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));
|
|
960
1031
|
}
|
|
961
1032
|
function readConditions(question) {
|
|
@@ -979,9 +1050,13 @@ function deriveWordings(question) {
|
|
|
979
1050
|
const seen = /* @__PURE__ */ new Set();
|
|
980
1051
|
const offer = (candidate, derivation) => {
|
|
981
1052
|
const clean = tidy(candidate);
|
|
982
|
-
if (clean === "")
|
|
1053
|
+
if (clean === "") {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
983
1056
|
const key = clean.toLowerCase();
|
|
984
|
-
if (seen.has(key))
|
|
1057
|
+
if (seen.has(key)) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
985
1060
|
seen.add(key);
|
|
986
1061
|
wordings.push({ query: clean, derivation });
|
|
987
1062
|
};
|
|
@@ -999,7 +1074,9 @@ function deriveWordings(question) {
|
|
|
999
1074
|
}
|
|
1000
1075
|
function namesDish(title, question) {
|
|
1001
1076
|
const words = namingWords(question);
|
|
1002
|
-
if (words.length === 0)
|
|
1077
|
+
if (words.length === 0) {
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1003
1080
|
return dishWordsMissing(title, question).length < words.length;
|
|
1004
1081
|
}
|
|
1005
1082
|
function dishWordsMissing(title, question) {
|
|
@@ -1016,19 +1093,22 @@ function dishWords(question) {
|
|
|
1016
1093
|
return readQuestion(question).dish;
|
|
1017
1094
|
}
|
|
1018
1095
|
function tokenise(value) {
|
|
1019
|
-
return value.toLowerCase().replace(/['’]/gu, " ").split(
|
|
1096
|
+
return value.toLowerCase().replace(/['’]/gu, " ").split(WORD_SEPARATORS).map((word) => word.replace(/^-+|-+$/g, "")).filter((word) => word !== "");
|
|
1020
1097
|
}
|
|
1021
1098
|
function tidy(value) {
|
|
1022
1099
|
return value.replace(/\s+/g, " ").trim();
|
|
1023
1100
|
}
|
|
1024
1101
|
|
|
1025
1102
|
// src/sources/client.ts
|
|
1103
|
+
var LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
1026
1104
|
var MAX_LIMIT_PER_SOURCE = 25;
|
|
1027
1105
|
function withGuarantees(config) {
|
|
1028
1106
|
const defaults = loadConfig({});
|
|
1029
1107
|
const bounded = (value, fallback, min, max) => {
|
|
1030
1108
|
const parsed = Number(value);
|
|
1031
|
-
if (!Number.isFinite(parsed))
|
|
1109
|
+
if (!Number.isFinite(parsed)) {
|
|
1110
|
+
return fallback;
|
|
1111
|
+
}
|
|
1032
1112
|
return Math.min(max, Math.max(min, parsed));
|
|
1033
1113
|
};
|
|
1034
1114
|
const claimed = typeof config.userAgent === "string" ? config.userAgent.trim() : "";
|
|
@@ -1089,13 +1169,31 @@ function heldBack(wording, because) {
|
|
|
1089
1169
|
error: null
|
|
1090
1170
|
};
|
|
1091
1171
|
}
|
|
1172
|
+
function keepNewRows(read, seen, rows, question) {
|
|
1173
|
+
let added = 0;
|
|
1174
|
+
let onTopic = 0;
|
|
1175
|
+
for (const row of read.rows) {
|
|
1176
|
+
if (seen.has(row.id)) {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
seen.add(row.id);
|
|
1180
|
+
rows.push(row);
|
|
1181
|
+
added += 1;
|
|
1182
|
+
if (namesDish(row.title, question)) {
|
|
1183
|
+
onTopic += 1;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return { added, onTopic };
|
|
1187
|
+
}
|
|
1092
1188
|
function interleave(groups) {
|
|
1093
1189
|
const merged = [];
|
|
1094
1190
|
const longest = Math.max(0, ...groups.map((group) => group.length));
|
|
1095
1191
|
for (let index = 0; index < longest; index += 1) {
|
|
1096
1192
|
for (const group of groups) {
|
|
1097
1193
|
const row = group[index];
|
|
1098
|
-
if (row)
|
|
1194
|
+
if (row) {
|
|
1195
|
+
merged.push(row);
|
|
1196
|
+
}
|
|
1099
1197
|
}
|
|
1100
1198
|
}
|
|
1101
1199
|
return merged;
|
|
@@ -1151,7 +1249,7 @@ var RecipesClient = class {
|
|
|
1151
1249
|
if (trimmed === "") {
|
|
1152
1250
|
throw invalidInput("A search needs something to look for.", "Name a dish or an ingredient.");
|
|
1153
1251
|
}
|
|
1154
|
-
if (
|
|
1252
|
+
if (!LETTER_OR_DIGIT.test(trimmed)) {
|
|
1155
1253
|
throw invalidInput(
|
|
1156
1254
|
`"${trimmed}" carries no letter and no digit, so there is no word to search for.`,
|
|
1157
1255
|
"Name a dish or an ingredient."
|
|
@@ -1163,10 +1261,10 @@ var RecipesClient = class {
|
|
|
1163
1261
|
const attempts = await Promise.all(
|
|
1164
1262
|
chosen.map((source) => this.searchOne(source, trimmed, limit, fanOut))
|
|
1165
1263
|
);
|
|
1166
|
-
const
|
|
1264
|
+
const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));
|
|
1167
1265
|
return {
|
|
1168
|
-
rows: interleave(
|
|
1169
|
-
reports:
|
|
1266
|
+
rows: interleave(answered.map((one) => one.rows)),
|
|
1267
|
+
reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows.length))
|
|
1170
1268
|
};
|
|
1171
1269
|
}
|
|
1172
1270
|
/**
|
|
@@ -1243,14 +1341,9 @@ var RecipesClient = class {
|
|
|
1243
1341
|
`${source.name} sent ${read.skipped} row(s) this server could not read; they were left out.`
|
|
1244
1342
|
);
|
|
1245
1343
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
seen.add(row.id);
|
|
1250
|
-
rows.push(row);
|
|
1251
|
-
added += 1;
|
|
1252
|
-
if (namesDish(row.title, question)) onTopic += 1;
|
|
1253
|
-
}
|
|
1344
|
+
const kept = keepNewRows(read, seen, rows, question);
|
|
1345
|
+
const added = kept.added;
|
|
1346
|
+
onTopic += kept.onTopic;
|
|
1254
1347
|
wordings.push({
|
|
1255
1348
|
query: wording.query,
|
|
1256
1349
|
derivation: wording.derivation,
|
|
@@ -1342,17 +1435,41 @@ function readLanguage(line, evidence = {
|
|
|
1342
1435
|
}) {
|
|
1343
1436
|
let french = 0;
|
|
1344
1437
|
let english = 0;
|
|
1345
|
-
if (FRENCH_WORDS.test(line))
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (
|
|
1438
|
+
if (FRENCH_WORDS.test(line)) {
|
|
1439
|
+
french += STRUCTURAL;
|
|
1440
|
+
}
|
|
1441
|
+
if (FRENCH_LETTERS.test(line)) {
|
|
1442
|
+
french += STRUCTURAL;
|
|
1443
|
+
}
|
|
1444
|
+
if (ENGLISH_WORDS.test(line)) {
|
|
1445
|
+
english += STRUCTURAL;
|
|
1446
|
+
}
|
|
1447
|
+
if (evidence.frenchUnit) {
|
|
1448
|
+
french += VOCABULARY;
|
|
1449
|
+
}
|
|
1450
|
+
if (evidence.englishUnit) {
|
|
1451
|
+
english += VOCABULARY;
|
|
1452
|
+
}
|
|
1453
|
+
if (FRENCH_FOODS.test(line)) {
|
|
1454
|
+
french += NAME;
|
|
1455
|
+
}
|
|
1456
|
+
if (ENGLISH_FOODS.test(line)) {
|
|
1457
|
+
english += NAME;
|
|
1458
|
+
}
|
|
1352
1459
|
return { language: french > english ? "fr" : "en", french, english };
|
|
1353
1460
|
}
|
|
1354
1461
|
|
|
1355
1462
|
// src/recipe/units.ts
|
|
1463
|
+
var AL_ENDING = /al$/i;
|
|
1464
|
+
var AUX_ENDING = /aux$/i;
|
|
1465
|
+
var EAUX_ENDING = /eaux$/i;
|
|
1466
|
+
var EAU_ENDING = /eau$/i;
|
|
1467
|
+
var FUL_MEASURE = /^[a-z]{3,}fuls?$/;
|
|
1468
|
+
var HEAD_BEFORE_DE = new RegExp("^\\s*(\\p{L}+)\\s+(?=(?:de|du|des)\\s|d')", "u");
|
|
1469
|
+
var HOUSEHOLD_MEASURE = /^(cup|tablespoon|Tbsp|teaspoon|tsp|cuillère à soupe|cuillère à café|tasse)$/;
|
|
1470
|
+
var SIBILANT_LETTER = /[sxz]$/i;
|
|
1471
|
+
var TRAILING_S_ANY_CASE = /s$/i;
|
|
1472
|
+
var VOWEL_THEN_S = /[aiou]s$/i;
|
|
1356
1473
|
var SHARED_UNITS = {
|
|
1357
1474
|
mg: { canonical: "mg", kind: "measured", system: "metric", symbol: true },
|
|
1358
1475
|
g: { canonical: "g", kind: "measured", system: "metric", symbol: true },
|
|
@@ -1686,12 +1803,20 @@ var NOT_A_MEASURE = /* @__PURE__ */ new Set([
|
|
|
1686
1803
|
"ensemble"
|
|
1687
1804
|
]);
|
|
1688
1805
|
function readPartitiveMeasure(text2) {
|
|
1689
|
-
const match =
|
|
1690
|
-
if (!match)
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if (
|
|
1806
|
+
const match = HEAD_BEFORE_DE.exec(text2);
|
|
1807
|
+
if (!match) {
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
const [word = ""] = match.slice(1);
|
|
1811
|
+
if (word.length < 3) {
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
if (NOT_A_MEASURE.has(normalizeUnitKey(word))) {
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
if (lookupUnit(word, "fr")) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1695
1820
|
const canonical = frenchSingular(word);
|
|
1696
1821
|
return {
|
|
1697
1822
|
unit: {
|
|
@@ -1705,21 +1830,37 @@ function readPartitiveMeasure(text2) {
|
|
|
1705
1830
|
}
|
|
1706
1831
|
function readContainerLoad(word) {
|
|
1707
1832
|
const key = normalizeUnitKey(word);
|
|
1708
|
-
if (
|
|
1833
|
+
if (!FUL_MEASURE.test(key)) {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1709
1836
|
const canonical = key.endsWith("fuls") ? key.slice(0, -1) : key;
|
|
1710
1837
|
return { canonical, kind: "approximate", system: "none", plural: `${canonical}s` };
|
|
1711
1838
|
}
|
|
1712
1839
|
function frenchSingular(word) {
|
|
1713
|
-
if (
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
if (
|
|
1840
|
+
if (EAUX_ENDING.test(word)) {
|
|
1841
|
+
return word.slice(0, -1);
|
|
1842
|
+
}
|
|
1843
|
+
if (AUX_ENDING.test(word)) {
|
|
1844
|
+
return `${word.slice(0, -3)}al`;
|
|
1845
|
+
}
|
|
1846
|
+
if (VOWEL_THEN_S.test(word)) {
|
|
1847
|
+
return word;
|
|
1848
|
+
}
|
|
1849
|
+
if (TRAILING_S_ANY_CASE.test(word) && word.length > 3) {
|
|
1850
|
+
return word.slice(0, -1);
|
|
1851
|
+
}
|
|
1717
1852
|
return word;
|
|
1718
1853
|
}
|
|
1719
1854
|
function frenchPlural(word) {
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1722
|
-
|
|
1855
|
+
if (SIBILANT_LETTER.test(word)) {
|
|
1856
|
+
return word;
|
|
1857
|
+
}
|
|
1858
|
+
if (EAU_ENDING.test(word)) {
|
|
1859
|
+
return `${word}x`;
|
|
1860
|
+
}
|
|
1861
|
+
if (AL_ENDING.test(word)) {
|
|
1862
|
+
return `${word.slice(0, -2)}aux`;
|
|
1863
|
+
}
|
|
1723
1864
|
return `${word}s`;
|
|
1724
1865
|
}
|
|
1725
1866
|
var APPROXIMATE_EQUIVALENT = {
|
|
@@ -1780,17 +1921,19 @@ var DEMOTIONS = {
|
|
|
1780
1921
|
};
|
|
1781
1922
|
function demoteUnit(unit) {
|
|
1782
1923
|
const step = DEMOTIONS[normalizeUnitKey(unit.canonical)];
|
|
1783
|
-
if (!step)
|
|
1924
|
+
if (!step) {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1784
1927
|
const target = lookupUnit(step.to, step.language);
|
|
1785
1928
|
return target ? { unit: target, per: step.per } : null;
|
|
1786
1929
|
}
|
|
1787
1930
|
function isSpoonMeasure(unit) {
|
|
1788
|
-
return
|
|
1789
|
-
unit.canonical
|
|
1790
|
-
);
|
|
1931
|
+
return HOUSEHOLD_MEASURE.test(unit.canonical);
|
|
1791
1932
|
}
|
|
1792
1933
|
function unitDivisibility(unit) {
|
|
1793
|
-
if (unit.kind === "approximate")
|
|
1934
|
+
if (unit.kind === "approximate") {
|
|
1935
|
+
return "whole";
|
|
1936
|
+
}
|
|
1794
1937
|
return QUARTERED_MEASURE.test(unit.canonical) ? "quarter" : "half";
|
|
1795
1938
|
}
|
|
1796
1939
|
var QUARTERED_MEASURE = /\b(pots?|bouteilles?|bottles?|jars?|blocs?|blocks?|tranches?|slices?)\b/i;
|
|
@@ -1805,7 +1948,9 @@ function chooseReadableUnit(unit, amount) {
|
|
|
1805
1948
|
let ratio = 1;
|
|
1806
1949
|
while (amount * ratio < 1) {
|
|
1807
1950
|
const step = demoteUnit(current);
|
|
1808
|
-
if (!step)
|
|
1951
|
+
if (!step) {
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1809
1954
|
ratio *= step.per;
|
|
1810
1955
|
current = step.unit;
|
|
1811
1956
|
}
|
|
@@ -1819,7 +1964,9 @@ function chooseReadableUnit(unit, amount) {
|
|
|
1819
1964
|
}
|
|
1820
1965
|
while (!writesExactly(amount * ratio)) {
|
|
1821
1966
|
const step = demoteUnit(current);
|
|
1822
|
-
if (!step)
|
|
1967
|
+
if (!step) {
|
|
1968
|
+
break;
|
|
1969
|
+
}
|
|
1823
1970
|
ratio *= step.per;
|
|
1824
1971
|
current = step.unit;
|
|
1825
1972
|
}
|
|
@@ -1829,13 +1976,64 @@ function writesExactly(value) {
|
|
|
1829
1976
|
return Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;
|
|
1830
1977
|
}
|
|
1831
1978
|
function formatUnit(unit, amount, language) {
|
|
1832
|
-
if (unit.symbol)
|
|
1979
|
+
if (unit.symbol) {
|
|
1980
|
+
return unit.canonical;
|
|
1981
|
+
}
|
|
1833
1982
|
const singular = language === "fr" ? amount < 2 : amount <= 1;
|
|
1834
|
-
if (singular)
|
|
1983
|
+
if (singular) {
|
|
1984
|
+
return unit.canonical;
|
|
1985
|
+
}
|
|
1835
1986
|
return unit.plural ?? `${unit.canonical}s`;
|
|
1836
1987
|
}
|
|
1837
1988
|
|
|
1838
1989
|
// src/recipe/quantity.ts
|
|
1990
|
+
var BRACKETED_TAIL = /\s*(\([^()]*\))\s*$/;
|
|
1991
|
+
var LEADING_ARTICLE = /^(?:of\s+)?an?\s+/i;
|
|
1992
|
+
var LEADING_ARTICLE_FRENCH = /^(?:une|un)\s+/i;
|
|
1993
|
+
var LEADING_INDEFINITE_WITH_SPACE = /^an?\s+/i;
|
|
1994
|
+
var LEADING_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+)/i;
|
|
1995
|
+
var OF_OPENING = /^of\s+/i;
|
|
1996
|
+
var OPENING_ARTICLE_FRENCH = /^\s*(un|une)\b\s*/i;
|
|
1997
|
+
var REPEATED_COMMA_GROUPS = /^\s*\d+,\d+,\d/;
|
|
1998
|
+
var SLASH_OUTSIDE_A_FRACTION = /(?<!\d)\/|\/(?!\d)/;
|
|
1999
|
+
var ATTACHED_DE = /\s+de\s+(?=\d)/i;
|
|
2000
|
+
var DECIMAL = /^(\d+(?:[.,]\d+)?)/;
|
|
2001
|
+
var DIGIT = /\d/;
|
|
2002
|
+
var ENGLISH_RANGE_JOINER = /^\s+(to|or)\s+/i;
|
|
2003
|
+
var FRACTION_WORD = /^(?:(a|an|one|two|three)[\s-]+)?(halves|half|thirds|third|quarters|quarter|fourths|fourth)\b/i;
|
|
2004
|
+
var FRENCH_RANGE_JOINER = /^\s+(à|a|ou)\s+/i;
|
|
2005
|
+
var GROUPED_DECIMAL = /^(\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?)/;
|
|
2006
|
+
var HYPHENATED_TAIL = new RegExp("^-\\p{L}", "u");
|
|
2007
|
+
var LEADING_DASH = /^\s*(–|—|-)\s*/;
|
|
2008
|
+
var LEADING_INDEFINITE = /^an?\s/i;
|
|
2009
|
+
var LEADING_OF = /^\s*of\s+/i;
|
|
2010
|
+
var LEADING_QUANTITY = /^[\s\d.,/½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞\-–—]+/u;
|
|
2011
|
+
var LEADING_WORD = new RegExp("^\\s*(\\p{L}+)\\s+", "u");
|
|
2012
|
+
var MIXED_FRACTION = /^(\d+)\s+(\d+)\s*\/\s*(\d+)/;
|
|
2013
|
+
var PARTITIVE_BEFORE_BRACKET = /^(?:de\s+|du\s+|des\s+|d'|of\s+)(?=\()/i;
|
|
2014
|
+
var PARTITIVE_OPENING = /^(?:de\s|d'|du\s|des\s|,)/i;
|
|
2015
|
+
var SIMPLE_FRACTION = /^(\d+)\s*\/\s*(\d+)/;
|
|
2016
|
+
var TRAILING_S = /s$/;
|
|
2017
|
+
var VAGUE_ARTICLE = /^(?:un|une|quelques|of\s+an?|an?)\s+/iu;
|
|
2018
|
+
var WHITESPACE = /\s+/;
|
|
2019
|
+
function leadingMeasure(direct, behind) {
|
|
2020
|
+
if (direct.unit) {
|
|
2021
|
+
return direct;
|
|
2022
|
+
}
|
|
2023
|
+
return behind?.unit ? behind : direct;
|
|
2024
|
+
}
|
|
2025
|
+
function alternateStyleOf(count2, slashed, trailing) {
|
|
2026
|
+
if (count2 === 0) {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
if (slashed) {
|
|
2030
|
+
return "slash";
|
|
2031
|
+
}
|
|
2032
|
+
if (trailing) {
|
|
2033
|
+
return "trailing";
|
|
2034
|
+
}
|
|
2035
|
+
return "bracket";
|
|
2036
|
+
}
|
|
1839
2037
|
var VULGAR_FRACTIONS = {
|
|
1840
2038
|
"\xBD": 0.5,
|
|
1841
2039
|
"\u2153": 1 / 3,
|
|
@@ -1875,13 +2073,18 @@ var NAMED_ENTITIES = {
|
|
|
1875
2073
|
mdash: "\u2014"
|
|
1876
2074
|
};
|
|
1877
2075
|
function decodeEntities(text2) {
|
|
1878
|
-
return text2.replace(
|
|
2076
|
+
return text2.replace(
|
|
2077
|
+
/&#x([0-9a-f]+);/gi,
|
|
2078
|
+
(whole, hex) => codePoint(Number.parseInt(hex, 16), whole)
|
|
2079
|
+
).replace(/&#(\d+);/g, (whole, digits) => codePoint(Number(digits), whole)).replace(
|
|
1879
2080
|
/&([a-z][a-z0-9]*);/gi,
|
|
1880
2081
|
(whole, name) => NAMED_ENTITIES[name.toLowerCase()] ?? whole
|
|
1881
2082
|
);
|
|
1882
2083
|
}
|
|
1883
2084
|
function codePoint(value, published) {
|
|
1884
|
-
if (!Number.isInteger(value) || value < 32 || value > 1114111)
|
|
2085
|
+
if (!Number.isInteger(value) || value < 32 || value > 1114111) {
|
|
2086
|
+
return published;
|
|
2087
|
+
}
|
|
1885
2088
|
try {
|
|
1886
2089
|
return String.fromCodePoint(value);
|
|
1887
2090
|
} catch {
|
|
@@ -1898,37 +2101,59 @@ var APPROXIMATION_PREFIX = {
|
|
|
1898
2101
|
};
|
|
1899
2102
|
var CONTAINER_CONTENTS = /^\s*(?:de\s|d'|du\s|des\s)/i;
|
|
1900
2103
|
function statesItemSize(item) {
|
|
1901
|
-
const attached =
|
|
1902
|
-
if (!attached)
|
|
2104
|
+
const attached = ATTACHED_DE.exec(item);
|
|
2105
|
+
if (!attached) {
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
1903
2108
|
const named = item.slice(0, attached.index).trim();
|
|
1904
|
-
if (!named ||
|
|
2109
|
+
if (!named || DIGIT.test(named)) {
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
1905
2112
|
return isStatedSize(item.slice(attached.index + attached[0].length));
|
|
1906
2113
|
}
|
|
1907
2114
|
function isStatedSize(text2) {
|
|
1908
2115
|
const size = parseLeadingQuantity(text2, "fr");
|
|
1909
|
-
if (!size)
|
|
2116
|
+
if (!size) {
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
1910
2119
|
const measure = takeUnit(text2.slice(size.length).trimStart(), "fr");
|
|
1911
|
-
if (
|
|
2120
|
+
if (measure.unit?.kind !== "measured") {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
1912
2123
|
return !CONTAINER_CONTENTS.test(measure.rest);
|
|
1913
2124
|
}
|
|
1914
2125
|
function takeCompoundMember(text2, unit, language) {
|
|
1915
|
-
if (!unit)
|
|
2126
|
+
if (!unit) {
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
1916
2129
|
const step = demoteUnit(unit);
|
|
1917
|
-
if (!step)
|
|
2130
|
+
if (!step) {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
1918
2133
|
const joiner = ADDS_UP.exec(text2);
|
|
1919
|
-
|
|
1920
|
-
const second = parseLeadingQuantity(
|
|
1921
|
-
if (!second || second.amount <= 0 || second.amount >= step.per)
|
|
1922
|
-
|
|
2134
|
+
const behindTheJoiner = joiner ? text2.slice(joiner[0].length) : text2;
|
|
2135
|
+
const second = parseLeadingQuantity(behindTheJoiner, language);
|
|
2136
|
+
if (!second || second.amount <= 0 || second.amount >= step.per) {
|
|
2137
|
+
return null;
|
|
2138
|
+
}
|
|
2139
|
+
const after = behindTheJoiner.slice(second.length);
|
|
1923
2140
|
const measure = takeUnit(after.trimStart(), language);
|
|
1924
2141
|
if (measure.unit) {
|
|
1925
|
-
if (measure.unit.canonical !== step.unit.canonical)
|
|
2142
|
+
if (measure.unit.canonical !== step.unit.canonical) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
1926
2145
|
return { adds: second.amount / step.per, rest: measure.rest.trimStart() };
|
|
1927
2146
|
}
|
|
1928
|
-
if (unit.kind !== "measured")
|
|
1929
|
-
|
|
2147
|
+
if (unit.kind !== "measured") {
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
if (!Number.isInteger(second.amount)) {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
1930
2153
|
const trailing = after.trimStart();
|
|
1931
|
-
if (trailing !== "" &&
|
|
2154
|
+
if (trailing !== "" && !PARTITIVE_OPENING.test(trailing)) {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
1932
2157
|
return { adds: second.amount / step.per, rest: trailing };
|
|
1933
2158
|
}
|
|
1934
2159
|
var ADDS_UP = /^\s*(?:\+|et|and|plus)\s+/i;
|
|
@@ -1976,12 +2201,17 @@ var MEASURE_ADJECTIVES = {
|
|
|
1976
2201
|
])
|
|
1977
2202
|
};
|
|
1978
2203
|
function takeMeasureAdjective(text2, language) {
|
|
1979
|
-
const match =
|
|
1980
|
-
if (!match)
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2204
|
+
const match = LEADING_WORD.exec(text2);
|
|
2205
|
+
if (!match) {
|
|
2206
|
+
return { adjective: null, rest: text2 };
|
|
2207
|
+
}
|
|
2208
|
+
const [adjective = ""] = match.slice(1);
|
|
2209
|
+
const folded = normalizeUnitKey(adjective);
|
|
2210
|
+
const listed = MEASURE_ADJECTIVES[language].has(folded) || MEASURE_ADJECTIVES[language].has(folded.replace(TRAILING_S, ""));
|
|
2211
|
+
if (!listed) {
|
|
2212
|
+
return { adjective: null, rest: text2 };
|
|
2213
|
+
}
|
|
2214
|
+
return { adjective, rest: text2.slice(match[0].length) };
|
|
1985
2215
|
}
|
|
1986
2216
|
var COMMA_GROUPED = /^\s*\d{1,3}(?:,\d{3})+(?!\d)/;
|
|
1987
2217
|
var COMMA_DECIMAL = /^\s*\d+,\d+/;
|
|
@@ -1991,10 +2221,10 @@ function parseLeadingQuantity(text2, language) {
|
|
|
1991
2221
|
const mixedGlyph = new RegExp(`^(\\d+)\\s*([${VULGAR_CLASS}])`).exec(trimmed);
|
|
1992
2222
|
if (mixedGlyph) {
|
|
1993
2223
|
const whole = Number(mixedGlyph[1]);
|
|
1994
|
-
const fraction2 = VULGAR_FRACTIONS[mixedGlyph[2]];
|
|
2224
|
+
const fraction2 = VULGAR_FRACTIONS[mixedGlyph[2] ?? ""] ?? Number.NaN;
|
|
1995
2225
|
return { amount: whole + fraction2, length: offset + mixedGlyph[0].length };
|
|
1996
2226
|
}
|
|
1997
|
-
const mixed =
|
|
2227
|
+
const mixed = MIXED_FRACTION.exec(trimmed);
|
|
1998
2228
|
if (mixed) {
|
|
1999
2229
|
const denominator = Number(mixed[3]);
|
|
2000
2230
|
if (denominator !== 0) {
|
|
@@ -2004,27 +2234,33 @@ function parseLeadingQuantity(text2, language) {
|
|
|
2004
2234
|
};
|
|
2005
2235
|
}
|
|
2006
2236
|
}
|
|
2007
|
-
const fraction =
|
|
2237
|
+
const fraction = SIMPLE_FRACTION.exec(trimmed);
|
|
2008
2238
|
if (fraction) {
|
|
2009
2239
|
const denominator = Number(fraction[2]);
|
|
2010
|
-
if (denominator === 0)
|
|
2240
|
+
if (denominator === 0) {
|
|
2241
|
+
return null;
|
|
2242
|
+
}
|
|
2011
2243
|
return { amount: Number(fraction[1]) / denominator, length: offset + fraction[0].length };
|
|
2012
2244
|
}
|
|
2013
2245
|
const glyph = trimmed[0];
|
|
2014
2246
|
if (glyph && glyph in VULGAR_FRACTIONS) {
|
|
2015
|
-
return { amount: VULGAR_FRACTIONS[glyph], length: offset + 1 };
|
|
2247
|
+
return { amount: VULGAR_FRACTIONS[glyph] ?? Number.NaN, length: offset + 1 };
|
|
2016
2248
|
}
|
|
2017
|
-
const decimal = (language === "fr" ?
|
|
2249
|
+
const decimal = (language === "fr" ? DECIMAL : GROUPED_DECIMAL).exec(trimmed);
|
|
2018
2250
|
if (decimal) {
|
|
2019
|
-
const written = decimal
|
|
2251
|
+
const [written = ""] = decimal.slice(1);
|
|
2020
2252
|
const amount = Number(
|
|
2021
2253
|
language === "fr" ? written.replace(",", ".") : written.replace(/,/g, "")
|
|
2022
2254
|
);
|
|
2023
|
-
if (Number.isFinite(amount))
|
|
2255
|
+
if (Number.isFinite(amount)) {
|
|
2256
|
+
return { amount, length: offset + decimal[0].length };
|
|
2257
|
+
}
|
|
2024
2258
|
}
|
|
2025
2259
|
if (language === "en") {
|
|
2026
2260
|
const written = parseWrittenFraction(trimmed);
|
|
2027
|
-
if (written)
|
|
2261
|
+
if (written) {
|
|
2262
|
+
return { amount: written.amount, length: offset + written.length };
|
|
2263
|
+
}
|
|
2028
2264
|
}
|
|
2029
2265
|
return null;
|
|
2030
2266
|
}
|
|
@@ -2040,30 +2276,40 @@ var WRITTEN_DENOMINATORS = {
|
|
|
2040
2276
|
fourths: 4
|
|
2041
2277
|
};
|
|
2042
2278
|
function parseWrittenFraction(text2) {
|
|
2043
|
-
const match =
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2279
|
+
const match = FRACTION_WORD.exec(text2);
|
|
2280
|
+
if (!match) {
|
|
2281
|
+
return null;
|
|
2282
|
+
}
|
|
2047
2283
|
const numerator = match[1] ? WRITTEN_NUMERATORS[match[1].toLowerCase()] : 1;
|
|
2048
|
-
const denominator = WRITTEN_DENOMINATORS[match[2].toLowerCase()];
|
|
2049
|
-
if (!numerator
|
|
2050
|
-
|
|
2051
|
-
|
|
2284
|
+
const denominator = WRITTEN_DENOMINATORS[(match[2] ?? "").toLowerCase()];
|
|
2285
|
+
if (!(numerator && denominator)) {
|
|
2286
|
+
return null;
|
|
2287
|
+
}
|
|
2288
|
+
const rest = text2.slice(match[0].length).replace(LEADING_OF, "").trimStart();
|
|
2289
|
+
if (!(LEADING_INDEFINITE.test(rest) || takeUnit(rest, "en").unit)) {
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2052
2292
|
return { amount: numerator / denominator, length: match[0].length };
|
|
2053
2293
|
}
|
|
2054
2294
|
function parseLeadingRange(text2, language) {
|
|
2055
2295
|
const low = parseLeadingQuantity(text2, language);
|
|
2056
|
-
if (!low)
|
|
2296
|
+
if (!low) {
|
|
2297
|
+
return null;
|
|
2298
|
+
}
|
|
2057
2299
|
const after = text2.slice(low.length);
|
|
2058
|
-
const written = language === "fr" ?
|
|
2059
|
-
const separator = written.exec(after) ??
|
|
2060
|
-
if (!separator)
|
|
2300
|
+
const written = language === "fr" ? FRENCH_RANGE_JOINER : ENGLISH_RANGE_JOINER;
|
|
2301
|
+
const separator = written.exec(after) ?? LEADING_DASH.exec(after);
|
|
2302
|
+
if (!separator) {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2061
2305
|
const high = parseLeadingQuantity(after.slice(separator[0].length), language);
|
|
2062
|
-
if (!high || high.amount <= low.amount)
|
|
2306
|
+
if (!high || high.amount <= low.amount) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2063
2309
|
return {
|
|
2064
2310
|
amount: low.amount,
|
|
2065
2311
|
max: high.amount,
|
|
2066
|
-
separator: separator[1],
|
|
2312
|
+
separator: separator[1] ?? "",
|
|
2067
2313
|
length: low.length + separator[0].length + high.length
|
|
2068
2314
|
};
|
|
2069
2315
|
}
|
|
@@ -2071,111 +2317,190 @@ var FRENCH_ARTICLES = { un: 1, une: 1 };
|
|
|
2071
2317
|
function takeUnit(text2, language) {
|
|
2072
2318
|
const normalized = normalizeUnitKey(text2);
|
|
2073
2319
|
for (const key of unitKeys(language)) {
|
|
2074
|
-
if (normalized !== key && !normalized.startsWith(`${key} `))
|
|
2320
|
+
if (normalized !== key && !normalized.startsWith(`${key} `)) {
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2075
2323
|
const unit = lookupUnit(key, language);
|
|
2076
|
-
if (!unit)
|
|
2324
|
+
if (!unit) {
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2077
2327
|
const rest = afterKey(text2, key);
|
|
2078
|
-
if (rest === null)
|
|
2328
|
+
if (rest === null) {
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2079
2331
|
return { unit, rest };
|
|
2080
2332
|
}
|
|
2081
2333
|
if (language === "en") {
|
|
2082
|
-
const words = text2.trimStart().split(
|
|
2334
|
+
const words = text2.trimStart().split(WHITESPACE);
|
|
2083
2335
|
const load = words[0] ? readContainerLoad(words[0]) : null;
|
|
2084
|
-
if (load)
|
|
2336
|
+
if (load) {
|
|
2337
|
+
return { unit: load, rest: words.slice(1).join(" ") };
|
|
2338
|
+
}
|
|
2085
2339
|
}
|
|
2086
2340
|
return { unit: null, rest: text2 };
|
|
2087
2341
|
}
|
|
2088
2342
|
function afterKey(text2, key) {
|
|
2089
|
-
const words = text2.trim().split(
|
|
2343
|
+
const words = text2.trim().split(WHITESPACE);
|
|
2090
2344
|
for (let count2 = 1; count2 <= words.length; count2 += 1) {
|
|
2091
|
-
if (normalizeUnitKey(words.slice(0, count2).join(" ")) !== key)
|
|
2345
|
+
if (normalizeUnitKey(words.slice(0, count2).join(" ")) !== key) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2092
2348
|
return words.slice(count2).join(" ");
|
|
2093
2349
|
}
|
|
2094
2350
|
return null;
|
|
2095
2351
|
}
|
|
2096
2352
|
function takeUnitEitherLanguage(text2, language) {
|
|
2097
2353
|
const first = takeUnit(text2, language);
|
|
2098
|
-
if (first.unit)
|
|
2354
|
+
if (first.unit) {
|
|
2355
|
+
return first;
|
|
2356
|
+
}
|
|
2099
2357
|
return takeUnit(text2, language === "fr" ? "en" : "fr");
|
|
2100
2358
|
}
|
|
2101
2359
|
function readEvidence(line) {
|
|
2102
2360
|
const text2 = line.trim();
|
|
2103
|
-
const afterFigures = text2.replace(
|
|
2104
|
-
const afterArticle = afterFigures.replace(
|
|
2361
|
+
const afterFigures = text2.replace(LEADING_QUANTITY, "").trimStart();
|
|
2362
|
+
const afterArticle = afterFigures.replace(VAGUE_ARTICLE, "");
|
|
2105
2363
|
const frenchUnit = takeUnit(afterArticle, "fr").unit !== null;
|
|
2106
2364
|
const englishUnit = takeUnit(afterArticle, "en").unit !== null;
|
|
2107
2365
|
return readLanguage(text2, { frenchUnit, englishUnit });
|
|
2108
2366
|
}
|
|
2109
|
-
function
|
|
2110
|
-
const original = line;
|
|
2111
|
-
const marked = readable(line);
|
|
2112
|
-
const decoration = takeDecoration(marked);
|
|
2113
|
-
const text2 = decoration ? marked.slice(decoration.length).trimStart() : marked;
|
|
2114
|
-
const evidence = readEvidence(text2);
|
|
2115
|
-
const language = choice === "auto" ? evidence.language : choice;
|
|
2116
|
-
const empty = (heldBack2) => ({
|
|
2117
|
-
original,
|
|
2118
|
-
language,
|
|
2119
|
-
heldBack: heldBack2,
|
|
2120
|
-
decoration,
|
|
2121
|
-
approximation: null,
|
|
2122
|
-
measureAdjective: null,
|
|
2123
|
-
amount: null,
|
|
2124
|
-
amountMax: null,
|
|
2125
|
-
rangeSeparator: null,
|
|
2126
|
-
unit: null,
|
|
2127
|
-
alternates: [],
|
|
2128
|
-
alternateStyle: null,
|
|
2129
|
-
alternateIntro: null,
|
|
2130
|
-
capacity: null,
|
|
2131
|
-
item: text2,
|
|
2132
|
-
articleWord: null,
|
|
2133
|
-
countMultiplier: null
|
|
2134
|
-
});
|
|
2135
|
-
if (unreadableComma(text2, language, evidence)) return empty("ambiguousDecimal");
|
|
2136
|
-
const loose = APPROXIMATION_PREFIX[language].exec(text2);
|
|
2137
|
-
const stated = loose ? text2.slice(loose[0].length) : text2;
|
|
2138
|
-
const range = parseLeadingRange(stated, language);
|
|
2139
|
-
const article = range ? null : readArticle(stated, language);
|
|
2140
|
-
const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;
|
|
2141
|
-
if (!quantity) return empty(null);
|
|
2142
|
-
const behindFigure = stated.slice(quantity.length);
|
|
2143
|
-
if (new RegExp("^-\\p{L}", "u").test(behindFigure)) return empty("sizeQualifier");
|
|
2144
|
-
if (quantity !== article && ORDINAL_SUFFIX.test(behindFigure)) return empty(null);
|
|
2145
|
-
if (TIME_UNIT[language].test(behindFigure.trimStart())) return empty("duration");
|
|
2146
|
-
let rest = stated.slice(quantity.length).trimStart();
|
|
2147
|
-
if (language === "en") rest = rest.replace(/^(?:of\s+)?an?\s+/i, "");
|
|
2148
|
-
const multiplier = readCountMultiplier(rest);
|
|
2149
|
-
if (multiplier) rest = multiplier.rest;
|
|
2150
|
-
const times = multiplier?.times ?? 1;
|
|
2151
|
-
const fromArticle = quantity === article;
|
|
2367
|
+
function readMeasure(rest, language, fromArticle) {
|
|
2152
2368
|
const direct = takeLeadingUnit(rest, language, fromArticle);
|
|
2153
2369
|
const described = direct.unit ? { adjective: null, rest } : takeMeasureAdjective(rest, language);
|
|
2154
2370
|
const behind = described.adjective ? takeLeadingUnit(described.rest, language, fromArticle) : null;
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
if (
|
|
2159
|
-
|
|
2160
|
-
|
|
2371
|
+
return { leading: leadingMeasure(direct, behind), described, behind };
|
|
2372
|
+
}
|
|
2373
|
+
function whatTheFigureIsNot(behindFigure, couldBeARank, language) {
|
|
2374
|
+
if (HYPHENATED_TAIL.test(behindFigure)) {
|
|
2375
|
+
return "sizeQualifier";
|
|
2376
|
+
}
|
|
2377
|
+
if (couldBeARank && ORDINAL_SUFFIX.test(behindFigure)) {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
if (TIME_UNIT[language].test(behindFigure.trimStart())) {
|
|
2381
|
+
return "duration";
|
|
2382
|
+
}
|
|
2383
|
+
return false;
|
|
2384
|
+
}
|
|
2385
|
+
function takeEquivalents(text2, language) {
|
|
2386
|
+
const introducedBracket = PARTITIVE_BEFORE_BRACKET.exec(text2);
|
|
2161
2387
|
const bracketed = takeAlternates(
|
|
2162
|
-
introducedBracket ?
|
|
2388
|
+
introducedBracket ? text2.slice(introducedBracket[0].length) : text2,
|
|
2163
2389
|
language
|
|
2164
2390
|
);
|
|
2165
|
-
rest = bracketed.measures.length > 0 ? bracketed.rest :
|
|
2391
|
+
let rest = bracketed.measures.length > 0 ? bracketed.rest : text2;
|
|
2166
2392
|
const slashed = bracketed.measures.length > 0 ? null : takeSlashAlternates(rest, language);
|
|
2167
|
-
if (slashed)
|
|
2393
|
+
if (slashed) {
|
|
2394
|
+
rest = slashed.rest;
|
|
2395
|
+
}
|
|
2168
2396
|
let item = stripItemLead(rest, language);
|
|
2169
2397
|
const trailing = bracketed.measures.length === 0 && !slashed ? takeTrailingAlternates(item, language) : null;
|
|
2170
|
-
if (trailing)
|
|
2398
|
+
if (trailing) {
|
|
2399
|
+
item = trailing.item;
|
|
2400
|
+
}
|
|
2171
2401
|
const group = trailing ?? (bracketed.measures.length > 0 ? bracketed : null);
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2402
|
+
return { rest, item, bracketed, slashed, trailing, group };
|
|
2403
|
+
}
|
|
2404
|
+
function whatTheFiguresSize(leading, item, language) {
|
|
2405
|
+
if (language === "fr" && !leading.unit && statesItemSize(item)) {
|
|
2406
|
+
return "itemSize";
|
|
2407
|
+
}
|
|
2408
|
+
if (language === "fr" && leading.partitive && isStatedSize(item)) {
|
|
2409
|
+
return "itemSize";
|
|
2410
|
+
}
|
|
2175
2411
|
if (leading.unit?.kind === "measured" && CONTAINER_NOUN.test(normalizeUnitKey(item))) {
|
|
2176
|
-
return
|
|
2412
|
+
return "containerSize";
|
|
2413
|
+
}
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
function whatTheCountIsOf(behindFigure, language) {
|
|
2417
|
+
let rest = behindFigure.trimStart();
|
|
2418
|
+
if (language === "en") {
|
|
2419
|
+
rest = rest.replace(LEADING_ARTICLE, "");
|
|
2420
|
+
}
|
|
2421
|
+
const multiplier = readCountMultiplier(rest);
|
|
2422
|
+
if (multiplier) {
|
|
2423
|
+
rest = multiplier.rest;
|
|
2424
|
+
}
|
|
2425
|
+
return { rest, multiplier };
|
|
2426
|
+
}
|
|
2427
|
+
function capacityOfTheContainer(group, measureName, item) {
|
|
2428
|
+
if (!group?.measures.every((measure) => measure.unit?.kind === "measured")) {
|
|
2429
|
+
return null;
|
|
2430
|
+
}
|
|
2431
|
+
if (!(namesContainer(measureName) || namesContainer(item))) {
|
|
2432
|
+
return null;
|
|
2177
2433
|
}
|
|
2178
|
-
|
|
2434
|
+
return group.published;
|
|
2435
|
+
}
|
|
2436
|
+
function parseIngredient(line, choice = "auto") {
|
|
2437
|
+
const original = line;
|
|
2438
|
+
const marked = readable(line);
|
|
2439
|
+
const decoration = takeDecoration(marked);
|
|
2440
|
+
const text2 = decoration ? marked.slice(decoration.length).trimStart() : marked;
|
|
2441
|
+
const evidence = readEvidence(text2);
|
|
2442
|
+
const language = choice === "auto" ? evidence.language : choice;
|
|
2443
|
+
const empty = (heldBack2) => ({
|
|
2444
|
+
original,
|
|
2445
|
+
language,
|
|
2446
|
+
heldBack: heldBack2,
|
|
2447
|
+
decoration,
|
|
2448
|
+
approximation: null,
|
|
2449
|
+
measureAdjective: null,
|
|
2450
|
+
amount: null,
|
|
2451
|
+
amountMax: null,
|
|
2452
|
+
rangeSeparator: null,
|
|
2453
|
+
unit: null,
|
|
2454
|
+
alternates: [],
|
|
2455
|
+
alternateStyle: null,
|
|
2456
|
+
alternateIntro: null,
|
|
2457
|
+
capacity: null,
|
|
2458
|
+
item: text2,
|
|
2459
|
+
articleWord: null,
|
|
2460
|
+
countMultiplier: null
|
|
2461
|
+
});
|
|
2462
|
+
if (unreadableComma(text2, language, evidence)) {
|
|
2463
|
+
return empty("ambiguousDecimal");
|
|
2464
|
+
}
|
|
2465
|
+
const loose = APPROXIMATION_PREFIX[language].exec(text2);
|
|
2466
|
+
const stated = loose ? text2.slice(loose[0].length) : text2;
|
|
2467
|
+
const range = parseLeadingRange(stated, language);
|
|
2468
|
+
const article = range ? null : readArticle(stated, language);
|
|
2469
|
+
const quantity = range ?? parseLeadingQuantity(stated, language) ?? article;
|
|
2470
|
+
if (!quantity) {
|
|
2471
|
+
return empty(null);
|
|
2472
|
+
}
|
|
2473
|
+
const notAnAmount = whatTheFigureIsNot(
|
|
2474
|
+
stated.slice(quantity.length),
|
|
2475
|
+
quantity !== article,
|
|
2476
|
+
language
|
|
2477
|
+
);
|
|
2478
|
+
if (notAnAmount !== false) {
|
|
2479
|
+
return empty(notAnAmount);
|
|
2480
|
+
}
|
|
2481
|
+
const counted = whatTheCountIsOf(stated.slice(quantity.length), language);
|
|
2482
|
+
let rest = counted.rest;
|
|
2483
|
+
const multiplier = counted.multiplier;
|
|
2484
|
+
const times = multiplier?.times ?? 1;
|
|
2485
|
+
const fromArticle = quantity === article;
|
|
2486
|
+
const measure = readMeasure(rest, language, fromArticle);
|
|
2487
|
+
const { leading, described, behind } = measure;
|
|
2488
|
+
rest = leading.rest;
|
|
2489
|
+
const compound = range ? null : takeCompoundMember(rest, leading.unit, language);
|
|
2490
|
+
if (compound) {
|
|
2491
|
+
rest = compound.rest;
|
|
2492
|
+
}
|
|
2493
|
+
const whole = quantity.amount + (compound?.adds ?? 0);
|
|
2494
|
+
const equivalents = takeEquivalents(rest, language);
|
|
2495
|
+
const { slashed, trailing, group } = equivalents;
|
|
2496
|
+
rest = equivalents.rest;
|
|
2497
|
+
const item = equivalents.item;
|
|
2498
|
+
const capacity = capacityOfTheContainer(group, leading.unit?.canonical, item);
|
|
2499
|
+
const sizedRatherThanCounted = whatTheFiguresSize(leading, item, language);
|
|
2500
|
+
if (sizedRatherThanCounted !== null) {
|
|
2501
|
+
return empty(sizedRatherThanCounted);
|
|
2502
|
+
}
|
|
2503
|
+
const alternates = capacity === null ? slashed?.measures ?? group?.measures ?? [] : [];
|
|
2179
2504
|
return {
|
|
2180
2505
|
original,
|
|
2181
2506
|
language,
|
|
@@ -2188,8 +2513,8 @@ function parseIngredient(line, choice = "auto") {
|
|
|
2188
2513
|
rangeSeparator: range?.separator ?? null,
|
|
2189
2514
|
unit: leading.unit,
|
|
2190
2515
|
alternates,
|
|
2191
|
-
alternateStyle: alternates.length
|
|
2192
|
-
alternateIntro: capacity
|
|
2516
|
+
alternateStyle: alternateStyleOf(alternates.length, slashed !== null, trailing !== null),
|
|
2517
|
+
alternateIntro: capacity === null ? group?.intro ?? null : null,
|
|
2193
2518
|
capacity,
|
|
2194
2519
|
item,
|
|
2195
2520
|
articleWord: fromArticle ? article?.word ?? null : null,
|
|
@@ -2201,21 +2526,35 @@ function unreadableComma(text2, language, evidence) {
|
|
|
2201
2526
|
const settled = evidence.french !== evidence.english;
|
|
2202
2527
|
return !settled;
|
|
2203
2528
|
}
|
|
2204
|
-
if (language === "en")
|
|
2205
|
-
|
|
2529
|
+
if (language === "en") {
|
|
2530
|
+
return COMMA_DECIMAL.test(text2);
|
|
2531
|
+
}
|
|
2532
|
+
return REPEATED_COMMA_GROUPS.test(text2);
|
|
2206
2533
|
}
|
|
2207
2534
|
function readArticle(text2, language) {
|
|
2208
2535
|
const counts = (rest) => takeLeadingUnit(rest, language, true).unit !== null || readCountMultiplier(rest) !== null;
|
|
2209
2536
|
if (language === "fr") {
|
|
2210
|
-
const match =
|
|
2211
|
-
if (!match)
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2537
|
+
const match = OPENING_ARTICLE_FRENCH.exec(text2);
|
|
2538
|
+
if (!match) {
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
if (!counts(text2.slice(match[0].length))) {
|
|
2542
|
+
return null;
|
|
2543
|
+
}
|
|
2544
|
+
const [word = ""] = match.slice(1);
|
|
2545
|
+
const amount = FRENCH_ARTICLES[word.toLowerCase()];
|
|
2546
|
+
if (amount === void 0) {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
return { amount, length: match[0].length, word };
|
|
2550
|
+
}
|
|
2551
|
+
const article = LEADING_INDEFINITE_WITH_SPACE.exec(text2);
|
|
2552
|
+
if (!article) {
|
|
2553
|
+
return null;
|
|
2554
|
+
}
|
|
2555
|
+
if (!counts(text2.slice(article[0].length))) {
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2219
2558
|
return { amount: 1, length: article[0].length, word: article[0].trim() };
|
|
2220
2559
|
}
|
|
2221
2560
|
var COUNT_MULTIPLIERS = {
|
|
@@ -2225,57 +2564,80 @@ var COUNT_MULTIPLIERS = {
|
|
|
2225
2564
|
douzaines: 12
|
|
2226
2565
|
};
|
|
2227
2566
|
function readCountMultiplier(text2) {
|
|
2228
|
-
const match =
|
|
2229
|
-
if (!match)
|
|
2230
|
-
|
|
2231
|
-
|
|
2567
|
+
const match = LEADING_WORD.exec(text2);
|
|
2568
|
+
if (!match) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const [word = ""] = match.slice(1);
|
|
2572
|
+
const times = COUNT_MULTIPLIERS[normalizeUnitKey(word)];
|
|
2573
|
+
if (times === void 0) {
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2232
2576
|
return { times, rest: text2.slice(match[0].length) };
|
|
2233
2577
|
}
|
|
2234
2578
|
function takeLeadingUnit(text2, language, fromArticle) {
|
|
2235
2579
|
const taken = takeUnit(text2, language);
|
|
2236
|
-
if (taken.unit)
|
|
2580
|
+
if (taken.unit) {
|
|
2581
|
+
return { ...taken, partitive: false };
|
|
2582
|
+
}
|
|
2237
2583
|
if (language === "fr" && fromArticle) {
|
|
2238
2584
|
const measure = readPartitiveMeasure(text2);
|
|
2239
|
-
if (measure)
|
|
2585
|
+
if (measure) {
|
|
2586
|
+
return { unit: measure.unit, rest: measure.rest, partitive: true };
|
|
2587
|
+
}
|
|
2240
2588
|
}
|
|
2241
2589
|
return { ...taken, partitive: false };
|
|
2242
2590
|
}
|
|
2243
2591
|
function stripItemLead(text2, language) {
|
|
2244
2592
|
if (language === "fr") {
|
|
2245
|
-
return text2.replace(
|
|
2593
|
+
return text2.replace(LEADING_PARTITIVE, "").replace(LEADING_ARTICLE_FRENCH, "").trim();
|
|
2246
2594
|
}
|
|
2247
|
-
return text2.replace(
|
|
2595
|
+
return text2.replace(OF_OPENING, "").replace(LEADING_INDEFINITE_WITH_SPACE, "").trim();
|
|
2248
2596
|
}
|
|
2249
2597
|
function takeAlternates(text2, language) {
|
|
2250
2598
|
const none = { measures: [], intro: null, published: "", rest: text2 };
|
|
2251
|
-
if (!text2.startsWith("("))
|
|
2599
|
+
if (!text2.startsWith("(")) {
|
|
2600
|
+
return none;
|
|
2601
|
+
}
|
|
2252
2602
|
const close = text2.indexOf(")");
|
|
2253
|
-
if (close < 0)
|
|
2603
|
+
if (close < 0) {
|
|
2604
|
+
return none;
|
|
2605
|
+
}
|
|
2254
2606
|
const read = readBracket(text2.slice(0, close + 1), language);
|
|
2255
|
-
if (!read)
|
|
2607
|
+
if (!read) {
|
|
2608
|
+
return none;
|
|
2609
|
+
}
|
|
2256
2610
|
return { ...read, rest: text2.slice(close + 1).trimStart() };
|
|
2257
2611
|
}
|
|
2258
2612
|
var EQUIVALENT_INTRODUCER = /^(?:soit|environ|about|approx\.?|approximately|or|ou|=)\s*/i;
|
|
2259
2613
|
var MEASURE_PARTITIVE = /^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+|of\s+)/i;
|
|
2260
2614
|
function takeMeasureAfterQuantity(text2, language) {
|
|
2261
2615
|
const direct = takeUnitEitherLanguage(text2, language);
|
|
2262
|
-
if (direct.unit)
|
|
2616
|
+
if (direct.unit) {
|
|
2617
|
+
return direct;
|
|
2618
|
+
}
|
|
2263
2619
|
const partitive = MEASURE_PARTITIVE.exec(text2);
|
|
2264
|
-
if (!partitive)
|
|
2620
|
+
if (!partitive) {
|
|
2621
|
+
return direct;
|
|
2622
|
+
}
|
|
2265
2623
|
return takeUnitEitherLanguage(text2.slice(partitive[0].length), language);
|
|
2266
2624
|
}
|
|
2267
2625
|
function readBracket(bracket, language) {
|
|
2268
2626
|
const inside = bracket.slice(1, -1);
|
|
2269
2627
|
const intro = EQUIVALENT_INTRODUCER.exec(inside);
|
|
2270
2628
|
const body = intro ? inside.slice(intro[0].length) : inside;
|
|
2271
|
-
const parts = body.split(
|
|
2629
|
+
const parts = body.split(SLASH_OUTSIDE_A_FRACTION).map((part) => part.trim());
|
|
2272
2630
|
const measures = [];
|
|
2273
2631
|
for (const part of parts) {
|
|
2274
2632
|
const range = parseLeadingRange(part, language);
|
|
2275
2633
|
const quantity = range ?? parseLeadingQuantity(part, language);
|
|
2276
|
-
if (!quantity)
|
|
2634
|
+
if (!quantity) {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2277
2637
|
const after = takeMeasureAfterQuantity(part.slice(quantity.length).trimStart(), language);
|
|
2278
|
-
if (!after.unit || after.rest.trim() !== "")
|
|
2638
|
+
if (!after.unit || after.rest.trim() !== "") {
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2279
2641
|
measures.push({
|
|
2280
2642
|
amount: quantity.amount,
|
|
2281
2643
|
amountMax: range?.max ?? null,
|
|
@@ -2283,16 +2645,24 @@ function readBracket(bracket, language) {
|
|
|
2283
2645
|
unit: after.unit
|
|
2284
2646
|
});
|
|
2285
2647
|
}
|
|
2286
|
-
if (measures.length === 0)
|
|
2648
|
+
if (measures.length === 0) {
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2287
2651
|
return { measures, intro: intro ? intro[0].trim() : null, published: bracket };
|
|
2288
2652
|
}
|
|
2289
2653
|
function takeTrailingAlternates(item, language) {
|
|
2290
|
-
const closing =
|
|
2291
|
-
if (!closing)
|
|
2292
|
-
|
|
2293
|
-
|
|
2654
|
+
const closing = BRACKETED_TAIL.exec(item);
|
|
2655
|
+
if (!closing) {
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
const read = readBracket(closing[1] ?? "", language);
|
|
2659
|
+
if (!read) {
|
|
2660
|
+
return null;
|
|
2661
|
+
}
|
|
2294
2662
|
const head = item.slice(0, closing.index).trim();
|
|
2295
|
-
if (head === "")
|
|
2663
|
+
if (head === "") {
|
|
2664
|
+
return null;
|
|
2665
|
+
}
|
|
2296
2666
|
return { ...read, item: head };
|
|
2297
2667
|
}
|
|
2298
2668
|
function takeSlashAlternates(text2, language) {
|
|
@@ -2302,9 +2672,13 @@ function takeSlashAlternates(text2, language) {
|
|
|
2302
2672
|
const after = rest.slice(1).trimStart();
|
|
2303
2673
|
const range = parseLeadingRange(after, language);
|
|
2304
2674
|
const quantity = range ?? parseLeadingQuantity(after, language);
|
|
2305
|
-
if (!quantity)
|
|
2675
|
+
if (!quantity) {
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2306
2678
|
const taken = takeUnitEitherLanguage(after.slice(quantity.length).trimStart(), language);
|
|
2307
|
-
if (!taken.unit)
|
|
2679
|
+
if (!taken.unit) {
|
|
2680
|
+
break;
|
|
2681
|
+
}
|
|
2308
2682
|
measures.push({
|
|
2309
2683
|
amount: quantity.amount,
|
|
2310
2684
|
amountMax: range?.max ?? null,
|
|
@@ -2321,9 +2695,15 @@ function formatAmount(amount, language, options = {}) {
|
|
|
2321
2695
|
const rendered = String(rounded);
|
|
2322
2696
|
return language === "fr" ? rendered.replace(".", ",") : rendered;
|
|
2323
2697
|
};
|
|
2324
|
-
if (!Number.isFinite(amount))
|
|
2325
|
-
|
|
2326
|
-
|
|
2698
|
+
if (!Number.isFinite(amount)) {
|
|
2699
|
+
return "";
|
|
2700
|
+
}
|
|
2701
|
+
if (Number.isInteger(amount)) {
|
|
2702
|
+
return String(amount);
|
|
2703
|
+
}
|
|
2704
|
+
if (options.fractions === false) {
|
|
2705
|
+
return decimal(amount);
|
|
2706
|
+
}
|
|
2327
2707
|
const whole = Math.floor(amount);
|
|
2328
2708
|
const rest = amount - whole;
|
|
2329
2709
|
const known = [
|
|
@@ -2334,20 +2714,52 @@ function formatAmount(amount, language, options = {}) {
|
|
|
2334
2714
|
[0.75, "3/4"]
|
|
2335
2715
|
];
|
|
2336
2716
|
for (const [value, label] of known) {
|
|
2337
|
-
if (Math.abs(rest - value) < 0.02)
|
|
2717
|
+
if (Math.abs(rest - value) < 0.02) {
|
|
2718
|
+
return whole > 0 ? `${whole} ${label}` : label;
|
|
2719
|
+
}
|
|
2338
2720
|
}
|
|
2339
2721
|
return decimal(amount);
|
|
2340
2722
|
}
|
|
2341
2723
|
|
|
2342
2724
|
// src/recipe/scale.ts
|
|
2725
|
+
var DASH_ONLY = /^[-–—]$/;
|
|
2726
|
+
var AL_ENDING2 = /al$/i;
|
|
2727
|
+
var AUX_ENDING2 = /aux$/i;
|
|
2728
|
+
var CLOVE = /\bcloves?\b/i;
|
|
2729
|
+
var CONSONANT_THEN_Y = /[^aeiou]y$/i;
|
|
2730
|
+
var DOUBLE_S_ENDING = /ss$/i;
|
|
2731
|
+
var EAUX_ENDING2 = /eaux$/i;
|
|
2732
|
+
var EAU_ENDING2 = /eau$/i;
|
|
2733
|
+
var F_ENDING = /(?:[^f]f|fe)$/i;
|
|
2734
|
+
var F_OR_FE_ENDING = /fe?$/i;
|
|
2735
|
+
var GARLIC = /\bgarlic\b/i;
|
|
2736
|
+
var IES_ENDING = /ies$/i;
|
|
2737
|
+
var LETTER = new RegExp("\\p{L}", "u");
|
|
2738
|
+
var LETTERS_ONLY = /^[A-Za-z]+$/;
|
|
2739
|
+
var OF_JOINER = / of /i;
|
|
2740
|
+
var PLURAL_ENDING = /s$|eaux$|aux$/i;
|
|
2741
|
+
var SIBILANT_ENDING = /(?:ch|sh|s|x|z)$/i;
|
|
2742
|
+
var SIBILANT_LETTER2 = /[sxz]$/i;
|
|
2743
|
+
var SIBILANT_PLURAL = /(?:ch|sh|s|x|z)es$/i;
|
|
2744
|
+
var TRAILING_S_ANY_CASE2 = /s$/i;
|
|
2745
|
+
var VOWEL_OPENING = /^[aeiouàâäéèêëîïôöûü]/i;
|
|
2746
|
+
var X_ENDING = /x$/i;
|
|
2343
2747
|
function roundTo(value, step) {
|
|
2344
2748
|
return Math.round(Math.round(value / step) * step * 100) / 100;
|
|
2345
2749
|
}
|
|
2346
2750
|
function roundMeasured(value) {
|
|
2347
|
-
if (Number.isInteger(value))
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
if (value >=
|
|
2751
|
+
if (Number.isInteger(value)) {
|
|
2752
|
+
return value;
|
|
2753
|
+
}
|
|
2754
|
+
if (value >= 100) {
|
|
2755
|
+
return roundTo(value, 5);
|
|
2756
|
+
}
|
|
2757
|
+
if (value >= 10) {
|
|
2758
|
+
return roundTo(value, 1);
|
|
2759
|
+
}
|
|
2760
|
+
if (value >= 1) {
|
|
2761
|
+
return roundTo(value, 0.1);
|
|
2762
|
+
}
|
|
2351
2763
|
return Math.round(value * 100) / 100;
|
|
2352
2764
|
}
|
|
2353
2765
|
var SMALLEST_USABLE_FRACTION = 0.25;
|
|
@@ -2363,38 +2775,50 @@ function trim(value) {
|
|
|
2363
2775
|
return Math.round(value * 100) / 100;
|
|
2364
2776
|
}
|
|
2365
2777
|
function roundCountable(value, divisibility, ceiling) {
|
|
2366
|
-
if (value <= 0)
|
|
2778
|
+
if (value <= 0) {
|
|
2779
|
+
return { value: 0, clamped: false };
|
|
2780
|
+
}
|
|
2367
2781
|
const floor = SMALLEST_USABLE[divisibility];
|
|
2368
2782
|
if (divisibility !== "whole" && value >= floor && isHalfStep(value)) {
|
|
2369
2783
|
return { value: trim(value), clamped: false };
|
|
2370
2784
|
}
|
|
2371
2785
|
if (divisibility === "whole") {
|
|
2372
|
-
if (value < 0.5)
|
|
2786
|
+
if (value < 0.5) {
|
|
2787
|
+
return { value: floor, clamped: true };
|
|
2788
|
+
}
|
|
2373
2789
|
return { value: Math.round(value), clamped: false };
|
|
2374
2790
|
}
|
|
2375
|
-
if (value < floor)
|
|
2791
|
+
if (value < floor) {
|
|
2792
|
+
return { value: floor, clamped: true };
|
|
2793
|
+
}
|
|
2376
2794
|
if (value < 1) {
|
|
2377
2795
|
const steps = divisibility === "quarter" ? [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 1] : [0.5, 1];
|
|
2378
2796
|
const candidates = steps.filter(
|
|
2379
2797
|
(candidate) => candidate >= floor && candidate <= Math.max(ceiling, floor)
|
|
2380
2798
|
);
|
|
2381
|
-
let closest = candidates[0];
|
|
2799
|
+
let closest = candidates[0] ?? floor;
|
|
2382
2800
|
for (const candidate of candidates) {
|
|
2383
|
-
if (Math.abs(value - candidate) < Math.abs(value - closest))
|
|
2801
|
+
if (Math.abs(value - candidate) < Math.abs(value - closest)) {
|
|
2802
|
+
closest = candidate;
|
|
2803
|
+
}
|
|
2384
2804
|
}
|
|
2385
2805
|
return { value: trim(closest), clamped: false };
|
|
2386
2806
|
}
|
|
2387
2807
|
return { value: Math.round(value), clamped: false };
|
|
2388
2808
|
}
|
|
2389
2809
|
function roundSpoon(value, ceiling) {
|
|
2390
|
-
if (value <= 0)
|
|
2810
|
+
if (value <= 0) {
|
|
2811
|
+
return { value: 0, clamped: false };
|
|
2812
|
+
}
|
|
2391
2813
|
if (value < 1) {
|
|
2392
2814
|
const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(
|
|
2393
2815
|
(candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION)
|
|
2394
2816
|
);
|
|
2395
|
-
let closest = candidates[0];
|
|
2817
|
+
let closest = candidates[0] ?? SMALLEST_USABLE_FRACTION;
|
|
2396
2818
|
for (const candidate of candidates) {
|
|
2397
|
-
if (Math.abs(value - candidate) < Math.abs(value - closest))
|
|
2819
|
+
if (Math.abs(value - candidate) < Math.abs(value - closest)) {
|
|
2820
|
+
closest = candidate;
|
|
2821
|
+
}
|
|
2398
2822
|
}
|
|
2399
2823
|
return { value: trim(closest), clamped: value < SMALLEST_USABLE_FRACTION };
|
|
2400
2824
|
}
|
|
@@ -2405,7 +2829,9 @@ function stepDownSpoon(unit, reference) {
|
|
|
2405
2829
|
let ratio = 1;
|
|
2406
2830
|
while (reference * ratio < 1 && !isHalfStep(reference * ratio)) {
|
|
2407
2831
|
const step = demoteUnit(current);
|
|
2408
|
-
if (!step)
|
|
2832
|
+
if (!step) {
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2409
2835
|
ratio *= step.per;
|
|
2410
2836
|
current = step.unit;
|
|
2411
2837
|
}
|
|
@@ -2415,24 +2841,33 @@ var EXACT_WITHIN = 0.01;
|
|
|
2415
2841
|
var EXACT_SHARE = 5e-3;
|
|
2416
2842
|
function landedExactly(exact, amount) {
|
|
2417
2843
|
const gap = Math.abs(exact - amount);
|
|
2418
|
-
if (gap > EXACT_WITHIN)
|
|
2844
|
+
if (gap > EXACT_WITHIN) {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2419
2847
|
return exact === 0 || gap / Math.abs(exact) <= EXACT_SHARE;
|
|
2420
2848
|
}
|
|
2421
2849
|
function boundLandedExactly(bound) {
|
|
2422
|
-
const fine =
|
|
2850
|
+
const fine = Math.max(bound.ratio, 1);
|
|
2423
2851
|
return landedExactly(bound.raw * fine, bound.amount * (fine / bound.ratio));
|
|
2424
2852
|
}
|
|
2425
2853
|
function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2854
|
+
const ends = high === null ? [{ published: low, raw: low * factor }] : [
|
|
2855
|
+
{ published: low, raw: low * factor },
|
|
2856
|
+
{ published: high, raw: high * factor }
|
|
2857
|
+
];
|
|
2858
|
+
const eachEnd = (read) => [
|
|
2859
|
+
read(ends[0]),
|
|
2860
|
+
...ends.slice(1).map(read)
|
|
2861
|
+
];
|
|
2862
|
+
const raws = ends.map((end) => end.raw);
|
|
2428
2863
|
const positive = raws.filter((raw) => raw > 0);
|
|
2429
|
-
const reference = positive.length > 0 ? Math.min(...positive) :
|
|
2864
|
+
const reference = positive.length > 0 ? Math.min(...positive) : low * factor;
|
|
2430
2865
|
const inUnit = (target, ratio) => ({
|
|
2431
|
-
bounds:
|
|
2866
|
+
bounds: eachEnd(({ published, raw }) => {
|
|
2432
2867
|
const exact = raw * ratio;
|
|
2433
2868
|
const rounded = ratio < 1 ? Number((roundMeasured(raw) * ratio).toPrecision(12)) : roundMeasured(exact);
|
|
2434
2869
|
const usable = rounded === 0 && exact > 0 ? Number(exact.toPrecision(2)) : rounded;
|
|
2435
|
-
const ceiling = factor < 1 ?
|
|
2870
|
+
const ceiling = factor < 1 ? published * ratio : Number.POSITIVE_INFINITY;
|
|
2436
2871
|
return {
|
|
2437
2872
|
amount: Math.min(usable, ceiling),
|
|
2438
2873
|
exact,
|
|
@@ -2449,16 +2884,18 @@ function scaleMeasure(low, high, unit, factor, divisibility) {
|
|
|
2449
2884
|
}
|
|
2450
2885
|
if (unit && isSpoonMeasure(unit)) {
|
|
2451
2886
|
const stepped = stepDownSpoon(unit, reference);
|
|
2452
|
-
if (stepped.ratio !== 1)
|
|
2453
|
-
|
|
2454
|
-
|
|
2887
|
+
if (stepped.ratio !== 1) {
|
|
2888
|
+
return inUnit(stepped.unit, stepped.ratio);
|
|
2889
|
+
}
|
|
2890
|
+
const bounds2 = eachEnd(({ published, raw }) => {
|
|
2891
|
+
const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;
|
|
2455
2892
|
const rounded = roundSpoon(raw, ceiling);
|
|
2456
2893
|
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
2457
2894
|
});
|
|
2458
2895
|
return { bounds: bounds2, unit };
|
|
2459
2896
|
}
|
|
2460
|
-
const bounds =
|
|
2461
|
-
const ceiling = factor < 1 ?
|
|
2897
|
+
const bounds = eachEnd(({ published, raw }) => {
|
|
2898
|
+
const ceiling = factor < 1 ? published : Number.POSITIVE_INFINITY;
|
|
2462
2899
|
const rounded = roundCountable(raw, divisibility, ceiling);
|
|
2463
2900
|
return { amount: rounded.value, exact: raw, clamped: rounded.clamped, raw, ratio: 1 };
|
|
2464
2901
|
});
|
|
@@ -2476,14 +2913,18 @@ var HALVED_ITEM = /\b(?:jus|juices?)\b/iu;
|
|
|
2476
2913
|
var WHOLE_ITEM = /\b(?:eggs?|yolks?|egg\s+whites?|oeufs?|œufs?|jaunes?\s+d['e]|clous?|zestes?|zests?)\b/iu;
|
|
2477
2914
|
var HALVED_CUT = /\b(?:breasts?|thighs?|drumsticks?|wings?|cutlets?|cuisses?|ailes?|pilons?|escalopes?|magrets?)\b/iu;
|
|
2478
2915
|
function cloveDivisibility(unit, item) {
|
|
2479
|
-
const counted = unit ? unit.canonical === "clove" :
|
|
2480
|
-
if (!counted)
|
|
2481
|
-
|
|
2916
|
+
const counted = unit ? unit.canonical === "clove" : CLOVE.test(item);
|
|
2917
|
+
if (!counted) {
|
|
2918
|
+
return null;
|
|
2919
|
+
}
|
|
2920
|
+
return GARLIC.test(item) ? "half" : "whole";
|
|
2482
2921
|
}
|
|
2483
2922
|
var BLANC_OF = /\bblancs?\s+d(?:e\s|['’])/iu;
|
|
2484
2923
|
var BLANC_OF_EGG = /\bblancs?\s+d(?:e\s|['’])\s*(?:oeufs?|œufs?)\b/iu;
|
|
2485
2924
|
function blancDivisibility(item) {
|
|
2486
|
-
if (!BLANC_OF.test(item))
|
|
2925
|
+
if (!BLANC_OF.test(item)) {
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2487
2928
|
return BLANC_OF_EGG.test(item) ? "whole" : "half";
|
|
2488
2929
|
}
|
|
2489
2930
|
function foldItem(item) {
|
|
@@ -2492,15 +2933,31 @@ function foldItem(item) {
|
|
|
2492
2933
|
function divisibilityOf(unit, item) {
|
|
2493
2934
|
const key = foldItem(item);
|
|
2494
2935
|
const clove = cloveDivisibility(unit, key);
|
|
2495
|
-
if (clove)
|
|
2496
|
-
|
|
2936
|
+
if (clove) {
|
|
2937
|
+
return clove;
|
|
2938
|
+
}
|
|
2939
|
+
if (unit && !countsBarePieces(unit)) {
|
|
2940
|
+
return unitDivisibility(unit);
|
|
2941
|
+
}
|
|
2497
2942
|
const blanc = blancDivisibility(key);
|
|
2498
|
-
if (blanc)
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
if (
|
|
2502
|
-
|
|
2503
|
-
|
|
2943
|
+
if (blanc) {
|
|
2944
|
+
return blanc;
|
|
2945
|
+
}
|
|
2946
|
+
if (WHOLE_ITEM.test(key)) {
|
|
2947
|
+
return "whole";
|
|
2948
|
+
}
|
|
2949
|
+
if (PORTION_SIZED_ITEM.test(key)) {
|
|
2950
|
+
return "whole";
|
|
2951
|
+
}
|
|
2952
|
+
if (HALVED_ITEM.test(key)) {
|
|
2953
|
+
return "half";
|
|
2954
|
+
}
|
|
2955
|
+
if (HALVED_CUT.test(key)) {
|
|
2956
|
+
return "half";
|
|
2957
|
+
}
|
|
2958
|
+
if (QUARTERED_MEASURE.test(key)) {
|
|
2959
|
+
return "quarter";
|
|
2960
|
+
}
|
|
2504
2961
|
return QUARTERED_ITEM.test(key) ? "quarter" : "half";
|
|
2505
2962
|
}
|
|
2506
2963
|
var INVARIABLE_ITEM = /* @__PURE__ */ new Set([
|
|
@@ -2589,54 +3046,87 @@ var IRREGULAR_SINGULAR = Object.fromEntries(
|
|
|
2589
3046
|
Object.entries(IRREGULAR_PLURAL).map(([one, many]) => [many, one])
|
|
2590
3047
|
);
|
|
2591
3048
|
function matchCase(source, replacement) {
|
|
2592
|
-
const hasLetter =
|
|
2593
|
-
if (hasLetter && source === source.toUpperCase())
|
|
2594
|
-
|
|
2595
|
-
|
|
3049
|
+
const hasLetter = LETTER.test(source);
|
|
3050
|
+
if (hasLetter && source === source.toUpperCase()) {
|
|
3051
|
+
return replacement.toUpperCase();
|
|
3052
|
+
}
|
|
3053
|
+
const first = source.slice(0, 1);
|
|
3054
|
+
if (first !== "" && first === first.toUpperCase() && source.slice(1) === source.slice(1).toLowerCase()) {
|
|
3055
|
+
return (replacement[0] ?? "").toUpperCase() + replacement.slice(1);
|
|
2596
3056
|
}
|
|
2597
3057
|
return replacement;
|
|
2598
3058
|
}
|
|
2599
3059
|
function toEnglishPlural(word) {
|
|
2600
3060
|
const key = word.toLowerCase();
|
|
2601
|
-
if (INVARIABLE_ITEM.has(key))
|
|
3061
|
+
if (INVARIABLE_ITEM.has(key)) {
|
|
3062
|
+
return word;
|
|
3063
|
+
}
|
|
2602
3064
|
const irregular = IRREGULAR_PLURAL[key];
|
|
2603
|
-
if (irregular)
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
if (
|
|
3065
|
+
if (irregular) {
|
|
3066
|
+
return matchCase(word, irregular);
|
|
3067
|
+
}
|
|
3068
|
+
if (SIBILANT_ENDING.test(word)) {
|
|
3069
|
+
return `${word}es`;
|
|
3070
|
+
}
|
|
3071
|
+
if (CONSONANT_THEN_Y.test(word)) {
|
|
3072
|
+
return `${word.slice(0, -1)}ies`;
|
|
3073
|
+
}
|
|
3074
|
+
if (F_ENDING.test(word)) {
|
|
3075
|
+
return `${word.replace(F_OR_FE_ENDING, "")}ves`;
|
|
3076
|
+
}
|
|
2607
3077
|
return `${word}s`;
|
|
2608
3078
|
}
|
|
2609
3079
|
function toEnglishSingular(word) {
|
|
2610
3080
|
const key = word.toLowerCase();
|
|
2611
|
-
if (INVARIABLE_ITEM.has(key))
|
|
3081
|
+
if (INVARIABLE_ITEM.has(key)) {
|
|
3082
|
+
return word;
|
|
3083
|
+
}
|
|
2612
3084
|
const irregular = IRREGULAR_SINGULAR[key];
|
|
2613
|
-
if (irregular)
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
if (
|
|
2617
|
-
|
|
3085
|
+
if (irregular) {
|
|
3086
|
+
return matchCase(word, irregular);
|
|
3087
|
+
}
|
|
3088
|
+
if (IES_ENDING.test(word) && word.length > 4) {
|
|
3089
|
+
return `${word.slice(0, -3)}y`;
|
|
3090
|
+
}
|
|
3091
|
+
if (SIBILANT_PLURAL.test(word)) {
|
|
3092
|
+
return word.slice(0, -2);
|
|
3093
|
+
}
|
|
3094
|
+
if (DOUBLE_S_ENDING.test(word)) {
|
|
3095
|
+
return word;
|
|
3096
|
+
}
|
|
3097
|
+
if (TRAILING_S_ANY_CASE2.test(word)) {
|
|
3098
|
+
return word.slice(0, -1);
|
|
3099
|
+
}
|
|
2618
3100
|
return word;
|
|
2619
3101
|
}
|
|
2620
3102
|
function agreeInEnglish(item, amount) {
|
|
2621
|
-
if (!item)
|
|
3103
|
+
if (!item) {
|
|
3104
|
+
return item;
|
|
3105
|
+
}
|
|
2622
3106
|
const comma = item.indexOf(",");
|
|
2623
3107
|
const head = comma < 0 ? item : item.slice(0, comma);
|
|
2624
3108
|
const tail = comma < 0 ? "" : item.slice(comma);
|
|
2625
|
-
const preposition =
|
|
3109
|
+
const preposition = OF_JOINER.exec(head);
|
|
2626
3110
|
if (preposition) {
|
|
2627
3111
|
const counted = agreeInEnglish(head.slice(0, preposition.index), amount);
|
|
2628
3112
|
return `${counted}${head.slice(preposition.index)}${tail}`;
|
|
2629
3113
|
}
|
|
2630
3114
|
const words = head.trimEnd().split(" ");
|
|
2631
|
-
const last = words
|
|
2632
|
-
if (
|
|
3115
|
+
const last = words.at(-1) ?? "";
|
|
3116
|
+
if (!LETTERS_ONLY.test(last) || last.length <= 2) {
|
|
3117
|
+
return item;
|
|
3118
|
+
}
|
|
2633
3119
|
const wantsPlural = amount > 1;
|
|
2634
3120
|
const plural = toEnglishPlural(last);
|
|
2635
3121
|
const singular = toEnglishSingular(last);
|
|
2636
3122
|
const isPlural = last.toLowerCase() !== singular.toLowerCase();
|
|
2637
|
-
if (wantsPlural && !isPlural)
|
|
2638
|
-
|
|
2639
|
-
else
|
|
3123
|
+
if (wantsPlural && !isPlural) {
|
|
3124
|
+
words[words.length - 1] = plural;
|
|
3125
|
+
} else if (!wantsPlural && isPlural) {
|
|
3126
|
+
words[words.length - 1] = singular;
|
|
3127
|
+
} else {
|
|
3128
|
+
return item;
|
|
3129
|
+
}
|
|
2640
3130
|
return `${words.join(" ")}${tail}`;
|
|
2641
3131
|
}
|
|
2642
3132
|
var FRENCH_OU_PLURAL_IN_X = /* @__PURE__ */ new Set([
|
|
@@ -2715,62 +3205,109 @@ function agreeTrailingAdjective(word, wantsPlural) {
|
|
|
2715
3205
|
const folded = foldWord2(word);
|
|
2716
3206
|
const isPlural = folded.endsWith("s");
|
|
2717
3207
|
const singular = isPlural ? word.slice(0, -1) : word;
|
|
2718
|
-
if (!AGREEABLE_ADJECTIVES.has(foldWord2(singular)))
|
|
2719
|
-
|
|
3208
|
+
if (!AGREEABLE_ADJECTIVES.has(foldWord2(singular))) {
|
|
3209
|
+
return null;
|
|
3210
|
+
}
|
|
3211
|
+
if (wantsPlural === isPlural) {
|
|
3212
|
+
return null;
|
|
3213
|
+
}
|
|
2720
3214
|
return wantsPlural ? `${word}s` : singular;
|
|
2721
3215
|
}
|
|
3216
|
+
function agreeFrenchHead(head, number) {
|
|
3217
|
+
const { wantsPlural, isPlural, ouPlural } = number;
|
|
3218
|
+
if (wantsPlural && !isPlural) {
|
|
3219
|
+
if (SIBILANT_LETTER2.test(head)) {
|
|
3220
|
+
return head;
|
|
3221
|
+
}
|
|
3222
|
+
if (EAU_ENDING2.test(head)) {
|
|
3223
|
+
return `${head}x`;
|
|
3224
|
+
}
|
|
3225
|
+
if (FRENCH_OU_PLURAL_IN_X.has(foldWord2(head))) {
|
|
3226
|
+
return `${head}x`;
|
|
3227
|
+
}
|
|
3228
|
+
if (AL_ENDING2.test(head)) {
|
|
3229
|
+
return `${head.slice(0, -2)}aux`;
|
|
3230
|
+
}
|
|
3231
|
+
return `${head}s`;
|
|
3232
|
+
}
|
|
3233
|
+
if (!wantsPlural && isPlural) {
|
|
3234
|
+
if (EAUX_ENDING2.test(head)) {
|
|
3235
|
+
return head.slice(0, -1);
|
|
3236
|
+
}
|
|
3237
|
+
if (AUX_ENDING2.test(head)) {
|
|
3238
|
+
return `${head.slice(0, -3)}al`;
|
|
3239
|
+
}
|
|
3240
|
+
if (ouPlural) {
|
|
3241
|
+
return head.slice(0, -1);
|
|
3242
|
+
}
|
|
3243
|
+
if (INVARIABLE_FRENCH_NOUN.has(foldWord2(head))) {
|
|
3244
|
+
return head;
|
|
3245
|
+
}
|
|
3246
|
+
return head.slice(0, -1);
|
|
3247
|
+
}
|
|
3248
|
+
return head;
|
|
3249
|
+
}
|
|
2722
3250
|
function agreeInFrench(item, amount) {
|
|
2723
|
-
if (!item)
|
|
3251
|
+
if (!item) {
|
|
3252
|
+
return item;
|
|
3253
|
+
}
|
|
2724
3254
|
const words = item.split(" ");
|
|
2725
3255
|
const head = words[0] ?? "";
|
|
2726
|
-
if (head.length <= 3)
|
|
2727
|
-
|
|
2728
|
-
const ouPlural = /x$/i.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord2(head.slice(0, -1)));
|
|
2729
|
-
const isPlural = /s$|eaux$|aux$/i.test(head) || ouPlural;
|
|
2730
|
-
if (wantsPlural && !isPlural) {
|
|
2731
|
-
if (/[sxz]$/i.test(head)) {
|
|
2732
|
-
} else if (/eau$/i.test(head)) words[0] = `${head}x`;
|
|
2733
|
-
else if (FRENCH_OU_PLURAL_IN_X.has(foldWord2(head))) words[0] = `${head}x`;
|
|
2734
|
-
else if (/al$/i.test(head)) words[0] = `${head.slice(0, -2)}aux`;
|
|
2735
|
-
else words[0] = `${head}s`;
|
|
2736
|
-
} else if (!wantsPlural && isPlural) {
|
|
2737
|
-
if (/eaux$/i.test(head)) words[0] = head.slice(0, -1);
|
|
2738
|
-
else if (/aux$/i.test(head)) words[0] = `${head.slice(0, -3)}al`;
|
|
2739
|
-
else if (ouPlural) words[0] = head.slice(0, -1);
|
|
2740
|
-
else if (INVARIABLE_FRENCH_NOUN.has(foldWord2(head))) {
|
|
2741
|
-
} else words[0] = head.slice(0, -1);
|
|
3256
|
+
if (head.length <= 3) {
|
|
3257
|
+
return item;
|
|
2742
3258
|
}
|
|
3259
|
+
const wantsPlural = amount >= 2;
|
|
3260
|
+
const ouPlural = X_ENDING.test(head) && FRENCH_OU_PLURAL_IN_X.has(foldWord2(head.slice(0, -1)));
|
|
3261
|
+
const isPlural = PLURAL_ENDING.test(head) || ouPlural;
|
|
3262
|
+
words[0] = agreeFrenchHead(head, { wantsPlural, isPlural, ouPlural });
|
|
2743
3263
|
const last = words.length - 1;
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3264
|
+
const trailing = last > 0 ? words[last] : void 0;
|
|
3265
|
+
if (trailing !== void 0) {
|
|
3266
|
+
const adjective = agreeTrailingAdjective(trailing, wantsPlural);
|
|
3267
|
+
if (adjective) {
|
|
3268
|
+
words[last] = adjective;
|
|
3269
|
+
}
|
|
2747
3270
|
}
|
|
2748
3271
|
return words.join(" ");
|
|
2749
3272
|
}
|
|
2750
3273
|
function agreeLeadingAdjective(word, amount, language) {
|
|
2751
|
-
if (language === "en")
|
|
3274
|
+
if (language === "en") {
|
|
3275
|
+
return word;
|
|
3276
|
+
}
|
|
2752
3277
|
const wantsPlural = amount >= 2;
|
|
2753
3278
|
const folded = foldWord2(word);
|
|
2754
3279
|
const isPlural = folded.endsWith("s") && !AGREEABLE_ADJECTIVES.has(folded);
|
|
2755
3280
|
const singular = isPlural ? word.slice(0, -1) : word;
|
|
2756
|
-
if (!AGREEABLE_ADJECTIVES.has(foldWord2(singular)))
|
|
2757
|
-
|
|
3281
|
+
if (!AGREEABLE_ADJECTIVES.has(foldWord2(singular))) {
|
|
3282
|
+
return word;
|
|
3283
|
+
}
|
|
3284
|
+
if (wantsPlural === isPlural) {
|
|
3285
|
+
return word;
|
|
3286
|
+
}
|
|
2758
3287
|
return wantsPlural ? `${singular}s` : singular;
|
|
2759
3288
|
}
|
|
2760
3289
|
function agreeWithAmount(item, amount, language) {
|
|
2761
3290
|
return language === "fr" ? agreeInFrench(item, amount) : agreeInEnglish(item, amount);
|
|
2762
3291
|
}
|
|
2763
3292
|
function agreeCountedContainer(item, amount, language) {
|
|
2764
|
-
if (language === "fr")
|
|
3293
|
+
if (language === "fr") {
|
|
3294
|
+
return agreeInFrench(item, amount);
|
|
3295
|
+
}
|
|
2765
3296
|
const space = item.indexOf(" ");
|
|
2766
|
-
if (space < 0)
|
|
3297
|
+
if (space < 0) {
|
|
3298
|
+
return agreeInEnglish(item, amount);
|
|
3299
|
+
}
|
|
2767
3300
|
return `${agreeInEnglish(item.slice(0, space), amount)}${item.slice(space)}`;
|
|
2768
3301
|
}
|
|
2769
3302
|
var MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\b/i;
|
|
2770
3303
|
function joinItem(item, language) {
|
|
2771
|
-
if (!item)
|
|
2772
|
-
|
|
2773
|
-
|
|
3304
|
+
if (!item) {
|
|
3305
|
+
return "";
|
|
3306
|
+
}
|
|
3307
|
+
if (language === "en") {
|
|
3308
|
+
return ` ${item}`;
|
|
3309
|
+
}
|
|
3310
|
+
const elides = VOWEL_OPENING.test(item) || MUTE_H_WORDS.test(item);
|
|
2774
3311
|
return elides ? ` d'${item}` : ` de ${item}`;
|
|
2775
3312
|
}
|
|
2776
3313
|
var BRANCH_SEPARATORS = {
|
|
@@ -2779,29 +3316,41 @@ var BRANCH_SEPARATORS = {
|
|
|
2779
3316
|
};
|
|
2780
3317
|
function splitBranch(text2, parsed) {
|
|
2781
3318
|
const itemStart = parsed.item ? text2.indexOf(parsed.item) : text2.length;
|
|
2782
|
-
if (itemStart < 0)
|
|
3319
|
+
if (itemStart < 0) {
|
|
3320
|
+
return null;
|
|
3321
|
+
}
|
|
2783
3322
|
const separator = BRANCH_SEPARATORS[parsed.language];
|
|
2784
3323
|
separator.lastIndex = 0;
|
|
2785
3324
|
for (let match = separator.exec(text2); match; match = separator.exec(text2)) {
|
|
2786
|
-
if (match.index < itemStart)
|
|
3325
|
+
if (match.index < itemStart) {
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
2787
3328
|
const tail = text2.slice(match.index + match[0].length);
|
|
2788
|
-
if (parseIngredient(tail, parsed.language).amount === null)
|
|
3329
|
+
if (parseIngredient(tail, parsed.language).amount === null) {
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
2789
3332
|
return { head: text2.slice(0, match.index), separator: match[0], tail };
|
|
2790
3333
|
}
|
|
2791
3334
|
return null;
|
|
2792
3335
|
}
|
|
2793
3336
|
function scaleIngredient(line, options) {
|
|
2794
3337
|
const { factor } = options;
|
|
2795
|
-
if (factor === 1)
|
|
3338
|
+
if (factor === 1) {
|
|
3339
|
+
return passthroughIngredient(line, options.language);
|
|
3340
|
+
}
|
|
2796
3341
|
const text2 = line.trim();
|
|
2797
3342
|
const parsed = parseIngredient(text2, options.language ?? "auto");
|
|
2798
3343
|
const branch = splitBranch(text2, parsed);
|
|
2799
|
-
if (branch)
|
|
3344
|
+
if (branch) {
|
|
3345
|
+
return scaleBranchedLine(line, branch, { ...options, language: parsed.language });
|
|
3346
|
+
}
|
|
2800
3347
|
return scaleSingleLine(line, options);
|
|
2801
3348
|
}
|
|
2802
3349
|
function scaleBranchedLine(line, branch, options) {
|
|
2803
3350
|
const head = scaleSingleLine(branch.head, options);
|
|
2804
|
-
if (head.scaling === "unscaled")
|
|
3351
|
+
if (head.scaling === "unscaled") {
|
|
3352
|
+
return { ...head, text: line.trim(), original: line };
|
|
3353
|
+
}
|
|
2805
3354
|
const tail = scaleAlternative(branch.tail, options);
|
|
2806
3355
|
const result = {
|
|
2807
3356
|
...head,
|
|
@@ -2816,11 +3365,128 @@ function scaleBranchedLine(line, branch, options) {
|
|
|
2816
3365
|
function scaleAlternative(tail, options) {
|
|
2817
3366
|
const parsed = parseIngredient(tail, options.language ?? "auto");
|
|
2818
3367
|
const published = tail.trim();
|
|
2819
|
-
if (parsed.amount === null)
|
|
3368
|
+
if (parsed.amount === null) {
|
|
3369
|
+
return { text: published, rewritten: false };
|
|
3370
|
+
}
|
|
2820
3371
|
const largest = (parsed.amountMax ?? parsed.amount) * options.factor;
|
|
2821
|
-
if (largest < 1)
|
|
3372
|
+
if (largest < 1) {
|
|
3373
|
+
return { text: published, rewritten: false };
|
|
3374
|
+
}
|
|
2822
3375
|
return { text: scaleIngredient(tail, options).text, rewritten: true };
|
|
2823
3376
|
}
|
|
3377
|
+
function alternateLabel(count2, style, texts, bracketed) {
|
|
3378
|
+
if (count2 === 0) {
|
|
3379
|
+
return "";
|
|
3380
|
+
}
|
|
3381
|
+
if (style === "slash") {
|
|
3382
|
+
return ` / ${texts.join(" / ")}`;
|
|
3383
|
+
}
|
|
3384
|
+
if (style === "trailing") {
|
|
3385
|
+
return "";
|
|
3386
|
+
}
|
|
3387
|
+
return bracketed;
|
|
3388
|
+
}
|
|
3389
|
+
function itemLabelFor(named, item, counted, language) {
|
|
3390
|
+
if (named) {
|
|
3391
|
+
return joinItem(item, language);
|
|
3392
|
+
}
|
|
3393
|
+
if (counted) {
|
|
3394
|
+
return ` ${counted}`;
|
|
3395
|
+
}
|
|
3396
|
+
return "";
|
|
3397
|
+
}
|
|
3398
|
+
function noteForScaledLine(outcome) {
|
|
3399
|
+
const {
|
|
3400
|
+
parsed,
|
|
3401
|
+
bounds: primaryBounds,
|
|
3402
|
+
unit,
|
|
3403
|
+
low,
|
|
3404
|
+
clamped,
|
|
3405
|
+
movedPrimary,
|
|
3406
|
+
movedAlternate,
|
|
3407
|
+
restated,
|
|
3408
|
+
collapsed,
|
|
3409
|
+
language,
|
|
3410
|
+
carriesMore
|
|
3411
|
+
} = outcome;
|
|
3412
|
+
let note;
|
|
3413
|
+
const asPublished = (value, source) => `${formatAmount(value, language, { fractions: false })}${source ? ` ${formatUnit(source, value, language)}` : ""}`;
|
|
3414
|
+
const sentences = [];
|
|
3415
|
+
if (clamped) {
|
|
3416
|
+
sentences.push(
|
|
3417
|
+
`Clamped up to ${formatAmount(clamped.amount, language)} from ${formatAmount(Math.round(clamped.raw * 1e3) / 1e3, language)}, the smallest amount worth measuring. This line no longer holds its share of the recipe.`
|
|
3418
|
+
);
|
|
3419
|
+
} else if (movedPrimary) {
|
|
3420
|
+
const moved = primaryBounds.filter((bound) => !boundLandedExactly(bound));
|
|
3421
|
+
sentences.push(
|
|
3422
|
+
moved.map(
|
|
3423
|
+
(bound) => `Rounded ${bound.amount > bound.exact ? "up" : "down"} from ${asPublished(bound.raw, parsed.unit)}.`
|
|
3424
|
+
).join(" ")
|
|
3425
|
+
);
|
|
3426
|
+
} else if (movedAlternate) {
|
|
3427
|
+
sentences.push(
|
|
3428
|
+
`The amount is exact; the equivalent ${restated ? "beside it" : "in brackets"} was rounded to stay readable.`
|
|
3429
|
+
);
|
|
3430
|
+
} else if (restated) {
|
|
3431
|
+
sentences.push(
|
|
3432
|
+
"This line states one quantity twice, and both readings were scaled. They agree as closely as the page wrote them, and no closer."
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
if (carriesMore) {
|
|
3436
|
+
sentences.push(
|
|
3437
|
+
"This line carries a further quantity after the first one, and only the first was scaled. Read the rest as published."
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
if (parsed.capacity) {
|
|
3441
|
+
sentences.push(
|
|
3442
|
+
`${parsed.capacity} is what one container holds, so it stays as published and the count is what the factor moved. Scaling this line is a matter of opening that many containers of the size named.`
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
if (collapsed) {
|
|
3446
|
+
sentences.push("The page gave a range, and at this size both ends come to the same amount.");
|
|
3447
|
+
}
|
|
3448
|
+
if (unit?.kind === "measured" && low.amount > 0 && low.amount < 0.05) {
|
|
3449
|
+
sentences.push(
|
|
3450
|
+
"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye."
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
if (parsed.approximation) {
|
|
3454
|
+
sentences.push(
|
|
3455
|
+
"The page gave this amount as an approximation, and the scaled figure is no firmer."
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
if (sentences.length > 0) {
|
|
3459
|
+
note = sentences.join(" ");
|
|
3460
|
+
}
|
|
3461
|
+
if (parsed.unit && parsed.unit.kind === "approximate") {
|
|
3462
|
+
note = withApproximateNote(parsed.unit, note);
|
|
3463
|
+
}
|
|
3464
|
+
if (parsed.articleWord) {
|
|
3465
|
+
const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);
|
|
3466
|
+
const read = `"${parsed.articleWord}" read as ${formatAmount(stood, language)}.`;
|
|
3467
|
+
note = note ? `${read} ${note}` : read;
|
|
3468
|
+
}
|
|
3469
|
+
return note;
|
|
3470
|
+
}
|
|
3471
|
+
function renderScaledLine(parsed, alternates, unit, shown, language) {
|
|
3472
|
+
const named = unit && !countsBarePieces(unit) ? unit : null;
|
|
3473
|
+
const adjective = named && parsed.measureAdjective ? ` ${agreeLeadingAdjective(parsed.measureAdjective, shown, language)}` : "";
|
|
3474
|
+
const unitLabel = named ? `${adjective} ${formatUnit(named, shown, language)}` : "";
|
|
3475
|
+
const alternateTexts = alternates.map((entry) => entry.text);
|
|
3476
|
+
const introduced = parsed.alternateIntro ? `${parsed.alternateIntro} ` : "";
|
|
3477
|
+
const bracketed = alternates.length === 0 ? "" : ` (${introduced}${alternateTexts.join(" / ")})`;
|
|
3478
|
+
const altLabel = alternateLabel(
|
|
3479
|
+
alternates.length,
|
|
3480
|
+
parsed.alternateStyle,
|
|
3481
|
+
alternateTexts,
|
|
3482
|
+
bracketed
|
|
3483
|
+
);
|
|
3484
|
+
const trailingLabel = parsed.alternateStyle === "trailing" ? bracketed : "";
|
|
3485
|
+
const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : "";
|
|
3486
|
+
const counted = parsed.capacity ? agreeCountedContainer(parsed.item, shown, language) : agreeWithAmount(parsed.item, shown, language);
|
|
3487
|
+
const itemLabel = itemLabelFor(named, parsed.item, counted, language);
|
|
3488
|
+
return { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel };
|
|
3489
|
+
}
|
|
2824
3490
|
var HELD_BACK_NOTE = {
|
|
2825
3491
|
sizeQualifier: "The figures here give the size of one item rather than how many, so the line is left as published.",
|
|
2826
3492
|
itemSize: "The measure standing behind the item gives the size of one of them rather than how many, so the line is left as published. Serving more people is a matter of taking a bigger one, and serving fewer a smaller one.",
|
|
@@ -2867,17 +3533,13 @@ function scaleSingleLine(line, options) {
|
|
|
2867
3533
|
high === null || collapsed ? null : asText(high.amount),
|
|
2868
3534
|
parsed.rangeSeparator
|
|
2869
3535
|
);
|
|
2870
|
-
const named
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
const trailingLabel = parsed.alternateStyle === "trailing" ? bracketed : "";
|
|
2878
|
-
const capacityLabel = parsed.capacity ? ` ${parsed.capacity}` : "";
|
|
2879
|
-
const counted = parsed.capacity ? agreeCountedContainer(parsed.item, shown, language) : agreeWithAmount(parsed.item, shown, language);
|
|
2880
|
-
const itemLabel = named ? joinItem(parsed.item, language) : counted ? ` ${counted}` : "";
|
|
3536
|
+
const { named, unitLabel, capacityLabel, altLabel, trailingLabel, itemLabel } = renderScaledLine(
|
|
3537
|
+
parsed,
|
|
3538
|
+
alternates,
|
|
3539
|
+
unit,
|
|
3540
|
+
shown,
|
|
3541
|
+
language
|
|
3542
|
+
);
|
|
2881
3543
|
const result = {
|
|
2882
3544
|
text: `${parsed.decoration ? `${parsed.decoration} ` : ""}${parsed.approximation ?? ""}${amountText}${unitLabel}${capacityLabel}${altLabel}${itemLabel}${trailingLabel}`.trim(),
|
|
2883
3545
|
original: parsed.original,
|
|
@@ -2887,59 +3549,21 @@ function scaleSingleLine(line, options) {
|
|
|
2887
3549
|
unit: named?.canonical ?? null,
|
|
2888
3550
|
language
|
|
2889
3551
|
};
|
|
2890
|
-
const
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
`The amount is exact; the equivalent ${restated ? "beside it" : "in brackets"} was rounded to stay readable.`
|
|
2906
|
-
);
|
|
2907
|
-
} else if (restated) {
|
|
2908
|
-
sentences.push(
|
|
2909
|
-
"This line states one quantity twice, and both readings were scaled. They agree as closely as the page wrote them, and no closer."
|
|
2910
|
-
);
|
|
2911
|
-
}
|
|
2912
|
-
if (carriesMore) {
|
|
2913
|
-
sentences.push(
|
|
2914
|
-
"This line carries a further quantity after the first one, and only the first was scaled. Read the rest as published."
|
|
2915
|
-
);
|
|
2916
|
-
}
|
|
2917
|
-
if (parsed.capacity) {
|
|
2918
|
-
sentences.push(
|
|
2919
|
-
`${parsed.capacity} is what one container holds, so it stays as published and the count is what the factor moved. Scaling this line is a matter of opening that many containers of the size named.`
|
|
2920
|
-
);
|
|
2921
|
-
}
|
|
2922
|
-
if (collapsed) {
|
|
2923
|
-
sentences.push("The page gave a range, and at this size both ends come to the same amount.");
|
|
2924
|
-
}
|
|
2925
|
-
if (unit?.kind === "measured" && low.amount > 0 && low.amount < 0.05) {
|
|
2926
|
-
sentences.push(
|
|
2927
|
-
"This is smaller than a kitchen scale resolves. Make a larger batch, or measure it by eye."
|
|
2928
|
-
);
|
|
2929
|
-
}
|
|
2930
|
-
if (parsed.approximation) {
|
|
2931
|
-
sentences.push(
|
|
2932
|
-
"The page gave this amount as an approximation, and the scaled figure is no firmer."
|
|
2933
|
-
);
|
|
2934
|
-
}
|
|
2935
|
-
if (sentences.length > 0) result.note = sentences.join(" ");
|
|
2936
|
-
if (parsed.unit && parsed.unit.kind === "approximate") {
|
|
2937
|
-
result.note = withApproximateNote(parsed.unit, result.note);
|
|
2938
|
-
}
|
|
2939
|
-
if (parsed.articleWord) {
|
|
2940
|
-
const stood = (parsed.amount ?? 0) / (parsed.countMultiplier ?? 1);
|
|
2941
|
-
const read = `"${parsed.articleWord}" read as ${formatAmount(stood, language)}.`;
|
|
2942
|
-
result.note = result.note ? `${read} ${result.note}` : read;
|
|
3552
|
+
const note = noteForScaledLine({
|
|
3553
|
+
parsed,
|
|
3554
|
+
bounds: primaryBounds,
|
|
3555
|
+
unit,
|
|
3556
|
+
low,
|
|
3557
|
+
clamped,
|
|
3558
|
+
movedPrimary,
|
|
3559
|
+
movedAlternate,
|
|
3560
|
+
restated,
|
|
3561
|
+
collapsed,
|
|
3562
|
+
language,
|
|
3563
|
+
carriesMore
|
|
3564
|
+
});
|
|
3565
|
+
if (note !== void 0) {
|
|
3566
|
+
result.note = note;
|
|
2943
3567
|
}
|
|
2944
3568
|
return result;
|
|
2945
3569
|
}
|
|
@@ -2971,8 +3595,10 @@ function renderMeasure(measure, factor, language) {
|
|
|
2971
3595
|
};
|
|
2972
3596
|
}
|
|
2973
3597
|
function renderRange(low, high, separator) {
|
|
2974
|
-
if (high === null || separator === null)
|
|
2975
|
-
|
|
3598
|
+
if (high === null || separator === null) {
|
|
3599
|
+
return low;
|
|
3600
|
+
}
|
|
3601
|
+
return DASH_ONLY.test(separator) ? `${low}${separator}${high}` : `${low} ${separator} ${high}`;
|
|
2976
3602
|
}
|
|
2977
3603
|
function scaleIngredients(lines, options) {
|
|
2978
3604
|
return lines.map((line) => scaleIngredient(line, options));
|
|
@@ -2989,9 +3615,11 @@ function passthroughIngredient(line, language = "auto") {
|
|
|
2989
3615
|
unit: held ? null : parsed.unit?.canonical ?? null,
|
|
2990
3616
|
language: parsed.language
|
|
2991
3617
|
};
|
|
2992
|
-
if (parsed.heldBack)
|
|
2993
|
-
|
|
2994
|
-
else if (parsed.
|
|
3618
|
+
if (parsed.heldBack) {
|
|
3619
|
+
result.note = HELD_BACK_NOTE[parsed.heldBack];
|
|
3620
|
+
} else if (parsed.amount === null) {
|
|
3621
|
+
result.note = "No quantity given; adjust to taste.";
|
|
3622
|
+
} else if (parsed.unit?.kind === "approximate") {
|
|
2995
3623
|
result.note = withApproximateNote(parsed.unit, void 0);
|
|
2996
3624
|
}
|
|
2997
3625
|
return result;
|
|
@@ -3006,7 +3634,9 @@ var HEADINGS = {
|
|
|
3006
3634
|
method: /^\s*(?:procedure|proc[ée]d[ée]|directions?|instructions?|method|preparation|pr[ée]paration|r[ée]alisation|steps?|[ée]tapes?)\b/i
|
|
3007
3635
|
};
|
|
3008
3636
|
function headingFor(part, headings) {
|
|
3009
|
-
if (headings === null)
|
|
3637
|
+
if (headings === null) {
|
|
3638
|
+
return null;
|
|
3639
|
+
}
|
|
3010
3640
|
return headings.find((heading) => HEADINGS[part].test(heading)) ?? null;
|
|
3011
3641
|
}
|
|
3012
3642
|
|
|
@@ -3138,7 +3768,12 @@ function noteTexts(notes) {
|
|
|
3138
3768
|
return notes.map(noteText);
|
|
3139
3769
|
}
|
|
3140
3770
|
function labelNote(label, note) {
|
|
3141
|
-
|
|
3771
|
+
if (typeof note === "string") {
|
|
3772
|
+
const plain = note;
|
|
3773
|
+
return `${label}: ${plain}`;
|
|
3774
|
+
}
|
|
3775
|
+
const said = note.text;
|
|
3776
|
+
return mustKeep(`${label}: ${said}`);
|
|
3142
3777
|
}
|
|
3143
3778
|
function reportNotes(reports) {
|
|
3144
3779
|
const notes = [];
|
|
@@ -3179,7 +3814,7 @@ function reportNotes(reports) {
|
|
|
3179
3814
|
);
|
|
3180
3815
|
}
|
|
3181
3816
|
if (report.reportedTotal !== null && report.reportedTotalMeans !== null) {
|
|
3182
|
-
const forWording = sent.length > 1 ? ` That number belongs to "${sent[0]
|
|
3817
|
+
const forWording = sent.length > 1 ? ` That number belongs to "${sent[0]?.query}" alone.` : "";
|
|
3183
3818
|
notes.push(
|
|
3184
3819
|
`${report.name} reported ${report.reportedTotal}: ${report.reportedTotalMeans}.${forWording}`
|
|
3185
3820
|
);
|
|
@@ -3215,7 +3850,9 @@ function buildTrailer(options) {
|
|
|
3215
3850
|
const held = /* @__PURE__ */ new Set();
|
|
3216
3851
|
for (const note of options.notes ?? []) {
|
|
3217
3852
|
const text2 = noteText(note);
|
|
3218
|
-
if (held.has(text2))
|
|
3853
|
+
if (held.has(text2)) {
|
|
3854
|
+
continue;
|
|
3855
|
+
}
|
|
3219
3856
|
held.add(text2);
|
|
3220
3857
|
kept.push(note);
|
|
3221
3858
|
}
|
|
@@ -3228,7 +3865,9 @@ function buildTrailer(options) {
|
|
|
3228
3865
|
break;
|
|
3229
3866
|
}
|
|
3230
3867
|
}
|
|
3231
|
-
if (victim === -1)
|
|
3868
|
+
if (victim === -1) {
|
|
3869
|
+
break;
|
|
3870
|
+
}
|
|
3232
3871
|
kept.splice(victim, 1);
|
|
3233
3872
|
}
|
|
3234
3873
|
const credit = options.credit ?? "Source: this server called none.";
|
|
@@ -3250,7 +3889,9 @@ ${trailer}`;
|
|
|
3250
3889
|
function toToolError(error) {
|
|
3251
3890
|
const known = error instanceof RecipesError ? error : new RecipesError("network_error", error instanceof Error ? error.message : String(error));
|
|
3252
3891
|
const lines = [`[${known.code}] ${quoteForeign(known.message)}`];
|
|
3253
|
-
if (known.details.hint)
|
|
3892
|
+
if (known.details.hint) {
|
|
3893
|
+
lines.push(`Hint: ${quoteForeign(known.details.hint)}`);
|
|
3894
|
+
}
|
|
3254
3895
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
|
|
3255
3896
|
}
|
|
3256
3897
|
function roomForBody(options = {}) {
|
|
@@ -3261,7 +3902,9 @@ function fitLines(lines, room) {
|
|
|
3261
3902
|
const kept = [];
|
|
3262
3903
|
let used = 0;
|
|
3263
3904
|
for (const line of lines) {
|
|
3264
|
-
if (kept.length >= ALWAYS_SHOWN && used + line.length > room)
|
|
3905
|
+
if (kept.length >= ALWAYS_SHOWN && used + line.length > room) {
|
|
3906
|
+
break;
|
|
3907
|
+
}
|
|
3265
3908
|
kept.push(line);
|
|
3266
3909
|
used += line.length + 1;
|
|
3267
3910
|
}
|
|
@@ -3271,7 +3914,9 @@ function omittedLinesLine(shown, total, what) {
|
|
|
3271
3914
|
return `\u2026 ${shown} of ${total} ${what} shown here; all ${total} are in the structured output`;
|
|
3272
3915
|
}
|
|
3273
3916
|
function truncate(text2, maxChars) {
|
|
3274
|
-
if (text2.length <= maxChars)
|
|
3917
|
+
if (text2.length <= maxChars) {
|
|
3918
|
+
return text2;
|
|
3919
|
+
}
|
|
3275
3920
|
return `${text2.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
|
|
3276
3921
|
}
|
|
3277
3922
|
function renderRows(rows) {
|
|
@@ -3282,7 +3927,9 @@ function renderRows(rows) {
|
|
|
3282
3927
|
}).join("\n");
|
|
3283
3928
|
}
|
|
3284
3929
|
function creditLine(contributors) {
|
|
3285
|
-
if (contributors.length === 0)
|
|
3930
|
+
if (contributors.length === 0) {
|
|
3931
|
+
return "No source contributed to this answer.";
|
|
3932
|
+
}
|
|
3286
3933
|
const names2 = contributors.map(
|
|
3287
3934
|
(entry) => entry.url ? `${entry.attribution} \u2014 ${quoteForeign(entry.url)}` : entry.attribution
|
|
3288
3935
|
);
|
|
@@ -3290,6 +3937,13 @@ function creditLine(contributors) {
|
|
|
3290
3937
|
}
|
|
3291
3938
|
|
|
3292
3939
|
// src/tools/recipeView.ts
|
|
3940
|
+
var SERVING_WORD = /personne|people|serving|portion|part\b/i;
|
|
3941
|
+
function whatTheOtherPartHolds(part, recipe) {
|
|
3942
|
+
if (part === "ingredients") {
|
|
3943
|
+
return recipe.steps.length > 0 ? `${recipe.steps.length} step(s) of method were read from it` : null;
|
|
3944
|
+
}
|
|
3945
|
+
return recipe.ingredients.length > 0 ? `${recipe.ingredients.length} ingredient line(s) were read from it` : null;
|
|
3946
|
+
}
|
|
3293
3947
|
var yieldSchema = z2.object({
|
|
3294
3948
|
original_count: z2.number().nullable().describe("Servings the page states as a number. Null when it states none."),
|
|
3295
3949
|
original_max: z2.number().nullable().describe("The upper end when the page states a span, as in '4 \xE0 6 personnes'."),
|
|
@@ -3346,7 +4000,9 @@ var SECTIONS = [
|
|
|
3346
4000
|
"equipment"
|
|
3347
4001
|
];
|
|
3348
4002
|
function resolveFactor(recipe, servings) {
|
|
3349
|
-
if (servings === null)
|
|
4003
|
+
if (servings === null) {
|
|
4004
|
+
return { factor: null, notes: [] };
|
|
4005
|
+
}
|
|
3350
4006
|
if (recipe.yieldCount === null || recipe.yieldCount <= 0) {
|
|
3351
4007
|
return {
|
|
3352
4008
|
factor: null,
|
|
@@ -3371,7 +4027,7 @@ function resolveFactor(recipe, servings) {
|
|
|
3371
4027
|
);
|
|
3372
4028
|
}
|
|
3373
4029
|
const unit = recipe.yieldUnit;
|
|
3374
|
-
if (unit &&
|
|
4030
|
+
if (unit && !SERVING_WORD.test(unit)) {
|
|
3375
4031
|
notes.push(
|
|
3376
4032
|
`This page states its yield in ${quoteForeign(unit)} rather than in servings, so asking for ${servings} multiplied the quantities by ${round(factor)} to give ${servings} ${quoteForeign(unit)}.`
|
|
3377
4033
|
);
|
|
@@ -3395,7 +4051,7 @@ var METHOD_PART = {
|
|
|
3395
4051
|
};
|
|
3396
4052
|
function emptyPartNote(recipe, words) {
|
|
3397
4053
|
const heading = headingFor(words.part, recipe.publishedSections);
|
|
3398
|
-
const counterpart = words.part
|
|
4054
|
+
const counterpart = whatTheOtherPartHolds(words.part, recipe);
|
|
3399
4055
|
const shown = [
|
|
3400
4056
|
heading === null ? null : `the page heads a section "${quoteForeign(heading)}"`,
|
|
3401
4057
|
counterpart
|
|
@@ -3498,18 +4154,33 @@ function buildRecipeView(recipe, options) {
|
|
|
3498
4154
|
}
|
|
3499
4155
|
function renderYield(payload) {
|
|
3500
4156
|
const published = payload.yield.original_text ?? "an amount the page does not state";
|
|
3501
|
-
if (payload.yield.factor === null)
|
|
4157
|
+
if (payload.yield.factor === null) {
|
|
4158
|
+
return `Yields ${quoteForeign(published)} (as published).`;
|
|
4159
|
+
}
|
|
3502
4160
|
return `Yields ${quoteForeign(published)} as published, scaled by ${payload.yield.factor} for ${payload.yield.requested}.`;
|
|
3503
4161
|
}
|
|
3504
4162
|
|
|
3505
4163
|
// src/tools/arguments.ts
|
|
3506
4164
|
import { z as z3 } from "zod";
|
|
3507
4165
|
var INVALID_INPUT = "invalid_input";
|
|
4166
|
+
var defaultMessage = z3.core.locales.en().localeError;
|
|
3508
4167
|
function strictInput(shape) {
|
|
3509
4168
|
const declared = Object.keys(shape);
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
4169
|
+
const refuse = (issue) => issue.code === "unrecognized_keys" ? unknownArgumentMessage(issue.keys, declared) : `[${INVALID_INPUT}] ${defaultMessage(issue)}`;
|
|
4170
|
+
for (const argument of Object.values(shape)) {
|
|
4171
|
+
carryTheCode(argument, refuse);
|
|
4172
|
+
}
|
|
4173
|
+
return z3.strictObject(shape, { error: refuse });
|
|
4174
|
+
}
|
|
4175
|
+
function carryTheCode(schema, refuse) {
|
|
4176
|
+
const definition = schema._zod.def;
|
|
4177
|
+
definition.error = refuse;
|
|
4178
|
+
for (const check of definition.checks ?? []) {
|
|
4179
|
+
check._zod.def.error = refuse;
|
|
4180
|
+
}
|
|
4181
|
+
if (definition.innerType) {
|
|
4182
|
+
carryTheCode(definition.innerType, refuse);
|
|
4183
|
+
}
|
|
3513
4184
|
}
|
|
3514
4185
|
function unknownArgumentMessage(keys, declared) {
|
|
3515
4186
|
const named = keys.map((key) => {
|
|
@@ -3521,15 +4192,21 @@ function unknownArgumentMessage(keys, declared) {
|
|
|
3521
4192
|
function nearestArgument(key, declared) {
|
|
3522
4193
|
const flatten = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3523
4194
|
const flat = flatten(key);
|
|
3524
|
-
if (flat.length === 0)
|
|
4195
|
+
if (flat.length === 0) {
|
|
4196
|
+
return void 0;
|
|
4197
|
+
}
|
|
3525
4198
|
const sameName = declared.find((name) => flatten(name) === flat);
|
|
3526
|
-
if (sameName)
|
|
4199
|
+
if (sameName) {
|
|
4200
|
+
return sameName;
|
|
4201
|
+
}
|
|
3527
4202
|
const overlapping = declared.find((name) => {
|
|
3528
4203
|
const other = flatten(name);
|
|
3529
4204
|
const [shorter, longer] = other.length < flat.length ? [other, flat] : [flat, other];
|
|
3530
4205
|
return shorter.length >= 3 && (longer.startsWith(shorter) || longer.endsWith(shorter));
|
|
3531
4206
|
});
|
|
3532
|
-
if (overlapping)
|
|
4207
|
+
if (overlapping) {
|
|
4208
|
+
return overlapping;
|
|
4209
|
+
}
|
|
3533
4210
|
let closest;
|
|
3534
4211
|
let shortest = Number.POSITIVE_INFINITY;
|
|
3535
4212
|
for (const name of declared) {
|
|
@@ -3543,18 +4220,34 @@ function nearestArgument(key, declared) {
|
|
|
3543
4220
|
}
|
|
3544
4221
|
function editDistance(left, right) {
|
|
3545
4222
|
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
4223
|
+
let distance = right.length;
|
|
3546
4224
|
for (let row = 1; row <= left.length; row += 1) {
|
|
3547
4225
|
const current = [row];
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
4226
|
+
let diagonal = row - 1;
|
|
4227
|
+
let leftward = row;
|
|
4228
|
+
for (const [column, above] of previous.entries()) {
|
|
4229
|
+
if (column === 0) {
|
|
4230
|
+
continue;
|
|
4231
|
+
}
|
|
4232
|
+
const substitution = diagonal + (left[row - 1] === right[column - 1] ? 0 : 1);
|
|
4233
|
+
const cell = Math.min(substitution, above + 1, leftward + 1);
|
|
4234
|
+
current.push(cell);
|
|
4235
|
+
diagonal = above;
|
|
4236
|
+
leftward = cell;
|
|
3551
4237
|
}
|
|
3552
4238
|
previous = current;
|
|
4239
|
+
distance = leftward;
|
|
3553
4240
|
}
|
|
3554
|
-
return
|
|
4241
|
+
return distance;
|
|
3555
4242
|
}
|
|
3556
4243
|
|
|
3557
4244
|
// src/tools/compareRecipes.ts
|
|
4245
|
+
function nothingWasCompared(someWereUnread, dish) {
|
|
4246
|
+
if (someWereUnread) {
|
|
4247
|
+
return `Every version of "${quoteForeign(dish)}" that was offered could not be read, so nothing was compared.`;
|
|
4248
|
+
}
|
|
4249
|
+
return `No source offered a recipe for "${quoteForeign(dish)}".`;
|
|
4250
|
+
}
|
|
3558
4251
|
var compareRecipesDescription = [
|
|
3559
4252
|
"Take a dish and show how each source writes it, side by side.",
|
|
3560
4253
|
"Each source's closest match is read in full, and all of them can be rescaled to the same number of servings so the ingredient lists stand comparison.",
|
|
@@ -3579,17 +4272,54 @@ var compareRecipesOutput = z4.object({
|
|
|
3579
4272
|
var TEXT_INGREDIENT_LINES = 8;
|
|
3580
4273
|
var TEXT_DIFFERENCE_LINES = 4;
|
|
3581
4274
|
function listNames(names2) {
|
|
3582
|
-
if (names2.length <= 1)
|
|
3583
|
-
|
|
4275
|
+
if (names2.length <= 1) {
|
|
4276
|
+
return names2.join("");
|
|
4277
|
+
}
|
|
4278
|
+
return `${names2.slice(0, -1).join(", ")} and ${names2.at(-1)}`;
|
|
4279
|
+
}
|
|
4280
|
+
function weighTitlesAgainstTheDish(payloads, dish, notes) {
|
|
4281
|
+
const weighed = payloads.map((payload) => ({
|
|
4282
|
+
payload,
|
|
4283
|
+
missing: dishWordsMissing(payload.title, dish)
|
|
4284
|
+
}));
|
|
4285
|
+
const carriesDish = weighed.some((one) => one.missing.length === 0);
|
|
4286
|
+
for (const { payload, missing } of weighed) {
|
|
4287
|
+
if (missing.length === 0) {
|
|
4288
|
+
continue;
|
|
4289
|
+
}
|
|
4290
|
+
notes.push(
|
|
4291
|
+
mustKeep(
|
|
4292
|
+
`${payload.source_name}'s closest row is ${quoteForeign(payload.title)}, whose title carries no ${missing.map((word) => `"${quoteForeign(word)}"`).join(" and no ")}. Read it as a candidate to check rather than as that dish.`
|
|
4293
|
+
)
|
|
4294
|
+
);
|
|
4295
|
+
}
|
|
4296
|
+
if (payloads.length > 0 && !carriesDish) {
|
|
4297
|
+
notes.push(
|
|
4298
|
+
`No version here carries the whole of "${quoteForeign(dish)}" in its title. These are the closest rows each source returned for that spelling, so read them as candidates to check rather than as that dish.`
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
if (payloads.length === 1 && carriesDish) {
|
|
4302
|
+
notes.push(
|
|
4303
|
+
mustKeep(
|
|
4304
|
+
"This is one version rather than a comparison. Read it as what that one source publishes."
|
|
4305
|
+
)
|
|
4306
|
+
);
|
|
4307
|
+
}
|
|
4308
|
+
return weighed;
|
|
3584
4309
|
}
|
|
3585
4310
|
function describeDifferences(recipes, payloads, sections) {
|
|
3586
|
-
if (recipes.length < 2)
|
|
4311
|
+
if (recipes.length < 2) {
|
|
4312
|
+
return [];
|
|
4313
|
+
}
|
|
3587
4314
|
const differences = [];
|
|
3588
4315
|
const nameOf = (recipe) => recipe.sourceName;
|
|
3589
|
-
const
|
|
3590
|
-
|
|
4316
|
+
const stated = payloads.map((payload) => ({
|
|
4317
|
+
name: payload.source_name,
|
|
4318
|
+
text: payload.yield.original_text ?? "no stated amount"
|
|
4319
|
+
}));
|
|
4320
|
+
if (new Set(stated.map((one) => one.text)).size > 1) {
|
|
3591
4321
|
differences.push(
|
|
3592
|
-
`${
|
|
4322
|
+
`${stated.map((one) => `${one.name} yields ${quoteForeign(one.text)}`).join("; ")}.`
|
|
3593
4323
|
);
|
|
3594
4324
|
}
|
|
3595
4325
|
const counts = payloads.map((payload) => payload.ingredients.length);
|
|
@@ -3626,14 +4356,17 @@ function describeDifferences(recipes, payloads, sections) {
|
|
|
3626
4356
|
`Only ${listNames(credited.map(nameOf))} credits an author; the rest are written by whoever edited the page.`
|
|
3627
4357
|
);
|
|
3628
4358
|
}
|
|
3629
|
-
const
|
|
3630
|
-
|
|
4359
|
+
for (const recipe of recipes) {
|
|
4360
|
+
const license = recipe.license;
|
|
4361
|
+
if (license === null) {
|
|
4362
|
+
continue;
|
|
4363
|
+
}
|
|
3631
4364
|
differences.push(
|
|
3632
|
-
`${nameOf(recipe)} publishes under ${quoteForeign(
|
|
4365
|
+
`${nameOf(recipe)} publishes under ${quoteForeign(license.title)}, which asks for attribution.`
|
|
3633
4366
|
);
|
|
3634
4367
|
}
|
|
3635
4368
|
const silent = recipes.filter((recipe) => recipe.license === null);
|
|
3636
|
-
if (silent.length > 0 &&
|
|
4369
|
+
if (silent.length > 0 && silent.length < recipes.length) {
|
|
3637
4370
|
differences.push(
|
|
3638
4371
|
`${listNames(silent.map(nameOf))} states no terms of use. Silence is not permission.`
|
|
3639
4372
|
);
|
|
@@ -3645,7 +4378,9 @@ async function runCompareRecipes(client, args) {
|
|
|
3645
4378
|
const merged = await client.searchRecipes(args.dish, 3);
|
|
3646
4379
|
const best = /* @__PURE__ */ new Map();
|
|
3647
4380
|
for (const row of merged.rows) {
|
|
3648
|
-
if (!best.has(row.source))
|
|
4381
|
+
if (!best.has(row.source)) {
|
|
4382
|
+
best.set(row.source, row.id);
|
|
4383
|
+
}
|
|
3649
4384
|
}
|
|
3650
4385
|
const offered = [...best.entries()];
|
|
3651
4386
|
const reads = await Promise.allSettled(offered.map(([, id]) => client.getRecipe(id)));
|
|
@@ -3653,7 +4388,10 @@ async function runCompareRecipes(client, args) {
|
|
|
3653
4388
|
const unread = /* @__PURE__ */ new Map();
|
|
3654
4389
|
const opened = /* @__PURE__ */ new Set();
|
|
3655
4390
|
reads.forEach((read, index) => {
|
|
3656
|
-
const source = offered[index][0];
|
|
4391
|
+
const source = offered[index]?.[0];
|
|
4392
|
+
if (source === void 0) {
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
3657
4395
|
opened.add(source);
|
|
3658
4396
|
if (read.status === "fulfilled") {
|
|
3659
4397
|
recipes.push(read.value.recipe);
|
|
@@ -3679,7 +4417,9 @@ async function runCompareRecipes(client, args) {
|
|
|
3679
4417
|
);
|
|
3680
4418
|
const payloads = views.map((view) => view.payload);
|
|
3681
4419
|
const notes = reportNotes(merged.reports);
|
|
3682
|
-
for (const view of views)
|
|
4420
|
+
for (const view of views) {
|
|
4421
|
+
notes.push(...view.notes);
|
|
4422
|
+
}
|
|
3683
4423
|
const omitted = SECTIONS.filter((section) => !args.sections.includes(section));
|
|
3684
4424
|
if (omitted.length > 0) {
|
|
3685
4425
|
notes.push(
|
|
@@ -3687,7 +4427,9 @@ async function runCompareRecipes(client, args) {
|
|
|
3687
4427
|
);
|
|
3688
4428
|
}
|
|
3689
4429
|
for (const report of merged.reports) {
|
|
3690
|
-
if (recipes.some((recipe) => recipe.source === report.source))
|
|
4430
|
+
if (recipes.some((recipe) => recipe.source === report.source)) {
|
|
4431
|
+
continue;
|
|
4432
|
+
}
|
|
3691
4433
|
const failedRead = unread.get(report.source);
|
|
3692
4434
|
if (report.status === "failed") {
|
|
3693
4435
|
notes.push(
|
|
@@ -3713,32 +4455,11 @@ async function runCompareRecipes(client, args) {
|
|
|
3713
4455
|
);
|
|
3714
4456
|
}
|
|
3715
4457
|
}
|
|
3716
|
-
const
|
|
3717
|
-
const named = payloads.filter((_payload, index) => shortfall[index].length === 0);
|
|
3718
|
-
const carriesDish = named.length > 0;
|
|
3719
|
-
payloads.forEach((payload, index) => {
|
|
3720
|
-
const missing = shortfall[index];
|
|
3721
|
-
if (missing.length === 0) return;
|
|
3722
|
-
notes.push(
|
|
3723
|
-
mustKeep(
|
|
3724
|
-
`${payload.source_name}'s closest row is ${quoteForeign(payload.title)}, whose title carries no ${missing.map((word) => `"${quoteForeign(word)}"`).join(" and no ")}. Read it as a candidate to check rather than as that dish.`
|
|
3725
|
-
)
|
|
3726
|
-
);
|
|
3727
|
-
});
|
|
3728
|
-
if (payloads.length > 0 && !carriesDish) {
|
|
3729
|
-
notes.push(
|
|
3730
|
-
`No version here carries the whole of "${quoteForeign(args.dish)}" in its title. These are the closest rows each source returned for that spelling, so read them as candidates to check rather than as that dish.`
|
|
3731
|
-
);
|
|
3732
|
-
}
|
|
3733
|
-
if (payloads.length === 1 && carriesDish) {
|
|
3734
|
-
notes.push(
|
|
3735
|
-
mustKeep(
|
|
3736
|
-
"This is one version rather than a comparison. Read it as what that one source publishes."
|
|
3737
|
-
)
|
|
3738
|
-
);
|
|
3739
|
-
}
|
|
4458
|
+
const weighed = weighTitlesAgainstTheDish(payloads, args.dish, notes);
|
|
3740
4459
|
const comparable = recipes.filter(
|
|
3741
|
-
(recipe) =>
|
|
4460
|
+
(recipe) => weighed.some(
|
|
4461
|
+
({ payload, missing }) => missing.length === 0 && payload.id === recipe.id && payload.source === recipe.source
|
|
4462
|
+
)
|
|
3742
4463
|
);
|
|
3743
4464
|
const differences = comparable.length === payloads.length ? describeDifferences(recipes, payloads, args.sections) : [];
|
|
3744
4465
|
if (payloads.length > 1 && differences.length === 0 && comparable.length < payloads.length) {
|
|
@@ -3773,8 +4494,8 @@ ${shownDifferences.map((line) => `- ${line}`).join("\n")}${moreDifferences > 0 ?
|
|
|
3773
4494
|
// for that many people.
|
|
3774
4495
|
` ${renderYield(payload)}`
|
|
3775
4496
|
];
|
|
3776
|
-
const
|
|
3777
|
-
const fitted = fitLines(
|
|
4497
|
+
const listed = payload.ingredients.slice(0, TEXT_INGREDIENT_LINES).map((entry) => ` - ${quoteForeign(entry.text)}`);
|
|
4498
|
+
const fitted = fitLines(listed, share - head.join("\n").length);
|
|
3778
4499
|
const lines = [...head, ...fitted.lines];
|
|
3779
4500
|
const hidden = payload.ingredients.length - fitted.lines.length;
|
|
3780
4501
|
if (hidden > 0) {
|
|
@@ -3784,7 +4505,7 @@ ${shownDifferences.map((line) => `- ${line}`).join("\n")}${moreDifferences > 0 ?
|
|
|
3784
4505
|
}
|
|
3785
4506
|
return lines.join("\n");
|
|
3786
4507
|
});
|
|
3787
|
-
const body = payloads.length === 0 ? unread.size > 0
|
|
4508
|
+
const body = payloads.length === 0 ? nothingWasCompared(unread.size > 0, args.dish) : versionBlocks.join("\n\n");
|
|
3788
4509
|
const summary = `${differencesBlock}${body}`;
|
|
3789
4510
|
return ok(
|
|
3790
4511
|
{
|
|
@@ -3795,7 +4516,9 @@ ${shownDifferences.map((line) => `- ${line}`).join("\n")}${moreDifferences > 0 ?
|
|
|
3795
4516
|
// what became of the row it offered.
|
|
3796
4517
|
per_source: merged.reports.map((report) => {
|
|
3797
4518
|
const payload = toReportPayload(report);
|
|
3798
|
-
if (!opened.has(report.source))
|
|
4519
|
+
if (!opened.has(report.source)) {
|
|
4520
|
+
return payload;
|
|
4521
|
+
}
|
|
3799
4522
|
const failedRead = unread.get(report.source);
|
|
3800
4523
|
return withRead(
|
|
3801
4524
|
payload,
|
|
@@ -3850,7 +4573,9 @@ async function runGetRecipe(client, args) {
|
|
|
3850
4573
|
`"${quoteForeign(args.id)}" was read as ${read.source.name}'s, because ${read.inferred}. Spell an id with its source to leave nothing to infer.`
|
|
3851
4574
|
);
|
|
3852
4575
|
}
|
|
3853
|
-
if (cached)
|
|
4576
|
+
if (cached) {
|
|
4577
|
+
notes.push("Served from this server's short-lived in-memory cache.");
|
|
4578
|
+
}
|
|
3854
4579
|
const payload = view.payload;
|
|
3855
4580
|
const credit = creditLine([{ attribution: payload.attribution, url: payload.url }]);
|
|
3856
4581
|
const head = [
|
|
@@ -3931,8 +4656,13 @@ function resolveFactor2(args) {
|
|
|
3931
4656
|
`factor=${args.factor} and ${args.from_servings}\u2192${args.to_servings} would be two different multipliers.`
|
|
3932
4657
|
);
|
|
3933
4658
|
}
|
|
3934
|
-
if (args.factor !== void 0)
|
|
3935
|
-
|
|
4659
|
+
if (args.factor !== void 0) {
|
|
4660
|
+
return args.factor;
|
|
4661
|
+
}
|
|
4662
|
+
const { from_servings: from, to_servings: to } = args;
|
|
4663
|
+
if (from !== void 0 && to !== void 0) {
|
|
4664
|
+
return to / from;
|
|
4665
|
+
}
|
|
3936
4666
|
if (args.from_servings !== void 0 || args.to_servings !== void 0) {
|
|
3937
4667
|
throw invalidInput(
|
|
3938
4668
|
"Serving counts come in pairs: give both 'from_servings' and 'to_servings'.",
|
|
@@ -4004,6 +4734,22 @@ ${body}`,
|
|
|
4004
4734
|
|
|
4005
4735
|
// src/tools/searchRecipes.ts
|
|
4006
4736
|
import { z as z7 } from "zod";
|
|
4737
|
+
var WHITESPACE2 = /\s+/;
|
|
4738
|
+
function howTheRowsAreOrdered(contributed) {
|
|
4739
|
+
if (contributed.length > 1) {
|
|
4740
|
+
return "One row from each source in turn, in the order each source returned them. No score orders them against each other.";
|
|
4741
|
+
}
|
|
4742
|
+
if (contributed.length === 1) {
|
|
4743
|
+
return `Every row came from ${contributed[0]?.name}, in the order it returned them.`;
|
|
4744
|
+
}
|
|
4745
|
+
return "No source contributed a row.";
|
|
4746
|
+
}
|
|
4747
|
+
function nothingCameBack(answeredCount, query, alsoSent) {
|
|
4748
|
+
if (answeredCount === 0) {
|
|
4749
|
+
return `No source answered for "${query}", so nothing here says whether such a recipe exists.`;
|
|
4750
|
+
}
|
|
4751
|
+
return `Nothing came back for "${query}".${alsoSent}`;
|
|
4752
|
+
}
|
|
4007
4753
|
var searchRecipesDescription = [
|
|
4008
4754
|
"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.",
|
|
4009
4755
|
"Each row carries the id get_recipe takes, and that id names the source it came from, so nothing has to be guessed afterwards.",
|
|
@@ -4046,7 +4792,7 @@ async function runSearchRecipes(client, args) {
|
|
|
4046
4792
|
const notes = reportNotes(merged.reports);
|
|
4047
4793
|
const answered = merged.reports.filter((report) => report.status === "answered");
|
|
4048
4794
|
const contributed = merged.reports.filter((report) => report.count > 0);
|
|
4049
|
-
const order = contributed
|
|
4795
|
+
const order = howTheRowsAreOrdered(contributed);
|
|
4050
4796
|
const conditions = readConditions(args.query);
|
|
4051
4797
|
const named = (kind) => conditions.conditions.filter((condition) => condition.kind === kind && condition.named !== null).map((condition) => `"${quoteForeign(condition.named)}"`).join(" and ");
|
|
4052
4798
|
const unread = (kind) => conditions.conditions.some(
|
|
@@ -4097,7 +4843,7 @@ async function runSearchRecipes(client, args) {
|
|
|
4097
4843
|
)
|
|
4098
4844
|
);
|
|
4099
4845
|
}
|
|
4100
|
-
if (args.query.trim().split(
|
|
4846
|
+
if (args.query.trim().split(WHITESPACE2).length > WORDS_BEFORE_SAYING_SO) {
|
|
4101
4847
|
notes.push(
|
|
4102
4848
|
mustKeep(
|
|
4103
4849
|
"This query went to each source as free text and no filter was applied to it. Anything the question said the recipe must not contain went out as words like the rest of it, so read the rows rather than assuming they meet it."
|
|
@@ -4124,7 +4870,7 @@ async function runSearchRecipes(client, args) {
|
|
|
4124
4870
|
const alsoSent = derived.length > 0 ? `
|
|
4125
4871
|
Also searched for: ${derived.map((wording) => `"${quoteForeign(wording)}"`).join(", ")}.` : "";
|
|
4126
4872
|
const body = results.length > 0 ? `${results.length} recipes for "${args.query}":
|
|
4127
|
-
${renderRows(results)}${alsoSent}` : answered.length
|
|
4873
|
+
${renderRows(results)}${alsoSent}` : nothingCameBack(answered.length, args.query, alsoSent);
|
|
4128
4874
|
return ok(
|
|
4129
4875
|
{
|
|
4130
4876
|
query: args.query,
|
|
@@ -4241,15 +4987,15 @@ async function main() {
|
|
|
4241
4987
|
await server.connect(transport);
|
|
4242
4988
|
const shutdown = async () => {
|
|
4243
4989
|
await server.close().catch(() => void 0);
|
|
4244
|
-
|
|
4990
|
+
process2.exit(0);
|
|
4245
4991
|
};
|
|
4246
|
-
|
|
4247
|
-
|
|
4992
|
+
process2.on("SIGINT", shutdown);
|
|
4993
|
+
process2.on("SIGTERM", shutdown);
|
|
4248
4994
|
}
|
|
4249
4995
|
main().catch((error) => {
|
|
4250
4996
|
const message = error instanceof Error ? error.message : String(error);
|
|
4251
|
-
|
|
4997
|
+
process2.stderr.write(`[mcp-recipes] fatal: ${message}
|
|
4252
4998
|
`);
|
|
4253
|
-
|
|
4999
|
+
process2.exit(1);
|
|
4254
5000
|
});
|
|
4255
5001
|
//# sourceMappingURL=index.js.map
|