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.
@@ -1,5 +1,8 @@
1
+ // src/config.ts
2
+ import process from "process";
3
+
1
4
  // src/version.ts
2
- var PKG_VERSION = "1.3.0";
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) return;
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() === "") return fallback;
35
+ if (raw === void 0 || raw.trim() === "") {
36
+ return fallback;
37
+ }
31
38
  const value = Number(raw);
32
- if (!Number.isFinite(value) || !Number.isInteger(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) return error;
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) return error;
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) details.url = known.details.url;
126
- if (known.details.status !== void 0) details.status = known.details.status;
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())) continue;
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) return all.join("");
256
- return `${all.slice(0, -1).join(", ")} and ${all[all.length - 1]}`;
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 || !title || !url) {
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") return { data: [], cached: false };
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 || !title || !url) {
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 = /^https?:\/\//i.test(reference) ? { url: reference } : { id: reference };
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) return sources;
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 === "") return;
857
- if (conditions.some((held) => held.named === trimmed && held.kind === kind)) return;
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 (FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key)) continue;
898
- if (FRENCH_FRAME.has(word) || ENGLISH_FRAME.has(word)) continue;
899
- if (/^\d+$/.test(word)) continue;
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])) index += 1;
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
- if (!namesFood(words[index])) break;
927
- named.push(words[index]);
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) return false;
946
- if (/^\d+$/.test(key)) return false;
947
- if (key === "free") return false;
948
- if (CONDITION_ENDS.has(key)) return false;
949
- if (NEGATORS.has(key) || HEALTH_MARKERS.has(key)) return false;
950
- if (CONDITION_UNITS.has(key)) return false;
951
- return !(FRENCH_FRAME.has(key) || ENGLISH_FRAME.has(key));
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
- return { conditions, servings };
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 === "") return;
1023
+ if (clean === "") {
1024
+ return;
1025
+ }
975
1026
  const key = clean.toLowerCase();
976
- if (seen.has(key)) return;
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) return true;
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(/[^\p{L}\p{N}-]+/u).map((word) => word.replace(/^-+|-+$/g, "")).filter((word) => word !== "");
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)) return fallback;
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) merged.push(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 (!/[\p{L}\p{N}]/u.test(trimmed)) {
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 groups = attempts.map((attempt) => attempt.rows.slice(0, limit));
1234
+ const answered = attempts.map((attempt) => ({ attempt, rows: attempt.rows.slice(0, limit) }));
1159
1235
  return {
1160
- rows: interleave(groups),
1161
- reports: attempts.map((attempt, index) => reportOf(attempt, groups[index].length))
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
- let added = 0;
1239
- for (const row of read.rows) {
1240
- if (seen.has(row.id)) continue;
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
- SOURCE_IDS,
1320
- deriveWordings,
1321
- interleave,
1322
- namesDish,
1323
- namespacedId,
1324
- readConditions,
1325
- resolveId
1389
+ interleave
1326
1390
  };
1327
1391
  //# sourceMappingURL=client.js.map