mcp-databnf 1.1.1 → 2.1.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.1.1";
5
+ var PKG_VERSION = "2.1.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) return;
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() === "") 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-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) return void 0;
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) return;
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) break;
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(/#.*$/, "").replace(/^http:/, "https:");
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) return null;
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) return Math.round(seconds * 1e3);
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)) return null;
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 = /^Virtuoso\s+(\w+)\s+Error\s+([\s\S]*)$/.exec(body.trim());
210
- if (!match) return null;
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
- await new Promise((resolve) => setTimeout(resolve, askedWaitMs));
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
- if (RETRYABLE.has(response.status)) limiter.pushBack();
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
- 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 (response.status === 400 || response.status === 422) {
307
- const engine = readEngineError(response.body);
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
- throw networkError(`data.bnf.fr answered HTTP ${response.status}.`, {
314
- url: SPARQL_ENDPOINT,
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
- if (error instanceof Error && error.name === "BnfError") throw error;
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,13 @@ function parseResults(body) {
383
436
  }
384
437
 
385
438
  // src/bnf/sparql.ts
439
+ var LEADING_JOINERS = /^['-]+/u;
440
+ var TRAILING_JOINERS = /['-]+$/u;
441
+ var ARK_IN_INPUT = /(?:^|\/)ark:\/12148\/(c[bc][0-9a-z]+)/i;
442
+ var LEADING_HASH = /^#/;
443
+ var FRAGMENT_OR_QUERY = /[#?]/;
444
+ var TEMP_WORK_IN_INPUT = /temp-work\/([0-9a-f]{32})/i;
445
+ var GALLICA_ARK = /ark:\/12148\/([0-9a-z]+)/i;
386
446
  function escapeLiteral(value) {
387
447
  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
448
  }
@@ -391,49 +451,20 @@ var INVISIBLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200
391
451
  function withoutInvisibles(input) {
392
452
  return input.replace(INVISIBLE, "");
393
453
  }
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
454
  function toSearchWords(input) {
419
455
  const cleaned = withoutInvisibles(input).normalize("NFC").replace(/[‘’ʼ]/gu, "'").replace(/[^\p{L}\p{N}'-]+/gu, " ");
420
456
  const words = [];
421
457
  for (const raw of cleaned.split(" ")) {
422
- const word = raw.replace(/^['-]+/u, "").replace(/['-]+$/u, "");
423
- if (word === "") continue;
458
+ const word = raw.replace(LEADING_JOINERS, "").replace(TRAILING_JOINERS, "");
459
+ if (word === "") {
460
+ continue;
461
+ }
424
462
  words.push(word.length > 60 ? word.slice(0, 60) : word);
425
- if (words.length >= 12) break;
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);
463
+ if (words.length >= 12) {
464
+ break;
434
465
  }
435
466
  }
436
- return terms;
467
+ return words;
437
468
  }
438
469
  function containsAllWords(words) {
439
470
  if (words.length === 0) {
@@ -452,8 +483,8 @@ function parseEntityId(input) {
452
483
  if (trimmed === "") {
453
484
  throw invalidInput("An identifier is required.");
454
485
  }
455
- const arkMatch = /(?:^|\/)ark:\/12148\/(c[bc][0-9a-z]+)/i.exec(trimmed);
456
- const bare = arkMatch ? arkMatch[1] : trimmed.replace(/^#/, "").split(/[#?]/)[0];
486
+ const arkMatch = ARK_IN_INPUT.exec(trimmed);
487
+ const bare = arkMatch ? arkMatch[1] : trimmed.replace(LEADING_HASH, "").split(FRAGMENT_OR_QUERY)[0];
457
488
  const candidate = (bare ?? "").toLowerCase();
458
489
  if (ARK_ID.test(candidate)) {
459
490
  return {
@@ -463,7 +494,7 @@ function parseEntityId(input) {
463
494
  pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`
464
495
  };
465
496
  }
466
- const tempMatch = /temp-work\/([0-9a-f]{32})/i.exec(trimmed);
497
+ const tempMatch = TEMP_WORK_IN_INPUT.exec(trimmed);
467
498
  const digest = (tempMatch?.[1] ?? candidate).toLowerCase();
468
499
  if (TEMP_WORK_ID.test(digest)) {
469
500
  return {
@@ -484,7 +515,7 @@ function hintFor(input) {
484
515
  const name = (page2[2] ?? "").replace(/_/g, " ");
485
516
  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
517
  }
487
- const gallicaArk = /ark:\/12148\/([0-9a-z]+)/i.exec(input)?.[1] ?? input;
518
+ const gallicaArk = GALLICA_ARK.exec(input)?.[1] ?? input;
488
519
  if (GALLICA_DOCUMENT.test(gallicaArk)) {
489
520
  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
521
  }
@@ -492,7 +523,7 @@ function hintFor(input) {
492
523
  }
493
524
  var iriOf = (id) => `<${id.iri}>`;
494
525
  function boundedInteger(value, min, max, name) {
495
- if (!Number.isFinite(value) || !Number.isInteger(value)) {
526
+ if (!(Number.isFinite(value) && Number.isInteger(value))) {
496
527
  throw invalidInput(`'${name}' has to be a whole number.`);
497
528
  }
498
529
  if (value < min || value > max) {
@@ -713,6 +744,14 @@ LIMIT 20`);
713
744
  }
714
745
 
715
746
  // src/bnf/parse.ts
747
+ var ARK_IN_IRI = /ark:\/12148\/(c[bc][0-9a-z]+)/i;
748
+ var TEMP_WORK_IN_IRI = /temp-work\/([0-9a-f]{32})/i;
749
+ var TRAILING_SEPARATOR = /[/#]$/;
750
+ var PATH_SEPARATOR = /[/#]/;
751
+ var ARK_SEGMENT = /ark:\/12148\/([^/?#]+)/;
752
+ var QUERY_START = /[?#]/;
753
+ var LEADING_DOTS = /^[./]+/;
754
+ var TRAILING_SLASHES = /\/+$/;
716
755
  var P = {
717
756
  type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
718
757
  label: "http://www.w3.org/2000/01/rdf-schema#label",
@@ -768,36 +807,50 @@ var ALIGNMENT_HOSTS = [
768
807
  { host: "catalogue.bnf.fr", name: "catalogue" }
769
808
  ];
770
809
  var text = (term) => {
771
- if (!term) return null;
810
+ if (!term) {
811
+ return null;
812
+ }
772
813
  const value = term.value.trim();
773
814
  return value === "" ? null : value;
774
815
  };
775
816
  var integer = (term) => {
776
817
  const raw = text(term);
777
- if (raw === null) return null;
818
+ if (raw === null) {
819
+ return null;
820
+ }
778
821
  const value = Number(raw);
779
822
  return Number.isInteger(value) ? value : null;
780
823
  };
781
824
  function idFromIri(iri) {
782
- const ark = /ark:\/12148\/(c[bc][0-9a-z]+)/i.exec(iri);
783
- if (ark?.[1]) return ark[1].toLowerCase();
784
- const temp = /temp-work\/([0-9a-f]{32})/i.exec(iri);
785
- if (temp?.[1]) return `temp-work/${temp[1].toLowerCase()}`;
825
+ const ark = ARK_IN_IRI.exec(iri);
826
+ if (ark?.[1]) {
827
+ return ark[1].toLowerCase();
828
+ }
829
+ const temp = TEMP_WORK_IN_IRI.exec(iri);
830
+ if (temp?.[1]) {
831
+ return `temp-work/${temp[1].toLowerCase()}`;
832
+ }
786
833
  return null;
787
834
  }
788
- var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(/[/#]$/, "").split(/[/#]/).pop() ?? iri);
835
+ var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(TRAILING_SEPARATOR, "").split(PATH_SEPARATOR).pop() ?? iri);
789
836
  function toDigitisedLink(address, role, fromId, fromTitle) {
790
- if (!address || !isGallicaAddress(address)) return null;
791
- const segment = /ark:\/12148\/([^/?#]+)/.exec(address)?.[1] ?? null;
792
- if (!segment) return null;
837
+ if (!(address && isGallicaAddress(address))) {
838
+ return null;
839
+ }
840
+ const segment = ARK_SEGMENT.exec(address)?.[1] ?? null;
841
+ if (!segment) {
842
+ return null;
843
+ }
793
844
  const ark = segment.split(".")[0] ?? segment;
794
- if (ark === "") return null;
845
+ if (ark === "") {
846
+ return null;
847
+ }
795
848
  return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };
796
849
  }
797
850
  function renderingOf(address, ark) {
798
- const path = address.split(/[?#]/)[0] ?? address;
851
+ const path = address.split(QUERY_START)[0] ?? address;
799
852
  const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);
800
- const rendering = after.replace(/^[./]+/, "").replace(/\/+$/, "");
853
+ const rendering = after.replace(LEADING_DOTS, "").replace(TRAILING_SLASHES, "");
801
854
  return rendering === "" ? null : rendering;
802
855
  }
803
856
  function page(rows, limit, skipped) {
@@ -807,9 +860,13 @@ function page(rows, limit, skipped) {
807
860
  function readWindowOccupancy(results) {
808
861
  for (const row of results.results.bindings) {
809
862
  const raw = text(row.windowRows);
810
- if (raw === null) continue;
863
+ if (raw === null) {
864
+ continue;
865
+ }
811
866
  const count = Number(raw);
812
- if (Number.isInteger(count)) return count >= TEXT_WINDOW;
867
+ if (Number.isInteger(count)) {
868
+ return count >= TEXT_WINDOW;
869
+ }
813
870
  }
814
871
  return null;
815
872
  }
@@ -818,10 +875,15 @@ function byPredicate(results) {
818
875
  const index = /* @__PURE__ */ new Map();
819
876
  for (const row of results.results.bindings) {
820
877
  const predicate = text(row.p);
821
- if (predicate === null) continue;
878
+ if (predicate === null) {
879
+ continue;
880
+ }
822
881
  const bucket = index.get(predicate);
823
- if (bucket) bucket.push(row);
824
- else index.set(predicate, [row]);
882
+ if (bucket) {
883
+ bucket.push(row);
884
+ } else {
885
+ index.set(predicate, [row]);
886
+ }
825
887
  }
826
888
  return index;
827
889
  }
@@ -830,7 +892,9 @@ var allValues = (index, predicate) => {
830
892
  const seen = /* @__PURE__ */ new Set();
831
893
  for (const row of index.get(predicate) ?? []) {
832
894
  const value = text(row.o);
833
- if (value !== null) seen.add(value);
895
+ if (value !== null) {
896
+ seen.add(value);
897
+ }
834
898
  }
835
899
  return [...seen];
836
900
  };
@@ -841,8 +905,12 @@ function alignments(index, selfId) {
841
905
  const found = {};
842
906
  for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {
843
907
  for (const address of allValues(index, predicate)) {
844
- if (!address.startsWith("http")) continue;
845
- if (selfId && address.includes(selfId)) continue;
908
+ if (!address.startsWith("http")) {
909
+ continue;
910
+ }
911
+ if (selfId && address.includes(selfId)) {
912
+ continue;
913
+ }
846
914
  let host;
847
915
  try {
848
916
  host = new URL(address).hostname.toLowerCase();
@@ -852,8 +920,12 @@ function alignments(index, selfId) {
852
920
  const known = ALIGNMENT_HOSTS.find(
853
921
  (entry) => host === entry.host || host.endsWith(`.${entry.host}`)
854
922
  );
855
- if (!known) continue;
856
- (found[known.name] ??= /* @__PURE__ */ new Set()).add(address);
923
+ if (!known) {
924
+ continue;
925
+ }
926
+ const addresses = found[known.name] ?? /* @__PURE__ */ new Set();
927
+ found[known.name] = addresses;
928
+ addresses.add(address);
857
929
  }
858
930
  }
859
931
  return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));
@@ -868,14 +940,18 @@ function toAuthorSummaries(results, limit) {
868
940
  const seen = /* @__PURE__ */ new Set();
869
941
  let skipped = 0;
870
942
  for (const row of results.results.bindings) {
871
- if (isOccupancyRow(row, "person")) continue;
943
+ if (isOccupancyRow(row, "person")) {
944
+ continue;
945
+ }
872
946
  const iri = text(row.person);
873
947
  const id = iri === null ? null : idFromIri(iri);
874
948
  if (iri === null || id === null) {
875
949
  skipped += 1;
876
950
  continue;
877
951
  }
878
- if (seen.has(id)) continue;
952
+ if (seen.has(id)) {
953
+ continue;
954
+ }
879
955
  seen.add(id);
880
956
  rows.push({
881
957
  id,
@@ -896,18 +972,24 @@ function toAuthorDetail(results, id, pageUrl) {
896
972
  const seenLabels = /* @__PURE__ */ new Set();
897
973
  for (const row of index.get(P.altLabel) ?? []) {
898
974
  const label = text(row.o);
899
- if (label === null || seenLabels.has(label)) continue;
975
+ if (label === null || seenLabels.has(label)) {
976
+ continue;
977
+ }
900
978
  seenLabels.add(label);
901
979
  otherNames.push({ label, language: text(row.lang) });
902
980
  }
903
981
  const sameAs = alignments(index, id);
904
982
  const isni = isniAddresses(index);
905
- if (isni.length > 0) sameAs.isni = [.../* @__PURE__ */ new Set([...sameAs.isni ?? [], ...isni])];
983
+ if (isni.length > 0) {
984
+ sameAs.isni = [.../* @__PURE__ */ new Set([...sameAs.isni ?? [], ...isni])];
985
+ }
906
986
  const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
907
987
  const depictions = [];
908
988
  for (const address of allValues(index, P.depiction)) {
909
989
  const link = toDigitisedLink(address, "depiction", id, null);
910
- if (link) depictions.push(link);
990
+ if (link) {
991
+ depictions.push(link);
992
+ }
911
993
  }
912
994
  return {
913
995
  id,
@@ -941,8 +1023,12 @@ function toAuthorDetail(results, id, pageUrl) {
941
1023
  function readStatus(statement, iri) {
942
1024
  if (statement !== null) {
943
1025
  const said = statement.toLowerCase();
944
- if (said.includes("provisional")) return "provisional";
945
- if (said.includes("established")) return "established";
1026
+ if (said.includes("provisional")) {
1027
+ return "provisional";
1028
+ }
1029
+ if (said.includes("established")) {
1030
+ return "established";
1031
+ }
946
1032
  }
947
1033
  return iri.includes("/temp-work/") ? "provisional" : "established";
948
1034
  }
@@ -951,7 +1037,9 @@ function toWorkSummaries(results, limit) {
951
1037
  const byId = /* @__PURE__ */ new Map();
952
1038
  let skipped = 0;
953
1039
  for (const row of results.results.bindings) {
954
- if (isOccupancyRow(row, "work")) continue;
1040
+ if (isOccupancyRow(row, "work")) {
1041
+ continue;
1042
+ }
955
1043
  const iri = text(row.work);
956
1044
  const id = iri === null ? null : idFromIri(iri);
957
1045
  if (iri === null || id === null) {
@@ -979,7 +1067,7 @@ function toWorkSummaries(results, limit) {
979
1067
  }
980
1068
  return {
981
1069
  ...page(
982
- order.map((id) => byId.get(id)),
1070
+ order.map((id) => byId.get(id)).filter((one) => one !== void 0),
983
1071
  limit,
984
1072
  skipped
985
1073
  ),
@@ -1012,13 +1100,15 @@ function toAuthoredWorks(results, limit) {
1012
1100
  order.push(id);
1013
1101
  }
1014
1102
  const form = text(row.form);
1015
- if (form !== null && form.startsWith("http")) {
1103
+ if (form?.startsWith("http")) {
1016
1104
  const term = vocabularyTerm(form);
1017
- if (!work.forms.includes(term)) work.forms.push(term);
1105
+ if (!work.forms.includes(term)) {
1106
+ work.forms.push(term);
1107
+ }
1018
1108
  }
1019
1109
  }
1020
1110
  return page(
1021
- order.map((id) => byId.get(id)),
1111
+ order.map((id) => byId.get(id)).filter((one) => one !== void 0),
1022
1112
  limit,
1023
1113
  skipped
1024
1114
  );
@@ -1031,13 +1121,17 @@ function toWorkDetail(results, id, iri, pageUrl) {
1031
1121
  for (const row of index.get(P.creator) ?? []) {
1032
1122
  const creatorIri = text(row.o);
1033
1123
  const creatorId = creatorIri ? idFromIri(creatorIri) : null;
1034
- if (!creatorId || creators.some((c) => c.id === creatorId)) continue;
1124
+ if (!creatorId || creators.some((c) => c.id === creatorId)) {
1125
+ continue;
1126
+ }
1035
1127
  creators.push({ id: creatorId, name: text(row.name) });
1036
1128
  }
1037
1129
  const depictions = [];
1038
1130
  for (const address of allValues(index, P.depiction)) {
1039
1131
  const link = toDigitisedLink(address, "depiction", id, null);
1040
- if (link) depictions.push(link);
1132
+ if (link) {
1133
+ depictions.push(link);
1134
+ }
1041
1135
  }
1042
1136
  const statusStatement = firstValue(index, P.statusOfIdentification);
1043
1137
  const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
@@ -1107,7 +1201,7 @@ function toEditions(results, limit) {
1107
1201
  }
1108
1202
  }
1109
1203
  return page(
1110
- order.map((id) => byId.get(id)),
1204
+ order.map((id) => byId.get(id)).filter((one) => one !== void 0),
1111
1205
  limit,
1112
1206
  skipped
1113
1207
  );
@@ -1126,7 +1220,9 @@ function toDigitisedLinks(results, limit) {
1126
1220
  ["depiction", "depiction"]
1127
1221
  ]) {
1128
1222
  const link = toDigitisedLink(text(row[column]), role, fromId, title);
1129
- if (!link || seen.has(link.url)) continue;
1223
+ if (!link || seen.has(link.url)) {
1224
+ continue;
1225
+ }
1130
1226
  seen.add(link.url);
1131
1227
  links.push(link);
1132
1228
  }
@@ -1137,7 +1233,9 @@ function toTypes(results) {
1137
1233
  const types = /* @__PURE__ */ new Set();
1138
1234
  for (const row of results.results.bindings) {
1139
1235
  const iri = text(row.type);
1140
- if (iri !== null) types.add(iri);
1236
+ if (iri !== null) {
1237
+ types.add(iri);
1238
+ }
1141
1239
  }
1142
1240
  return [...types];
1143
1241
  }
@@ -1178,7 +1276,9 @@ var RateLimiter = class {
1178
1276
  async beforeRequest() {
1179
1277
  const elapsed = Date.now() - this.lastStartedAt;
1180
1278
  const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);
1181
- if (wait > 0) await sleep(wait);
1279
+ if (wait > 0) {
1280
+ await sleep(wait);
1281
+ }
1182
1282
  this.lastStartedAt = Date.now();
1183
1283
  }
1184
1284
  /** Called when the service asks for room: double the gap, up to the ceiling. */
@@ -1191,9 +1291,13 @@ var RateLimiter = class {
1191
1291
  * response after a rough patch does not undo the caution that earned it.
1192
1292
  */
1193
1293
  succeeded() {
1194
- if (this.intervalMs === this.baseIntervalMs) return;
1294
+ if (this.intervalMs === this.baseIntervalMs) {
1295
+ return;
1296
+ }
1195
1297
  this.calmStreak += 1;
1196
- if (this.calmStreak < 3) return;
1298
+ if (this.calmStreak < 3) {
1299
+ return;
1300
+ }
1197
1301
  this.calmStreak = 0;
1198
1302
  this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));
1199
1303
  }
@@ -1212,7 +1316,9 @@ function withGuarantees(config) {
1212
1316
  const defaults = loadConfig({});
1213
1317
  const bounded = (value, fallback, min, max) => {
1214
1318
  const parsed = Number(value);
1215
- if (!Number.isFinite(parsed)) return fallback;
1319
+ if (!Number.isFinite(parsed)) {
1320
+ return fallback;
1321
+ }
1216
1322
  return Math.min(max, Math.max(min, parsed));
1217
1323
  };
1218
1324
  const claimed = typeof config.userAgent === "string" ? config.userAgent.trim() : "";
@@ -1263,9 +1369,6 @@ var BnfClient = class {
1263
1369
  return this.config.timeoutMs;
1264
1370
  }
1265
1371
  /** The one address this client sends anything to. */
1266
- get endpoint() {
1267
- return SPARQL_ENDPOINT;
1268
- }
1269
1372
  /**
1270
1373
  * `cacheKey` is stated rather than taken from the query, because two reads of
1271
1374
  * one query can produce different values: a list cut to five rows is not the
@@ -1309,32 +1412,7 @@ var BnfClient = class {
1309
1412
  return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };
1310
1413
  }
1311
1414
  /** Read an identifier a caller wrote, refusing anything this dataset cannot address. */
1312
- identify(input) {
1313
- return parseEntityId(input);
1314
- }
1315
1415
  /** 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
1416
  searchAuthors(name, limit, offset) {
1339
1417
  const words = toSearchWords(name);
1340
1418
  const query = searchAuthorsQuery(words, limit, offset);
@@ -1443,6 +1521,7 @@ var BnfClient = class {
1443
1521
  }
1444
1522
  };
1445
1523
  export {
1446
- BnfClient
1524
+ BnfClient,
1525
+ parseEntityId
1447
1526
  };
1448
1527
  //# sourceMappingURL=client.js.map