@glossarist/concept-browser 0.7.66 → 0.7.67
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/cli/index.mjs +38 -33
- package/package.json +2 -2
- package/scripts/__tests__/cli-script-contract.test.mjs +39 -0
- package/scripts/__tests__/first-non-empty.test.mjs +51 -0
- package/scripts/__tests__/process-about-pages.test.mjs +106 -0
- package/scripts/build-edges.js +10 -2
- package/scripts/fetch-datasets.mjs +61 -54
- package/scripts/generate-data.mjs +38 -26
- package/scripts/lib/first-non-empty.mjs +19 -0
- package/scripts/process-about-pages.mjs +63 -15
- package/scripts/validate-shacl.mjs +1 -1
- package/src/__tests__/concept-rdf-view.test.ts +21 -13
- package/src/__tests__/perf/serialization-perf.test.ts +13 -30
- package/src/__tests__/use-rdf-document.test.ts +125 -0
- package/src/components/ConceptRdfView.vue +4 -4
- package/src/components/use-rdf-document.ts +137 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +0 -110
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +0 -159
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +0 -119
- package/src/__tests__/concept-rdf/coerce-date.test.ts +0 -97
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +0 -258
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +0 -224
- package/src/__tests__/concept-rdf/differential.test.ts +0 -96
- package/src/__tests__/concept-rdf/group-emitter.test.ts +0 -109
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +0 -135
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +0 -109
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +0 -177
- package/src/__tests__/concept-rdf/property-based.test.ts +0 -179
- package/src/__tests__/concept-rdf/provenance.test.ts +0 -110
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +0 -43
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +0 -47
- package/src/__tests__/concept-rdf/rdf-components.test.ts +0 -145
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +0 -115
- package/src/__tests__/concept-rdf/round-trip.test.ts +0 -243
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +0 -126
- package/src/__tests__/concept-rdf/sections-builder.test.ts +0 -94
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +0 -110
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +0 -138
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +0 -75
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +0 -142
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +0 -114
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +0 -246
- package/src/__tests__/concept-rdf/version-emitter.test.ts +0 -120
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +0 -100
- package/src/__tests__/perf/bundle-layout.test.ts +0 -50
- package/src/components/concept-rdf/agents-emitter.ts +0 -82
- package/src/components/concept-rdf/bibliography-emitter.ts +0 -83
- package/src/components/concept-rdf/build-activity-emitter.ts +0 -89
- package/src/components/concept-rdf/concept-emitter.ts +0 -443
- package/src/components/concept-rdf/dataset-emitter.ts +0 -95
- package/src/components/concept-rdf/group-emitter.ts +0 -69
- package/src/components/concept-rdf/image-variant-emitter.ts +0 -46
- package/src/components/concept-rdf/jsonld-writer.ts +0 -82
- package/src/components/concept-rdf/predicates.ts +0 -261
- package/src/components/concept-rdf/provenance.ts +0 -80
- package/src/components/concept-rdf/rdf-graph.ts +0 -211
- package/src/components/concept-rdf/sections-builder.ts +0 -62
- package/src/components/concept-rdf/table-formula-emitter.ts +0 -101
- package/src/components/concept-rdf/turtle-writer.ts +0 -116
- package/src/components/concept-rdf/use-rdf-document.ts +0 -72
- package/src/components/concept-rdf/version-emitter.ts +0 -65
- package/src/components/concept-rdf/vocabulary-emitter.ts +0 -62
- /package/src/components/{concept-rdf/RdfInstanceHeader.vue → RdfInstanceHeader.vue} +0 -0
- /package/src/components/{concept-rdf/RdfInstanceSection.vue → RdfInstanceSection.vue} +0 -0
- /package/src/components/{concept-rdf/RdfPrefixLegend.vue → RdfPrefixLegend.vue} +0 -0
- /package/src/components/{concept-rdf/RdfSourcePanel.vue → RdfSourcePanel.vue} +0 -0
- /package/src/components/{concept-rdf/rdf-prefixes.ts → rdf-prefixes.ts} +0 -0
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { computed, ref, nextTick } from 'vue';
|
|
3
|
-
import { Concept } from 'glossarist';
|
|
4
|
-
import { useRdfDocument } from '../../components/concept-rdf/use-rdf-document';
|
|
5
|
-
import { RDF_PREFIXES, findPrefix } from '../../components/concept-rdf/rdf-prefixes';
|
|
6
|
-
|
|
7
|
-
function makeConcept(): Concept {
|
|
8
|
-
return Concept.fromJSON({
|
|
9
|
-
id: '3.1.1',
|
|
10
|
-
uri: 'https://glossarist.org/test/concept/3.1.1',
|
|
11
|
-
status: 'valid',
|
|
12
|
-
localizations: {
|
|
13
|
-
eng: {
|
|
14
|
-
language_code: 'eng',
|
|
15
|
-
entry_status: 'valid',
|
|
16
|
-
terms: [
|
|
17
|
-
{ type: 'expression', designation: 'atomic data unit', normative_status: 'preferred' },
|
|
18
|
-
{ type: 'expression', designation: 'ADU', normative_status: 'admitted' },
|
|
19
|
-
],
|
|
20
|
-
definition: [{ content: 'A data unit that cannot be subdivided.' }],
|
|
21
|
-
notes: [{ content: 'Note here.' }],
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeConceptWithNonVerbal(): Concept {
|
|
28
|
-
return Concept.fromJSON({
|
|
29
|
-
id: '3.1.2',
|
|
30
|
-
uri: 'https://glossarist.org/test/concept/3.1.2',
|
|
31
|
-
status: 'valid',
|
|
32
|
-
localizations: {
|
|
33
|
-
eng: {
|
|
34
|
-
language_code: 'eng',
|
|
35
|
-
entry_status: 'valid',
|
|
36
|
-
terms: [{ type: 'expression', designation: 'angle of repose', normative_status: 'preferred' }],
|
|
37
|
-
definition: [{ content: 'Angle formed by a material at rest.' }],
|
|
38
|
-
non_verbal_rep: [
|
|
39
|
-
{ type: 'figure', caption: 'Angle of repose diagram', description: 'Schematic diagram showing the angle', images: [{ src: 'fig_A.23.svg' }] },
|
|
40
|
-
{ type: 'formula', caption: 'tan(θ) = μ' },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('useRdfDocument — Turtle emission contract', () => {
|
|
48
|
-
it('declares skosxl: prefix and never xl:', () => {
|
|
49
|
-
const c = ref(makeConcept());
|
|
50
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
51
|
-
expect(turtle.value).toContain('@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#>');
|
|
52
|
-
expect(turtle.value).not.toContain('@prefix xl:');
|
|
53
|
-
expect(turtle.value).not.toMatch(/\bxl:/);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('emits BOTH skosxl:prefLabel AND skos:prefLabel', () => {
|
|
57
|
-
const c = ref(makeConcept());
|
|
58
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
59
|
-
expect(turtle.value).toMatch(/skosxl:prefLabel\s+<[^>]+\/eng\/desig\//);
|
|
60
|
-
expect(turtle.value).toMatch(/skos:prefLabel "atomic data unit"@eng/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('emits BOTH skosxl:altLabel AND skos:altLabel for admitted designations', () => {
|
|
64
|
-
const c = ref(makeConcept());
|
|
65
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
66
|
-
expect(turtle.value).toMatch(/skosxl:altLabel\s+<[^>]+\/eng\/desig\//);
|
|
67
|
-
expect(turtle.value).toMatch(/skos:altLabel "ADU"@eng/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('emits skos:definition with language tag', () => {
|
|
71
|
-
const c = ref(makeConcept());
|
|
72
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
73
|
-
expect(turtle.value).toMatch(/skos:definition "A data unit that cannot be subdivided\."@eng/);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('emits gloss:hasDefinition as a typed gloss:DetailedDefinition with rdf:value', () => {
|
|
77
|
-
const c = ref(makeConcept());
|
|
78
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
79
|
-
expect(turtle.value).toMatch(/gloss:hasDefinition \[ (a|rdf:type) gloss:DetailedDefinition ; rdf:value "[^"]+"@eng \]/);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('terminates the concept resource with a full stop (not a trailing semicolon)', () => {
|
|
83
|
-
const c = ref(makeConcept());
|
|
84
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
85
|
-
const block = turtle.value.split('\n\n')[1] ?? '';
|
|
86
|
-
expect(block).toMatch(/<[^>]+> a gloss:Concept, skos:Concept ;[\s\S]+gloss:hasLocalization <[^>]+>/);
|
|
87
|
-
expect(block).toMatch(/\.$/);
|
|
88
|
-
expect(block).not.toMatch(/;\s*$/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('attaches provenance triples to the concept resource', () => {
|
|
92
|
-
const c = ref(makeConcept());
|
|
93
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
94
|
-
expect(turtle.value).toMatch(/prov:wasGeneratedBy <activity\/serializers\/concept-browser\//);
|
|
95
|
-
expect(turtle.value).toMatch(/prov:generatedAtTime "[^"]+"\^\^xsd:dateTime/);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('useRdfDocument — JSON-LD emission contract', () => {
|
|
100
|
-
it('produces a parseable JSON-LD document with @context and @graph', () => {
|
|
101
|
-
const c = ref(makeConcept());
|
|
102
|
-
const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
103
|
-
const doc = JSON.parse(jsonld.value);
|
|
104
|
-
expect(doc['@context']).toBeDefined();
|
|
105
|
-
expect(Array.isArray(doc['@graph'])).toBe(true);
|
|
106
|
-
expect(doc['@context'].skosxl).toBe('http://www.w3.org/2008/05/skos-xl#');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('emits the concept node and at least one localized concept', () => {
|
|
110
|
-
const c = ref(makeConcept());
|
|
111
|
-
const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
112
|
-
const doc = JSON.parse(jsonld.value);
|
|
113
|
-
const types = doc['@graph'].map((n: any) => n['@type']);
|
|
114
|
-
expect(types.some((t: any) => Array.isArray(t) && t.includes('gloss:Concept'))).toBe(true);
|
|
115
|
-
expect(types.some((t: any) => Array.isArray(t) && t.includes('gloss:LocalizedConcept'))).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('useRdfDocument — non-verbal representation emission (WS K)', () => {
|
|
120
|
-
it('emits gloss:hasNonVerbalRepresentation for NVR with caption (K4 URI form)', () => {
|
|
121
|
-
const c = ref(makeConceptWithNonVerbal());
|
|
122
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
123
|
-
expect(turtle.value).toMatch(/gloss:hasNonVerbalRepresentation <[^>]+\/nvr\/[^>]+>/);
|
|
124
|
-
expect(turtle.value).toMatch(/(a|rdf:type) gloss:NonVerbalRep/);
|
|
125
|
-
expect(turtle.value).toMatch(/skosxl:prefLabel \[/);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('emits gloss:image as xsd:anyURI on the NVR resource', () => {
|
|
129
|
-
const c = ref(makeConceptWithNonVerbal());
|
|
130
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
131
|
-
expect(turtle.value).toMatch(/gloss:image "fig_A\.23\.svg"\^\^xsd:anyURI/);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('emits NVR URIs in JSON-LD with NonVerbalRep type', () => {
|
|
135
|
-
const c = ref(makeConceptWithNonVerbal());
|
|
136
|
-
const { jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
137
|
-
const doc = JSON.parse(jsonld.value);
|
|
138
|
-
const nvrNodes = doc['@graph'].filter((n: any) => {
|
|
139
|
-
const t = n['@type'];
|
|
140
|
-
return Array.isArray(t) ? t.includes('gloss:NonVerbalRep') : t === 'gloss:NonVerbalRep';
|
|
141
|
-
});
|
|
142
|
-
expect(nvrNodes.length).toBeGreaterThanOrEqual(1);
|
|
143
|
-
const lcNode = doc['@graph'].find((n: any) => Array.isArray(n['@type']) && n['@type'].includes('gloss:LocalizedConcept'));
|
|
144
|
-
expect(lcNode).toBeDefined();
|
|
145
|
-
const refRaw = lcNode['gloss:hasNonVerbalRepresentation'];
|
|
146
|
-
const refArr = Array.isArray(refRaw) ? refRaw : [refRaw];
|
|
147
|
-
expect(refArr.length).toBeGreaterThanOrEqual(1);
|
|
148
|
-
expect(refArr[0]['@id']).toMatch(/\/nvr\/[^/]+$/);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('includes non-verbal reps in the sections view', () => {
|
|
152
|
-
const c = ref(makeConceptWithNonVerbal());
|
|
153
|
-
const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
154
|
-
const lcSection = sections.value.find(s => s.classId === 'gloss:LocalizedConcept');
|
|
155
|
-
expect(lcSection).toBeDefined();
|
|
156
|
-
const nvrProps = lcSection!.props.filter(p =>
|
|
157
|
-
p.predicate === 'gloss:hasNonVerbalRepresentation' || p.predicate === 'gloss:hasNonVerbalRep',
|
|
158
|
-
);
|
|
159
|
-
expect(nvrProps.length).toBeGreaterThanOrEqual(1);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('omits non-verbal emission when the concept has no non-verbal reps', () => {
|
|
163
|
-
const c = ref(makeConcept());
|
|
164
|
-
const { turtle, jsonld } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
165
|
-
expect(turtle.value).not.toMatch(/gloss:hasNonVerbalRep/);
|
|
166
|
-
expect(turtle.value).not.toMatch(/gloss:hasNonVerbalRepresentation/);
|
|
167
|
-
const doc = JSON.parse(jsonld.value);
|
|
168
|
-
const lcNode = doc['@graph'].find((n: any) => Array.isArray(n['@type']) && n['@type'].includes('gloss:LocalizedConcept'));
|
|
169
|
-
expect(lcNode['gloss:hasNonVerbalRep']).toBeUndefined();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('useRdfDocument — reactivity', () => {
|
|
174
|
-
it('recomputes turtle when the concept changes', async () => {
|
|
175
|
-
const c = ref(makeConcept());
|
|
176
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
177
|
-
const before = turtle.value;
|
|
178
|
-
expect(before).toContain('atomic data unit');
|
|
179
|
-
|
|
180
|
-
const next = Concept.fromJSON({
|
|
181
|
-
id: '4.5.6',
|
|
182
|
-
uri: 'https://glossarist.org/test/concept/4.5.6',
|
|
183
|
-
status: 'valid',
|
|
184
|
-
localizations: {
|
|
185
|
-
eng: {
|
|
186
|
-
language_code: 'eng',
|
|
187
|
-
entry_status: 'valid',
|
|
188
|
-
terms: [{ type: 'expression', designation: 'different designation', normative_status: 'preferred' }],
|
|
189
|
-
definition: [{ content: 'Different definition.' }],
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
c.value = next;
|
|
194
|
-
await nextTick();
|
|
195
|
-
expect(turtle.value).not.toBe(before);
|
|
196
|
-
expect(turtle.value).toContain('different designation');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('returns one section per concept + localized concept + designation', () => {
|
|
200
|
-
const c = ref(makeConcept());
|
|
201
|
-
const { sections } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
202
|
-
const labels = sections.value.map(s => s.classId);
|
|
203
|
-
expect(labels).toContain('gloss:Concept');
|
|
204
|
-
expect(labels).toContain('gloss:LocalizedConcept');
|
|
205
|
-
expect(labels.filter(l => l === 'gloss:Expression').length).toBe(2);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('emits a type chain ending in gloss:Concept', () => {
|
|
209
|
-
const c = ref(makeConcept());
|
|
210
|
-
const { typeChain } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
211
|
-
expect(typeChain.value[typeChain.value.length - 1]).toBe('gloss:Concept');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('can be consumed inside another computed without re-triggering the emitter', () => {
|
|
215
|
-
const c = ref(makeConcept());
|
|
216
|
-
const { turtle } = useRdfDocument(() => c.value, () => c.value.uri ?? '');
|
|
217
|
-
const upper = computed(() => turtle.value.split('\n').length);
|
|
218
|
-
const initial = upper.value;
|
|
219
|
-
expect(initial).toBeGreaterThan(5);
|
|
220
|
-
// No mutation: reading upper twice doesn't recompute turtle.
|
|
221
|
-
expect(upper.value).toBe(initial);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('rdf-prefixes legend', () => {
|
|
226
|
-
it('declares all canonical prefixes used by the emitter', () => {
|
|
227
|
-
const declared = new Set(RDF_PREFIXES.map(p => p.prefix));
|
|
228
|
-
for (const required of ['gloss', 'skos', 'skosxl', 'rdf', 'dcterms']) {
|
|
229
|
-
expect(declared.has(required)).toBe(true);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('lookup returns the matching prefix entry', () => {
|
|
234
|
-
expect(findPrefix('skosxl')?.iri).toBe('http://www.w3.org/2008/05/skos-xl#');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('returns undefined for unknown prefixes', () => {
|
|
238
|
-
expect(findPrefix('does-not-exist')).toBeUndefined();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('skosxl prefix resolves to the canonical IRI (settlement decision)', () => {
|
|
242
|
-
const skosxl = findPrefix('skosxl');
|
|
243
|
-
expect(skosxl).toBeDefined();
|
|
244
|
-
expect(skosxl!.iri).toBe('http://www.w3.org/2008/05/skos-xl#');
|
|
245
|
-
});
|
|
246
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { Parser, Store } from 'n3';
|
|
3
|
-
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
4
|
-
import { emitVersionGraph, emitVersionHistory } from '../../components/concept-rdf/version-emitter';
|
|
5
|
-
|
|
6
|
-
const PROV = 'http://www.w3.org/ns/prov#';
|
|
7
|
-
const DCTERMS = 'http://purl.org/dc/terms/';
|
|
8
|
-
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
9
|
-
|
|
10
|
-
function parse(turtle: string): Store {
|
|
11
|
-
const parser = new Parser({ format: 'Turtle' });
|
|
12
|
-
const store = new Store();
|
|
13
|
-
store.addQuads(parser.parse(turtle));
|
|
14
|
-
return store;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const VERSION_IRI = 'https://glossarist.org/test/versions/1.0';
|
|
18
|
-
|
|
19
|
-
function makeInput() {
|
|
20
|
-
return {
|
|
21
|
-
registerId: 'test',
|
|
22
|
-
version: '1.0',
|
|
23
|
-
versionIri: VERSION_IRI,
|
|
24
|
-
datasetIri: 'https://glossarist.org/test/',
|
|
25
|
-
generatedAt: '2026-06-28T12:00:00Z',
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe('emitVersionGraph — J6 dataset versioning', () => {
|
|
30
|
-
it('types the version as prov:Entity', () => {
|
|
31
|
-
const graph = emitVersionGraph(makeInput());
|
|
32
|
-
const store = parse(writeTurtle(graph));
|
|
33
|
-
const types = store.getObjects(VERSION_IRI, RDF_TYPE, null).map(q => q.value);
|
|
34
|
-
expect(types).toContain(`${PROV}Entity`);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('emits dcterms:isVersionOf pointing at the canonical dataset IRI', () => {
|
|
38
|
-
const graph = emitVersionGraph(makeInput());
|
|
39
|
-
const store = parse(writeTurtle(graph));
|
|
40
|
-
const targets = store.getObjects(VERSION_IRI, `${DCTERMS}isVersionOf`, null).map(q => q.value);
|
|
41
|
-
expect(targets).toContain('https://glossarist.org/test/');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('emits prov:generatedAtTime as xsd:dateTime', () => {
|
|
45
|
-
const graph = emitVersionGraph(makeInput());
|
|
46
|
-
const store = parse(writeTurtle(graph));
|
|
47
|
-
const ts = store.getObjects(VERSION_IRI, `${PROV}generatedAtTime`, null).map(q => q.value);
|
|
48
|
-
expect(ts).toContain('2026-06-28T12:00:00Z');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('emits prov:wasRevisionOf when a previous version is provided', () => {
|
|
52
|
-
const graph = emitVersionGraph({ ...makeInput(), previousVersionIri: 'https://glossarist.org/test/versions/0.9' });
|
|
53
|
-
const store = parse(writeTurtle(graph));
|
|
54
|
-
const prev = store.getObjects(VERSION_IRI, `${PROV}wasRevisionOf`, null).map(q => q.value);
|
|
55
|
-
expect(prev).toContain('https://glossarist.org/test/versions/0.9');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('omits prov:wasRevisionOf when no previous version', () => {
|
|
59
|
-
const graph = emitVersionGraph(makeInput());
|
|
60
|
-
const store = parse(writeTurtle(graph));
|
|
61
|
-
const prev = store.getObjects(VERSION_IRI, `${PROV}wasRevisionOf`, null);
|
|
62
|
-
expect(prev).toHaveLength(0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('records change summary and associated agent when provided', () => {
|
|
66
|
-
const graph = emitVersionGraph({
|
|
67
|
-
...makeInput(),
|
|
68
|
-
changeSummary: 'Initial release',
|
|
69
|
-
associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
|
|
70
|
-
});
|
|
71
|
-
const store = parse(writeTurtle(graph));
|
|
72
|
-
const summaries = store.getObjects(VERSION_IRI, `${DCTERMS}description`, null).map(q => q.value);
|
|
73
|
-
expect(summaries).toContain('Initial release');
|
|
74
|
-
const agents = store.getObjects(VERSION_IRI, `${PROV}wasAssociatedWith`, null).map(q => q.value);
|
|
75
|
-
expect(agents).toContain('https://glossarist.org/agent/ci-bot');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('emitVersionHistory', () => {
|
|
80
|
-
it('emits a chain of versions with prov:wasRevisionOf links', () => {
|
|
81
|
-
const graph = emitVersionHistory({
|
|
82
|
-
registerId: 'test',
|
|
83
|
-
datasetIri: 'https://glossarist.org/test/',
|
|
84
|
-
versions: [
|
|
85
|
-
{ version: '1.0', generatedAt: '2024-01-01T00:00:00Z' },
|
|
86
|
-
{ version: '1.1', generatedAt: '2024-06-01T00:00:00Z', changeSummary: 'Add 50 terms' },
|
|
87
|
-
{ version: '2.0', generatedAt: '2025-01-01T00:00:00Z', changeSummary: 'Major release' },
|
|
88
|
-
],
|
|
89
|
-
associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const store = parse(writeTurtle(graph));
|
|
93
|
-
const iri10 = 'https://glossarist.org/test/versions/1.0';
|
|
94
|
-
const iri11 = 'https://glossarist.org/test/versions/1.1';
|
|
95
|
-
const iri20 = 'https://glossarist.org/test/versions/2.0';
|
|
96
|
-
|
|
97
|
-
expect(store.getObjects(iri10, RDF_TYPE, null).length).toBeGreaterThan(0);
|
|
98
|
-
expect(store.getObjects(iri11, RDF_TYPE, null).length).toBeGreaterThan(0);
|
|
99
|
-
expect(store.getObjects(iri20, RDF_TYPE, null).length).toBeGreaterThan(0);
|
|
100
|
-
|
|
101
|
-
const v11Prev = store.getObjects(iri11, `${PROV}wasRevisionOf`, null).map(q => q.value);
|
|
102
|
-
expect(v11Prev).toContain(iri10);
|
|
103
|
-
|
|
104
|
-
const v20Prev = store.getObjects(iri20, `${PROV}wasRevisionOf`, null).map(q => q.value);
|
|
105
|
-
expect(v20Prev).toContain(iri11);
|
|
106
|
-
|
|
107
|
-
const v10Prev = store.getObjects(iri10, `${PROV}wasRevisionOf`, null);
|
|
108
|
-
expect(v10Prev).toHaveLength(0);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('emits an empty graph when versions array is empty', () => {
|
|
112
|
-
const graph = emitVersionHistory({
|
|
113
|
-
registerId: 'test',
|
|
114
|
-
datasetIri: 'https://glossarist.org/test/',
|
|
115
|
-
versions: [],
|
|
116
|
-
});
|
|
117
|
-
const resources = Array.from(graph.resources());
|
|
118
|
-
expect(resources.length).toBe(0);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { Parser, Store } from 'n3';
|
|
5
|
-
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
6
|
-
import { emitVocabularyGraph, VOCAB_SCHEMES } from '../../components/concept-rdf/vocabulary-emitter';
|
|
7
|
-
|
|
8
|
-
const VOCAB_JSON = JSON.parse(readFileSync(join(process.cwd(), 'data', 'glossarist-vocab.json'), 'utf8'));
|
|
9
|
-
|
|
10
|
-
function parse(turtle: string): Store {
|
|
11
|
-
const parser = new Parser({ format: 'Turtle' });
|
|
12
|
-
const store = new Store();
|
|
13
|
-
store.addQuads(parser.parse(turtle));
|
|
14
|
-
return store;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const SKOS = 'http://www.w3.org/2004/02/skos/core#';
|
|
18
|
-
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
19
|
-
|
|
20
|
-
const EXPECTED_REL_TYPES = [
|
|
21
|
-
'deprecates', 'deprecated_by', 'supersedes', 'superseded_by', 'replaces', 'replaced_by',
|
|
22
|
-
'invalidates', 'invalidated_by', 'retires', 'retired_by',
|
|
23
|
-
'narrower', 'broader', 'broader_generic', 'narrower_generic',
|
|
24
|
-
'broader_partitive', 'narrower_partitive', 'has_part', 'is_part_of',
|
|
25
|
-
'broader_instantial', 'narrower_instantial', 'instance_of', 'has_instance',
|
|
26
|
-
'equivalent', 'exact_match', 'close_match', 'broad_match', 'narrow_match', 'related_match',
|
|
27
|
-
'compare', 'contrast',
|
|
28
|
-
'see', 'references', 'related_concept', 'related_concept_broader', 'related_concept_narrower',
|
|
29
|
-
'sequentially_related', 'spatially_related', 'temporally_related',
|
|
30
|
-
'homograph', 'false_friend',
|
|
31
|
-
'has_concept', 'is_concept_of', 'has_definition', 'definition_of',
|
|
32
|
-
'inherits', 'inherited_by',
|
|
33
|
-
'has_version', 'version_of', 'current_version', 'current_version_of',
|
|
34
|
-
'derived',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
describe('A1/A2/C1 — vocabulary SSOT', () => {
|
|
38
|
-
it('VOCAB_SCHEMES loads from data/glossarist-vocab.json at module init', () => {
|
|
39
|
-
expect(VOCAB_SCHEMES.length).toBe(VOCAB_JSON.schemes.length);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('every scheme entry in JSON appears in VOCAB_SCHEMES', () => {
|
|
43
|
-
const expected = new Set(VOCAB_JSON.schemes.map((s: any) => s.schemeIri));
|
|
44
|
-
const actual = new Set(VOCAB_SCHEMES.map(s => s.schemeIri));
|
|
45
|
-
expect(actual).toEqual(expected);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('TS and mjs consumers read the same JSON file (single source of truth)', () => {
|
|
49
|
-
const tsCount = VOCAB_SCHEMES.reduce((acc, s) => acc + s.terms.length, 0);
|
|
50
|
-
const jsonCount = VOCAB_JSON.schemes.reduce(
|
|
51
|
-
(acc: number, s: any) => acc + s.terms.length, 0,
|
|
52
|
-
);
|
|
53
|
-
expect(tsCount).toBe(jsonCount);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('C1 — all 52 relationship types are declared', () => {
|
|
58
|
-
it('relationship-type scheme declares every type in EXPECTED_REL_TYPES', () => {
|
|
59
|
-
const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
|
|
60
|
-
expect(relScheme).toBeDefined();
|
|
61
|
-
const declared = new Set(relScheme!.terms.map(t => t.label));
|
|
62
|
-
for (const expected of EXPECTED_REL_TYPES) {
|
|
63
|
-
expect(declared.has(expected)).toBe(true);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('relationship-type count matches glossarist-ruby config.yml (50 canonical + legacy)', () => {
|
|
68
|
-
const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
|
|
69
|
-
// glossarist-ruby config.yml related_concept.type lists 50 types. We add 'derived' as a legacy alias.
|
|
70
|
-
expect(relScheme!.terms.length).toBeGreaterThanOrEqual(50);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('every relationship type IRI follows the gloss:rel/{type} pattern', () => {
|
|
74
|
-
const relScheme = VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme');
|
|
75
|
-
for (const term of relScheme!.terms) {
|
|
76
|
-
expect(term.iri).toMatch(/^gloss:rel\/[a-z_]+$/);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('every relationship type is a skos:Concept in the emitted graph', () => {
|
|
81
|
-
const ttl = writeTurtle(emitVocabularyGraph());
|
|
82
|
-
const store = parse(ttl);
|
|
83
|
-
for (const term of VOCAB_SCHEMES.find(s => s.schemeIri === 'gloss:rel-scheme')!.terms) {
|
|
84
|
-
const iri = `https://www.glossarist.org/ontologies/rel/${term.label}`;
|
|
85
|
-
const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
|
|
86
|
-
expect(types).toContain(`${SKOS}Concept`);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('C1 — vocabulary emits without drift between TS and mjs', () => {
|
|
92
|
-
it('emitted Turtle contains every scheme and every term', () => {
|
|
93
|
-
const ttl = writeTurtle(emitVocabularyGraph());
|
|
94
|
-
for (const scheme of VOCAB_SCHEMES) {
|
|
95
|
-
for (const term of scheme.terms) {
|
|
96
|
-
expect(ttl).toContain(term.label);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const DIST_DIR = 'dist';
|
|
6
|
-
const ASSETS_DIR = join(DIST_DIR, 'assets');
|
|
7
|
-
|
|
8
|
-
function distExists(): boolean {
|
|
9
|
-
return existsSync(ASSETS_DIR);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function readAsset(name: string): string {
|
|
13
|
-
return readFileSync(join(ASSETS_DIR, name), 'utf8');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function listJsAssets(): string[] {
|
|
17
|
-
return readdirSync(ASSETS_DIR).filter(f => f.endsWith('.js'));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const MAIN_CHUNK_RE = /^index-[^/]*\.js$/;
|
|
21
|
-
const CONCEPT_CHUNK_RE = /^ConceptView-[^/]*\.js$/;
|
|
22
|
-
|
|
23
|
-
describe.skipIf(!distExists())('bundle: RDF serializer stays out of main chunk', () => {
|
|
24
|
-
it('the main index-* chunk does not embed the RDF emitter', () => {
|
|
25
|
-
const mainChunks = listJsAssets().filter(f => MAIN_CHUNK_RE.test(f));
|
|
26
|
-
expect(mainChunks.length).toBeGreaterThan(0);
|
|
27
|
-
|
|
28
|
-
for (const chunk of mainChunks) {
|
|
29
|
-
const text = readAsset(chunk);
|
|
30
|
-
expect(text, `${chunk} should not embed use-rdf-document`).not.toMatch(/use-rdf-document/);
|
|
31
|
-
expect(text, `${chunk} should not emit skosxl:literalForm literals`).not.toMatch(/skosxl:literalForm/);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('the ConceptView chunk DOES contain the RDF emitter', () => {
|
|
36
|
-
const conceptChunks = listJsAssets().filter(f => CONCEPT_CHUNK_RE.test(f));
|
|
37
|
-
expect(conceptChunks.length).toBeGreaterThan(0);
|
|
38
|
-
|
|
39
|
-
const merged = conceptChunks.map(readAsset).join('\n');
|
|
40
|
-
expect(merged).toMatch(/skosxl:literalForm/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('no chunk emits the legacy `xl:` prefix', () => {
|
|
44
|
-
for (const chunk of listJsAssets()) {
|
|
45
|
-
const text = readAsset(chunk);
|
|
46
|
-
expect(text, `${chunk} should not contain the legacy xl: prefix`).not.toMatch(/\bxl:prefLabel/);
|
|
47
|
-
expect(text, `${chunk} should not contain the legacy xl:altLabel/`).not.toMatch(/\bxl:altLabel/);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { FOAF, PROV, DCTERMS, RDFS } from './predicates';
|
|
2
|
-
import { RdfGraph } from './rdf-graph';
|
|
3
|
-
|
|
4
|
-
export interface AgentInput {
|
|
5
|
-
readonly slug: string;
|
|
6
|
-
readonly name: string;
|
|
7
|
-
readonly role?: string;
|
|
8
|
-
readonly organization?: string;
|
|
9
|
-
readonly url?: string;
|
|
10
|
-
readonly email?: string;
|
|
11
|
-
readonly agentIri: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AgentEmissionResult {
|
|
15
|
-
readonly graph: RdfGraph;
|
|
16
|
-
readonly personIris: readonly string[];
|
|
17
|
-
readonly organizationIris: readonly string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function slugify(input: string): string {
|
|
21
|
-
return input
|
|
22
|
-
.toLowerCase()
|
|
23
|
-
.normalize('NFKD')
|
|
24
|
-
.replace(/[̀-ͯ]/g, '')
|
|
25
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
26
|
-
.replace(/^-+|-+$/g, '');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function agentsFromContributors(
|
|
30
|
-
contributors: readonly { name: string; role?: string; organization?: string; url?: string; email?: string }[],
|
|
31
|
-
agentBase = 'https://glossarist.org/agent',
|
|
32
|
-
): readonly AgentInput[] {
|
|
33
|
-
return contributors.map(c => ({
|
|
34
|
-
slug: slugify(c.name),
|
|
35
|
-
name: c.name,
|
|
36
|
-
role: c.role,
|
|
37
|
-
organization: c.organization,
|
|
38
|
-
url: c.url,
|
|
39
|
-
email: c.email,
|
|
40
|
-
agentIri: `${agentBase}/${slugify(c.name)}`,
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function emitAgentsGraph(agents: readonly AgentInput[]): AgentEmissionResult {
|
|
45
|
-
const graph = new RdfGraph();
|
|
46
|
-
const personIris: string[] = [];
|
|
47
|
-
const orgSlugs = new Set<string>();
|
|
48
|
-
const organizationIris: string[] = [];
|
|
49
|
-
|
|
50
|
-
for (const a of agents) {
|
|
51
|
-
const w = graph.declare(a.agentIri, {
|
|
52
|
-
types: [FOAF.Person, PROV.Person, PROV.Agent],
|
|
53
|
-
label: a.name,
|
|
54
|
-
classLabel: 'Person',
|
|
55
|
-
classId: FOAF.Person,
|
|
56
|
-
});
|
|
57
|
-
w.literal(FOAF.name, a.name);
|
|
58
|
-
if (a.email) w.iri(FOAF.mbox, `mailto:${a.email}`);
|
|
59
|
-
if (a.url) w.iri(RDFS.seeAlso, a.url);
|
|
60
|
-
if (a.role) w.literal(DCTERMS.description, a.role);
|
|
61
|
-
personIris.push(a.agentIri);
|
|
62
|
-
|
|
63
|
-
if (a.organization) {
|
|
64
|
-
const orgSlug = slugify(a.organization);
|
|
65
|
-
const orgIri = `https://glossarist.org/org/${orgSlug}`;
|
|
66
|
-
w.iri(PROV.actedOnBehalfOf, orgIri);
|
|
67
|
-
if (!orgSlugs.has(orgSlug)) {
|
|
68
|
-
orgSlugs.add(orgSlug);
|
|
69
|
-
const orgW = graph.declare(orgIri, {
|
|
70
|
-
types: [FOAF.Organization, PROV.Organization, PROV.Agent],
|
|
71
|
-
label: a.organization,
|
|
72
|
-
classLabel: 'Organization',
|
|
73
|
-
classId: FOAF.Organization,
|
|
74
|
-
});
|
|
75
|
-
orgW.literal(FOAF.name, a.organization);
|
|
76
|
-
organizationIris.push(orgIri);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { graph, personIris, organizationIris };
|
|
82
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { DCTERMS, FOAF, RDF } from './predicates';
|
|
2
|
-
import { RdfGraph } from './rdf-graph';
|
|
3
|
-
|
|
4
|
-
export interface BibliographyEntry {
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly reference: string;
|
|
7
|
-
readonly title?: string;
|
|
8
|
-
readonly link?: string;
|
|
9
|
-
readonly type?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface BibliographyInput {
|
|
13
|
-
readonly registerId: string;
|
|
14
|
-
readonly entries: readonly BibliographyEntry[];
|
|
15
|
-
readonly baseUri?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type BibliographyDataShape =
|
|
19
|
-
| { [id: string]: BibliographyEntryLike }
|
|
20
|
-
| { bibliography: readonly BibliographyEntryLike[] };
|
|
21
|
-
|
|
22
|
-
interface BibliographyEntryLike {
|
|
23
|
-
readonly id?: string;
|
|
24
|
-
readonly reference?: string;
|
|
25
|
-
readonly title?: string;
|
|
26
|
-
readonly link?: string;
|
|
27
|
-
readonly type?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function bibliographyEntryIri(registerId: string, id: string, baseUri = 'https://glossarist.org'): string {
|
|
31
|
-
return `${baseUri}/${registerId}/bib/${id}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function normalizeBibliographyData(raw: unknown): readonly BibliographyEntry[] {
|
|
35
|
-
if (!raw || typeof raw !== 'object') return [];
|
|
36
|
-
const obj = raw as Record<string, unknown>;
|
|
37
|
-
|
|
38
|
-
if (Array.isArray(obj.bibliography)) {
|
|
39
|
-
return (obj.bibliography as readonly BibliographyEntryLike[]).map(e => entryFromV3(e));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const entries: BibliographyEntry[] = [];
|
|
43
|
-
for (const [id, value] of Object.entries(obj)) {
|
|
44
|
-
if (!value || typeof value !== 'object') continue;
|
|
45
|
-
const entry = entryFromV3(value as BibliographyEntryLike, id);
|
|
46
|
-
entries.push(entry);
|
|
47
|
-
}
|
|
48
|
-
return entries;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function entryFromV3(e: BibliographyEntryLike, fallbackId?: string): BibliographyEntry {
|
|
52
|
-
const id = e.id ?? fallbackId ?? '';
|
|
53
|
-
const reference = e.reference ?? '';
|
|
54
|
-
return {
|
|
55
|
-
id,
|
|
56
|
-
reference,
|
|
57
|
-
title: e.title,
|
|
58
|
-
link: e.link,
|
|
59
|
-
type: e.type,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function emitBibliographyGraph(input: BibliographyInput): RdfGraph {
|
|
64
|
-
const baseUri = input.baseUri ?? 'https://glossarist.org';
|
|
65
|
-
const graph = new RdfGraph();
|
|
66
|
-
for (const entry of input.entries) {
|
|
67
|
-
if (!entry.id || !entry.reference) continue;
|
|
68
|
-
const iri = bibliographyEntryIri(input.registerId, entry.id, baseUri);
|
|
69
|
-
const w = graph.declare(iri, {
|
|
70
|
-
types: ['dcterms:BibliographicResource'],
|
|
71
|
-
label: entry.title ?? entry.reference,
|
|
72
|
-
classLabel: 'BibliographicResource',
|
|
73
|
-
classId: 'dcterms:BibliographicResource',
|
|
74
|
-
});
|
|
75
|
-
w.literal(DCTERMS.identifier, entry.id);
|
|
76
|
-
w.literal(DCTERMS.bibliographicCitation, entry.reference);
|
|
77
|
-
if (entry.title) w.literal(DCTERMS.title, entry.title);
|
|
78
|
-
if (entry.link) w.iri(FOAF.page, entry.link);
|
|
79
|
-
if (entry.type) w.iri(DCTERMS.type, `gloss:bibtype/${entry.type}`);
|
|
80
|
-
w.iri(DCTERMS.isPartOf, `${baseUri}/${input.registerId}/`);
|
|
81
|
-
}
|
|
82
|
-
return graph;
|
|
83
|
-
}
|