@kgdiff/formats 0.0.0-stage → 0.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/detect.d.ts +47 -0
  4. package/dist/detect.d.ts.map +1 -0
  5. package/dist/detect.js +425 -0
  6. package/dist/detect.js.map +1 -0
  7. package/dist/index.d.ts +203 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +114 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/iri.d.ts +13 -0
  12. package/dist/iri.d.ts.map +1 -0
  13. package/dist/iri.js +91 -0
  14. package/dist/iri.js.map +1 -0
  15. package/dist/json.d.ts +30 -0
  16. package/dist/json.d.ts.map +1 -0
  17. package/dist/json.js +749 -0
  18. package/dist/json.js.map +1 -0
  19. package/dist/jsonld.d.ts +16 -0
  20. package/dist/jsonld.d.ts.map +1 -0
  21. package/dist/jsonld.js +1249 -0
  22. package/dist/jsonld.js.map +1 -0
  23. package/dist/obo.d.ts +25 -0
  24. package/dist/obo.d.ts.map +1 -0
  25. package/dist/obo.js +478 -0
  26. package/dist/obo.js.map +1 -0
  27. package/dist/prefixes.d.ts +49 -0
  28. package/dist/prefixes.d.ts.map +1 -0
  29. package/dist/prefixes.js +177 -0
  30. package/dist/prefixes.js.map +1 -0
  31. package/dist/rdf.d.ts +46 -0
  32. package/dist/rdf.d.ts.map +1 -0
  33. package/dist/rdf.js +1047 -0
  34. package/dist/rdf.js.map +1 -0
  35. package/dist/rdfxml.d.ts +8 -0
  36. package/dist/rdfxml.d.ts.map +1 -0
  37. package/dist/rdfxml.js +529 -0
  38. package/dist/rdfxml.js.map +1 -0
  39. package/dist/shared.d.ts +117 -0
  40. package/dist/shared.d.ts.map +1 -0
  41. package/dist/shared.js +355 -0
  42. package/dist/shared.js.map +1 -0
  43. package/dist/xml.d.ts +50 -0
  44. package/dist/xml.d.ts.map +1 -0
  45. package/dist/xml.js +508 -0
  46. package/dist/xml.js.map +1 -0
  47. package/package.json +55 -3
  48. package/src/detect.ts +435 -0
  49. package/src/index.ts +296 -0
  50. package/src/iri.ts +86 -0
  51. package/src/json.ts +835 -0
  52. package/src/jsonld.ts +1376 -0
  53. package/src/obo.ts +557 -0
  54. package/src/prefixes.ts +195 -0
  55. package/src/rdf.ts +1181 -0
  56. package/src/rdfxml.ts +633 -0
  57. package/src/shared.ts +408 -0
  58. package/src/xml.ts +550 -0
@@ -0,0 +1,195 @@
1
+ /**
2
+ * IRI compaction: well-known prefixes, declared prefixes and the OBO PURL rule
3
+ * (`http://purl.obolibrary.org/obo/GO_0008150` → `GO:0008150`).
4
+ */
5
+ import { compareStrings, getOwn, setOwn } from './shared.js';
6
+
7
+ export const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
8
+ export const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
9
+ export const OWL_NS = 'http://www.w3.org/2002/07/owl#';
10
+ export const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
11
+ export const SKOS_NS = 'http://www.w3.org/2004/02/skos/core#';
12
+ export const OBO_IN_OWL_NS = 'http://www.geneontology.org/formats/oboInOwl#';
13
+ export const OBO_PURL = 'http://purl.obolibrary.org/obo/';
14
+
15
+ /** Prefixes every parser knows without a declaration. */
16
+ export const WELL_KNOWN_PREFIXES: Readonly<Record<string, string>> = Object.freeze({
17
+ dc: 'http://purl.org/dc/elements/1.1/',
18
+ dcterms: 'http://purl.org/dc/terms/',
19
+ foaf: 'http://xmlns.com/foaf/0.1/',
20
+ obo: OBO_PURL,
21
+ oboInOwl: OBO_IN_OWL_NS,
22
+ owl: OWL_NS,
23
+ prov: 'http://www.w3.org/ns/prov#',
24
+ rdf: RDF_NS,
25
+ rdfs: RDFS_NS,
26
+ schema: 'https://schema.org/',
27
+ skos: SKOS_NS,
28
+ xsd: XSD_NS,
29
+ });
30
+
31
+ /** Namespaces that compact to a well-known prefix besides its canonical namespace. */
32
+ const WELL_KNOWN_VARIANTS: readonly (readonly [string, string])[] = [
33
+ ['schema', 'http://schema.org/'],
34
+ ];
35
+
36
+ /** OBO Foundry id spaces: a letter followed by letters / digits, then `_` and a local id. */
37
+ const OBO_LOCAL_ID = /^([A-Za-z][A-Za-z0-9]*)_([^\s/#?]+)$/;
38
+
39
+ /** `http://purl.obolibrary.org/obo/GO_0008150` → `GO:0008150`; `undefined` for other IRIs. */
40
+ export function oboCurie(iri: string): string | undefined {
41
+ if (!iri.startsWith(OBO_PURL)) return undefined;
42
+ const match = OBO_LOCAL_ID.exec(iri.slice(OBO_PURL.length));
43
+ return match ? `${match[1]}:${match[2]}` : undefined;
44
+ }
45
+
46
+ /**
47
+ * Stable node id for an IRI: OBO PURLs become CURIEs, every other IRI stays whole so the same
48
+ * resource gets the same id whatever prefixes a file declares.
49
+ */
50
+ export function nodeIdForIri(iri: string): string {
51
+ return oboCurie(iri) ?? iri;
52
+ }
53
+
54
+ /** Where a prefix came from. */
55
+ export type PrefixSource = 'user' | 'well-known' | 'declared';
56
+
57
+ /** Preference when several names share one namespace (lower wins). */
58
+ const SOURCE_PREFERENCE: Readonly<Record<PrefixSource, number>> = {
59
+ user: 0,
60
+ 'well-known': 1,
61
+ declared: 2,
62
+ };
63
+
64
+ /** Override order per name: well-known, then declared, then user. */
65
+ const SOURCE_OVERRIDE: Readonly<Record<PrefixSource, number>> = {
66
+ 'well-known': 0,
67
+ declared: 1,
68
+ user: 2,
69
+ };
70
+
71
+ interface Entry {
72
+ readonly prefix: string;
73
+ readonly namespace: string;
74
+ readonly preference: number;
75
+ }
76
+
77
+ /**
78
+ * Compiled prefix map. Compaction picks the longest matching namespace; when several prefixes
79
+ * share a namespace, explicit options beat well-known names, which beat file declarations,
80
+ * so `rdfs:` stays `rdfs:` even in a file that declares it under another name.
81
+ */
82
+ export class PrefixTable {
83
+ /** Name → namespace, as reported in `ParseResult.prefixes` (sorted by name). */
84
+ readonly record: Readonly<Record<string, string>>;
85
+ private readonly entries: readonly Entry[];
86
+ /** Name → namespace for lookups (a Map: prefix names like `constructor` are ordinary). */
87
+ private readonly namespaces = new Map<string, string>();
88
+ private readonly cache = new Map<string, string>();
89
+
90
+ constructor(layers: readonly (readonly [PrefixSource, Readonly<Record<string, string>>])[]) {
91
+ // Name → namespace: declared names override well-known ones, user names override both.
92
+ const byName = new Map<string, Entry>();
93
+ const ordered = [...layers].sort(([a], [b]) => SOURCE_OVERRIDE[a] - SOURCE_OVERRIDE[b]);
94
+ for (const [source, prefixes] of ordered) {
95
+ const preference = SOURCE_PREFERENCE[source];
96
+ for (const [prefix, namespace] of Object.entries(prefixes)) {
97
+ if (namespace) byName.set(prefix, { prefix, namespace, preference });
98
+ }
99
+ }
100
+ const record: Record<string, string> = {};
101
+ for (const name of [...byName.keys()].sort(compareStrings)) {
102
+ const entry = byName.get(name);
103
+ if (!entry) continue;
104
+ setOwn(record, name, entry.namespace);
105
+ this.namespaces.set(name, entry.namespace);
106
+ }
107
+ this.record = record;
108
+
109
+ const entries = [...byName.values()];
110
+ for (const [prefix, namespace] of WELL_KNOWN_VARIANTS) {
111
+ if (byName.get(prefix)?.namespace === getOwn(WELL_KNOWN_PREFIXES, prefix)) {
112
+ entries.push({ prefix, namespace, preference: SOURCE_PREFERENCE['well-known'] });
113
+ }
114
+ }
115
+ // Longest namespace first; for equal namespaces the preferred name first.
116
+ this.entries = entries.sort(
117
+ (a, b) =>
118
+ b.namespace.length - a.namespace.length ||
119
+ compareStrings(a.namespace, b.namespace) ||
120
+ a.preference - b.preference ||
121
+ a.prefix.length - b.prefix.length ||
122
+ compareStrings(a.prefix, b.prefix),
123
+ );
124
+ }
125
+
126
+ /** Compact form of `iri` (cached), or `iri` itself when no prefix applies. */
127
+ compact(iri: string): string {
128
+ let compact = this.cache.get(iri);
129
+ if (compact === undefined) {
130
+ compact = this.compute(iri);
131
+ this.cache.set(iri, compact);
132
+ }
133
+ return compact;
134
+ }
135
+
136
+ /**
137
+ * Expands a CURIE with a known prefix (or an OBO-style `GO:0008150`) to a full IRI.
138
+ * Strings that already look like absolute IRIs are returned unchanged.
139
+ */
140
+ expand(value: string): string {
141
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || value.startsWith('urn:')) return value;
142
+ const colon = value.indexOf(':');
143
+ if (colon < 0) return value;
144
+ const namespace = this.namespaces.get(value.slice(0, colon));
145
+ if (namespace !== undefined) return namespace + value.slice(colon + 1);
146
+ const prefix = value.slice(0, colon);
147
+ // Undeclared upper-case id spaces (`IAO:0100001`) follow the OBO PURL convention.
148
+ if (/^[A-Z][A-Za-z0-9]*$/.test(prefix)) return `${OBO_PURL}${prefix}_${value.slice(colon + 1)}`;
149
+ return value;
150
+ }
151
+
152
+ private compute(iri: string): string {
153
+ const obo = oboCurie(iri);
154
+ if (obo !== undefined) {
155
+ const idSpace = obo.slice(0, obo.indexOf(':'));
156
+ const declared = this.namespaces.get(idSpace);
157
+ // Respect a file that binds the id space to something else.
158
+ if (declared === undefined || declared === `${OBO_PURL}${idSpace}_`) return obo;
159
+ }
160
+ for (const entry of this.entries) {
161
+ if (iri.startsWith(entry.namespace))
162
+ return `${entry.prefix}:${iri.slice(entry.namespace.length)}`;
163
+ }
164
+ return iri;
165
+ }
166
+ }
167
+
168
+ const defaultTable = new PrefixTable([['well-known', WELL_KNOWN_PREFIXES]]);
169
+ const tables = new WeakMap<Readonly<Record<string, string>>, PrefixTable>();
170
+
171
+ /** Table for `compactIri`: well-known prefixes overlaid with `prefixes` (cached per object). */
172
+ export function tableFor(prefixes?: Readonly<Record<string, string>>): PrefixTable {
173
+ if (!prefixes) return defaultTable;
174
+ let table = tables.get(prefixes);
175
+ if (!table) {
176
+ table = new PrefixTable([
177
+ ['well-known', WELL_KNOWN_PREFIXES],
178
+ ['user', prefixes],
179
+ ]);
180
+ tables.set(prefixes, table);
181
+ }
182
+ return table;
183
+ }
184
+
185
+ /**
186
+ * Readable form of a predicate or id, mirroring core's `predicateLabel`: the local part after
187
+ * the last `#`, `/` or `:`, with `_` turned into spaces. Opaque local parts (digits only) keep
188
+ * the whole compact form: `BFO:0000050` stays `BFO:0000050`.
189
+ */
190
+ export function readableName(value: string): string {
191
+ const cut = Math.max(value.lastIndexOf('#'), value.lastIndexOf('/'), value.lastIndexOf(':'));
192
+ const local = value.slice(cut + 1);
193
+ if (local === '' || /^\d+$/.test(local)) return value;
194
+ return local.replace(/_/g, ' ');
195
+ }