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/sources/client.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import process from "process";
|
|
3
|
+
|
|
1
4
|
// src/version.ts
|
|
2
|
-
var PKG_VERSION = "
|
|
5
|
+
var PKG_VERSION = "2.0.0";
|
|
3
6
|
var REPO_URL = "https://github.com/smeet666/mcp-recipes";
|
|
4
7
|
var ISSUES_URL = "https://github.com/smeet666/mcp-recipes/issues";
|
|
5
8
|
|
|
@@ -12,7 +15,9 @@ var DEFAULT_USER_AGENT = `mcp-recipes/${PKG_VERSION} (+${REPO_URL})`;
|
|
|
12
15
|
function createLogger(level) {
|
|
13
16
|
const rank = LOG_LEVELS.indexOf(level);
|
|
14
17
|
const write = (at, message) => {
|
|
15
|
-
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank)
|
|
18
|
+
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
16
21
|
process.stderr.write(`[mcp-recipes] ${at}: ${message}
|
|
17
22
|
`);
|
|
18
23
|
};
|
|
@@ -27,9 +32,11 @@ function createLogger(level) {
|
|
|
27
32
|
}
|
|
28
33
|
function readInteger(env, name, fallback, min, max) {
|
|
29
34
|
const raw = env[name];
|
|
30
|
-
if (raw === void 0 || raw.trim() === "")
|
|
35
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
31
38
|
const value = Number(raw);
|
|
32
|
-
if (!Number.isFinite(value)
|
|
39
|
+
if (!(Number.isFinite(value) && Number.isInteger(value))) {
|
|
33
40
|
process.stderr.write(
|
|
34
41
|
`[mcp-recipes] error: ${name}="${raw}" is not a whole number; using ${fallback}.
|
|
35
42
|
`
|
|
@@ -88,14 +95,14 @@ var RecipesError = class extends Error {
|
|
|
88
95
|
code;
|
|
89
96
|
details;
|
|
90
97
|
constructor(code, message, details = {}) {
|
|
91
|
-
super(message);
|
|
98
|
+
super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
|
|
92
99
|
this.name = "RecipesError";
|
|
93
100
|
this.code = code;
|
|
94
101
|
this.details = details;
|
|
95
102
|
}
|
|
96
103
|
};
|
|
97
104
|
var notFound = (message, details) => new RecipesError("not_found", message, details ?? {});
|
|
98
|
-
var invalidInput = (message, hint) => new RecipesError("invalid_input", message, hint ? { hint } : {});
|
|
105
|
+
var invalidInput = (message, hint, cause) => new RecipesError("invalid_input", message, { ...hint ? { hint } : {}, cause });
|
|
99
106
|
var rateLimited = (message, details) => new RecipesError("rate_limited", message, {
|
|
100
107
|
hint: "Wait a moment and ask again. This says nothing about whether the recipe exists.",
|
|
101
108
|
...details
|
|
@@ -107,7 +114,9 @@ var parseFailure = (message, details) => new RecipesError("parse_failure", messa
|
|
|
107
114
|
var networkError = (message, details) => new RecipesError("network_error", message, details ?? {});
|
|
108
115
|
var timeout = (message, details) => new RecipesError("timeout", message, details ?? {});
|
|
109
116
|
function toRecipesError(error) {
|
|
110
|
-
if (error instanceof RecipesError)
|
|
117
|
+
if (error instanceof RecipesError) {
|
|
118
|
+
return error;
|
|
119
|
+
}
|
|
111
120
|
const message = error instanceof Error ? error.message : String(error);
|
|
112
121
|
const raw = error;
|
|
113
122
|
const code = typeof raw?.code === "string" ? raw.code : "";
|
|
@@ -116,14 +125,20 @@ function toRecipesError(error) {
|
|
|
116
125
|
return new RecipesError(known ?? "network_error", message, details);
|
|
117
126
|
}
|
|
118
127
|
function fromSource(error, sourceName) {
|
|
119
|
-
if (error instanceof RecipesError)
|
|
128
|
+
if (error instanceof RecipesError) {
|
|
129
|
+
return error;
|
|
130
|
+
}
|
|
120
131
|
const known = toRecipesError(error);
|
|
121
132
|
const reported = known.message.replace(/\s+/g, " ").trim();
|
|
122
133
|
const opening = known.code === "not_found" ? `${sourceName} holds nothing under this name.` : `${sourceName} could not be read.`;
|
|
123
134
|
const message = reported === "" ? opening : `${opening} It reports: "${reported}"`;
|
|
124
135
|
const details = {};
|
|
125
|
-
if (known.details.url !== void 0)
|
|
126
|
-
|
|
136
|
+
if (known.details.url !== void 0) {
|
|
137
|
+
details.url = known.details.url;
|
|
138
|
+
}
|
|
139
|
+
if (known.details.status !== void 0) {
|
|
140
|
+
details.status = known.details.status;
|
|
141
|
+
}
|
|
127
142
|
switch (known.code) {
|
|
128
143
|
case "not_found":
|
|
129
144
|
return notFound(message, details);
|
|
@@ -140,72 +155,6 @@ function fromSource(error, sourceName) {
|
|
|
140
155
|
}
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
// src/sources/adapter.ts
|
|
144
|
-
function text(value) {
|
|
145
|
-
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
146
|
-
}
|
|
147
|
-
function textList(value) {
|
|
148
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
149
|
-
}
|
|
150
|
-
function count(value) {
|
|
151
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
152
|
-
}
|
|
153
|
-
function required(value, field, source) {
|
|
154
|
-
const found = text(value);
|
|
155
|
-
if (found === null) {
|
|
156
|
-
throw parseFailure(
|
|
157
|
-
`${source.name} answered without a readable "${field}", so there is no recipe to return.`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
return found;
|
|
161
|
-
}
|
|
162
|
-
function rowsOf(value, source) {
|
|
163
|
-
if (!Array.isArray(value)) {
|
|
164
|
-
throw parseFailure(
|
|
165
|
-
`${source.name} answered in a shape this server could not read: the list of results was missing.`
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return value;
|
|
169
|
-
}
|
|
170
|
-
function namespacedId(source, reference) {
|
|
171
|
-
return `${source}:${reference}`;
|
|
172
|
-
}
|
|
173
|
-
function publishedFigures(panel) {
|
|
174
|
-
if (typeof panel !== "object" || panel === null) return null;
|
|
175
|
-
const kept = {};
|
|
176
|
-
for (const [key, value] of Object.entries(panel)) {
|
|
177
|
-
if (typeof value === "string" && value.trim() !== "") kept[key] = value.trim();
|
|
178
|
-
}
|
|
179
|
-
return Object.keys(kept).length > 0 ? kept : null;
|
|
180
|
-
}
|
|
181
|
-
function readYieldSpan(published) {
|
|
182
|
-
if (published === null) return { count: null, max: null, unit: null };
|
|
183
|
-
const span = /^\s*(\d+(?:[.,]\d+)?)\s*(?:à|a|to|-|–|—|ou|or)\s*(\d+(?:[.,]\d+)?)\s*(.*)$/i.exec(
|
|
184
|
-
published
|
|
185
|
-
);
|
|
186
|
-
if (span) {
|
|
187
|
-
const low = Number(span[1].replace(",", "."));
|
|
188
|
-
const high = Number(span[2].replace(",", "."));
|
|
189
|
-
if (Number.isFinite(low) && Number.isFinite(high) && high > low) {
|
|
190
|
-
return { count: low, max: high, unit: text(span[3]) };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const single = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/.exec(published);
|
|
194
|
-
if (single) {
|
|
195
|
-
const value = Number(single[1].replace(",", "."));
|
|
196
|
-
if (Number.isFinite(value)) return { count: value, max: null, unit: text(single[2]) };
|
|
197
|
-
}
|
|
198
|
-
return { count: null, max: null, unit: null };
|
|
199
|
-
}
|
|
200
|
-
function detailBase(source) {
|
|
201
|
-
return {
|
|
202
|
-
source: source.id,
|
|
203
|
-
sourceName: source.name,
|
|
204
|
-
language: source.language,
|
|
205
|
-
attribution: source.attribution
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
158
|
// src/sources/ids.ts
|
|
210
159
|
var SEPARATOR = ":";
|
|
211
160
|
function resolveId(rawId, sources) {
|
|
@@ -218,7 +167,9 @@ function resolveId(rawId, sources) {
|
|
|
218
167
|
}
|
|
219
168
|
for (const source of sources) {
|
|
220
169
|
const prefix = `${source.id}${SEPARATOR}`;
|
|
221
|
-
if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
170
|
+
if (!trimmed.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
222
173
|
const reference = trimmed.slice(prefix.length).trim();
|
|
223
174
|
if (reference === "") {
|
|
224
175
|
throw invalidInput(
|
|
@@ -252,8 +203,10 @@ function resolveId(rawId, sources) {
|
|
|
252
203
|
}
|
|
253
204
|
function names(sources) {
|
|
254
205
|
const all = sources.map((source) => source.name);
|
|
255
|
-
if (all.length <= 1)
|
|
256
|
-
|
|
206
|
+
if (all.length <= 1) {
|
|
207
|
+
return all.join("");
|
|
208
|
+
}
|
|
209
|
+
return `${all.slice(0, -1).join(", ")} and ${all.at(-1)}`;
|
|
257
210
|
}
|
|
258
211
|
function example(sources) {
|
|
259
212
|
const first = sources[0];
|
|
@@ -264,6 +217,82 @@ function example(sources) {
|
|
|
264
217
|
import { MarmitonClient } from "mcp-marmiton/client";
|
|
265
218
|
import { CookbookClient } from "mcp-wikibooks-cookbook/client";
|
|
266
219
|
|
|
220
|
+
// src/sources/adapter.ts
|
|
221
|
+
var NUMBER_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/;
|
|
222
|
+
var RANGE_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(?:à|a|to|-|–|—|ou|or)\s*(\d+(?:[.,]\d+)?)\s*(.*)$/i;
|
|
223
|
+
function text(value) {
|
|
224
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
225
|
+
}
|
|
226
|
+
function textList(value) {
|
|
227
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
228
|
+
}
|
|
229
|
+
function count(value) {
|
|
230
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
231
|
+
}
|
|
232
|
+
function required(value, field, source) {
|
|
233
|
+
const found = text(value);
|
|
234
|
+
if (found === null) {
|
|
235
|
+
throw parseFailure(
|
|
236
|
+
`${source.name} answered without a readable "${field}", so there is no recipe to return.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return found;
|
|
240
|
+
}
|
|
241
|
+
function rowsOf(value, source) {
|
|
242
|
+
if (!Array.isArray(value)) {
|
|
243
|
+
throw parseFailure(
|
|
244
|
+
`${source.name} answered in a shape this server could not read: the list of results was missing.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
function namespacedId(source, reference) {
|
|
250
|
+
return `${source}:${reference}`;
|
|
251
|
+
}
|
|
252
|
+
function publishedFigures(panel) {
|
|
253
|
+
if (typeof panel !== "object" || panel === null) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const kept = {};
|
|
257
|
+
for (const [key, value] of Object.entries(panel)) {
|
|
258
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
259
|
+
kept[key] = value.trim();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return Object.keys(kept).length > 0 ? kept : null;
|
|
263
|
+
}
|
|
264
|
+
function readYieldSpan(published) {
|
|
265
|
+
if (published === null) {
|
|
266
|
+
return { count: null, max: null, unit: null };
|
|
267
|
+
}
|
|
268
|
+
const span = RANGE_AND_REST.exec(published);
|
|
269
|
+
if (span) {
|
|
270
|
+
const [lowText = "", highText = ""] = span.slice(1);
|
|
271
|
+
const low = Number(lowText.replace(",", "."));
|
|
272
|
+
const high = Number(highText.replace(",", "."));
|
|
273
|
+
if (Number.isFinite(low) && Number.isFinite(high) && high > low) {
|
|
274
|
+
return { count: low, max: high, unit: text(span[3]) };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const single = NUMBER_AND_REST.exec(published);
|
|
278
|
+
if (single) {
|
|
279
|
+
const [figure = ""] = single.slice(1);
|
|
280
|
+
const value = Number(figure.replace(",", "."));
|
|
281
|
+
if (Number.isFinite(value)) {
|
|
282
|
+
return { count: value, max: null, unit: text(single[2]) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { count: null, max: null, unit: null };
|
|
286
|
+
}
|
|
287
|
+
function detailBase(source) {
|
|
288
|
+
return {
|
|
289
|
+
source: source.id,
|
|
290
|
+
sourceName: source.name,
|
|
291
|
+
language: source.language,
|
|
292
|
+
attribution: source.attribution
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
267
296
|
// src/sources/cookbook.ts
|
|
268
297
|
var COOKBOOK_PROFILE = {
|
|
269
298
|
id: "cookbook",
|
|
@@ -287,10 +316,11 @@ function cookbookAdapter(reader) {
|
|
|
287
316
|
let key;
|
|
288
317
|
try {
|
|
289
318
|
key = decodeURIComponent(raw.replace(SITE_URL, "")).replace(/_/g, " ");
|
|
290
|
-
} catch {
|
|
319
|
+
} catch (cause) {
|
|
291
320
|
throw invalidInput(
|
|
292
321
|
`"${raw}" carries a percent sign that is not a valid escape, so the page name cannot be read.`,
|
|
293
|
-
"Pass the id search_recipes returned instead of the address."
|
|
322
|
+
"Pass the id search_recipes returned instead of the address.",
|
|
323
|
+
cause
|
|
294
324
|
);
|
|
295
325
|
}
|
|
296
326
|
return {
|
|
@@ -320,7 +350,7 @@ function cookbookAdapter(reader) {
|
|
|
320
350
|
const key = text(summary?.key);
|
|
321
351
|
const title = text(summary?.title) ?? key;
|
|
322
352
|
const url = text(summary?.sourceUrl);
|
|
323
|
-
if (!key
|
|
353
|
+
if (!(key && title && url)) {
|
|
324
354
|
skipped += 1;
|
|
325
355
|
continue;
|
|
326
356
|
}
|
|
@@ -393,6 +423,7 @@ function cookbookDetail(payload) {
|
|
|
393
423
|
}
|
|
394
424
|
|
|
395
425
|
// src/sources/marmiton.ts
|
|
426
|
+
var ABSOLUTE_URL = /^https?:\/\//i;
|
|
396
427
|
var MARMITON_PROFILE = {
|
|
397
428
|
id: "marmiton",
|
|
398
429
|
name: "Marmiton",
|
|
@@ -409,7 +440,9 @@ async function readSearch(reader, query) {
|
|
|
409
440
|
return await reader.search(query);
|
|
410
441
|
} catch (error) {
|
|
411
442
|
const code = error?.code;
|
|
412
|
-
if (code === "not_found")
|
|
443
|
+
if (code === "not_found") {
|
|
444
|
+
return { data: [], cached: false };
|
|
445
|
+
}
|
|
413
446
|
throw error;
|
|
414
447
|
}
|
|
415
448
|
}
|
|
@@ -438,7 +471,7 @@ function marmitonAdapter(reader) {
|
|
|
438
471
|
const id = text(summary?.id);
|
|
439
472
|
const title = text(summary?.title);
|
|
440
473
|
const url = text(summary?.url);
|
|
441
|
-
if (!id
|
|
474
|
+
if (!(id && title && url)) {
|
|
442
475
|
skipped += 1;
|
|
443
476
|
continue;
|
|
444
477
|
}
|
|
@@ -466,7 +499,7 @@ function marmitonAdapter(reader) {
|
|
|
466
499
|
};
|
|
467
500
|
},
|
|
468
501
|
async getRecipe(reference) {
|
|
469
|
-
const ref =
|
|
502
|
+
const ref = ABSOLUTE_URL.test(reference) ? { url: reference } : { id: reference };
|
|
470
503
|
const outcome = await reader.getRecipe(ref);
|
|
471
504
|
return { recipe: marmitonDetail(outcome.data), cached: outcome.cached };
|
|
472
505
|
}
|
|
@@ -529,7 +562,9 @@ function buildSources(config, readers = {}) {
|
|
|
529
562
|
];
|
|
530
563
|
}
|
|
531
564
|
function selectSources(sources, wanted) {
|
|
532
|
-
if (!wanted)
|
|
565
|
+
if (!wanted) {
|
|
566
|
+
return sources;
|
|
567
|
+
}
|
|
533
568
|
const unknown = wanted.filter((id) => !sources.some((source) => source.id === id));
|
|
534
569
|
if (unknown.length > 0) {
|
|
535
570
|
throw invalidInput(
|
|
@@ -548,6 +583,8 @@ function selectSources(sources, wanted) {
|
|
|
548
583
|
}
|
|
549
584
|
|
|
550
585
|
// src/sources/wordings.ts
|
|
586
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
587
|
+
var WORD_SEPARATORS = /[^\p{L}\p{N}-]+/u;
|
|
551
588
|
var MAX_WORDINGS_PER_SOURCE = 3;
|
|
552
589
|
var FRENCH_FRAME = /* @__PURE__ */ new Set([
|
|
553
590
|
"a",
|
|
@@ -837,28 +874,41 @@ var CONDITION_JOINERS = /* @__PURE__ */ new Set([
|
|
|
837
874
|
]);
|
|
838
875
|
var FOOD_LINKERS = /* @__PURE__ */ new Set(["d", "de", "des", "du", "of"]);
|
|
839
876
|
var CONDITION_WORDS = 3;
|
|
840
|
-
var SERVING_PHRASES = [
|
|
841
|
-
/\bpour\s+(\d{1,3})\s*(?:personnes?|parts?|couverts?)\b/iu,
|
|
842
|
-
/\bfor\s+(\d{1,3})\s*(?:people|persons?|servings?|portions?)\b/iu,
|
|
843
|
-
/\bserves?\s+(\d{1,3})\b/iu,
|
|
844
|
-
/\b(\d{1,3})\s*(?:personnes?|people|servings?)\b/iu
|
|
845
|
-
];
|
|
846
877
|
var FREE_OF = /\b([\p{L}]{3,})[\s-]?free\b/iu;
|
|
847
878
|
function foldWord(word) {
|
|
848
879
|
return word.toLowerCase().normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").replace(/œ/gu, "oe");
|
|
849
880
|
}
|
|
881
|
+
function namesTheDish(word, key) {
|
|
882
|
+
if (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
if (DIGITS_ONLY.test(word)) {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
if (word.length < 2) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
return !CONDITION_UNITS.has(word);
|
|
895
|
+
}
|
|
850
896
|
function readQuestion(question) {
|
|
851
897
|
const words = tokenise(question);
|
|
852
898
|
const conditions = [];
|
|
853
899
|
const dish = [];
|
|
854
900
|
const state = (named, kind) => {
|
|
855
901
|
const trimmed = named === null ? null : named.trim();
|
|
856
|
-
if (trimmed === "")
|
|
857
|
-
|
|
902
|
+
if (trimmed === "") {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (conditions.some((held) => held.named === trimmed && held.kind === kind)) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
858
908
|
conditions.push({ named: trimmed, kind });
|
|
859
909
|
};
|
|
860
910
|
for (let index = 0; index < words.length; index += 1) {
|
|
861
|
-
const word = words[index];
|
|
911
|
+
const word = words[index] ?? "";
|
|
862
912
|
const key = foldWord(word);
|
|
863
913
|
const hyphen = word.indexOf("-");
|
|
864
914
|
if (hyphen > 0 && NEGATORS.has(foldWord(word.slice(0, hyphen)))) {
|
|
@@ -867,7 +917,7 @@ function readQuestion(question) {
|
|
|
867
917
|
}
|
|
868
918
|
const free = FREE_OF.exec(word);
|
|
869
919
|
if (free) {
|
|
870
|
-
state(free[1], "excluded");
|
|
920
|
+
state(free[1] ?? "", "excluded");
|
|
871
921
|
continue;
|
|
872
922
|
}
|
|
873
923
|
if (key === "free") {
|
|
@@ -894,12 +944,9 @@ function readQuestion(question) {
|
|
|
894
944
|
state(word, "diet");
|
|
895
945
|
continue;
|
|
896
946
|
}
|
|
897
|
-
if (
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
if (word.length < 2) continue;
|
|
901
|
-
if (CONDITION_UNITS.has(word)) continue;
|
|
902
|
-
if (!dish.includes(word)) dish.push(word);
|
|
947
|
+
if (namesTheDish(word, key) && !dish.includes(word)) {
|
|
948
|
+
dish.push(word);
|
|
949
|
+
}
|
|
903
950
|
}
|
|
904
951
|
return { dish, conditions };
|
|
905
952
|
}
|
|
@@ -920,15 +967,20 @@ function scopeOf(words, marker, dish) {
|
|
|
920
967
|
}
|
|
921
968
|
function takeFood(words, from) {
|
|
922
969
|
let index = from;
|
|
923
|
-
while (index < words.length && isFiller(words[index]))
|
|
970
|
+
while (index < words.length && isFiller(words[index] ?? "")) {
|
|
971
|
+
index += 1;
|
|
972
|
+
}
|
|
924
973
|
const named = [];
|
|
925
974
|
while (index < words.length && named.length < CONDITION_WORDS) {
|
|
926
|
-
|
|
927
|
-
|
|
975
|
+
const word = words[index] ?? "";
|
|
976
|
+
if (!namesFood(word)) {
|
|
977
|
+
break;
|
|
978
|
+
}
|
|
979
|
+
named.push(word);
|
|
928
980
|
index += 1;
|
|
929
981
|
const beyond = words[index + 1];
|
|
930
982
|
if (named.length + 2 <= CONDITION_WORDS && FOOD_LINKERS.has(foldWord(words[index] ?? "")) && beyond !== void 0 && namesFood(beyond)) {
|
|
931
|
-
named.push(words[index]);
|
|
983
|
+
named.push(words[index] ?? "");
|
|
932
984
|
index += 1;
|
|
933
985
|
continue;
|
|
934
986
|
}
|
|
@@ -942,28 +994,25 @@ function isFiller(word) {
|
|
|
942
994
|
}
|
|
943
995
|
function namesFood(word) {
|
|
944
996
|
const key = foldWord(word);
|
|
945
|
-
if (key.length < 2)
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
function readConditions(question) {
|
|
954
|
-
const { conditions } = readQuestion(question);
|
|
955
|
-
let servings = null;
|
|
956
|
-
for (const phrase of SERVING_PHRASES) {
|
|
957
|
-
const found = phrase.exec(question);
|
|
958
|
-
if (found) {
|
|
959
|
-
const value = Number(found[1]);
|
|
960
|
-
if (Number.isFinite(value) && value > 0) {
|
|
961
|
-
servings = value;
|
|
962
|
-
break;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
997
|
+
if (key.length < 2) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
if (DIGITS_ONLY.test(key)) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
if (key === "free") {
|
|
1004
|
+
return false;
|
|
965
1005
|
}
|
|
966
|
-
|
|
1006
|
+
if (CONDITION_ENDS.has(key)) {
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
if (NEGATORS.has(key) || HEALTH_MARKERS.has(key)) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
if (CONDITION_UNITS.has(key)) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));
|
|
967
1016
|
}
|
|
968
1017
|
function deriveWordings(question) {
|
|
969
1018
|
const asked = tidy(question);
|
|
@@ -971,9 +1020,13 @@ function deriveWordings(question) {
|
|
|
971
1020
|
const seen = /* @__PURE__ */ new Set();
|
|
972
1021
|
const offer = (candidate, derivation) => {
|
|
973
1022
|
const clean = tidy(candidate);
|
|
974
|
-
if (clean === "")
|
|
1023
|
+
if (clean === "") {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
975
1026
|
const key = clean.toLowerCase();
|
|
976
|
-
if (seen.has(key))
|
|
1027
|
+
if (seen.has(key)) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
977
1030
|
seen.add(key);
|
|
978
1031
|
wordings.push({ query: clean, derivation });
|
|
979
1032
|
};
|
|
@@ -991,7 +1044,9 @@ function deriveWordings(question) {
|
|
|
991
1044
|
}
|
|
992
1045
|
function namesDish(title, question) {
|
|
993
1046
|
const words = namingWords(question);
|
|
994
|
-
if (words.length === 0)
|
|
1047
|
+
if (words.length === 0) {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
995
1050
|
return dishWordsMissing(title, question).length < words.length;
|
|
996
1051
|
}
|
|
997
1052
|
function dishWordsMissing(title, question) {
|
|
@@ -1008,19 +1063,22 @@ function dishWords(question) {
|
|
|
1008
1063
|
return readQuestion(question).dish;
|
|
1009
1064
|
}
|
|
1010
1065
|
function tokenise(value) {
|
|
1011
|
-
return value.toLowerCase().replace(/['’]/gu, " ").split(
|
|
1066
|
+
return value.toLowerCase().replace(/['’]/gu, " ").split(WORD_SEPARATORS).map((word) => word.replace(/^-+|-+$/g, "")).filter((word) => word !== "");
|
|
1012
1067
|
}
|
|
1013
1068
|
function tidy(value) {
|
|
1014
1069
|
return value.replace(/\s+/g, " ").trim();
|
|
1015
1070
|
}
|
|
1016
1071
|
|
|
1017
1072
|
// src/sources/client.ts
|
|
1073
|
+
var LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
1018
1074
|
var MAX_LIMIT_PER_SOURCE = 25;
|
|
1019
1075
|
function withGuarantees(config) {
|
|
1020
1076
|
const defaults = loadConfig({});
|
|
1021
1077
|
const bounded = (value, fallback, min, max) => {
|
|
1022
1078
|
const parsed = Number(value);
|
|
1023
|
-
if (!Number.isFinite(parsed))
|
|
1079
|
+
if (!Number.isFinite(parsed)) {
|
|
1080
|
+
return fallback;
|
|
1081
|
+
}
|
|
1024
1082
|
return Math.min(max, Math.max(min, parsed));
|
|
1025
1083
|
};
|
|
1026
1084
|
const claimed = typeof config.userAgent === "string" ? config.userAgent.trim() : "";
|
|
@@ -1081,13 +1139,31 @@ function heldBack(wording, because) {
|
|
|
1081
1139
|
error: null
|
|
1082
1140
|
};
|
|
1083
1141
|
}
|
|
1142
|
+
function keepNewRows(read, seen, rows, question) {
|
|
1143
|
+
let added = 0;
|
|
1144
|
+
let onTopic = 0;
|
|
1145
|
+
for (const row of read.rows) {
|
|
1146
|
+
if (seen.has(row.id)) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
seen.add(row.id);
|
|
1150
|
+
rows.push(row);
|
|
1151
|
+
added += 1;
|
|
1152
|
+
if (namesDish(row.title, question)) {
|
|
1153
|
+
onTopic += 1;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return { added, onTopic };
|
|
1157
|
+
}
|
|
1084
1158
|
function interleave(groups) {
|
|
1085
1159
|
const merged = [];
|
|
1086
1160
|
const longest = Math.max(0, ...groups.map((group) => group.length));
|
|
1087
1161
|
for (let index = 0; index < longest; index += 1) {
|
|
1088
1162
|
for (const group of groups) {
|
|
1089
1163
|
const row = group[index];
|
|
1090
|
-
if (row)
|
|
1164
|
+
if (row) {
|
|
1165
|
+
merged.push(row);
|
|
1166
|
+
}
|
|
1091
1167
|
}
|
|
1092
1168
|
}
|
|
1093
1169
|
return merged;
|
|
@@ -1143,7 +1219,7 @@ var RecipesClient = class {
|
|
|
1143
1219
|
if (trimmed === "") {
|
|
1144
1220
|
throw invalidInput("A search needs something to look for.", "Name a dish or an ingredient.");
|
|
1145
1221
|
}
|
|
1146
|
-
if (
|
|
1222
|
+
if (!LETTER_OR_DIGIT.test(trimmed)) {
|
|
1147
1223
|
throw invalidInput(
|
|
1148
1224
|
`"${trimmed}" carries no letter and no digit, so there is no word to search for.`,
|
|
1149
1225
|
"Name a dish or an ingredient."
|
|
@@ -1155,10 +1231,10 @@ var RecipesClient = class {
|
|
|
1155
1231
|
const attempts = await Promise.all(
|
|
1156
1232
|
chosen.map((source) => this.searchOne(source, trimmed, limit, fanOut))
|
|
1157
1233
|
);
|
|
1158
|
-
const
|
|
1234
|
+
const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));
|
|
1159
1235
|
return {
|
|
1160
|
-
rows: interleave(
|
|
1161
|
-
reports:
|
|
1236
|
+
rows: interleave(answered.map((one) => one.rows)),
|
|
1237
|
+
reports: answered.map(({ attempt, rows }) => reportOf(attempt, rows.length))
|
|
1162
1238
|
};
|
|
1163
1239
|
}
|
|
1164
1240
|
/**
|
|
@@ -1235,14 +1311,9 @@ var RecipesClient = class {
|
|
|
1235
1311
|
`${source.name} sent ${read.skipped} row(s) this server could not read; they were left out.`
|
|
1236
1312
|
);
|
|
1237
1313
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
seen.add(row.id);
|
|
1242
|
-
rows.push(row);
|
|
1243
|
-
added += 1;
|
|
1244
|
-
if (namesDish(row.title, question)) onTopic += 1;
|
|
1245
|
-
}
|
|
1314
|
+
const kept = keepNewRows(read, seen, rows, question);
|
|
1315
|
+
const added = kept.added;
|
|
1316
|
+
onTopic += kept.onTopic;
|
|
1246
1317
|
wordings.push({
|
|
1247
1318
|
query: wording.query,
|
|
1248
1319
|
derivation: wording.derivation,
|
|
@@ -1314,14 +1385,7 @@ var RecipesClient = class {
|
|
|
1314
1385
|
};
|
|
1315
1386
|
export {
|
|
1316
1387
|
MAX_LIMIT_PER_SOURCE,
|
|
1317
|
-
MAX_WORDINGS_PER_SOURCE,
|
|
1318
1388
|
RecipesClient,
|
|
1319
|
-
|
|
1320
|
-
deriveWordings,
|
|
1321
|
-
interleave,
|
|
1322
|
-
namesDish,
|
|
1323
|
-
namespacedId,
|
|
1324
|
-
readConditions,
|
|
1325
|
-
resolveId
|
|
1389
|
+
interleave
|
|
1326
1390
|
};
|
|
1327
1391
|
//# sourceMappingURL=client.js.map
|