mcp-databnf 1.1.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 +44 -6
- package/dist/bnf/client.d.ts +4 -33
- package/dist/bnf/client.js +260 -232
- package/dist/bnf/client.js.map +1 -1
- package/dist/index.js +551 -307
- package/dist/index.js.map +1 -1
- package/package.json +18 -10
- package/server.json +73 -0
package/dist/bnf/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-databnf";
|
|
4
7
|
|
|
5
8
|
// src/config.ts
|
|
@@ -12,7 +15,9 @@ var DEFAULT_USER_AGENT = `mcp-databnf/${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-databnf] ${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-databnf] error: ${name}="${raw}" is not a whole number; using ${fallback}.
|
|
35
42
|
`
|
|
@@ -80,7 +87,7 @@ var BnfError = class extends Error {
|
|
|
80
87
|
code;
|
|
81
88
|
details;
|
|
82
89
|
constructor(code, message, details = {}) {
|
|
83
|
-
super(message);
|
|
90
|
+
super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
|
|
84
91
|
this.name = "BnfError";
|
|
85
92
|
this.code = code;
|
|
86
93
|
this.details = details;
|
|
@@ -110,7 +117,9 @@ var Cache = class {
|
|
|
110
117
|
entries = /* @__PURE__ */ new Map();
|
|
111
118
|
get(key) {
|
|
112
119
|
const entry = this.entries.get(key);
|
|
113
|
-
if (!entry)
|
|
120
|
+
if (!entry) {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
114
123
|
if (entry.expiresAt <= Date.now()) {
|
|
115
124
|
this.entries.delete(key);
|
|
116
125
|
return void 0;
|
|
@@ -120,12 +129,16 @@ var Cache = class {
|
|
|
120
129
|
return { value: entry.value, retrievedAt: entry.retrievedAt };
|
|
121
130
|
}
|
|
122
131
|
set(key, value, retrievedAt) {
|
|
123
|
-
if (this.ttlMs <= 0)
|
|
132
|
+
if (this.ttlMs <= 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
124
135
|
this.entries.delete(key);
|
|
125
136
|
this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs, retrievedAt });
|
|
126
137
|
while (this.entries.size > this.maxEntries) {
|
|
127
138
|
const oldest = this.entries.keys().next();
|
|
128
|
-
if (oldest.done)
|
|
139
|
+
if (oldest.done) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
129
142
|
this.entries.delete(oldest.value);
|
|
130
143
|
}
|
|
131
144
|
}
|
|
@@ -135,6 +148,9 @@ var Cache = class {
|
|
|
135
148
|
};
|
|
136
149
|
|
|
137
150
|
// src/bnf/endpoint.ts
|
|
151
|
+
var TRAILING_DOT = /\.$/;
|
|
152
|
+
var FRAGMENT = /#.*$/;
|
|
153
|
+
var HTTP_SCHEME = /^http:/;
|
|
138
154
|
var SPARQL_ENDPOINT = "https://data.bnf.fr/sparql";
|
|
139
155
|
var ALLOWED_HOST = "data.bnf.fr";
|
|
140
156
|
var GALLICA_HOST = "gallica.bnf.fr";
|
|
@@ -142,22 +158,23 @@ function assertRequestable(url) {
|
|
|
142
158
|
let parsed;
|
|
143
159
|
try {
|
|
144
160
|
parsed = new URL(url);
|
|
145
|
-
} catch {
|
|
146
|
-
throw refusal(`mcp-databnf refuses to request "${url}": it is not an address
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
throw refusal(`mcp-databnf refuses to request "${url}": it is not an address.`, cause);
|
|
147
163
|
}
|
|
148
|
-
const host = parsed.hostname.toLowerCase().replace(
|
|
164
|
+
const host = parsed.hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
149
165
|
if (host !== ALLOWED_HOST) {
|
|
150
166
|
throw refusal(
|
|
151
167
|
`mcp-databnf requests ${ALLOWED_HOST} and nothing else; "${host}" was refused. Gallica in particular is described from the catalogue and never read.`
|
|
152
168
|
);
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
|
-
var refusal = (message) => new BnfError("network_error", message, {
|
|
156
|
-
hint: "This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host."
|
|
171
|
+
var refusal = (message, cause) => new BnfError("network_error", message, {
|
|
172
|
+
hint: "This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host.",
|
|
173
|
+
cause
|
|
157
174
|
});
|
|
158
175
|
function isGallicaAddress(address) {
|
|
159
176
|
try {
|
|
160
|
-
const host = new URL(address).hostname.toLowerCase().replace(
|
|
177
|
+
const host = new URL(address).hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
161
178
|
return host === GALLICA_HOST || host.endsWith(`.${GALLICA_HOST}`);
|
|
162
179
|
} catch {
|
|
163
180
|
return false;
|
|
@@ -184,21 +201,28 @@ var PREFIXES = {
|
|
|
184
201
|
rdafrbr: "http://rdvocab.info/uri/schema/FRBRentitiesRDA/"
|
|
185
202
|
};
|
|
186
203
|
var PREFIX_BLOCK = Object.entries(PREFIXES).map(([name, iri]) => `PREFIX ${name}: <${iri}>`).join("\n");
|
|
187
|
-
var publicPageFor = (entityIri) => entityIri.replace(
|
|
204
|
+
var publicPageFor = (entityIri) => entityIri.replace(FRAGMENT, "").replace(HTTP_SCHEME, "https:");
|
|
188
205
|
|
|
189
206
|
// src/bnf/http.ts
|
|
207
|
+
var VIRTUOSO_ERROR = /^Virtuoso\s+(\w+)\s+Error\s+([\s\S]*)$/;
|
|
190
208
|
var RETRYABLE = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
191
209
|
var PUSH_BACK = /* @__PURE__ */ new Set([429, 503]);
|
|
192
210
|
var LONGEST_WAIT_MS = 3e4;
|
|
193
211
|
var RETRIES_AFTER_SILENCE = 1;
|
|
194
212
|
var MAX_REDIRECTS = 2;
|
|
195
213
|
function parseRetryAfter(value, now = Date.now()) {
|
|
196
|
-
if (!value)
|
|
214
|
+
if (!value) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
197
217
|
const trimmed = value.trim();
|
|
198
218
|
const seconds = Number(trimmed);
|
|
199
|
-
if (Number.isFinite(seconds) && seconds >= 0)
|
|
219
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
220
|
+
return Math.round(seconds * 1e3);
|
|
221
|
+
}
|
|
200
222
|
const at = Date.parse(trimmed);
|
|
201
|
-
if (Number.isNaN(at))
|
|
223
|
+
if (Number.isNaN(at)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
202
226
|
return Math.max(0, at - now);
|
|
203
227
|
}
|
|
204
228
|
function backoffMs(attempt) {
|
|
@@ -206,8 +230,10 @@ function backoffMs(attempt) {
|
|
|
206
230
|
return base + Math.floor(Math.random() * 500);
|
|
207
231
|
}
|
|
208
232
|
function readEngineError(body) {
|
|
209
|
-
const match =
|
|
210
|
-
if (!match)
|
|
233
|
+
const match = VIRTUOSO_ERROR.exec(body.trim());
|
|
234
|
+
if (!match) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
211
237
|
const state = match[1] ?? "";
|
|
212
238
|
const text2 = (match[2] ?? "").split("\n")[0]?.trim() ?? "";
|
|
213
239
|
return { kind: state === "37000" ? "compile" : "runtime", text: text2 };
|
|
@@ -249,6 +275,78 @@ async function postOnce(options, signal) {
|
|
|
249
275
|
url: SPARQL_ENDPOINT
|
|
250
276
|
});
|
|
251
277
|
}
|
|
278
|
+
function readRefusal(response, attempt, maxRetries) {
|
|
279
|
+
const pushBack = RETRYABLE.has(response.status);
|
|
280
|
+
if (PUSH_BACK.has(response.status)) {
|
|
281
|
+
const asked = parseRetryAfter(response.headers.get("retry-after"));
|
|
282
|
+
if (asked !== null && asked > LONGEST_WAIT_MS) {
|
|
283
|
+
return {
|
|
284
|
+
kind: "refused",
|
|
285
|
+
pushBack,
|
|
286
|
+
error: rateLimited(
|
|
287
|
+
`data.bnf.fr asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
|
|
288
|
+
{ url: SPARQL_ENDPOINT, status: response.status }
|
|
289
|
+
)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (attempt >= maxRetries) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "refused",
|
|
295
|
+
pushBack,
|
|
296
|
+
error: rateLimited(
|
|
297
|
+
`data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,
|
|
298
|
+
{
|
|
299
|
+
url: SPARQL_ENDPOINT,
|
|
300
|
+
status: response.status
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return { kind: "again", pushBack, waitMs: asked ?? backoffMs(attempt) };
|
|
306
|
+
}
|
|
307
|
+
if (RETRYABLE.has(response.status) && attempt < maxRetries) {
|
|
308
|
+
return { kind: "again", pushBack, waitMs: backoffMs(attempt) };
|
|
309
|
+
}
|
|
310
|
+
if (response.status === 400 || response.status === 422) {
|
|
311
|
+
const engine = readEngineError(response.body);
|
|
312
|
+
return {
|
|
313
|
+
kind: "refused",
|
|
314
|
+
pushBack,
|
|
315
|
+
error: invalidInput(
|
|
316
|
+
`data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : "."}`,
|
|
317
|
+
"Try fewer or plainer words. If this keeps happening with ordinary input, please report it."
|
|
318
|
+
)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
kind: "refused",
|
|
323
|
+
pushBack,
|
|
324
|
+
error: networkError(`data.bnf.fr answered HTTP ${response.status}.`, {
|
|
325
|
+
url: SPARQL_ENDPOINT,
|
|
326
|
+
status: response.status
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function readFailure(error, attempts) {
|
|
331
|
+
const { attempt, maxRetries, timeoutMs } = attempts;
|
|
332
|
+
if (error instanceof Error && error.name === "BnfError") {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
336
|
+
if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
|
|
337
|
+
throw timeout(
|
|
338
|
+
`No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,
|
|
339
|
+
{ url: SPARQL_ENDPOINT }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return error;
|
|
343
|
+
}
|
|
344
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
345
|
+
if (attempt >= maxRetries) {
|
|
346
|
+
throw networkError(`Could not reach data.bnf.fr: ${failure.message}`, { url: SPARQL_ENDPOINT });
|
|
347
|
+
}
|
|
348
|
+
return failure;
|
|
349
|
+
}
|
|
252
350
|
async function runQuery(options) {
|
|
253
351
|
const { timeoutMs, maxRetries, limiter, logger } = options;
|
|
254
352
|
let lastError = null;
|
|
@@ -256,7 +354,8 @@ async function runQuery(options) {
|
|
|
256
354
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
257
355
|
if (askedWaitMs > 0) {
|
|
258
356
|
logger.debug(`waiting ${askedWaitMs}ms, as asked`);
|
|
259
|
-
|
|
357
|
+
const asked = askedWaitMs;
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, asked));
|
|
260
359
|
askedWaitMs = 0;
|
|
261
360
|
}
|
|
262
361
|
await limiter.beforeRequest();
|
|
@@ -276,66 +375,19 @@ async function runQuery(options) {
|
|
|
276
375
|
limiter.succeeded();
|
|
277
376
|
return parsed;
|
|
278
377
|
}
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
if (asked !== null && asked > LONGEST_WAIT_MS) {
|
|
283
|
-
throw rateLimited(
|
|
284
|
-
`data.bnf.fr asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
|
|
285
|
-
{ url: SPARQL_ENDPOINT, status: response.status }
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
if (attempt >= maxRetries) {
|
|
289
|
-
throw rateLimited(
|
|
290
|
-
`data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,
|
|
291
|
-
{
|
|
292
|
-
url: SPARQL_ENDPOINT,
|
|
293
|
-
status: response.status
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
askedWaitMs = asked ?? backoffMs(attempt);
|
|
298
|
-
lastError = new Error(`HTTP ${response.status}`);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (RETRYABLE.has(response.status) && attempt < maxRetries) {
|
|
302
|
-
lastError = new Error(`HTTP ${response.status}`);
|
|
303
|
-
askedWaitMs = backoffMs(attempt);
|
|
304
|
-
continue;
|
|
378
|
+
const verdict = readRefusal(response, attempt, maxRetries);
|
|
379
|
+
if (verdict.pushBack) {
|
|
380
|
+
limiter.pushBack();
|
|
305
381
|
}
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
throw invalidInput(
|
|
309
|
-
`data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : "."}`,
|
|
310
|
-
"Try fewer or plainer words. If this keeps happening with ordinary input, please report it."
|
|
311
|
-
);
|
|
382
|
+
if (verdict.kind === "refused") {
|
|
383
|
+
throw verdict.error;
|
|
312
384
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
status: response.status
|
|
316
|
-
});
|
|
385
|
+
askedWaitMs = verdict.waitMs;
|
|
386
|
+
lastError = new Error(`HTTP ${response.status}`);
|
|
317
387
|
} catch (error) {
|
|
318
388
|
clearTimeout(deadline);
|
|
319
|
-
|
|
320
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
321
|
-
lastError = error;
|
|
322
|
-
if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
|
|
323
|
-
throw timeout(
|
|
324
|
-
`No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,
|
|
325
|
-
{ url: SPARQL_ENDPOINT }
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
askedWaitMs = backoffMs(attempt);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
332
|
-
if (attempt >= maxRetries) {
|
|
333
|
-
throw networkError(`Could not reach data.bnf.fr: ${lastError.message}`, {
|
|
334
|
-
url: SPARQL_ENDPOINT
|
|
335
|
-
});
|
|
336
|
-
}
|
|
389
|
+
lastError = readFailure(error, { attempt, maxRetries, timeoutMs });
|
|
337
390
|
askedWaitMs = backoffMs(attempt);
|
|
338
|
-
continue;
|
|
339
391
|
} finally {
|
|
340
392
|
clearTimeout(deadline);
|
|
341
393
|
}
|
|
@@ -369,9 +421,10 @@ function parseResults(body) {
|
|
|
369
421
|
let payload;
|
|
370
422
|
try {
|
|
371
423
|
payload = JSON.parse(body);
|
|
372
|
-
} catch {
|
|
424
|
+
} catch (cause) {
|
|
373
425
|
throw parseFailure("data.bnf.fr answered with something that is not JSON.", {
|
|
374
|
-
url: SPARQL_ENDPOINT
|
|
426
|
+
url: SPARQL_ENDPOINT,
|
|
427
|
+
cause
|
|
375
428
|
});
|
|
376
429
|
}
|
|
377
430
|
if (typeof payload !== "object" || payload === null || !("results" in payload) || typeof payload.results !== "object" || !Array.isArray(payload.results?.bindings)) {
|
|
@@ -383,6 +436,8 @@ function parseResults(body) {
|
|
|
383
436
|
}
|
|
384
437
|
|
|
385
438
|
// src/bnf/sparql.ts
|
|
439
|
+
var LEADING_JOINERS = /^['-]+/u;
|
|
440
|
+
var TRAILING_JOINERS = /['-]+$/u;
|
|
386
441
|
function escapeLiteral(value) {
|
|
387
442
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "");
|
|
388
443
|
}
|
|
@@ -391,49 +446,20 @@ var INVISIBLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200
|
|
|
391
446
|
function withoutInvisibles(input) {
|
|
392
447
|
return input.replace(INVISIBLE, "");
|
|
393
448
|
}
|
|
394
|
-
function invisibleCharacters(input) {
|
|
395
|
-
const found = [];
|
|
396
|
-
for (const character of input.match(INVISIBLE) ?? []) {
|
|
397
|
-
if (!found.includes(character)) found.push(character);
|
|
398
|
-
}
|
|
399
|
-
return found;
|
|
400
|
-
}
|
|
401
|
-
function charactersSetAside(input) {
|
|
402
|
-
const found = [];
|
|
403
|
-
for (const character of withoutInvisibles(input).normalize("NFC")) {
|
|
404
|
-
if (/[\p{L}\p{N}'\-\s]/u.test(character) || /[‘’ʼ]/u.test(character)) continue;
|
|
405
|
-
if (!found.includes(character)) found.push(character);
|
|
406
|
-
}
|
|
407
|
-
return found;
|
|
408
|
-
}
|
|
409
|
-
function readSearchText(input) {
|
|
410
|
-
const words = toSearchWords(input);
|
|
411
|
-
return {
|
|
412
|
-
words,
|
|
413
|
-
terms: toIndexTerms(words),
|
|
414
|
-
invisible: invisibleCharacters(input),
|
|
415
|
-
setAside: charactersSetAside(input)
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
449
|
function toSearchWords(input) {
|
|
419
450
|
const cleaned = withoutInvisibles(input).normalize("NFC").replace(/[‘’ʼ]/gu, "'").replace(/[^\p{L}\p{N}'-]+/gu, " ");
|
|
420
451
|
const words = [];
|
|
421
452
|
for (const raw of cleaned.split(" ")) {
|
|
422
|
-
const word = raw.replace(
|
|
423
|
-
if (word === "")
|
|
453
|
+
const word = raw.replace(LEADING_JOINERS, "").replace(TRAILING_JOINERS, "");
|
|
454
|
+
if (word === "") {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
424
457
|
words.push(word.length > 60 ? word.slice(0, 60) : word);
|
|
425
|
-
if (words.length >= 12)
|
|
426
|
-
|
|
427
|
-
return words;
|
|
428
|
-
}
|
|
429
|
-
function toIndexTerms(words) {
|
|
430
|
-
const terms = [];
|
|
431
|
-
for (const word of words) {
|
|
432
|
-
for (const piece of word.split(/['-]+/u)) {
|
|
433
|
-
if (piece !== "" && !terms.includes(piece)) terms.push(piece);
|
|
458
|
+
if (words.length >= 12) {
|
|
459
|
+
break;
|
|
434
460
|
}
|
|
435
461
|
}
|
|
436
|
-
return
|
|
462
|
+
return words;
|
|
437
463
|
}
|
|
438
464
|
function containsAllWords(words) {
|
|
439
465
|
if (words.length === 0) {
|
|
@@ -445,54 +471,9 @@ function containsAllWords(words) {
|
|
|
445
471
|
const clause = words.map((word) => `'${word.replace(/'/g, "''")}'`).join(" AND ");
|
|
446
472
|
return literal(clause);
|
|
447
473
|
}
|
|
448
|
-
var ARK_ID = /^c[bc][0-9a-z]{6,20}$/;
|
|
449
|
-
var TEMP_WORK_ID = /^[0-9a-f]{32}$/;
|
|
450
|
-
function parseEntityId(input) {
|
|
451
|
-
const trimmed = input.trim();
|
|
452
|
-
if (trimmed === "") {
|
|
453
|
-
throw invalidInput("An identifier is required.");
|
|
454
|
-
}
|
|
455
|
-
const arkMatch = /(?:^|\/)ark:\/12148\/(c[bc][0-9a-z]+)/i.exec(trimmed);
|
|
456
|
-
const bare = arkMatch ? arkMatch[1] : trimmed.replace(/^#/, "").split(/[#?]/)[0];
|
|
457
|
-
const candidate = (bare ?? "").toLowerCase();
|
|
458
|
-
if (ARK_ID.test(candidate)) {
|
|
459
|
-
return {
|
|
460
|
-
kind: "ark",
|
|
461
|
-
id: candidate,
|
|
462
|
-
iri: `http://data.bnf.fr/ark:/12148/${candidate}#about`,
|
|
463
|
-
pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
const tempMatch = /temp-work\/([0-9a-f]{32})/i.exec(trimmed);
|
|
467
|
-
const digest = (tempMatch?.[1] ?? candidate).toLowerCase();
|
|
468
|
-
if (TEMP_WORK_ID.test(digest)) {
|
|
469
|
-
return {
|
|
470
|
-
kind: "temp-work",
|
|
471
|
-
id: `temp-work/${digest}`,
|
|
472
|
-
iri: `http://data.bnf.fr/temp-work/${digest}/#about`,
|
|
473
|
-
pageUrl: `https://data.bnf.fr/temp-work/${digest}/`
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
throw invalidInput(`"${input}" is not a data.bnf.fr identifier.`, hintFor(trimmed));
|
|
477
|
-
}
|
|
478
|
-
var GALLICA_DOCUMENT = /^(?:bpt6k|btv1b|bd6t|cb32|btv1b)[0-9a-z]+$/i;
|
|
479
|
-
var PAGE_ADDRESS = /data\.bnf\.fr\/(?:[a-z]{2}\/)?(\d{6,12})\/([a-z0-9_]+)/i;
|
|
480
|
-
var GENERAL_HINT = "An identifier looks like 'cb119219976' for an established record, or 'temp-work/22d7f68c1a4bdd081ad7ca791fd3b730' for a provisional one. Take it from a search result rather than building one.";
|
|
481
|
-
function hintFor(input) {
|
|
482
|
-
const page2 = PAGE_ADDRESS.exec(input);
|
|
483
|
-
if (page2) {
|
|
484
|
-
const name = (page2[2] ?? "").replace(/_/g, " ");
|
|
485
|
-
return `That is the readable address of a data.bnf.fr page, and ${page2[1]} in it is the record's FRBNF number rather than the identifier these tools take. Search for "${name}" with search_authors or search_works, and use the identifier the row carries.`;
|
|
486
|
-
}
|
|
487
|
-
const gallicaArk = /ark:\/12148\/([0-9a-z]+)/i.exec(input)?.[1] ?? input;
|
|
488
|
-
if (GALLICA_DOCUMENT.test(gallicaArk)) {
|
|
489
|
-
return "That is a Gallica identifier: it names a digitised document rather than a record in the catalogue, and these tools read the catalogue. Search for the work with search_works, then list_editions will give you the Gallica link alongside the edition it belongs to.";
|
|
490
|
-
}
|
|
491
|
-
return GENERAL_HINT;
|
|
492
|
-
}
|
|
493
474
|
var iriOf = (id) => `<${id.iri}>`;
|
|
494
475
|
function boundedInteger(value, min, max, name) {
|
|
495
|
-
if (!Number.isFinite(value)
|
|
476
|
+
if (!(Number.isFinite(value) && Number.isInteger(value))) {
|
|
496
477
|
throw invalidInput(`'${name}' has to be a whole number.`);
|
|
497
478
|
}
|
|
498
479
|
if (value < min || value > max) {
|
|
@@ -713,6 +694,14 @@ LIMIT 20`);
|
|
|
713
694
|
}
|
|
714
695
|
|
|
715
696
|
// src/bnf/parse.ts
|
|
697
|
+
var ARK_IN_IRI = /ark:\/12148\/(c[bc][0-9a-z]+)/i;
|
|
698
|
+
var TEMP_WORK_IN_IRI = /temp-work\/([0-9a-f]{32})/i;
|
|
699
|
+
var TRAILING_SEPARATOR = /[/#]$/;
|
|
700
|
+
var PATH_SEPARATOR = /[/#]/;
|
|
701
|
+
var ARK_SEGMENT = /ark:\/12148\/([^/?#]+)/;
|
|
702
|
+
var QUERY_START = /[?#]/;
|
|
703
|
+
var LEADING_DOTS = /^[./]+/;
|
|
704
|
+
var TRAILING_SLASHES = /\/+$/;
|
|
716
705
|
var P = {
|
|
717
706
|
type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
|
718
707
|
label: "http://www.w3.org/2000/01/rdf-schema#label",
|
|
@@ -768,36 +757,50 @@ var ALIGNMENT_HOSTS = [
|
|
|
768
757
|
{ host: "catalogue.bnf.fr", name: "catalogue" }
|
|
769
758
|
];
|
|
770
759
|
var text = (term) => {
|
|
771
|
-
if (!term)
|
|
760
|
+
if (!term) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
772
763
|
const value = term.value.trim();
|
|
773
764
|
return value === "" ? null : value;
|
|
774
765
|
};
|
|
775
766
|
var integer = (term) => {
|
|
776
767
|
const raw = text(term);
|
|
777
|
-
if (raw === null)
|
|
768
|
+
if (raw === null) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
778
771
|
const value = Number(raw);
|
|
779
772
|
return Number.isInteger(value) ? value : null;
|
|
780
773
|
};
|
|
781
774
|
function idFromIri(iri) {
|
|
782
|
-
const ark =
|
|
783
|
-
if (ark?.[1])
|
|
784
|
-
|
|
785
|
-
|
|
775
|
+
const ark = ARK_IN_IRI.exec(iri);
|
|
776
|
+
if (ark?.[1]) {
|
|
777
|
+
return ark[1].toLowerCase();
|
|
778
|
+
}
|
|
779
|
+
const temp = TEMP_WORK_IN_IRI.exec(iri);
|
|
780
|
+
if (temp?.[1]) {
|
|
781
|
+
return `temp-work/${temp[1].toLowerCase()}`;
|
|
782
|
+
}
|
|
786
783
|
return null;
|
|
787
784
|
}
|
|
788
|
-
var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(
|
|
785
|
+
var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(TRAILING_SEPARATOR, "").split(PATH_SEPARATOR).pop() ?? iri);
|
|
789
786
|
function toDigitisedLink(address, role, fromId, fromTitle) {
|
|
790
|
-
if (!address
|
|
791
|
-
|
|
792
|
-
|
|
787
|
+
if (!(address && isGallicaAddress(address))) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const segment = ARK_SEGMENT.exec(address)?.[1] ?? null;
|
|
791
|
+
if (!segment) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
793
794
|
const ark = segment.split(".")[0] ?? segment;
|
|
794
|
-
if (ark === "")
|
|
795
|
+
if (ark === "") {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
795
798
|
return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };
|
|
796
799
|
}
|
|
797
800
|
function renderingOf(address, ark) {
|
|
798
|
-
const path = address.split(
|
|
801
|
+
const path = address.split(QUERY_START)[0] ?? address;
|
|
799
802
|
const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);
|
|
800
|
-
const rendering = after.replace(
|
|
803
|
+
const rendering = after.replace(LEADING_DOTS, "").replace(TRAILING_SLASHES, "");
|
|
801
804
|
return rendering === "" ? null : rendering;
|
|
802
805
|
}
|
|
803
806
|
function page(rows, limit, skipped) {
|
|
@@ -807,9 +810,13 @@ function page(rows, limit, skipped) {
|
|
|
807
810
|
function readWindowOccupancy(results) {
|
|
808
811
|
for (const row of results.results.bindings) {
|
|
809
812
|
const raw = text(row.windowRows);
|
|
810
|
-
if (raw === null)
|
|
813
|
+
if (raw === null) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
811
816
|
const count = Number(raw);
|
|
812
|
-
if (Number.isInteger(count))
|
|
817
|
+
if (Number.isInteger(count)) {
|
|
818
|
+
return count >= TEXT_WINDOW;
|
|
819
|
+
}
|
|
813
820
|
}
|
|
814
821
|
return null;
|
|
815
822
|
}
|
|
@@ -818,10 +825,15 @@ function byPredicate(results) {
|
|
|
818
825
|
const index = /* @__PURE__ */ new Map();
|
|
819
826
|
for (const row of results.results.bindings) {
|
|
820
827
|
const predicate = text(row.p);
|
|
821
|
-
if (predicate === null)
|
|
828
|
+
if (predicate === null) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
822
831
|
const bucket = index.get(predicate);
|
|
823
|
-
if (bucket)
|
|
824
|
-
|
|
832
|
+
if (bucket) {
|
|
833
|
+
bucket.push(row);
|
|
834
|
+
} else {
|
|
835
|
+
index.set(predicate, [row]);
|
|
836
|
+
}
|
|
825
837
|
}
|
|
826
838
|
return index;
|
|
827
839
|
}
|
|
@@ -830,7 +842,9 @@ var allValues = (index, predicate) => {
|
|
|
830
842
|
const seen = /* @__PURE__ */ new Set();
|
|
831
843
|
for (const row of index.get(predicate) ?? []) {
|
|
832
844
|
const value = text(row.o);
|
|
833
|
-
if (value !== null)
|
|
845
|
+
if (value !== null) {
|
|
846
|
+
seen.add(value);
|
|
847
|
+
}
|
|
834
848
|
}
|
|
835
849
|
return [...seen];
|
|
836
850
|
};
|
|
@@ -841,8 +855,12 @@ function alignments(index, selfId) {
|
|
|
841
855
|
const found = {};
|
|
842
856
|
for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {
|
|
843
857
|
for (const address of allValues(index, predicate)) {
|
|
844
|
-
if (!address.startsWith("http"))
|
|
845
|
-
|
|
858
|
+
if (!address.startsWith("http")) {
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (selfId && address.includes(selfId)) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
846
864
|
let host;
|
|
847
865
|
try {
|
|
848
866
|
host = new URL(address).hostname.toLowerCase();
|
|
@@ -852,8 +870,12 @@ function alignments(index, selfId) {
|
|
|
852
870
|
const known = ALIGNMENT_HOSTS.find(
|
|
853
871
|
(entry) => host === entry.host || host.endsWith(`.${entry.host}`)
|
|
854
872
|
);
|
|
855
|
-
if (!known)
|
|
856
|
-
|
|
873
|
+
if (!known) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const addresses = found[known.name] ?? /* @__PURE__ */ new Set();
|
|
877
|
+
found[known.name] = addresses;
|
|
878
|
+
addresses.add(address);
|
|
857
879
|
}
|
|
858
880
|
}
|
|
859
881
|
return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));
|
|
@@ -868,14 +890,18 @@ function toAuthorSummaries(results, limit) {
|
|
|
868
890
|
const seen = /* @__PURE__ */ new Set();
|
|
869
891
|
let skipped = 0;
|
|
870
892
|
for (const row of results.results.bindings) {
|
|
871
|
-
if (isOccupancyRow(row, "person"))
|
|
893
|
+
if (isOccupancyRow(row, "person")) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
872
896
|
const iri = text(row.person);
|
|
873
897
|
const id = iri === null ? null : idFromIri(iri);
|
|
874
898
|
if (iri === null || id === null) {
|
|
875
899
|
skipped += 1;
|
|
876
900
|
continue;
|
|
877
901
|
}
|
|
878
|
-
if (seen.has(id))
|
|
902
|
+
if (seen.has(id)) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
879
905
|
seen.add(id);
|
|
880
906
|
rows.push({
|
|
881
907
|
id,
|
|
@@ -896,18 +922,24 @@ function toAuthorDetail(results, id, pageUrl) {
|
|
|
896
922
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
897
923
|
for (const row of index.get(P.altLabel) ?? []) {
|
|
898
924
|
const label = text(row.o);
|
|
899
|
-
if (label === null || seenLabels.has(label))
|
|
925
|
+
if (label === null || seenLabels.has(label)) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
900
928
|
seenLabels.add(label);
|
|
901
929
|
otherNames.push({ label, language: text(row.lang) });
|
|
902
930
|
}
|
|
903
931
|
const sameAs = alignments(index, id);
|
|
904
932
|
const isni = isniAddresses(index);
|
|
905
|
-
if (isni.length > 0)
|
|
933
|
+
if (isni.length > 0) {
|
|
934
|
+
sameAs.isni = [.../* @__PURE__ */ new Set([...sameAs.isni ?? [], ...isni])];
|
|
935
|
+
}
|
|
906
936
|
const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
|
|
907
937
|
const depictions = [];
|
|
908
938
|
for (const address of allValues(index, P.depiction)) {
|
|
909
939
|
const link = toDigitisedLink(address, "depiction", id, null);
|
|
910
|
-
if (link)
|
|
940
|
+
if (link) {
|
|
941
|
+
depictions.push(link);
|
|
942
|
+
}
|
|
911
943
|
}
|
|
912
944
|
return {
|
|
913
945
|
id,
|
|
@@ -941,8 +973,12 @@ function toAuthorDetail(results, id, pageUrl) {
|
|
|
941
973
|
function readStatus(statement, iri) {
|
|
942
974
|
if (statement !== null) {
|
|
943
975
|
const said = statement.toLowerCase();
|
|
944
|
-
if (said.includes("provisional"))
|
|
945
|
-
|
|
976
|
+
if (said.includes("provisional")) {
|
|
977
|
+
return "provisional";
|
|
978
|
+
}
|
|
979
|
+
if (said.includes("established")) {
|
|
980
|
+
return "established";
|
|
981
|
+
}
|
|
946
982
|
}
|
|
947
983
|
return iri.includes("/temp-work/") ? "provisional" : "established";
|
|
948
984
|
}
|
|
@@ -951,7 +987,9 @@ function toWorkSummaries(results, limit) {
|
|
|
951
987
|
const byId = /* @__PURE__ */ new Map();
|
|
952
988
|
let skipped = 0;
|
|
953
989
|
for (const row of results.results.bindings) {
|
|
954
|
-
if (isOccupancyRow(row, "work"))
|
|
990
|
+
if (isOccupancyRow(row, "work")) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
955
993
|
const iri = text(row.work);
|
|
956
994
|
const id = iri === null ? null : idFromIri(iri);
|
|
957
995
|
if (iri === null || id === null) {
|
|
@@ -979,7 +1017,7 @@ function toWorkSummaries(results, limit) {
|
|
|
979
1017
|
}
|
|
980
1018
|
return {
|
|
981
1019
|
...page(
|
|
982
|
-
order.map((id) => byId.get(id)),
|
|
1020
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
983
1021
|
limit,
|
|
984
1022
|
skipped
|
|
985
1023
|
),
|
|
@@ -1012,13 +1050,15 @@ function toAuthoredWorks(results, limit) {
|
|
|
1012
1050
|
order.push(id);
|
|
1013
1051
|
}
|
|
1014
1052
|
const form = text(row.form);
|
|
1015
|
-
if (form
|
|
1053
|
+
if (form?.startsWith("http")) {
|
|
1016
1054
|
const term = vocabularyTerm(form);
|
|
1017
|
-
if (!work.forms.includes(term))
|
|
1055
|
+
if (!work.forms.includes(term)) {
|
|
1056
|
+
work.forms.push(term);
|
|
1057
|
+
}
|
|
1018
1058
|
}
|
|
1019
1059
|
}
|
|
1020
1060
|
return page(
|
|
1021
|
-
order.map((id) => byId.get(id)),
|
|
1061
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
1022
1062
|
limit,
|
|
1023
1063
|
skipped
|
|
1024
1064
|
);
|
|
@@ -1031,13 +1071,17 @@ function toWorkDetail(results, id, iri, pageUrl) {
|
|
|
1031
1071
|
for (const row of index.get(P.creator) ?? []) {
|
|
1032
1072
|
const creatorIri = text(row.o);
|
|
1033
1073
|
const creatorId = creatorIri ? idFromIri(creatorIri) : null;
|
|
1034
|
-
if (!creatorId || creators.some((c) => c.id === creatorId))
|
|
1074
|
+
if (!creatorId || creators.some((c) => c.id === creatorId)) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1035
1077
|
creators.push({ id: creatorId, name: text(row.name) });
|
|
1036
1078
|
}
|
|
1037
1079
|
const depictions = [];
|
|
1038
1080
|
for (const address of allValues(index, P.depiction)) {
|
|
1039
1081
|
const link = toDigitisedLink(address, "depiction", id, null);
|
|
1040
|
-
if (link)
|
|
1082
|
+
if (link) {
|
|
1083
|
+
depictions.push(link);
|
|
1084
|
+
}
|
|
1041
1085
|
}
|
|
1042
1086
|
const statusStatement = firstValue(index, P.statusOfIdentification);
|
|
1043
1087
|
const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
|
|
@@ -1107,7 +1151,7 @@ function toEditions(results, limit) {
|
|
|
1107
1151
|
}
|
|
1108
1152
|
}
|
|
1109
1153
|
return page(
|
|
1110
|
-
order.map((id) => byId.get(id)),
|
|
1154
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
1111
1155
|
limit,
|
|
1112
1156
|
skipped
|
|
1113
1157
|
);
|
|
@@ -1126,7 +1170,9 @@ function toDigitisedLinks(results, limit) {
|
|
|
1126
1170
|
["depiction", "depiction"]
|
|
1127
1171
|
]) {
|
|
1128
1172
|
const link = toDigitisedLink(text(row[column]), role, fromId, title);
|
|
1129
|
-
if (!link || seen.has(link.url))
|
|
1173
|
+
if (!link || seen.has(link.url)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1130
1176
|
seen.add(link.url);
|
|
1131
1177
|
links.push(link);
|
|
1132
1178
|
}
|
|
@@ -1137,7 +1183,9 @@ function toTypes(results) {
|
|
|
1137
1183
|
const types = /* @__PURE__ */ new Set();
|
|
1138
1184
|
for (const row of results.results.bindings) {
|
|
1139
1185
|
const iri = text(row.type);
|
|
1140
|
-
if (iri !== null)
|
|
1186
|
+
if (iri !== null) {
|
|
1187
|
+
types.add(iri);
|
|
1188
|
+
}
|
|
1141
1189
|
}
|
|
1142
1190
|
return [...types];
|
|
1143
1191
|
}
|
|
@@ -1178,7 +1226,9 @@ var RateLimiter = class {
|
|
|
1178
1226
|
async beforeRequest() {
|
|
1179
1227
|
const elapsed = Date.now() - this.lastStartedAt;
|
|
1180
1228
|
const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);
|
|
1181
|
-
if (wait > 0)
|
|
1229
|
+
if (wait > 0) {
|
|
1230
|
+
await sleep(wait);
|
|
1231
|
+
}
|
|
1182
1232
|
this.lastStartedAt = Date.now();
|
|
1183
1233
|
}
|
|
1184
1234
|
/** Called when the service asks for room: double the gap, up to the ceiling. */
|
|
@@ -1191,9 +1241,13 @@ var RateLimiter = class {
|
|
|
1191
1241
|
* response after a rough patch does not undo the caution that earned it.
|
|
1192
1242
|
*/
|
|
1193
1243
|
succeeded() {
|
|
1194
|
-
if (this.intervalMs === this.baseIntervalMs)
|
|
1244
|
+
if (this.intervalMs === this.baseIntervalMs) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1195
1247
|
this.calmStreak += 1;
|
|
1196
|
-
if (this.calmStreak < 3)
|
|
1248
|
+
if (this.calmStreak < 3) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1197
1251
|
this.calmStreak = 0;
|
|
1198
1252
|
this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));
|
|
1199
1253
|
}
|
|
@@ -1212,7 +1266,9 @@ function withGuarantees(config) {
|
|
|
1212
1266
|
const defaults = loadConfig({});
|
|
1213
1267
|
const bounded = (value, fallback, min, max) => {
|
|
1214
1268
|
const parsed = Number(value);
|
|
1215
|
-
if (!Number.isFinite(parsed))
|
|
1269
|
+
if (!Number.isFinite(parsed)) {
|
|
1270
|
+
return fallback;
|
|
1271
|
+
}
|
|
1216
1272
|
return Math.min(max, Math.max(min, parsed));
|
|
1217
1273
|
};
|
|
1218
1274
|
const claimed = typeof config.userAgent === "string" ? config.userAgent.trim() : "";
|
|
@@ -1263,9 +1319,6 @@ var BnfClient = class {
|
|
|
1263
1319
|
return this.config.timeoutMs;
|
|
1264
1320
|
}
|
|
1265
1321
|
/** The one address this client sends anything to. */
|
|
1266
|
-
get endpoint() {
|
|
1267
|
-
return SPARQL_ENDPOINT;
|
|
1268
|
-
}
|
|
1269
1322
|
/**
|
|
1270
1323
|
* `cacheKey` is stated rather than taken from the query, because two reads of
|
|
1271
1324
|
* one query can produce different values: a list cut to five rows is not the
|
|
@@ -1309,32 +1362,7 @@ var BnfClient = class {
|
|
|
1309
1362
|
return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };
|
|
1310
1363
|
}
|
|
1311
1364
|
/** Read an identifier a caller wrote, refusing anything this dataset cannot address. */
|
|
1312
|
-
identify(input) {
|
|
1313
|
-
return parseEntityId(input);
|
|
1314
|
-
}
|
|
1315
1365
|
/** The words a search will actually look for, which a tool reports back. */
|
|
1316
|
-
words(input) {
|
|
1317
|
-
return toSearchWords(input);
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* The terms the index requires, which is what a row's presence rests on.
|
|
1321
|
-
*
|
|
1322
|
-
* The index splits inside a word at an apostrophe and at a hyphen, so a word
|
|
1323
|
-
* a caller wrote as one can reach it as two, each required on its own.
|
|
1324
|
-
*/
|
|
1325
|
-
indexTerms(words) {
|
|
1326
|
-
return toIndexTerms(words);
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* How a piece of caller text became the terms the search was run with.
|
|
1330
|
-
*
|
|
1331
|
-
* A tool reports the reading back, so the same reading has to be the one the
|
|
1332
|
-
* query was built from: the words, the terms the index makes of them, and
|
|
1333
|
-
* the characters that reached neither.
|
|
1334
|
-
*/
|
|
1335
|
-
searchReading(input) {
|
|
1336
|
-
return readSearchText(input);
|
|
1337
|
-
}
|
|
1338
1366
|
searchAuthors(name, limit, offset) {
|
|
1339
1367
|
const words = toSearchWords(name);
|
|
1340
1368
|
const query = searchAuthorsQuery(words, limit, offset);
|