mcp-databnf 1.0.0 → 1.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.
- package/CHANGELOG.md +169 -0
- package/LICENSE +0 -0
- package/README.md +78 -18
- package/dist/bnf/client.d.ts +73 -0
- package/dist/bnf/client.js +207 -38
- package/dist/bnf/client.js.map +1 -1
- package/dist/index.d.ts +0 -0
- package/dist/index.js +728 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bnf/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PKG_VERSION = "1.
|
|
2
|
+
var PKG_VERSION = "1.1.0";
|
|
3
3
|
var REPO_URL = "https://github.com/smeet666/mcp-databnf";
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
@@ -387,8 +387,36 @@ function escapeLiteral(value) {
|
|
|
387
387
|
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
388
|
}
|
|
389
389
|
var literal = (value) => `"${escapeLiteral(value)}"`;
|
|
390
|
+
var INVISIBLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/gu;
|
|
391
|
+
function withoutInvisibles(input) {
|
|
392
|
+
return input.replace(INVISIBLE, "");
|
|
393
|
+
}
|
|
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
|
+
}
|
|
390
418
|
function toSearchWords(input) {
|
|
391
|
-
const cleaned = input.normalize("NFC").replace(/[‘’ʼ]/gu, "'").replace(/[^\p{L}\p{N}'-]+/gu, " ");
|
|
419
|
+
const cleaned = withoutInvisibles(input).normalize("NFC").replace(/[‘’ʼ]/gu, "'").replace(/[^\p{L}\p{N}'-]+/gu, " ");
|
|
392
420
|
const words = [];
|
|
393
421
|
for (const raw of cleaned.split(" ")) {
|
|
394
422
|
const word = raw.replace(/^['-]+/u, "").replace(/['-]+$/u, "");
|
|
@@ -398,6 +426,15 @@ function toSearchWords(input) {
|
|
|
398
426
|
}
|
|
399
427
|
return words;
|
|
400
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);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return terms;
|
|
437
|
+
}
|
|
401
438
|
function containsAllWords(words) {
|
|
402
439
|
if (words.length === 0) {
|
|
403
440
|
throw invalidInput(
|
|
@@ -469,31 +506,47 @@ var TEXT_WINDOW = 400;
|
|
|
469
506
|
var prefixed = (body) => `${PREFIX_BLOCK}
|
|
470
507
|
${body.trim()}
|
|
471
508
|
`;
|
|
509
|
+
function windowOccupancy(predicate, words) {
|
|
510
|
+
return `
|
|
511
|
+
{
|
|
512
|
+
SELECT (COUNT(*) AS ?windowRows) WHERE {
|
|
513
|
+
SELECT DISTINCT ?windowEntity ?windowText WHERE {
|
|
514
|
+
?windowEntity ${predicate} ?windowText .
|
|
515
|
+
?windowText bif:contains ${containsAllWords(words)} .
|
|
516
|
+
}
|
|
517
|
+
LIMIT ${TEXT_WINDOW}
|
|
518
|
+
}
|
|
519
|
+
}`;
|
|
520
|
+
}
|
|
472
521
|
function searchAuthorsQuery(words, limit, offset) {
|
|
473
522
|
const take = boundedInteger(limit, 1, 50, "limit") + 1;
|
|
474
523
|
const skip = boundedInteger(offset, 0, 5e3, "offset");
|
|
475
524
|
return prefixed(`
|
|
476
|
-
SELECT ?person ?name ?label ?birthYear ?deathYear ?role WHERE {
|
|
525
|
+
SELECT ?person ?name ?label ?birthYear ?deathYear ?role ?windowRows WHERE {
|
|
477
526
|
{
|
|
478
|
-
|
|
479
|
-
{
|
|
480
|
-
|
|
481
|
-
?person
|
|
482
|
-
|
|
527
|
+
{
|
|
528
|
+
SELECT DISTINCT ?person WHERE {
|
|
529
|
+
{
|
|
530
|
+
SELECT DISTINCT ?person ?name WHERE {
|
|
531
|
+
?person foaf:name ?name .
|
|
532
|
+
?name bif:contains ${containsAllWords(words)} .
|
|
533
|
+
}
|
|
534
|
+
LIMIT ${TEXT_WINDOW}
|
|
483
535
|
}
|
|
484
|
-
|
|
536
|
+
?person a foaf:Person .
|
|
485
537
|
}
|
|
486
|
-
?person
|
|
538
|
+
ORDER BY ?person
|
|
539
|
+
LIMIT ${take} OFFSET ${skip}
|
|
487
540
|
}
|
|
488
|
-
|
|
489
|
-
|
|
541
|
+
OPTIONAL { ?person foaf:name ?name }
|
|
542
|
+
OPTIONAL { ?record foaf:focus ?person ; skos:prefLabel ?label }
|
|
543
|
+
OPTIONAL { ?person bnf:firstYear ?birthYear }
|
|
544
|
+
OPTIONAL { ?person bnf:lastYear ?deathYear }
|
|
545
|
+
OPTIONAL { ?person rdag2:biographicalInformation ?role }
|
|
490
546
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
OPTIONAL { ?person bnf:lastYear ?deathYear }
|
|
495
|
-
OPTIONAL { ?person rdag2:biographicalInformation ?role }
|
|
496
|
-
}`);
|
|
547
|
+
UNION${windowOccupancy("foaf:name", words)}
|
|
548
|
+
}
|
|
549
|
+
ORDER BY ?person`);
|
|
497
550
|
}
|
|
498
551
|
var MOST_TRIPLES = 2e3;
|
|
499
552
|
function authorQuery(id) {
|
|
@@ -516,29 +569,54 @@ function searchWorksQuery(words, limit, offset) {
|
|
|
516
569
|
const take = boundedInteger(limit, 1, 50, "limit") + 1;
|
|
517
570
|
const skip = boundedInteger(offset, 0, 5e3, "offset");
|
|
518
571
|
return prefixed(`
|
|
519
|
-
SELECT ?work ?title ?date ?status ?creator ?creatorName WHERE {
|
|
572
|
+
SELECT ?work ?title ?date ?status ?creator ?creatorName ?windowRows WHERE {
|
|
520
573
|
{
|
|
521
|
-
|
|
522
|
-
{
|
|
523
|
-
|
|
524
|
-
?work
|
|
525
|
-
|
|
574
|
+
{
|
|
575
|
+
SELECT DISTINCT ?work WHERE {
|
|
576
|
+
{
|
|
577
|
+
SELECT DISTINCT ?work ?title WHERE {
|
|
578
|
+
?work dcterms:title ?title .
|
|
579
|
+
?title bif:contains ${containsAllWords(words)} .
|
|
580
|
+
}
|
|
581
|
+
LIMIT ${TEXT_WINDOW}
|
|
526
582
|
}
|
|
527
|
-
|
|
583
|
+
?work a rdafrbr:Work .
|
|
528
584
|
}
|
|
529
|
-
?work
|
|
585
|
+
ORDER BY ?work
|
|
586
|
+
LIMIT ${take} OFFSET ${skip}
|
|
587
|
+
}
|
|
588
|
+
OPTIONAL { ?work dcterms:title ?title }
|
|
589
|
+
OPTIONAL { ?work dcterms:date ?date }
|
|
590
|
+
OPTIONAL { ?work rdae:statusOfIdentification ?status }
|
|
591
|
+
OPTIONAL {
|
|
592
|
+
?work dcterms:creator ?creator .
|
|
593
|
+
OPTIONAL { ?creator foaf:name ?creatorName }
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
UNION${windowOccupancy("dcterms:title", words)}
|
|
597
|
+
}
|
|
598
|
+
ORDER BY ?work`);
|
|
599
|
+
}
|
|
600
|
+
function worksByCreatorQuery(id, limit, offset) {
|
|
601
|
+
const person = iriOf(id);
|
|
602
|
+
const take = boundedInteger(limit, 1, 50, "limit") + 1;
|
|
603
|
+
const skip = boundedInteger(offset, 0, 5e3, "offset");
|
|
604
|
+
return prefixed(`
|
|
605
|
+
SELECT ?work ?title ?date ?year ?status ?form WHERE {
|
|
606
|
+
{
|
|
607
|
+
SELECT DISTINCT ?work WHERE {
|
|
608
|
+
?work dcterms:creator ${person} .
|
|
530
609
|
}
|
|
531
610
|
ORDER BY ?work
|
|
532
611
|
LIMIT ${take} OFFSET ${skip}
|
|
533
612
|
}
|
|
534
613
|
OPTIONAL { ?work dcterms:title ?title }
|
|
535
614
|
OPTIONAL { ?work dcterms:date ?date }
|
|
615
|
+
OPTIONAL { ?work bnf:firstYear ?year }
|
|
536
616
|
OPTIONAL { ?work rdae:statusOfIdentification ?status }
|
|
537
|
-
OPTIONAL {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
}`);
|
|
617
|
+
OPTIONAL { ?work rdae:formOfWork ?form }
|
|
618
|
+
}
|
|
619
|
+
ORDER BY ?work`);
|
|
542
620
|
}
|
|
543
621
|
function workQuery(id) {
|
|
544
622
|
const work = iriOf(id);
|
|
@@ -576,7 +654,8 @@ SELECT ?edition ?title ?date ?year ?publisher ?place ?editionStatement
|
|
|
576
654
|
OPTIONAL { ?edition rdfs:seeAlso ?catalogue }
|
|
577
655
|
OPTIONAL { ?edition rdarel:electronicReproduction ?reproduction }
|
|
578
656
|
OPTIONAL { ?edition bnf:OCR ?ocr }
|
|
579
|
-
}
|
|
657
|
+
}
|
|
658
|
+
ORDER BY ?edition`);
|
|
580
659
|
}
|
|
581
660
|
function digitisedForWorkQuery(id, limit) {
|
|
582
661
|
const work = iriOf(id);
|
|
@@ -713,12 +792,28 @@ function toDigitisedLink(address, role, fromId, fromTitle) {
|
|
|
713
792
|
if (!segment) return null;
|
|
714
793
|
const ark = segment.split(".")[0] ?? segment;
|
|
715
794
|
if (ark === "") return null;
|
|
716
|
-
return { ark, url: address, role, fromId, fromTitle };
|
|
795
|
+
return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };
|
|
796
|
+
}
|
|
797
|
+
function renderingOf(address, ark) {
|
|
798
|
+
const path = address.split(/[?#]/)[0] ?? address;
|
|
799
|
+
const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);
|
|
800
|
+
const rendering = after.replace(/^[./]+/, "").replace(/\/+$/, "");
|
|
801
|
+
return rendering === "" ? null : rendering;
|
|
717
802
|
}
|
|
718
803
|
function page(rows, limit, skipped) {
|
|
719
804
|
const more = rows.length > limit;
|
|
720
805
|
return skipped > 0 ? { rows: rows.slice(0, limit), hasMore: more, skipped } : { rows: rows.slice(0, limit), hasMore: more };
|
|
721
806
|
}
|
|
807
|
+
function readWindowOccupancy(results) {
|
|
808
|
+
for (const row of results.results.bindings) {
|
|
809
|
+
const raw = text(row.windowRows);
|
|
810
|
+
if (raw === null) continue;
|
|
811
|
+
const count = Number(raw);
|
|
812
|
+
if (Number.isInteger(count)) return count >= TEXT_WINDOW;
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
var isOccupancyRow = (row, entity) => row.windowRows !== void 0 && row[entity] === void 0;
|
|
722
817
|
function byPredicate(results) {
|
|
723
818
|
const index = /* @__PURE__ */ new Map();
|
|
724
819
|
for (const row of results.results.bindings) {
|
|
@@ -773,6 +868,7 @@ function toAuthorSummaries(results, limit) {
|
|
|
773
868
|
const seen = /* @__PURE__ */ new Set();
|
|
774
869
|
let skipped = 0;
|
|
775
870
|
for (const row of results.results.bindings) {
|
|
871
|
+
if (isOccupancyRow(row, "person")) continue;
|
|
776
872
|
const iri = text(row.person);
|
|
777
873
|
const id = iri === null ? null : idFromIri(iri);
|
|
778
874
|
if (iri === null || id === null) {
|
|
@@ -791,7 +887,7 @@ function toAuthorSummaries(results, limit) {
|
|
|
791
887
|
sourceUrl: publicPageFor(iri)
|
|
792
888
|
});
|
|
793
889
|
}
|
|
794
|
-
return page(rows, limit, skipped);
|
|
890
|
+
return { ...page(rows, limit, skipped), indexWindowFull: readWindowOccupancy(results) };
|
|
795
891
|
}
|
|
796
892
|
function toAuthorDetail(results, id, pageUrl) {
|
|
797
893
|
const index = byPredicate(results);
|
|
@@ -855,6 +951,7 @@ function toWorkSummaries(results, limit) {
|
|
|
855
951
|
const byId = /* @__PURE__ */ new Map();
|
|
856
952
|
let skipped = 0;
|
|
857
953
|
for (const row of results.results.bindings) {
|
|
954
|
+
if (isOccupancyRow(row, "work")) continue;
|
|
858
955
|
const iri = text(row.work);
|
|
859
956
|
const id = iri === null ? null : idFromIri(iri);
|
|
860
957
|
if (iri === null || id === null) {
|
|
@@ -880,6 +977,46 @@ function toWorkSummaries(results, limit) {
|
|
|
880
977
|
work.creators.push({ id: creatorId, name: text(row.creatorName) });
|
|
881
978
|
}
|
|
882
979
|
}
|
|
980
|
+
return {
|
|
981
|
+
...page(
|
|
982
|
+
order.map((id) => byId.get(id)),
|
|
983
|
+
limit,
|
|
984
|
+
skipped
|
|
985
|
+
),
|
|
986
|
+
indexWindowFull: readWindowOccupancy(results)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
function toAuthoredWorks(results, limit) {
|
|
990
|
+
const order = [];
|
|
991
|
+
const byId = /* @__PURE__ */ new Map();
|
|
992
|
+
let skipped = 0;
|
|
993
|
+
for (const row of results.results.bindings) {
|
|
994
|
+
const iri = text(row.work);
|
|
995
|
+
const id = iri === null ? null : idFromIri(iri);
|
|
996
|
+
if (iri === null || id === null) {
|
|
997
|
+
skipped += 1;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
let work = byId.get(id);
|
|
1001
|
+
if (!work) {
|
|
1002
|
+
work = {
|
|
1003
|
+
id,
|
|
1004
|
+
title: text(row.title),
|
|
1005
|
+
date: text(row.date),
|
|
1006
|
+
year: integer(row.year),
|
|
1007
|
+
forms: [],
|
|
1008
|
+
status: readStatus(text(row.status), iri),
|
|
1009
|
+
sourceUrl: publicPageFor(iri)
|
|
1010
|
+
};
|
|
1011
|
+
byId.set(id, work);
|
|
1012
|
+
order.push(id);
|
|
1013
|
+
}
|
|
1014
|
+
const form = text(row.form);
|
|
1015
|
+
if (form !== null && form.startsWith("http")) {
|
|
1016
|
+
const term = vocabularyTerm(form);
|
|
1017
|
+
if (!work.forms.includes(term)) work.forms.push(term);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
883
1020
|
return page(
|
|
884
1021
|
order.map((id) => byId.get(id)),
|
|
885
1022
|
limit,
|
|
@@ -1179,6 +1316,25 @@ var BnfClient = class {
|
|
|
1179
1316
|
words(input) {
|
|
1180
1317
|
return toSearchWords(input);
|
|
1181
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
|
+
}
|
|
1182
1338
|
searchAuthors(name, limit, offset) {
|
|
1183
1339
|
const words = toSearchWords(name);
|
|
1184
1340
|
const query = searchAuthorsQuery(words, limit, offset);
|
|
@@ -1195,7 +1351,7 @@ var BnfClient = class {
|
|
|
1195
1351
|
(results) => toAuthorDetail(results, id.id, id.pageUrl)
|
|
1196
1352
|
);
|
|
1197
1353
|
if (!result.data.types.includes(PERSON_CLASS)) {
|
|
1198
|
-
await this.refuse(id,
|
|
1354
|
+
await this.refuse(id, "a person", "get_work reads a work.");
|
|
1199
1355
|
}
|
|
1200
1356
|
return result;
|
|
1201
1357
|
}
|
|
@@ -1206,8 +1362,8 @@ var BnfClient = class {
|
|
|
1206
1362
|
* something else are two different answers, and only one of them means the
|
|
1207
1363
|
* caller should go looking elsewhere.
|
|
1208
1364
|
*/
|
|
1209
|
-
async refuse(id,
|
|
1210
|
-
const known =
|
|
1365
|
+
async refuse(id, wanted, hint) {
|
|
1366
|
+
const known = (await this.read(`types:${id.id}`, kindQuery(id), toTypes)).data;
|
|
1211
1367
|
if (known.length === 0) {
|
|
1212
1368
|
throw notFound(`data.bnf.fr describes nothing at "${id.id}".`, {
|
|
1213
1369
|
hint: "Find the identifier with search_authors or search_works rather than writing one.",
|
|
@@ -1237,13 +1393,26 @@ var BnfClient = class {
|
|
|
1237
1393
|
if (!result.data.types.some((type) => WORK_CLASSES.has(type))) {
|
|
1238
1394
|
await this.refuse(
|
|
1239
1395
|
id,
|
|
1240
|
-
result.data.types,
|
|
1241
1396
|
"a work",
|
|
1242
1397
|
"get_author reads a person, and list_editions reads the editions of a work."
|
|
1243
1398
|
);
|
|
1244
1399
|
}
|
|
1245
1400
|
return result;
|
|
1246
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The works one person is named the creator of.
|
|
1404
|
+
*
|
|
1405
|
+
* The record is not checked here. A person the catalogue credits with nothing
|
|
1406
|
+
* and an address that names no person both answer with no rows, and telling
|
|
1407
|
+
* them apart costs a second query that only an empty answer needs.
|
|
1408
|
+
*/
|
|
1409
|
+
worksByAuthor(id, limit, offset) {
|
|
1410
|
+
return this.read(
|
|
1411
|
+
`author-works:${id.id}:${limit}:${offset}`,
|
|
1412
|
+
worksByCreatorQuery(id, limit, offset),
|
|
1413
|
+
(results) => toAuthoredWorks(results, limit)
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1247
1416
|
listEditions(id, limit, offset) {
|
|
1248
1417
|
const query = editionsQuery(id, limit, offset);
|
|
1249
1418
|
return this.read(
|