@glossarist/concept-browser 0.7.51 → 0.7.52
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 +32 -0
- package/env.d.ts +15 -0
- package/package.json +12 -2
- package/scripts/__tests__/doctor.test.mjs +147 -0
- package/scripts/doctor.mjs +327 -0
- package/scripts/generate-data.mjs +136 -0
- package/scripts/generate-ontology-data.mjs +3 -3
- package/scripts/generate-ontology-schema.mjs +3 -3
- package/scripts/lib/agents-turtle.mjs +64 -0
- package/scripts/lib/bibliography-turtle.mjs +54 -0
- package/scripts/lib/build-activity-turtle.mjs +92 -0
- package/scripts/lib/build-cache.mjs +70 -0
- package/scripts/lib/dataset-turtle.mjs +79 -0
- package/scripts/lib/turtle-escape.mjs +0 -0
- package/scripts/lib/version-turtle.mjs +56 -0
- package/scripts/lib/vocab-turtle.mjs +64 -0
- package/scripts/normalize-yaml.mjs +99 -0
- package/scripts/sync-concept-model.mjs +86 -0
- package/scripts/validate-shacl.mjs +194 -0
- package/src/__fixtures__/concept-shape.ttl +20 -0
- package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
- package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
- package/src/__fixtures__/shacl/good/concept.ttl +8 -0
- package/src/__tests__/__fixtures__/concepts.ts +221 -0
- package/src/__tests__/adapters/concept-identity.test.ts +76 -0
- package/src/__tests__/components/error-boundary.test.ts +109 -0
- package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
- package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
- package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
- package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
- package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
- package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
- package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
- package/src/__tests__/concept-rdf/differential.test.ts +96 -0
- package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
- package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
- package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
- package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
- package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
- package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
- package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
- package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
- package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
- package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
- package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
- package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
- package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
- package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
- package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
- package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
- package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
- package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
- package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
- package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
- package/src/__tests__/concept-rdf-view.test.ts +136 -0
- package/src/__tests__/dataset-style.test.ts +12 -7
- package/src/__tests__/errors/errors.test.ts +142 -0
- package/src/__tests__/format-downloads.test.ts +47 -65
- package/src/__tests__/markdown-lite.test.ts +19 -0
- package/src/__tests__/perf/bundle-layout.test.ts +50 -0
- package/src/__tests__/perf/serialization-perf.test.ts +121 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
- package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
- package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
- package/src/__tests__/scripts/build-cache.test.ts +78 -0
- package/src/__tests__/scripts/build-integration.test.ts +134 -0
- package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
- package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
- package/src/__tests__/scripts/stryker-config.test.ts +33 -0
- package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
- package/src/__tests__/scripts/version-turtle.test.ts +72 -0
- package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
- package/src/__tests__/use-format-registry.test.ts +125 -0
- package/src/__tests__/utils/bcp47.test.ts +166 -0
- package/src/__tests__/utils/color-theme.test.ts +143 -0
- package/src/__tests__/utils/url-safety.test.ts +65 -0
- package/src/__tests__/validate-shacl.test.ts +100 -0
- package/src/adapters/DatasetAdapter.ts +11 -5
- package/src/adapters/GraphDataSource.ts +2 -1
- package/src/adapters/UriRouter.ts +2 -1
- package/src/adapters/concept-identity.ts +69 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
- package/src/adapters/non-verbal-resolver.ts +2 -1
- package/src/components/AppSidebar.vue +189 -93
- package/src/components/ConceptDetail.vue +8 -0
- package/src/components/ConceptEditionRail.vue +222 -0
- package/src/components/ConceptRdfView.vue +37 -377
- package/src/components/DatasetSeriesCard.vue +270 -0
- package/src/components/ErrorBoundary.vue +95 -0
- package/src/components/FormatDownloads.vue +17 -13
- package/src/components/HomeSeriesSection.vue +277 -0
- package/src/components/RelationSphere.vue +1672 -0
- package/src/components/SidebarSeriesSection.vue +239 -0
- package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
- package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
- package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
- package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
- package/src/components/concept-rdf/agents-emitter.ts +82 -0
- package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
- package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
- package/src/components/concept-rdf/concept-emitter.ts +443 -0
- package/src/components/concept-rdf/dataset-emitter.ts +95 -0
- package/src/components/concept-rdf/group-emitter.ts +69 -0
- package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
- package/src/components/concept-rdf/jsonld-writer.ts +82 -0
- package/src/components/concept-rdf/predicates.ts +261 -0
- package/src/components/concept-rdf/provenance.ts +80 -0
- package/src/components/concept-rdf/rdf-graph.ts +211 -0
- package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
- package/src/components/concept-rdf/sections-builder.ts +62 -0
- package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
- package/src/components/concept-rdf/turtle-writer.ts +116 -0
- package/src/components/concept-rdf/use-rdf-document.ts +72 -0
- package/src/components/concept-rdf/version-emitter.ts +65 -0
- package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
- package/src/composables/use-color-theme.ts +82 -0
- package/src/composables/use-format-registry.ts +42 -0
- package/src/composables/useDatasetSeries.ts +258 -0
- package/src/composables/useSphereProjection.ts +125 -0
- package/src/config/group-types.ts +92 -0
- package/src/config/types.ts +81 -2
- package/src/config/use-site-config.ts +2 -1
- package/src/errors.ts +136 -0
- package/src/i18n/locales/eng.yml +24 -0
- package/src/i18n/locales/fra.yml +24 -0
- package/src/stores/vocabulary.ts +3 -1
- package/src/style.css +17 -2
- package/src/types/agents-version-turtle.d.ts +27 -0
- package/src/types/bibliography-turtle.d.ts +12 -0
- package/src/types/build-activity-turtle.d.ts +16 -0
- package/src/types/build-cache.d.ts +20 -0
- package/src/types/dataset-turtle.d.ts +32 -0
- package/src/types/normalize-yaml.d.ts +16 -0
- package/src/types/turtle-escape.d.ts +6 -0
- package/src/types/vocab-turtle.d.ts +13 -0
- package/src/utils/asciidoc-lite.ts +11 -6
- package/src/utils/bcp47.ts +141 -0
- package/src/utils/color-theme-integration.ts +11 -0
- package/src/utils/color-theme.ts +129 -0
- package/src/utils/dataset-style.ts +31 -6
- package/src/utils/locale.ts +6 -14
- package/src/utils/markdown-lite.ts +6 -1
- package/src/utils/relation-sphere-styling.ts +63 -0
- package/src/utils/relationship-categories.ts +30 -0
- package/src/utils/url-safety.ts +30 -0
- package/src/views/ConceptView.vue +183 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Parser, Store } from 'n3';
|
|
5
|
+
|
|
6
|
+
const SHAPES_PATH = join(process.cwd(), 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
|
|
7
|
+
|
|
8
|
+
const SH = 'http://www.w3.org/ns/shacl#';
|
|
9
|
+
const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
|
10
|
+
const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
|
11
|
+
|
|
12
|
+
function parseShapes(): Store {
|
|
13
|
+
const text = readFileSync(SHAPES_PATH, 'utf8');
|
|
14
|
+
const parser = new Parser({ format: 'Turtle' });
|
|
15
|
+
const store = new Store();
|
|
16
|
+
store.addQuads(parser.parse(text));
|
|
17
|
+
return store;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let store: Store;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
store = parseShapes();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function allQuads(): any[] {
|
|
27
|
+
return [...(store as any)];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isShapeQuad(q: any): boolean {
|
|
31
|
+
return q.predicate.value === `${RDF}type` && q.object.value === `${SH}NodeShape`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('WS P7 — canonical SHACL shape self-consistency', () => {
|
|
35
|
+
it('every sh:NodeShape declares an sh:targetClass', () => {
|
|
36
|
+
const shapes = allQuads().filter(isShapeQuad);
|
|
37
|
+
expect(shapes.length).toBeGreaterThan(0);
|
|
38
|
+
for (const shape of shapes) {
|
|
39
|
+
const targetClasses = (store as any).getObjects(shape.subject, `${SH}targetClass`, null);
|
|
40
|
+
expect(targetClasses.length).toBeGreaterThan(0);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('every sh:targetClass IRI uses a known namespace', () => {
|
|
45
|
+
const targets = allQuads()
|
|
46
|
+
.filter((q: any) => q.predicate.value === `${SH}targetClass`)
|
|
47
|
+
.map((q: any) => q.object.value);
|
|
48
|
+
expect(targets.length).toBeGreaterThan(0);
|
|
49
|
+
const knownPrefixes = [
|
|
50
|
+
'https://www.glossarist.org/',
|
|
51
|
+
'http://www.w3.org/2004/02/skos-core#',
|
|
52
|
+
'http://www.w3.org/2008/05/skos-xl#',
|
|
53
|
+
'http://www.w3.org/ns/shacl#',
|
|
54
|
+
'http://xmlns.com/foaf/0.1/',
|
|
55
|
+
'http://www.w3.org/ns/prov#',
|
|
56
|
+
'http://purl.org/dc/terms/',
|
|
57
|
+
];
|
|
58
|
+
for (const target of targets) {
|
|
59
|
+
const known = knownPrefixes.some(p => target === p || target.startsWith(p));
|
|
60
|
+
expect(known).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('every sh:datatype references a built-in XSD datatype (when XSD-prefixed)', () => {
|
|
65
|
+
const builtins = new Set([
|
|
66
|
+
`${XSD}string`, `${XSD}integer`, `${XSD}dateTime`, `${XSD}date`, `${XSD}anyURI`,
|
|
67
|
+
`${XSD}boolean`, `${XSD}decimal`, `${XSD}double`, `${XSD}float`,
|
|
68
|
+
`${XSD}nonNegativeInteger`, `${XSD}positiveInteger`, `${XSD}duration`,
|
|
69
|
+
]);
|
|
70
|
+
const datatypes = allQuads().filter((q: any) => q.predicate.value === `${SH}datatype`);
|
|
71
|
+
expect(datatypes.length).toBeGreaterThan(0);
|
|
72
|
+
for (const dt of datatypes) {
|
|
73
|
+
const value = dt.object.value;
|
|
74
|
+
if (value.startsWith(XSD)) {
|
|
75
|
+
expect(builtins.has(value)).toBe(true);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('every sh:property has exactly one sh:path', () => {
|
|
81
|
+
const props = allQuads().filter((q: any) => q.predicate.value === `${SH}property`);
|
|
82
|
+
expect(props.length).toBeGreaterThan(0);
|
|
83
|
+
for (const prop of props) {
|
|
84
|
+
const paths = (store as any).getObjects(prop.object, `${SH}path`, null);
|
|
85
|
+
expect(paths.length).toBe(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('no two NodeShapes share both a target class AND the same predicate path', () => {
|
|
90
|
+
const shapes = allQuads().filter(isShapeQuad);
|
|
91
|
+
|
|
92
|
+
const seen = new Map<string, string[]>();
|
|
93
|
+
for (const shape of shapes) {
|
|
94
|
+
const shapeIri = String(shape.subject.value);
|
|
95
|
+
const targetClasses = (store as any)
|
|
96
|
+
.getObjects(shape.subject, `${SH}targetClass`, null)
|
|
97
|
+
.map((t: any) => String(t.value));
|
|
98
|
+
const properties = (store as any).getObjects(shape.subject, `${SH}property`, null);
|
|
99
|
+
const paths: string[] = [];
|
|
100
|
+
for (const prop of properties) {
|
|
101
|
+
const propPaths = (store as any)
|
|
102
|
+
.getObjects(prop, `${SH}path`, null)
|
|
103
|
+
.map((t: any) => String(t.value));
|
|
104
|
+
paths.push(...propPaths);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const tc of targetClasses) {
|
|
108
|
+
for (const p of paths) {
|
|
109
|
+
const key = `${tc}|${p}`;
|
|
110
|
+
const existing = seen.get(key) ?? [];
|
|
111
|
+
existing.push(shapeIri);
|
|
112
|
+
seen.set(key, existing);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [key, shapeIris] of seen.entries()) {
|
|
118
|
+
if (shapeIris.length > 1) {
|
|
119
|
+
expect.fail(`Conflicting NodeShapes at ${key}: ${shapeIris.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('no NodeShape has both sh:property and a contradiction (path declared twice)', () => {
|
|
125
|
+
const shapes = allQuads().filter(isShapeQuad);
|
|
126
|
+
for (const shape of shapes) {
|
|
127
|
+
const properties = (store as any).getObjects(shape.subject, `${SH}property`, null);
|
|
128
|
+
const seenPaths = new Set<string>();
|
|
129
|
+
for (const prop of properties) {
|
|
130
|
+
const paths = (store as any).getObjects(prop, `${SH}path`, null).map((t: any) => String(t.value));
|
|
131
|
+
for (const p of paths) {
|
|
132
|
+
expect(seenPaths.has(p)).toBe(false);
|
|
133
|
+
seenPaths.add(p);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
|
|
6
|
+
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
7
|
+
import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = join(__dirname, '..', '..', '..');
|
|
11
|
+
const SNAPSHOTS_DIR = join(ROOT, 'test', 'snapshots', 'js');
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('WS P1 — JS snapshot generator', () => {
|
|
18
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
19
|
+
it(`emits a canonical Turtle snapshot for ${fixture.name}`, () => {
|
|
20
|
+
const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
|
|
21
|
+
const ttl = writeTurtle(graph);
|
|
22
|
+
const target = join(SNAPSHOTS_DIR, `${fixture.name}.ttl`);
|
|
23
|
+
writeFileSync(target, ttl);
|
|
24
|
+
expect(existsSync(target)).toBe(true);
|
|
25
|
+
expect(ttl.length).toBeGreaterThan(0);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it('snapshot directory contains one .ttl per fixture after generation', () => {
|
|
30
|
+
const files = existsSync(SNAPSHOTS_DIR)
|
|
31
|
+
? readdirSync(SNAPSHOTS_DIR).filter(f => f.endsWith('.ttl'))
|
|
32
|
+
: [];
|
|
33
|
+
expect(files.length).toBeGreaterThanOrEqual(CONCEPT_FIXTURES.length);
|
|
34
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
35
|
+
expect(files).toContain(`${fixture.name}.ttl`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('re-emitting the same fixture produces byte-identical Turtle (deterministic)', () => {
|
|
40
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
41
|
+
const a = writeTurtle(emitConceptGraph(fixture.concept, fixture.uri).graph);
|
|
42
|
+
const b = writeTurtle(emitConceptGraph(fixture.concept, fixture.uri).graph);
|
|
43
|
+
expect(a).toBe(b);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('WS P1 — JS snapshots are stable across runs (drift detector)', () => {
|
|
49
|
+
for (const fixture of CONCEPT_FIXTURES) {
|
|
50
|
+
it(`${fixture.name}: emitted snapshot matches the file on disk (run npm test to refresh)`, () => {
|
|
51
|
+
const { graph } = emitConceptGraph(fixture.concept, fixture.uri);
|
|
52
|
+
const ttl = writeTurtle(graph);
|
|
53
|
+
const target = join(SNAPSHOTS_DIR, `${fixture.name}.ttl`);
|
|
54
|
+
if (!existsSync(target)) {
|
|
55
|
+
writeFileSync(target, ttl);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const existing = readFileSync(target, 'utf8');
|
|
59
|
+
if (existing !== ttl) {
|
|
60
|
+
writeFileSync(target, ttl);
|
|
61
|
+
expect.fail(
|
|
62
|
+
`Snapshot drift for ${fixture.name}.\n` +
|
|
63
|
+
`Updated ${target}.\n` +
|
|
64
|
+
`Commit the change if the new output is the canonical shape.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
expect(existing).toBe(ttl);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function readdirSync(p: string): string[] {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
74
|
+
return require('node:fs').readdirSync(p);
|
|
75
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
emitFormulaGraph,
|
|
6
|
+
emitTableGraph,
|
|
7
|
+
formulaIri,
|
|
8
|
+
tableIri,
|
|
9
|
+
} from '../../components/concept-rdf/table-formula-emitter';
|
|
10
|
+
|
|
11
|
+
const G = 'https://www.glossarist.org/ontologies/';
|
|
12
|
+
const DCTERMS = 'http://purl.org/dc/terms/';
|
|
13
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
14
|
+
|
|
15
|
+
function parse(turtle: string): Store {
|
|
16
|
+
const parser = new Parser({ format: 'Turtle' });
|
|
17
|
+
const store = new Store();
|
|
18
|
+
store.addQuads(parser.parse(turtle));
|
|
19
|
+
return store;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('WS K3 — Formula representation', () => {
|
|
23
|
+
it('types each formula as gloss:Formula', () => {
|
|
24
|
+
const graph = emitFormulaGraph({
|
|
25
|
+
registerId: 'iso-geodetic',
|
|
26
|
+
formulaId: 'f1',
|
|
27
|
+
expression: 'E = mc^2',
|
|
28
|
+
});
|
|
29
|
+
const store = parse(writeTurtle(graph));
|
|
30
|
+
const iri = formulaIri({ registerId: 'iso-geodetic', formulaId: 'f1', expression: '' });
|
|
31
|
+
const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
|
|
32
|
+
expect(types).toContain(`${G}Formula`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('emits gloss:expression as the canonical form', () => {
|
|
36
|
+
const graph = emitFormulaGraph({
|
|
37
|
+
registerId: 'r',
|
|
38
|
+
formulaId: 'f1',
|
|
39
|
+
expression: 'tan(θ) = μ',
|
|
40
|
+
});
|
|
41
|
+
const store = parse(writeTurtle(graph));
|
|
42
|
+
const iri = formulaIri({ registerId: 'r', formulaId: 'f1', expression: '' });
|
|
43
|
+
const exprs = store.getObjects(iri, `${G}expression`, null).map(q => q.value);
|
|
44
|
+
expect(exprs).toContain('tan(θ) = μ');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('emits gloss:latexForm when provided', () => {
|
|
48
|
+
const graph = emitFormulaGraph({
|
|
49
|
+
registerId: 'r',
|
|
50
|
+
formulaId: 'f1',
|
|
51
|
+
expression: 'E = mc^2',
|
|
52
|
+
latexForm: '\\mathrm{E} = mc^2',
|
|
53
|
+
});
|
|
54
|
+
const store = parse(writeTurtle(graph));
|
|
55
|
+
const iri = formulaIri({ registerId: 'r', formulaId: 'f1', expression: '' });
|
|
56
|
+
const latex = store.getObjects(iri, `${G}latexForm`, null).map(q => q.value);
|
|
57
|
+
expect(latex).toContain('\\mathrm{E} = mc^2');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('emits dcterms:description when provided', () => {
|
|
61
|
+
const graph = emitFormulaGraph({
|
|
62
|
+
registerId: 'r',
|
|
63
|
+
formulaId: 'f1',
|
|
64
|
+
expression: 'E = mc^2',
|
|
65
|
+
description: 'Mass-energy equivalence',
|
|
66
|
+
lang: 'eng',
|
|
67
|
+
});
|
|
68
|
+
const store = parse(writeTurtle(graph));
|
|
69
|
+
const iri = formulaIri({ registerId: 'r', formulaId: 'f1', expression: '' });
|
|
70
|
+
const desc = store.getObjects(iri, `${DCTERMS}description`, null).map(q => q.value);
|
|
71
|
+
expect(desc).toContain('Mass-energy equivalence');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('WS K3 — Table representation', () => {
|
|
76
|
+
it('types each table as gloss:Table', () => {
|
|
77
|
+
const graph = emitTableGraph({
|
|
78
|
+
registerId: 'r',
|
|
79
|
+
tableId: 't1',
|
|
80
|
+
title: 'Conversion factors',
|
|
81
|
+
});
|
|
82
|
+
const store = parse(writeTurtle(graph));
|
|
83
|
+
const iri = tableIri({ registerId: 'r', tableId: 't1' });
|
|
84
|
+
const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
|
|
85
|
+
expect(types).toContain(`${G}Table`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('emits dcterms:title and gloss:caption when provided', () => {
|
|
89
|
+
const graph = emitTableGraph({
|
|
90
|
+
registerId: 'r',
|
|
91
|
+
tableId: 't1',
|
|
92
|
+
title: 'Conversion factors',
|
|
93
|
+
caption: 'Common unit conversions',
|
|
94
|
+
lang: 'eng',
|
|
95
|
+
});
|
|
96
|
+
const store = parse(writeTurtle(graph));
|
|
97
|
+
const iri = tableIri({ registerId: 'r', tableId: 't1' });
|
|
98
|
+
expect(store.getObjects(iri, `${DCTERMS}title`, null).map(q => q.value)).toContain('Conversion factors');
|
|
99
|
+
expect(store.getObjects(iri, `${G}caption`, null).map(q => q.value)).toContain('Common unit conversions');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('emits a MarkupTable content blank when markup is provided', () => {
|
|
103
|
+
const graph = emitTableGraph({
|
|
104
|
+
registerId: 'r',
|
|
105
|
+
tableId: 't1',
|
|
106
|
+
markup: '<table><tr><td>1</td></tr></table>',
|
|
107
|
+
markupFormat: 'html',
|
|
108
|
+
});
|
|
109
|
+
const store = parse(writeTurtle(graph));
|
|
110
|
+
const iri = tableIri({ registerId: 'r', tableId: 't1' });
|
|
111
|
+
const contentBlanks = store.getObjects(iri, `${G}content`, null);
|
|
112
|
+
expect(contentBlanks.length).toBe(1);
|
|
113
|
+
const contentBlank = contentBlanks[0];
|
|
114
|
+
const innerTypes = store.getObjects(contentBlank, RDF_TYPE, null).map(q => q.value);
|
|
115
|
+
expect(innerTypes).toContain(`${G}MarkupTable`);
|
|
116
|
+
const markupLiterals = store.getObjects(contentBlank, `${G}content`, null).map(q => q.value);
|
|
117
|
+
expect(markupLiterals).toContain('<table><tr><td>1</td></tr></table>');
|
|
118
|
+
const formats = store.getObjects(contentBlank, `${DCTERMS}format`, null).map(q => q.value);
|
|
119
|
+
expect(formats).toContain('html');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('emits a StructuredTable content blank when headers and rows are provided', () => {
|
|
123
|
+
const graph = emitTableGraph({
|
|
124
|
+
registerId: 'r',
|
|
125
|
+
tableId: 't1',
|
|
126
|
+
headers: [{ value: 'Term' }, { value: 'Definition' }],
|
|
127
|
+
rows: [
|
|
128
|
+
{ cells: ['metre', 'unit of length'] },
|
|
129
|
+
{ cells: ['second', 'unit of time'] },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const store = parse(writeTurtle(graph));
|
|
133
|
+
const iri = tableIri({ registerId: 'r', tableId: 't1' });
|
|
134
|
+
const contentBlanks = store.getObjects(iri, `${G}content`, null);
|
|
135
|
+
expect(contentBlanks.length).toBe(1);
|
|
136
|
+
const contentBlank = contentBlanks[0];
|
|
137
|
+
const innerTypes = store.getObjects(contentBlank, RDF_TYPE, null).map(q => q.value);
|
|
138
|
+
expect(innerTypes).toContain(`${G}StructuredTable`);
|
|
139
|
+
const rows = store.getObjects(contentBlank, `${G}hasRow`, null);
|
|
140
|
+
expect(rows.length).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
3
|
+
import { RdfGraph, lit, iri, blank, triple } from '../../components/concept-rdf/rdf-graph';
|
|
4
|
+
|
|
5
|
+
describe('writeTurtle', () => {
|
|
6
|
+
it('declares all canonical @prefix lines', () => {
|
|
7
|
+
const g = new RdfGraph();
|
|
8
|
+
const out = writeTurtle(g);
|
|
9
|
+
expect(out).toContain('@prefix gloss: <https://www.glossarist.org/ontologies/> .');
|
|
10
|
+
expect(out).toContain('@prefix skos: <http://www.w3.org/2004/02/skos/core#> .');
|
|
11
|
+
expect(out).toContain('@prefix skosxl: <http://www.w3.org/2008/05/skos-xl#> .');
|
|
12
|
+
expect(out).toContain('@prefix dcterms: <http://purl.org/dc/terms/> .');
|
|
13
|
+
expect(out).toContain('@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('writes types after `a` keyword', () => {
|
|
17
|
+
const g = new RdfGraph();
|
|
18
|
+
g.declare('https://ex/c', { types: ['gloss:Concept', 'skos:Concept'] })
|
|
19
|
+
.literal('gloss:identifier', '1');
|
|
20
|
+
const out = writeTurtle(g);
|
|
21
|
+
expect(out).toMatch(/<https:\/\/ex\/c> a gloss:Concept, skos:Concept ;/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('terminates resources with full stops', () => {
|
|
25
|
+
const g = new RdfGraph();
|
|
26
|
+
g.declare('https://ex/c', { types: ['gloss:Concept'] })
|
|
27
|
+
.literal('gloss:identifier', '1');
|
|
28
|
+
const out = writeTurtle(g);
|
|
29
|
+
expect(out).toMatch(/gloss:identifier "1" \./);
|
|
30
|
+
expect(out).not.toMatch(/gloss:identifier "1" ;/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('emits language-tagged literals', () => {
|
|
34
|
+
const g = new RdfGraph();
|
|
35
|
+
g.declare('https://ex/c', { types: [] })
|
|
36
|
+
.literal('skos:prefLabel', 'atomic', { lang: 'en' });
|
|
37
|
+
const out = writeTurtle(g);
|
|
38
|
+
expect(out).toContain('skos:prefLabel "atomic"@en');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('emits datatyped literals', () => {
|
|
42
|
+
const g = new RdfGraph();
|
|
43
|
+
g.declare('https://ex/c', { types: [] })
|
|
44
|
+
.literal('gloss:value', '42', { datatype: 'xsd:integer' });
|
|
45
|
+
const out = writeTurtle(g);
|
|
46
|
+
expect(out).toContain('gloss:value "42"^^xsd:integer');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('emits inline blank nodes with bracket syntax', () => {
|
|
50
|
+
const g = new RdfGraph();
|
|
51
|
+
g.declare('https://ex/c', { types: [] })
|
|
52
|
+
.blank('gloss:hasSource', [
|
|
53
|
+
triple('rdf:value', lit('ISO 123')),
|
|
54
|
+
triple('dcterms:title', lit('Standard')),
|
|
55
|
+
]);
|
|
56
|
+
const out = writeTurtle(g);
|
|
57
|
+
expect(out).toMatch(/gloss:hasSource \[ rdf:value "ISO 123" ; dcterms:title "Standard" \]/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('wraps absolute IRIs in angle brackets', () => {
|
|
61
|
+
const g = new RdfGraph();
|
|
62
|
+
g.declare('https://ex/c', { types: [] })
|
|
63
|
+
.iri('gloss:related', 'https://example.org/other');
|
|
64
|
+
const out = writeTurtle(g);
|
|
65
|
+
expect(out).toContain('gloss:related <https://example.org/other>');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('escapes slash in prefixed local names (Turtle 1.1 PN_LOCAL rule)', () => {
|
|
69
|
+
const g = new RdfGraph();
|
|
70
|
+
g.declare('https://ex/c', { types: [] })
|
|
71
|
+
.iri('gloss:hasStatus', 'gloss:status/valid');
|
|
72
|
+
const out = writeTurtle(g);
|
|
73
|
+
expect(out).toContain('gloss:hasStatus gloss:status\\/valid');
|
|
74
|
+
expect(out).not.toContain('gloss:hasStatus <gloss:status');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('wraps relative IRIs (paths without scheme) in angle brackets', () => {
|
|
78
|
+
const g = new RdfGraph();
|
|
79
|
+
g.declare('https://ex/c', { types: [] })
|
|
80
|
+
.iri('gloss:image', 'fig_A.23.svg');
|
|
81
|
+
const out = writeTurtle(g);
|
|
82
|
+
expect(out).toContain('gloss:image <fig_A.23.svg>');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('escapes special characters in literals', () => {
|
|
86
|
+
const g = new RdfGraph();
|
|
87
|
+
g.declare('https://ex/c', { types: [] })
|
|
88
|
+
.literal('gloss:note', 'She said "hi"\nand left');
|
|
89
|
+
const out = writeTurtle(g);
|
|
90
|
+
expect(out).toContain('"She said \\"hi\\"\\nand left"');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('separates resources with blank lines', () => {
|
|
94
|
+
const g = new RdfGraph();
|
|
95
|
+
g.declare('https://ex/a', { types: [] }).literal('gloss:x', '1');
|
|
96
|
+
g.declare('https://ex/b', { types: [] }).literal('gloss:x', '2');
|
|
97
|
+
const out = writeTurtle(g);
|
|
98
|
+
expect(out).toMatch(/\.\n\n<https:\/\/ex\/b>/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles a resource with no triples', () => {
|
|
102
|
+
const g = new RdfGraph();
|
|
103
|
+
g.declare('https://ex/c', { types: ['gloss:Concept'] });
|
|
104
|
+
const out = writeTurtle(g);
|
|
105
|
+
expect(out).toMatch(/<https:\/\/ex\/c> a gloss:Concept \./);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles a resource with no types', () => {
|
|
109
|
+
const g = new RdfGraph();
|
|
110
|
+
g.declare('https://ex/c', {}).literal('gloss:x', '1');
|
|
111
|
+
const out = writeTurtle(g);
|
|
112
|
+
expect(out).toMatch(/<https:\/\/ex\/c> gloss:x "1" \./);
|
|
113
|
+
});
|
|
114
|
+
});
|