@endday/search-mcp 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/index.js +4724 -0
  2. package/dist/search-mcp.js +4715 -0
  3. package/package.json +14 -14
  4. package/data/blocklist.generated.js +0 -2
  5. package/envs.js +0 -129
  6. package/index.js +0 -6
  7. package/mcp/search-mcp.js +0 -8
  8. package/src/content/extract.impl.js +0 -228
  9. package/src/content/extract.js +0 -1
  10. package/src/content/fetch.impl.js +0 -400
  11. package/src/content/fetch.js +0 -1
  12. package/src/core/crypto.js +0 -7
  13. package/src/core/errors.impl.js +0 -52
  14. package/src/core/errors.js +0 -1
  15. package/src/core/html.impl.js +0 -69
  16. package/src/core/html.js +0 -1
  17. package/src/mcp/config.js +0 -75
  18. package/src/mcp/format.js +0 -44
  19. package/src/mcp/index.js +0 -10
  20. package/src/mcp/local/content.js +0 -26
  21. package/src/mcp/local/search.js +0 -233
  22. package/src/mcp/schemas.js +0 -132
  23. package/src/mcp/server.js +0 -97
  24. package/src/mcp/tools/content.js +0 -31
  25. package/src/mcp/tools/jinaContent.js +0 -38
  26. package/src/mcp/tools/newsSearch.js +0 -22
  27. package/src/mcp/tools/webSearch.js +0 -57
  28. package/src/platform/auth.impl.js +0 -166
  29. package/src/platform/auth.js +0 -1
  30. package/src/platform/cache.impl.js +0 -166
  31. package/src/platform/cache.js +0 -1
  32. package/src/platform/health.impl.js +0 -133
  33. package/src/platform/health.js +0 -1
  34. package/src/platform/http.impl.js +0 -108
  35. package/src/platform/http.js +0 -1
  36. package/src/platform/logger.impl.js +0 -51
  37. package/src/platform/logger.js +0 -1
  38. package/src/platform/metrics.impl.js +0 -43
  39. package/src/platform/metrics.js +0 -1
  40. package/src/platform/nodeHttpClient.js +0 -104
  41. package/src/platform/rateLimit.impl.js +0 -141
  42. package/src/platform/rateLimit.js +0 -1
  43. package/src/platform/requestContext.impl.js +0 -10
  44. package/src/platform/requestContext.js +0 -1
  45. package/src/platform/session.impl.js +0 -198
  46. package/src/platform/session.js +0 -1
  47. package/src/platform/stateKv.impl.js +0 -18
  48. package/src/platform/stateKv.js +0 -1
  49. package/src/platform/tasks.impl.js +0 -17
  50. package/src/platform/tasks.js +0 -1
  51. package/src/routes/requestParams.impl.js +0 -12
  52. package/src/routes/requestParams.js +0 -1
  53. package/src/search/engineRegistry.impl.js +0 -117
  54. package/src/search/engineRegistry.js +0 -1
  55. package/src/search/engineRequest.impl.js +0 -377
  56. package/src/search/engineRequest.js +0 -1
  57. package/src/search/engineUtils.impl.js +0 -227
  58. package/src/search/engineUtils.js +0 -1
  59. package/src/search/engines/baidu.impl.js +0 -145
  60. package/src/search/engines/baidu.js +0 -2
  61. package/src/search/engines/bing.impl.js +0 -509
  62. package/src/search/engines/bing.js +0 -2
  63. package/src/search/engines/brave.impl.js +0 -223
  64. package/src/search/engines/brave.js +0 -2
  65. package/src/search/engines/duckduckgo.impl.js +0 -164
  66. package/src/search/engines/duckduckgo.js +0 -2
  67. package/src/search/engines/mojeek.impl.js +0 -115
  68. package/src/search/engines/mojeek.js +0 -2
  69. package/src/search/engines/qwant.impl.js +0 -188
  70. package/src/search/engines/qwant.js +0 -2
  71. package/src/search/engines/startpage.impl.js +0 -237
  72. package/src/search/engines/startpage.js +0 -2
  73. package/src/search/engines/toutiao.impl.js +0 -265
  74. package/src/search/engines/toutiao.js +0 -2
  75. package/src/search/engines/yahoo.impl.js +0 -379
  76. package/src/search/engines/yahoo.js +0 -2
  77. package/src/search/gateway.impl.js +0 -423
  78. package/src/search/gateway.js +0 -1
  79. package/src/search/ranking.impl.js +0 -381
  80. package/src/search/ranking.js +0 -1
  81. package/src/search/requestPolicy.impl.js +0 -137
  82. package/src/search/requestPolicy.js +0 -1
  83. package/src/search/upstreamSession.impl.js +0 -148
  84. package/src/search/upstreamSession.js +0 -1
  85. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -1,377 +0,0 @@
1
- import { ApiError } from "../core/errors.js";
2
- import {
3
- getBrowserProfileById,
4
- getBrowserProfilesForEngine,
5
- getRandomBrowserProfile,
6
- getAcceptLanguageHeader,
7
- } from "./engineUtils.js";
8
- import {
9
- buildEnginePolicy,
10
- enforceEngineThrottle,
11
- sleepBeforeRetry,
12
- shouldRetryUpstream,
13
- } from "./requestPolicy.js";
14
- import {
15
- getUpstreamSession,
16
- createDeferredUpstreamSessionWriter,
17
- } from "./upstreamSession.js";
18
- import { runDeferredTask } from "../platform/tasks.js";
19
- import { fetchWithOptionalCurlImpersonate } from "../platform/nodeHttpClient.js";
20
-
21
- function appendParam(target, key, value) {
22
- if (value === undefined || value === null || value === "") {
23
- return;
24
- }
25
-
26
- if (Array.isArray(value)) {
27
- value.forEach((item) => appendParam(target, key, item));
28
- return;
29
- }
30
-
31
- target.append(key, String(value));
32
- }
33
-
34
- function applyParams(target, params) {
35
- if (!params) {
36
- return;
37
- }
38
-
39
- if (params instanceof URLSearchParams) {
40
- for (const [key, value] of params.entries()) {
41
- appendParam(target, key, value);
42
- }
43
- return;
44
- }
45
-
46
- for (const [key, value] of Object.entries(params)) {
47
- appendParam(target, key, value);
48
- }
49
- }
50
-
51
- function normalizeHeaders(headers = {}) {
52
- const normalized = {};
53
- const entries =
54
- headers instanceof Headers ? headers.entries() : Object.entries(headers);
55
-
56
- for (const [key, value] of entries) {
57
- if (value === undefined || value === null || value === "") {
58
- continue;
59
- }
60
-
61
- normalized[String(key).toLowerCase()] = String(value);
62
- }
63
-
64
- return normalized;
65
- }
66
-
67
- function toFormBody(form) {
68
- const params = new URLSearchParams();
69
- applyParams(params, form);
70
- return params.toString();
71
- }
72
-
73
- export function buildCookieHeader(cookies) {
74
- if (!cookies) {
75
- return "";
76
- }
77
-
78
- if (typeof cookies === "string") {
79
- return cookies.trim();
80
- }
81
-
82
- const parts = [];
83
- const entries = Array.isArray(cookies) ? cookies : Object.entries(cookies);
84
-
85
- for (const entry of entries) {
86
- if (!Array.isArray(entry) || entry.length < 2) {
87
- continue;
88
- }
89
-
90
- const [key, value] = entry;
91
- if (!key || value === undefined || value === null || value === "") {
92
- continue;
93
- }
94
-
95
- parts.push(`${String(key).trim()}=${String(value).trim()}`);
96
- }
97
-
98
- return parts.join("; ");
99
- }
100
-
101
- function mergeCookieHeaders(baseCookies, extraCookies) {
102
- const merged = [buildCookieHeader(baseCookies), buildCookieHeader(extraCookies)]
103
- .filter(Boolean)
104
- .join("; ");
105
-
106
- return merged || "";
107
- }
108
-
109
- function parseResponseCookies(response) {
110
- const setCookie = response.headers.get("set-cookie");
111
- if (!setCookie) {
112
- return {};
113
- }
114
-
115
- const parsed = {};
116
- for (const item of setCookie.split(/,(?=[^;]+=[^;]+)/)) {
117
- const [pair] = item.split(";");
118
- const separatorIndex = pair.indexOf("=");
119
- if (separatorIndex <= 0) {
120
- continue;
121
- }
122
-
123
- const key = pair.slice(0, separatorIndex).trim();
124
- const value = pair.slice(separatorIndex + 1).trim();
125
- if (key) {
126
- parsed[key] = value;
127
- }
128
- }
129
-
130
- return parsed;
131
- }
132
-
133
- function buildNavigationContext({ referrer, origin, headers, requestMethod }) {
134
- const normalizedHeaders = normalizeHeaders(headers);
135
- const resolvedReferrer = referrer || normalizedHeaders.referer || normalizedHeaders.referrer;
136
- let derivedOrigin = "";
137
- if (resolvedReferrer) {
138
- try {
139
- derivedOrigin = new URL(resolvedReferrer).origin;
140
- } catch (_) {
141
- derivedOrigin = "";
142
- }
143
- }
144
- const resolvedOrigin =
145
- origin ||
146
- normalizedHeaders.origin ||
147
- derivedOrigin;
148
- const hasBody = requestMethod !== "GET";
149
- const sameOriginReferrer =
150
- resolvedReferrer && resolvedOrigin && resolvedReferrer.startsWith(resolvedOrigin);
151
-
152
- return {
153
- referrer: resolvedReferrer,
154
- origin: resolvedOrigin,
155
- secFetchSite:
156
- normalizedHeaders["sec-fetch-site"] ||
157
- (sameOriginReferrer ? "same-origin" : hasBody ? "same-site" : "none"),
158
- };
159
- }
160
-
161
- export function isChallengeResponse(source, patterns = []) {
162
- const text = String(source || "");
163
-
164
- return patterns.some((pattern) => {
165
- if (!pattern) {
166
- return false;
167
- }
168
-
169
- if (pattern instanceof RegExp) {
170
- pattern.lastIndex = 0;
171
- return pattern.test(text);
172
- }
173
-
174
- return text.includes(String(pattern));
175
- });
176
- }
177
-
178
- export function createBlockedUpstreamError({
179
- engine = "Upstream",
180
- surface = "response",
181
- message,
182
- details,
183
- } = {}) {
184
- return new ApiError({
185
- status: 502,
186
- code: "UPSTREAM_BLOCKED",
187
- category: "upstream",
188
- message: message || `${engine} returned a bot-detection challenge (${surface})`,
189
- details,
190
- });
191
- }
192
-
193
- export function throwBlockedUpstreamError(options = {}) {
194
- throw createBlockedUpstreamError(options);
195
- }
196
-
197
- export function buildEngineRequest(
198
- url,
199
- {
200
- engine,
201
- signal,
202
- language,
203
- method,
204
- searchParams,
205
- form,
206
- body,
207
- headers = {},
208
- cookies,
209
- referrer,
210
- accept,
211
- acceptLanguage,
212
- userAgent,
213
- origin,
214
- sessionHeaders = {},
215
- profile,
216
- extraCookies,
217
- } = {}
218
- ) {
219
- const requestUrl = new URL(url);
220
- const requestMethod = String(method || (form ? "POST" : "GET")).toUpperCase();
221
- const selectedProfile = profile || getRandomBrowserProfile(engine);
222
- const navigation = buildNavigationContext({
223
- referrer,
224
- origin,
225
- headers,
226
- requestMethod,
227
- });
228
- const requestHeaders = normalizeHeaders({
229
- accept:
230
- accept ||
231
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
232
- "accept-language": acceptLanguage || getAcceptLanguageHeader(language),
233
- "user-agent": userAgent || selectedProfile.ua,
234
- "sec-fetch-dest": "document",
235
- "sec-fetch-mode": "navigate",
236
- "sec-fetch-site": navigation.secFetchSite,
237
- "sec-fetch-user": "?1",
238
- "cache-control": requestMethod === "GET" ? "max-age=0" : "no-cache",
239
- pragma: "no-cache",
240
- "upgrade-insecure-requests": "1",
241
- ...normalizeHeaders(sessionHeaders),
242
- ...normalizeHeaders(headers),
243
- });
244
-
245
- // Chrome Client Hints; Firefox skips these
246
- if (selectedProfile.headers?.["sec-ch-ua"] && !requestHeaders["sec-ch-ua"]) {
247
- requestHeaders["sec-ch-ua"] = selectedProfile.headers["sec-ch-ua"];
248
- requestHeaders["sec-ch-ua-platform"] = selectedProfile.headers["sec-ch-ua-platform"];
249
- requestHeaders["sec-ch-ua-mobile"] = selectedProfile.headers["sec-ch-ua-mobile"];
250
- }
251
- const cookieHeader = mergeCookieHeaders(cookies, extraCookies);
252
-
253
- if (cookieHeader) {
254
- requestHeaders.cookie = cookieHeader;
255
- }
256
-
257
- if (navigation.origin) {
258
- requestHeaders.origin = navigation.origin;
259
- }
260
-
261
- applyParams(requestUrl.searchParams, searchParams);
262
-
263
- let requestBody = body;
264
- if (form && requestBody === undefined) {
265
- requestBody = toFormBody(form);
266
- if (!requestHeaders["content-type"]) {
267
- requestHeaders["content-type"] =
268
- "application/x-www-form-urlencoded; charset=UTF-8";
269
- }
270
- }
271
-
272
- const init = {
273
- method: requestMethod,
274
- signal,
275
- redirect: "follow",
276
- referrer: navigation.referrer,
277
- headers: requestHeaders,
278
- };
279
-
280
- if (requestBody !== undefined && requestMethod !== "GET") {
281
- init.body = requestBody;
282
- }
283
-
284
- return {
285
- url: requestUrl.toString(),
286
- init,
287
- };
288
- }
289
-
290
- export async function fetchSearchText(
291
- url,
292
- {
293
- engine,
294
- engineLabel,
295
- clientId,
296
- blockedStatuses = [],
297
- isBlocked,
298
- blockedSurface = "response",
299
- runtimeContext,
300
- ...requestOptions
301
- } = {}
302
- ) {
303
- const policy = buildEnginePolicy({ name: engine, requestPolicy: requestOptions.requestPolicy });
304
- const profiles = getBrowserProfilesForEngine(engine);
305
- const upstreamSession = await getUpstreamSession(clientId, engine, profiles);
306
- const selectedProfile =
307
- getBrowserProfileById(upstreamSession?.profileId) ||
308
- profiles[0] ||
309
- getRandomBrowserProfile(engine);
310
-
311
- for (let attempt = 0; ; attempt += 1) {
312
- await enforceEngineThrottle(engine || "upstream", policy);
313
- const { url: requestUrl, init } = buildEngineRequest(url, {
314
- ...requestOptions,
315
- engine,
316
- profile: selectedProfile,
317
- extraCookies: upstreamSession?.cookies,
318
- });
319
-
320
- try {
321
- const response = await fetchWithOptionalCurlImpersonate(requestUrl, init, {
322
- profile: selectedProfile,
323
- });
324
- const details = {
325
- engine,
326
- upstream_status: response.status,
327
- url: requestUrl,
328
- attempt,
329
- };
330
-
331
- if (blockedStatuses.includes(response.status)) {
332
- throw createBlockedUpstreamError({
333
- engine: engineLabel || engine || "Upstream",
334
- surface: `status ${response.status}`,
335
- details,
336
- });
337
- }
338
-
339
- if (!response.ok) {
340
- throw new ApiError({
341
- status: 502,
342
- code: "UPSTREAM_BAD_STATUS",
343
- category: "upstream",
344
- message: `Upstream request failed with status ${response.status}`,
345
- details,
346
- });
347
- }
348
-
349
- const text = await response.text();
350
-
351
- if (typeof isBlocked === "function" && isBlocked(text, response)) {
352
- throw createBlockedUpstreamError({
353
- engine: engineLabel || engine || "Upstream",
354
- surface: blockedSurface,
355
- details,
356
- });
357
- }
358
-
359
- const responseCookies = parseResponseCookies(response);
360
- if (clientId && (upstreamSession?.profileId || Object.keys(responseCookies).length > 0)) {
361
- const writeSession = createDeferredUpstreamSessionWriter(clientId, engine, {
362
- profileId: upstreamSession?.profileId || selectedProfile.id,
363
- cookies: responseCookies,
364
- });
365
- await runDeferredTask(runtimeContext, `upstream-session:${engine}`, writeSession);
366
- }
367
-
368
- return text;
369
- } catch (error) {
370
- if (!shouldRetryUpstream(error, attempt, policy)) {
371
- throw error;
372
- }
373
-
374
- await sleepBeforeRetry(policy);
375
- }
376
- }
377
- }
@@ -1 +0,0 @@
1
- export * from "./engineRequest.impl.js";
@@ -1,227 +0,0 @@
1
- import { ApiError } from "../core/errors.js";
2
- import { fetchWithOptionalCurlImpersonate } from "../platform/nodeHttpClient.js";
3
-
4
- const PROFILE_CATALOG = {
5
- default: [
6
- {
7
- id: "chrome-win",
8
- ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
9
- headers: {
10
- accept:
11
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
12
- "sec-ch-ua": '"Chromium";v="126", "Google Chrome";v="126", "Not-A.Brand";v="99"',
13
- "sec-ch-ua-platform": '"Windows"',
14
- "sec-ch-ua-mobile": "?0",
15
- "sec-fetch-dest": "document",
16
- "sec-fetch-mode": "navigate",
17
- "sec-fetch-user": "?1",
18
- "upgrade-insecure-requests": "1",
19
- },
20
- },
21
- {
22
- id: "chrome-mac",
23
- ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
24
- headers: {
25
- accept:
26
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
27
- "sec-ch-ua": '"Chromium";v="126", "Google Chrome";v="126", "Not-A.Brand";v="99"',
28
- "sec-ch-ua-platform": '"macOS"',
29
- "sec-ch-ua-mobile": "?0",
30
- "sec-fetch-dest": "document",
31
- "sec-fetch-mode": "navigate",
32
- "sec-fetch-user": "?1",
33
- "upgrade-insecure-requests": "1",
34
- },
35
- },
36
- {
37
- id: "firefox-win",
38
- ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
39
- headers: {
40
- accept:
41
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
42
- "sec-fetch-dest": "document",
43
- "sec-fetch-mode": "navigate",
44
- "sec-fetch-user": "?1",
45
- "upgrade-insecure-requests": "1",
46
- },
47
- },
48
- ],
49
- bing: ["chrome-win", "chrome-mac"],
50
- startpage: ["chrome-win", "firefox-win"],
51
- duckduckgo: ["chrome-mac", "firefox-win"],
52
- brave: ["chrome-win", "chrome-mac"],
53
- yahoo: ["chrome-win", "firefox-win"],
54
- qwant: ["chrome-mac", "firefox-win"],
55
- mojeek: ["chrome-win", "chrome-mac"],
56
- toutiao: ["chrome-mac"],
57
- };
58
-
59
- const PROFILE_LOOKUP = new Map(
60
- PROFILE_CATALOG.default.map((profile) => [profile.id, profile])
61
- );
62
-
63
- let profileIndex = 0;
64
-
65
- function getProfileIdsForEngine(engine) {
66
- const normalizedEngine = String(engine || "").toLowerCase();
67
- if (normalizedEngine === "default") {
68
- return PROFILE_CATALOG.default.map((profile) => profile.id);
69
- }
70
-
71
- const configured = PROFILE_CATALOG[normalizedEngine];
72
- if (!configured || configured.length === 0) {
73
- return PROFILE_CATALOG.default.map((profile) => profile.id);
74
- }
75
-
76
- return configured;
77
- }
78
-
79
- export function getBrowserProfilesForEngine(engine) {
80
- return getProfileIdsForEngine(engine)
81
- .map((id) => PROFILE_LOOKUP.get(id))
82
- .filter(Boolean);
83
- }
84
-
85
- export function getRandomUserAgent(engine = "default") {
86
- const profiles = getBrowserProfilesForEngine(engine);
87
- profileIndex = (profileIndex + 1) % profiles.length;
88
- return profiles[profileIndex].ua;
89
- }
90
-
91
- export function getRandomBrowserProfile(engine = "default") {
92
- const profiles = getBrowserProfilesForEngine(engine);
93
- profileIndex = (profileIndex + 1) % profiles.length;
94
- return profiles[profileIndex];
95
- }
96
-
97
- export function getBrowserProfileById(profileId) {
98
- return PROFILE_LOOKUP.get(profileId) || null;
99
- }
100
-
101
- export function getAcceptLanguageHeader(language) {
102
- const normalized = String(language || "").trim().toLowerCase();
103
-
104
- if (!normalized) {
105
- return "en-US,en;q=0.9";
106
- }
107
-
108
- if (normalized.startsWith("zh")) {
109
- return "zh-CN,zh;q=0.9,en;q=0.8";
110
- }
111
-
112
- if (normalized.startsWith("en-gb")) {
113
- return "en-GB,en;q=0.9";
114
- }
115
-
116
- if (normalized.startsWith("en")) {
117
- return "en-US,en;q=0.9";
118
- }
119
-
120
- return `${language},en;q=0.8`;
121
- }
122
-
123
- export function getDefaultFetchHeaders(language, extraHeaders = {}, engine = "default") {
124
- const profile = getRandomBrowserProfile(engine);
125
- return {
126
- ...profile.headers,
127
- "accept-language": getAcceptLanguageHeader(language),
128
- "user-agent": profile.ua,
129
- "sec-fetch-site": "none",
130
- ...extraHeaders,
131
- };
132
- }
133
-
134
- export async function fetchText(
135
- url,
136
- { signal, language, headers = {}, referrer, engine = "default" } = {}
137
- ) {
138
- const response = await fetchWithOptionalCurlImpersonate(url, {
139
- signal,
140
- redirect: "follow",
141
- referrer,
142
- headers: getDefaultFetchHeaders(language, headers, engine),
143
- });
144
-
145
- if (!response.ok) {
146
- throw new ApiError({
147
- status: 502,
148
- code: "UPSTREAM_BAD_STATUS",
149
- category: "upstream",
150
- message: `Upstream request failed with status ${response.status}`,
151
- details: {
152
- upstream_status: response.status,
153
- url,
154
- },
155
- });
156
- }
157
-
158
- return response.text();
159
- }
160
-
161
- export async function fetchJson(
162
- url,
163
- { signal, language, headers = {}, referrer, engine = "default" } = {}
164
- ) {
165
- const response = await fetchWithOptionalCurlImpersonate(url, {
166
- signal,
167
- redirect: "follow",
168
- referrer,
169
- headers: getDefaultFetchHeaders(
170
- language,
171
- {
172
- accept: "application/json,text/plain;q=0.9,*/*;q=0.8",
173
- ...headers,
174
- },
175
- engine
176
- ),
177
- });
178
-
179
- if (!response.ok) {
180
- throw new ApiError({
181
- status: 502,
182
- code: "UPSTREAM_BAD_STATUS",
183
- category: "upstream",
184
- message: `Upstream request failed with status ${response.status}`,
185
- details: {
186
- upstream_status: response.status,
187
- url,
188
- },
189
- });
190
- }
191
-
192
- return response.json();
193
- }
194
-
195
- export function ensureAbsoluteUrl(url, baseUrl) {
196
- try {
197
- return new URL(url, baseUrl).toString();
198
- } catch (_) {
199
- return String(url || "").trim();
200
- }
201
- }
202
-
203
- export function mapTimeRange(timeRange, mapping) {
204
- if (!timeRange) {
205
- return "";
206
- }
207
-
208
- return mapping[String(timeRange).toLowerCase()] || "";
209
- }
210
-
211
- export function mapLanguage(language, mapping, fallback = "") {
212
- if (!language) {
213
- return fallback;
214
- }
215
-
216
- const normalized = String(language).toLowerCase();
217
- const short = normalized.split("-")[0];
218
- return mapping[normalized] || mapping[short] || fallback;
219
- }
220
-
221
- export function resolvePageNumber(value) {
222
- const parsed = Number.parseInt(value ?? "0", 10);
223
- if (Number.isNaN(parsed) || parsed < 0) {
224
- return 0;
225
- }
226
- return parsed;
227
- }
@@ -1 +0,0 @@
1
- export * from "./engineUtils.impl.js";