@glossarist/concept-browser 0.7.51 → 0.7.53

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 (159) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/App.vue +2 -0
  21. package/src/__fixtures__/concept-shape.ttl +20 -0
  22. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  23. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  24. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  25. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  26. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  27. package/src/__tests__/components/error-boundary.test.ts +109 -0
  28. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  29. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  30. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  31. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  32. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  33. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  34. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  35. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  36. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  37. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  38. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  39. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  40. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  41. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  43. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  44. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  45. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  46. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  47. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  48. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  49. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  50. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  51. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  52. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  53. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  54. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  55. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  56. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  57. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  58. package/src/__tests__/config/group-renderers.test.ts +35 -0
  59. package/src/__tests__/config/group-types.test.ts +76 -0
  60. package/src/__tests__/dataset-style.test.ts +12 -7
  61. package/src/__tests__/errors/errors.test.ts +142 -0
  62. package/src/__tests__/format-downloads.test.ts +47 -65
  63. package/src/__tests__/markdown-lite.test.ts +19 -0
  64. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  65. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  66. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  67. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  68. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  69. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  70. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  71. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  72. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  73. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  74. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  75. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  76. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  77. package/src/__tests__/use-format-registry.test.ts +125 -0
  78. package/src/__tests__/utils/bcp47.test.ts +166 -0
  79. package/src/__tests__/utils/color-theme.test.ts +143 -0
  80. package/src/__tests__/utils/url-safety.test.ts +65 -0
  81. package/src/__tests__/validate-shacl.test.ts +100 -0
  82. package/src/adapters/DatasetAdapter.ts +11 -5
  83. package/src/adapters/GraphDataSource.ts +2 -1
  84. package/src/adapters/UriRouter.ts +2 -1
  85. package/src/adapters/concept-identity.ts +69 -0
  86. package/src/adapters/factory.ts +3 -2
  87. package/src/adapters/model-bridge.ts +2 -1
  88. package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
  89. package/src/adapters/non-verbal-resolver.ts +2 -1
  90. package/src/components/AppSidebar.vue +189 -93
  91. package/src/components/ConceptDetail.vue +8 -0
  92. package/src/components/ConceptEditionRail.vue +222 -0
  93. package/src/components/ConceptRdfView.vue +37 -377
  94. package/src/components/DatasetSeriesCard.vue +270 -0
  95. package/src/components/ErrorBoundary.vue +95 -0
  96. package/src/components/FormatDownloads.vue +17 -13
  97. package/src/components/HomeSeriesSection.vue +277 -0
  98. package/src/components/RelationSphere.vue +1672 -0
  99. package/src/components/SidebarSeriesSection.vue +239 -0
  100. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  101. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  102. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  103. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  104. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  105. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  106. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  107. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  108. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  109. package/src/components/concept-rdf/group-emitter.ts +69 -0
  110. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  111. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  112. package/src/components/concept-rdf/predicates.ts +261 -0
  113. package/src/components/concept-rdf/provenance.ts +80 -0
  114. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  115. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  116. package/src/components/concept-rdf/sections-builder.ts +62 -0
  117. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  118. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  119. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  120. package/src/components/concept-rdf/version-emitter.ts +65 -0
  121. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  122. package/src/components/groups/DatasetGroupRenderer.vue +32 -0
  123. package/src/components/groups/DefaultGroupSidebar.vue +50 -0
  124. package/src/components/groups/LineageGroupSidebar.vue +75 -0
  125. package/src/composables/use-color-theme.ts +82 -0
  126. package/src/composables/use-format-registry.ts +42 -0
  127. package/src/composables/useDatasetSeries.ts +258 -0
  128. package/src/composables/useSphereProjection.ts +125 -0
  129. package/src/config/group-renderers.ts +27 -0
  130. package/src/config/group-types.ts +92 -0
  131. package/src/config/types.ts +81 -2
  132. package/src/config/use-site-config.ts +2 -1
  133. package/src/errors.ts +136 -0
  134. package/src/i18n/locales/eng.yml +24 -0
  135. package/src/i18n/locales/fra.yml +24 -0
  136. package/src/stores/vocabulary.ts +3 -1
  137. package/src/style.css +17 -2
  138. package/src/types/agents-version-turtle.d.ts +27 -0
  139. package/src/types/bibliography-turtle.d.ts +12 -0
  140. package/src/types/build-activity-turtle.d.ts +16 -0
  141. package/src/types/build-cache.d.ts +20 -0
  142. package/src/types/dataset-turtle.d.ts +32 -0
  143. package/src/types/normalize-yaml.d.ts +16 -0
  144. package/src/types/turtle-escape.d.ts +6 -0
  145. package/src/types/vocab-turtle.d.ts +13 -0
  146. package/src/utils/asciidoc-lite.ts +11 -6
  147. package/src/utils/bcp47.ts +141 -0
  148. package/src/utils/color-theme-integration.ts +11 -0
  149. package/src/utils/color-theme.ts +129 -0
  150. package/src/utils/dataset-style.ts +31 -6
  151. package/src/utils/locale.ts +6 -14
  152. package/src/utils/markdown-lite.ts +6 -1
  153. package/src/utils/relation-sphere-styling.ts +63 -0
  154. package/src/utils/relationship-categories.ts +30 -0
  155. package/src/utils/url-safety.ts +30 -0
  156. package/src/views/ConceptView.vue +187 -9
  157. package/src/views/DatasetView.vue +6 -0
  158. package/src/views/HomeView.vue +5 -0
  159. package/vite.config.ts +7 -0
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import { emitGroupGraph } from '../../components/concept-rdf/group-emitter';
5
+
6
+ const DCAT = 'http://www.w3.org/ns/dcat#';
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
+ describe('emitGroupGraph — lineage → dcat:DatasetSeries', () => {
18
+ it('types lineage group as dcat:DatasetSeries', () => {
19
+ const graph = emitGroupGraph({
20
+ groupId: 'viml', groupIri: 'https://x.test/group/viml',
21
+ kind: 'lineage', title: 'VIML',
22
+ memberIris: ['https://x.test/dataset/viml-2022'],
23
+ });
24
+ const store = parse(writeTurtle(graph));
25
+ const types = store.getObjects('https://x.test/group/viml', RDF_TYPE, null).map(q => q.value);
26
+ expect(types).toContain(`${DCAT}DatasetSeries`);
27
+ });
28
+
29
+ it('emits dcat:hasVersion for each member', () => {
30
+ const graph = emitGroupGraph({
31
+ groupId: 'viml', groupIri: 'https://x.test/group/viml',
32
+ kind: 'lineage', title: 'VIML',
33
+ memberIris: ['https://x.test/dataset/viml-2013', 'https://x.test/dataset/viml-2022'],
34
+ });
35
+ const store = parse(writeTurtle(graph));
36
+ const versions = store.getObjects('https://x.test/group/viml', `${DCAT}hasVersion`, null).map(q => q.value);
37
+ expect(versions).toContain('https://x.test/dataset/viml-2013');
38
+ expect(versions).toContain('https://x.test/dataset/viml-2022');
39
+ });
40
+
41
+ it('emits gloss:hasCurrentVersion for the current member', () => {
42
+ const graph = emitGroupGraph({
43
+ groupId: 'viml', groupIri: 'https://x.test/group/viml',
44
+ kind: 'lineage', title: 'VIML',
45
+ memberIris: ['https://x.test/dataset/viml-2022'],
46
+ currentMemberIri: 'https://x.test/dataset/viml-2022',
47
+ });
48
+ const store = parse(writeTurtle(graph));
49
+ const current = store.getObjects('https://x.test/group/viml', 'https://www.glossarist.org/ontologies/hasCurrentVersion', null).map(q => q.value);
50
+ expect(current).toContain('https://x.test/dataset/viml-2022');
51
+ });
52
+ });
53
+
54
+ describe('emitGroupGraph — topic/family/collection → dcat:Catalog', () => {
55
+ it('types topic group as dcat:Catalog', () => {
56
+ const graph = emitGroupGraph({
57
+ groupId: 'its', groupIri: 'https://x.test/group/its',
58
+ kind: 'topic', title: 'ITS Standards',
59
+ memberIris: ['https://x.test/dataset/iso-14817'],
60
+ });
61
+ const store = parse(writeTurtle(graph));
62
+ const types = store.getObjects('https://x.test/group/its', RDF_TYPE, null).map(q => q.value);
63
+ expect(types).toContain(`${DCAT}Catalog`);
64
+ });
65
+
66
+ it('emits dcat:dataset for each member', () => {
67
+ const graph = emitGroupGraph({
68
+ groupId: 'its', groupIri: 'https://x.test/group/its',
69
+ kind: 'topic', title: 'ITS',
70
+ memberIris: ['https://x.test/dataset/a', 'https://x.test/dataset/b'],
71
+ });
72
+ const store = parse(writeTurtle(graph));
73
+ const datasets = store.getObjects('https://x.test/group/its', `${DCAT}dataset`, null).map(q => q.value);
74
+ expect(datasets).toContain('https://x.test/dataset/a');
75
+ expect(datasets).toContain('https://x.test/dataset/b');
76
+ });
77
+ });
78
+
79
+ describe('emitGroupGraph — default kind → no emission', () => {
80
+ it('produces an empty graph for default kind', () => {
81
+ const graph = emitGroupGraph({
82
+ groupId: 'misc', groupIri: 'https://x.test/group/misc',
83
+ kind: 'default', title: 'Misc',
84
+ memberIris: [],
85
+ });
86
+ expect(Array.from(graph.resources()).length).toBe(0);
87
+ });
88
+ });
89
+
90
+ describe('emitGroupGraph — metadata', () => {
91
+ it('emits title, description, identifier, subject, keywords', () => {
92
+ const graph = emitGroupGraph({
93
+ groupId: 'viml', groupIri: 'https://x.test/group/viml',
94
+ kind: 'lineage', title: 'VIML',
95
+ description: 'Legal metrology vocabulary',
96
+ subject: 'legal metrology',
97
+ keywords: ['metrology', 'legal'],
98
+ memberIris: [],
99
+ });
100
+ const store = parse(writeTurtle(graph));
101
+ expect(store.getObjects('https://x.test/group/viml', `${DCTERMS}title`, null).map(q => q.value)).toContain('VIML');
102
+ expect(store.getObjects('https://x.test/group/viml', `${DCTERMS}description`, null).map(q => q.value)).toContain('Legal metrology vocabulary');
103
+ expect(store.getObjects('https://x.test/group/viml', `${DCTERMS}identifier`, null).map(q => q.value)).toContain('viml');
104
+ expect(store.getObjects('https://x.test/group/viml', `${DCTERMS}subject`, null).map(q => q.value)).toContain('legal metrology');
105
+ const keywords = store.getObjects('https://x.test/group/viml', `${DCAT}keyword`, null).map(q => q.value);
106
+ expect(keywords).toContain('metrology');
107
+ expect(keywords).toContain('legal');
108
+ });
109
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import {
5
+ emitImageVariantGraph,
6
+ imageVariantIri,
7
+ } from '../../components/concept-rdf/image-variant-emitter';
8
+
9
+ const FOAF = 'http://xmlns.com/foaf/0.1/';
10
+ const DCTERMS = 'http://purl.org/dc/terms/';
11
+ const DCAT = 'http://www.w3.org/ns/dcat#';
12
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
13
+
14
+ function parse(turtle: string): Store {
15
+ const parser = new Parser({ format: 'Turtle' });
16
+ const store = new Store();
17
+ store.addQuads(parser.parse(turtle));
18
+ return store;
19
+ }
20
+
21
+ describe('WS K2 — image binary references', () => {
22
+ it('types each image variant as foaf:Image', () => {
23
+ const graph = emitImageVariantGraph({
24
+ registerId: 'iso-geodetic',
25
+ figureId: 'fig_1',
26
+ lang: 'eng',
27
+ format: 'svg',
28
+ downloadUrl: 'https://glossarist.org/data/iso-geodetic/images/fig_1.svg',
29
+ });
30
+ const store = parse(writeTurtle(graph));
31
+ const iri = imageVariantIri({
32
+ registerId: 'iso-geodetic',
33
+ figureId: 'fig_1',
34
+ lang: 'eng',
35
+ format: 'svg',
36
+ downloadUrl: 'x',
37
+ });
38
+ const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
39
+ expect(types).toContain(`${FOAF}Image`);
40
+ });
41
+
42
+ it('emits dcterms:format with the correct MIME type', () => {
43
+ const cases: Array<{ format: 'svg' | 'png' | 'webp' | 'jpg' | 'gif'; mime: string }> = [
44
+ { format: 'svg', mime: 'image/svg+xml' },
45
+ { format: 'png', mime: 'image/png' },
46
+ { format: 'webp', mime: 'image/webp' },
47
+ { format: 'jpg', mime: 'image/jpeg' },
48
+ { format: 'gif', mime: 'image/gif' },
49
+ ];
50
+ for (const { format, mime } of cases) {
51
+ const graph = emitImageVariantGraph({
52
+ registerId: 'r',
53
+ figureId: 'f',
54
+ format,
55
+ downloadUrl: 'x',
56
+ });
57
+ const store = parse(writeTurtle(graph));
58
+ const iri = imageVariantIri({ registerId: 'r', figureId: 'f', format, downloadUrl: 'x' });
59
+ const formats = store.getObjects(iri, `${DCTERMS}format`, null).map(q => q.value);
60
+ expect(formats).toContain(mime);
61
+ }
62
+ });
63
+
64
+ it('emits dcterms:language when lang is provided', () => {
65
+ const graph = emitImageVariantGraph({
66
+ registerId: 'r',
67
+ figureId: 'f',
68
+ lang: 'fra',
69
+ format: 'svg',
70
+ downloadUrl: 'x',
71
+ });
72
+ const store = parse(writeTurtle(graph));
73
+ const iri = imageVariantIri({ registerId: 'r', figureId: 'f', lang: 'fra', format: 'svg', downloadUrl: 'x' });
74
+ const langs = store.getObjects(iri, `${DCTERMS}language`, null).map(q => q.value);
75
+ expect(langs).toContain('fra');
76
+ });
77
+
78
+ it('omits dcterms:language when lang is absent', () => {
79
+ const graph = emitImageVariantGraph({
80
+ registerId: 'r',
81
+ figureId: 'f',
82
+ format: 'svg',
83
+ downloadUrl: 'x',
84
+ });
85
+ const store = parse(writeTurtle(graph));
86
+ const iri = imageVariantIri({ registerId: 'r', figureId: 'f', format: 'svg', downloadUrl: 'x' });
87
+ const langs = store.getObjects(iri, `${DCTERMS}language`, null);
88
+ expect(langs.length).toBe(0);
89
+ });
90
+
91
+ it('emits dcat:byteSize as xsd:integer when byteSize is provided', () => {
92
+ const graph = emitImageVariantGraph({
93
+ registerId: 'r',
94
+ figureId: 'f',
95
+ format: 'png',
96
+ byteSize: 12345,
97
+ downloadUrl: 'x',
98
+ });
99
+ const store = parse(writeTurtle(graph));
100
+ const iri = imageVariantIri({ registerId: 'r', figureId: 'f', format: 'png', byteSize: 0, downloadUrl: 'x' });
101
+ const sizes = store.getObjects(iri, `${DCAT}byteSize`, null);
102
+ expect(sizes.length).toBe(1);
103
+ expect((sizes[0] as any).value).toBe('12345');
104
+ expect((sizes[0] as any).datatype.value).toBe('http://www.w3.org/2001/XMLSchema#integer');
105
+ });
106
+
107
+ it('emits dcat:downloadURL pointing at the binary asset', () => {
108
+ const graph = emitImageVariantGraph({
109
+ registerId: 'iso-geodetic',
110
+ figureId: 'fig_1',
111
+ lang: 'eng',
112
+ format: 'svg',
113
+ downloadUrl: 'https://glossarist.org/data/iso-geodetic/images/fig_1.svg',
114
+ });
115
+ const store = parse(writeTurtle(graph));
116
+ const iri = imageVariantIri({
117
+ registerId: 'iso-geodetic',
118
+ figureId: 'fig_1',
119
+ lang: 'eng',
120
+ format: 'svg',
121
+ downloadUrl: 'x',
122
+ });
123
+ const urls = store.getObjects(iri, `${DCAT}downloadURL`, null).map(q => q.value);
124
+ expect(urls).toContain('https://glossarist.org/data/iso-geodetic/images/fig_1.svg');
125
+ });
126
+
127
+ it('produces distinct IRIs per (figure, lang, format) combination', () => {
128
+ const iri1 = imageVariantIri({ registerId: 'r', figureId: 'f', lang: 'eng', format: 'svg', downloadUrl: 'x' });
129
+ const iri2 = imageVariantIri({ registerId: 'r', figureId: 'f', lang: 'eng', format: 'png', downloadUrl: 'x' });
130
+ const iri3 = imageVariantIri({ registerId: 'r', figureId: 'f', lang: 'fra', format: 'svg', downloadUrl: 'x' });
131
+ expect(iri1).not.toBe(iri2);
132
+ expect(iri1).not.toBe(iri3);
133
+ expect(iri2).not.toBe(iri3);
134
+ });
135
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { writeJsonLd } from '../../components/concept-rdf/jsonld-writer';
3
+ import { RdfGraph, lit, iri, blank, triple } from '../../components/concept-rdf/rdf-graph';
4
+
5
+ describe('writeJsonLd', () => {
6
+ it('produces a JSON-LD document with @context and @graph', () => {
7
+ const g = new RdfGraph();
8
+ const out = writeJsonLd(g);
9
+ const doc = JSON.parse(out);
10
+ expect(doc['@context']).toBeDefined();
11
+ expect(Array.isArray(doc['@graph'])).toBe(true);
12
+ expect(doc['@context'].gloss).toBe('https://www.glossarist.org/ontologies/');
13
+ expect(doc['@context'].skosxl).toBe('http://www.w3.org/2008/05/skos-xl#');
14
+ });
15
+
16
+ it('emits @id and @type for each resource', () => {
17
+ const g = new RdfGraph();
18
+ g.declare('https://ex/c', { types: ['gloss:Concept', 'skos:Concept'] });
19
+ const out = writeJsonLd(g);
20
+ const doc = JSON.parse(out);
21
+ const node = doc['@graph'][0];
22
+ expect(node['@id']).toBe('https://ex/c');
23
+ expect(node['@type']).toEqual(['gloss:Concept', 'skos:Concept']);
24
+ });
25
+
26
+ it('emits plain string literals for untagged values', () => {
27
+ const g = new RdfGraph();
28
+ g.declare('https://ex/c', { types: [] })
29
+ .literal('gloss:identifier', 'X');
30
+ const out = writeJsonLd(g);
31
+ const doc = JSON.parse(out);
32
+ expect(doc['@graph'][0]['gloss:identifier']).toBe('X');
33
+ });
34
+
35
+ it('emits language-tagged literals as @value/@language objects', () => {
36
+ const g = new RdfGraph();
37
+ g.declare('https://ex/c', { types: [] })
38
+ .literal('skos:prefLabel', 'term', { lang: 'eng' });
39
+ const out = writeJsonLd(g);
40
+ const doc = JSON.parse(out);
41
+ expect(doc['@graph'][0]['skos:prefLabel']).toEqual({ '@value': 'term', '@language': 'eng' });
42
+ });
43
+
44
+ it('emits datatyped literals as @value/@type objects', () => {
45
+ const g = new RdfGraph();
46
+ g.declare('https://ex/c', { types: [] })
47
+ .literal('gloss:value', '42', { datatype: 'xsd:integer' });
48
+ const out = writeJsonLd(g);
49
+ const doc = JSON.parse(out);
50
+ expect(doc['@graph'][0]['gloss:value']).toEqual({ '@value': '42', '@type': 'xsd:integer' });
51
+ });
52
+
53
+ it('emits IRI objects as @id references', () => {
54
+ const g = new RdfGraph();
55
+ g.declare('https://ex/c', { types: [] })
56
+ .iri('gloss:related', 'https://ex/other');
57
+ const out = writeJsonLd(g);
58
+ const doc = JSON.parse(out);
59
+ expect(doc['@graph'][0]['gloss:related']).toEqual({ '@id': 'https://ex/other' });
60
+ });
61
+
62
+ it('emits single object for predicates with one value', () => {
63
+ const g = new RdfGraph();
64
+ g.declare('https://ex/c', { types: [] })
65
+ .literal('gloss:note', 'one');
66
+ const out = writeJsonLd(g);
67
+ const doc = JSON.parse(out);
68
+ expect(doc['@graph'][0]['gloss:note']).toBe('one');
69
+ });
70
+
71
+ it('emits arrays for predicates with multiple values', () => {
72
+ const g = new RdfGraph();
73
+ g.declare('https://ex/c', { types: [] })
74
+ .literal('gloss:note', 'one')
75
+ .literal('gloss:note', 'two');
76
+ const out = writeJsonLd(g);
77
+ const doc = JSON.parse(out);
78
+ expect(doc['@graph'][0]['gloss:note']).toEqual(['one', 'two']);
79
+ });
80
+
81
+ it('serializes blank nodes as nested objects', () => {
82
+ const g = new RdfGraph();
83
+ g.declare('https://ex/c', { types: [] })
84
+ .blank('gloss:hasSource', [
85
+ triple('rdf:value', lit('ISO 123')),
86
+ triple('dcterms:title', lit('Standard')),
87
+ ]);
88
+ const out = writeJsonLd(g);
89
+ const doc = JSON.parse(out);
90
+ expect(doc['@graph'][0]['gloss:hasSource']).toEqual({
91
+ 'rdf:value': 'ISO 123',
92
+ 'dcterms:title': 'Standard',
93
+ });
94
+ });
95
+
96
+ it('serializes multiple blank nodes for the same predicate as an array', () => {
97
+ const g = new RdfGraph();
98
+ g.declare('https://ex/c', { types: [] })
99
+ .blank('gloss:hasSource', [triple('rdf:value', lit('A'))])
100
+ .blank('gloss:hasSource', [triple('rdf:value', lit('B'))]);
101
+ const out = writeJsonLd(g);
102
+ const doc = JSON.parse(out);
103
+ expect(Array.isArray(doc['@graph'][0]['gloss:hasSource'])).toBe(true);
104
+ expect(doc['@graph'][0]['gloss:hasSource']).toEqual([
105
+ { 'rdf:value': 'A' },
106
+ { 'rdf:value': 'B' },
107
+ ]);
108
+ });
109
+ });
@@ -0,0 +1,177 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
5
+ import { Concept } from 'glossarist';
6
+
7
+ const G = 'https://www.glossarist.org/ontologies/';
8
+ const SKOSXL = 'http://www.w3.org/2008/05/skos-xl#';
9
+ const SKOS = 'http://www.w3.org/2004/02/skos/core#';
10
+ const PROV = 'http://www.w3.org/ns/prov#';
11
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
12
+ const XSD = 'http://www.w3.org/2001/XMLSchema#';
13
+
14
+ function parse(turtle: string): Store {
15
+ const parser = new Parser({ format: 'Turtle' });
16
+ const store = new Store();
17
+ store.addQuads(parser.parse(turtle));
18
+ return store;
19
+ }
20
+
21
+ const BASE = 'https://glossarist.org/fixtures/k4';
22
+
23
+ function emitAndParse(concept: Concept) {
24
+ const { graph } = emitConceptGraph(concept, concept.uri ?? '');
25
+ return { graph, store: parse(writeTurtle(graph)) };
26
+ }
27
+
28
+ describe('WS K4 — NVRs as first-class URIs', () => {
29
+ it('emits an NVR URI when the NVR has a caption (prefLabel material)', () => {
30
+ const concept = Concept.fromJSON({
31
+ id: 'k4.1', uri: `${BASE}/k4.1`, status: 'valid',
32
+ localizations: {
33
+ eng: {
34
+ language_code: 'eng', entry_status: 'valid',
35
+ terms: [{ type: 'expression', designation: 'K4 concept', normative_status: 'preferred' }],
36
+ definition: [{ content: 'Concept with a referenced NVR.' }],
37
+ non_verbal_rep: [
38
+ { type: 'expression', caption: 'Diagram label', images: [{ src: 'https://glossarist.org/figs/1.svg' }] },
39
+ ],
40
+ },
41
+ },
42
+ });
43
+
44
+ const { graph, store } = emitAndParse(concept);
45
+ const nvrUri = `${BASE}/k4.1/nvr/1_svg`;
46
+ expect(graph.get(nvrUri)).toBeDefined();
47
+
48
+ const lcUri = `${BASE}/k4.1/eng`;
49
+ const links = store.getObjects(lcUri, `${G}hasNonVerbalRepresentation`, null).map(q => q.value);
50
+ expect(links).toContain(nvrUri);
51
+
52
+ const nvrTypes = store.getObjects(nvrUri, RDF_TYPE, null).map(q => q.value);
53
+ expect(nvrTypes).toContain(`${G}NonVerbalRep`);
54
+ expect(nvrTypes).toContain(`${G}Expression`);
55
+ expect(nvrTypes).toContain(`${SKOS}Concept`);
56
+
57
+ const prefLabelBlanks = store.getObjects(nvrUri, `${SKOSXL}prefLabel`, null);
58
+ expect(prefLabelBlanks.length).toBe(1);
59
+ const literalForms = store.getObjects(prefLabelBlanks[0], `${SKOSXL}literalForm`, null);
60
+ expect(literalForms.length).toBe(1);
61
+ expect((literalForms[0] as any).value).toBe('Diagram label');
62
+ expect((literalForms[0] as any).language).toBe('eng');
63
+ });
64
+
65
+ it('emits gloss:image as xsd:anyURI on the NVR', () => {
66
+ const concept = Concept.fromJSON({
67
+ id: 'k4.2', uri: `${BASE}/k4.2`, status: 'valid',
68
+ localizations: {
69
+ eng: {
70
+ language_code: 'eng', entry_status: 'valid',
71
+ terms: [{ type: 'expression', designation: 'K4 with image', normative_status: 'preferred' }],
72
+ definition: [{ content: 'Has an NVR with image.' }],
73
+ non_verbal_rep: [
74
+ { type: 'expression', caption: 'Fig', images: [{ src: 'https://x.test/fig.svg' }] },
75
+ ],
76
+ },
77
+ },
78
+ });
79
+ const { store } = emitAndParse(concept);
80
+ const nvrUri = `${BASE}/k4.2/nvr/fig_svg`;
81
+ const images = store.getObjects(nvrUri, `${G}image`, null);
82
+ expect(images.length).toBe(1);
83
+ expect((images[0] as any).value).toBe('https://x.test/fig.svg');
84
+ expect((images[0] as any).datatype.value).toBe(`${XSD}anyURI`);
85
+ });
86
+
87
+ it('emits prov:wasDerivedFrom from sources[0].origin.link', () => {
88
+ const concept = Concept.fromJSON({
89
+ id: 'k4.3', uri: `${BASE}/k4.3`, status: 'valid',
90
+ localizations: {
91
+ eng: {
92
+ language_code: 'eng', entry_status: 'valid',
93
+ terms: [{ type: 'expression', designation: 'K4 with source', normative_status: 'preferred' }],
94
+ definition: [{ content: 'Has an NVR with source.' }],
95
+ non_verbal_rep: [
96
+ {
97
+ type: 'expression', caption: 'Fig',
98
+ sources: [{ status: 'identical', type: 'authoritative', origin: { ref: { source: 'ISO', id: 'X' }, link: 'https://x.test/source' } }],
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ });
104
+ const { store } = emitAndParse(concept);
105
+ const derived = store.getObjects(`${BASE}/k4.3/nvr/Fig`, `${PROV}wasDerivedFrom`, null).map(q => q.value);
106
+ expect(derived).toContain('https://x.test/source');
107
+ });
108
+
109
+ it('falls back to gloss:hasNonVerbalRep blank-node emission when NVR has no caption and no image', () => {
110
+ const concept = Concept.fromJSON({
111
+ id: 'k4.4', uri: `${BASE}/k4.4`, status: 'valid',
112
+ localizations: {
113
+ eng: {
114
+ language_code: 'eng', entry_status: 'valid',
115
+ terms: [{ type: 'expression', designation: 'K4 fallback', normative_status: 'preferred' }],
116
+ definition: [{ content: 'Has an NVR with no useful content.' }],
117
+ non_verbal_rep: [{ type: 'expression' }],
118
+ },
119
+ },
120
+ });
121
+ const { store } = emitAndParse(concept);
122
+ const lcUri = `${BASE}/k4.4/eng`;
123
+ const uriLinks = store.getObjects(lcUri, `${G}hasNonVerbalRepresentation`, null);
124
+ expect(uriLinks.length).toBe(0);
125
+ const blankLinks = store.getObjects(lcUri, `${G}hasNonVerbalRep`, null);
126
+ expect(blankLinks.length).toBe(1);
127
+ });
128
+
129
+ it('produces NVR URIs keyed by index when multiple NVRs are present', () => {
130
+ const concept = Concept.fromJSON({
131
+ id: 'k4.5', uri: `${BASE}/k4.5`, status: 'valid',
132
+ localizations: {
133
+ eng: {
134
+ language_code: 'eng', entry_status: 'valid',
135
+ terms: [{ type: 'expression', designation: 'K4 multiple', normative_status: 'preferred' }],
136
+ definition: [{ content: 'Has multiple NVRs.' }],
137
+ non_verbal_rep: [
138
+ { type: 'expression', caption: 'One', images: [{ src: 'https://x.test/1.svg' }] },
139
+ { type: 'expression', caption: 'Two', images: [{ src: 'https://x.test/2.svg' }] },
140
+ { type: 'expression', caption: 'Three', images: [{ src: 'https://x.test/3.svg' }] },
141
+ ],
142
+ },
143
+ },
144
+ });
145
+ const { store } = emitAndParse(concept);
146
+ const lcUri = `${BASE}/k4.5/eng`;
147
+ const links = store.getObjects(lcUri, `${G}hasNonVerbalRepresentation`, null).map(q => q.value);
148
+ expect(links).toContain(`${BASE}/k4.5/nvr/1_svg`);
149
+ expect(links).toContain(`${BASE}/k4.5/nvr/2_svg`);
150
+ expect(links).toContain(`${BASE}/k4.5/nvr/3_svg`);
151
+ });
152
+ });
153
+
154
+ describe('WS K4 — URI NVR conforms to canonical NonVerbalRep shape', () => {
155
+ it('emits NVR resources that satisfy gloss:NonVerbalRepShape (skosxl:prefLabel min 1)', () => {
156
+ const concept = Concept.fromJSON({
157
+ id: 'k4.6', uri: `${BASE}/k4.6`, status: 'valid',
158
+ localizations: {
159
+ eng: {
160
+ language_code: 'eng', entry_status: 'valid',
161
+ terms: [{ type: 'expression', designation: 'shape-conformant', normative_status: 'preferred' }],
162
+ definition: [{ content: 'NVR conforms to shape.' }],
163
+ non_verbal_rep: [
164
+ { type: 'expression', caption: 'Label', images: [{ src: 'https://x.test/fig.svg' }] },
165
+ ],
166
+ },
167
+ },
168
+ });
169
+ const { store } = emitAndParse(concept);
170
+ const nvrUri = `${BASE}/k4.6/nvr/fig_svg`;
171
+ const prefLabels = store.getObjects(nvrUri, `${SKOSXL}prefLabel`, null);
172
+ expect(prefLabels.length).toBeGreaterThanOrEqual(1);
173
+ const nvrTypes = store.getObjects(nvrUri, RDF_TYPE, null).map(q => q.value);
174
+ expect(nvrTypes).toContain(`${G}NonVerbalRep`);
175
+ expect(nvrTypes).toContain(`${SKOS}Concept`);
176
+ });
177
+ });