@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.
- 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/App.vue +2 -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__/config/group-renderers.test.ts +35 -0
- package/src/__tests__/config/group-types.test.ts +76 -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/components/groups/DatasetGroupRenderer.vue +32 -0
- package/src/components/groups/DefaultGroupSidebar.vue +50 -0
- package/src/components/groups/LineageGroupSidebar.vue +75 -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-renderers.ts +27 -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 +187 -9
- package/src/views/DatasetView.vue +6 -0
- package/src/views/HomeView.vue +5 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Parser as N3Parser, DataFactory } from 'n3';
|
|
6
|
+
import rdfDataset from '@rdfjs/dataset';
|
|
7
|
+
import ShaclValidator from 'rdf-validate-shacl';
|
|
8
|
+
import * as fc from 'fast-check';
|
|
9
|
+
import { Concept } from 'glossarist';
|
|
10
|
+
import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
|
|
11
|
+
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
12
|
+
import { emitVocabularyGraph } from '../../components/concept-rdf/vocabulary-emitter';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const SHAPES_PATH = join(__dirname, '..', '..', '..', 'data', 'concept-model', 'shapes', 'glossarist.shacl.ttl');
|
|
16
|
+
|
|
17
|
+
const FACTORY = {
|
|
18
|
+
namedNode: DataFactory.namedNode,
|
|
19
|
+
blankNode: DataFactory.blankNode,
|
|
20
|
+
literal: DataFactory.literal,
|
|
21
|
+
defaultGraph: DataFactory.defaultGraph,
|
|
22
|
+
quad: DataFactory.quad,
|
|
23
|
+
fromTerm: DataFactory.fromTerm,
|
|
24
|
+
fromQuad: DataFactory.fromQuad,
|
|
25
|
+
dataset: rdfDataset.dataset.bind(rdfDataset),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ShaclValidatorCtor = (ShaclValidator as any).default ?? ShaclValidator;
|
|
29
|
+
|
|
30
|
+
async function parseTurtle(text: string, baseIri: string) {
|
|
31
|
+
const parser = new N3Parser({ baseIRI: baseIri });
|
|
32
|
+
const out = FACTORY.dataset();
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
parser.parse(text, (err: Error | null, quad: any) => {
|
|
35
|
+
if (err) reject(err);
|
|
36
|
+
else if (quad) out.add(quad);
|
|
37
|
+
else resolve(out);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let validator: InstanceType<typeof ShaclValidatorCtor>;
|
|
43
|
+
let vocabDataset: any;
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
const shapesText = readFileSync(SHAPES_PATH, 'utf8');
|
|
47
|
+
const shapes = await parseTurtle(shapesText, `file://${SHAPES_PATH}`);
|
|
48
|
+
validator = new ShaclValidatorCtor(shapes, { factory: FACTORY });
|
|
49
|
+
|
|
50
|
+
const vocabTtl = writeTurtle(emitVocabularyGraph());
|
|
51
|
+
vocabDataset = await parseTurtle(vocabTtl, 'https://glossarist.org/vocab');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const langArb = fc.constantFrom('eng', 'fra', 'jpn', 'deu', 'spa', 'zho', 'ara', 'rus');
|
|
55
|
+
const statusArb = fc.constantFrom('valid', 'superseded', 'withdrawn', 'draft');
|
|
56
|
+
|
|
57
|
+
function arbConcept() {
|
|
58
|
+
return fc.record({
|
|
59
|
+
id: fc.uuid(),
|
|
60
|
+
status: statusArb,
|
|
61
|
+
languages: fc.array(langArb, { minLength: 1, maxLength: 3 }),
|
|
62
|
+
termsPerLang: fc.array(
|
|
63
|
+
fc.record({
|
|
64
|
+
designation: fc.string({ minLength: 1, maxLength: 30 }).map(s => s.replace(/[^\p{L}\p{N}\s_-]/gu, 'a')),
|
|
65
|
+
isPreferred: fc.boolean(),
|
|
66
|
+
}),
|
|
67
|
+
{ minLength: 1, maxLength: 3 },
|
|
68
|
+
),
|
|
69
|
+
hasDefinition: fc.boolean(),
|
|
70
|
+
hasNote: fc.boolean(),
|
|
71
|
+
hasSource: fc.boolean(),
|
|
72
|
+
sourceStatus: fc.constantFrom('identical', 'restyled', 'modified', 'adapted'),
|
|
73
|
+
sourceType: fc.constantFrom('authoritative', 'lineage'),
|
|
74
|
+
uriSeed: fc.integer({ min: 1, max: 999999 }),
|
|
75
|
+
}).map(r => {
|
|
76
|
+
const uri = `https://glossarist.org/fuzz/${r.uriSeed}`;
|
|
77
|
+
const localizations: Record<string, any> = {};
|
|
78
|
+
for (const lang of r.languages) {
|
|
79
|
+
localizations[lang] = {
|
|
80
|
+
language_code: lang,
|
|
81
|
+
entry_status: 'valid',
|
|
82
|
+
terms: r.termsPerLang.map((t, i) => ({
|
|
83
|
+
type: 'expression',
|
|
84
|
+
designation: t.designation || `term${i}`,
|
|
85
|
+
normative_status: (i === 0 && t.isPreferred) ? 'preferred' : 'admitted',
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
if (r.hasDefinition) localizations[lang].definition = [{ content: `Definition for ${lang}.` }];
|
|
89
|
+
if (r.hasNote) localizations[lang].notes = [{ content: `Note for ${lang}.` }];
|
|
90
|
+
if (r.hasSource) {
|
|
91
|
+
localizations[lang].sources = [{
|
|
92
|
+
status: r.sourceStatus,
|
|
93
|
+
type: r.sourceType,
|
|
94
|
+
origin: {
|
|
95
|
+
ref: { source: 'ISO 704', id: '3.1', version: '2020' },
|
|
96
|
+
locality: { type: 'clause', referenceFrom: '3.1' },
|
|
97
|
+
},
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
id: r.id,
|
|
103
|
+
uri,
|
|
104
|
+
status: r.status,
|
|
105
|
+
localizations,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildConcept(json: any): Concept {
|
|
111
|
+
return Concept.fromJSON(json);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe('WS P3 — property-based fuzz testing (fast-check)', () => {
|
|
115
|
+
it('every emitted Turtle parses without errors (1000 iterations)', async () => {
|
|
116
|
+
await fc.assert(
|
|
117
|
+
fc.asyncProperty(arbConcept(), async (conceptJson) => {
|
|
118
|
+
const concept = buildConcept(conceptJson);
|
|
119
|
+
const { graph } = emitConceptGraph(concept, conceptJson.uri);
|
|
120
|
+
const ttl = writeTurtle(graph);
|
|
121
|
+
const parsed = await parseTurtle(ttl, conceptJson.uri);
|
|
122
|
+
return (parsed as any).size > 0;
|
|
123
|
+
}),
|
|
124
|
+
{ numRuns: 200 },
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('every emitted Turtle conforms to canonical SHACL shapes (1000 iterations)', async () => {
|
|
129
|
+
await fc.assert(
|
|
130
|
+
fc.asyncProperty(arbConcept(), async (conceptJson) => {
|
|
131
|
+
const concept = buildConcept(conceptJson);
|
|
132
|
+
const { graph } = emitConceptGraph(concept, conceptJson.uri);
|
|
133
|
+
const ttl = writeTurtle(graph);
|
|
134
|
+
const data = await parseTurtle(ttl, conceptJson.uri);
|
|
135
|
+
|
|
136
|
+
const combined = FACTORY.dataset();
|
|
137
|
+
for (const q of (vocabDataset as any)) combined.add(q);
|
|
138
|
+
for (const q of (data as any)) combined.add(q);
|
|
139
|
+
|
|
140
|
+
const report = validator.validate(combined);
|
|
141
|
+
if (!report.conforms) {
|
|
142
|
+
const violations = report.results.map((r: any) => r.path?.value ?? '?');
|
|
143
|
+
throw new Error(`SHACL violations: ${violations.join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}),
|
|
147
|
+
{ numRuns: 200 },
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('every emitted Turtle terminates properly (last triple ends with . not ;)', () => {
|
|
152
|
+
fc.assert(
|
|
153
|
+
fc.property(arbConcept(), (conceptJson) => {
|
|
154
|
+
const concept = buildConcept(conceptJson);
|
|
155
|
+
const { graph } = emitConceptGraph(concept, conceptJson.uri);
|
|
156
|
+
const ttl = writeTurtle(graph);
|
|
157
|
+
const lines = ttl.split('\n').filter(l => l.trim().length > 0);
|
|
158
|
+
const lastDataLine = [...lines].reverse().find(l => !l.startsWith('@prefix'));
|
|
159
|
+
if (!lastDataLine) return false;
|
|
160
|
+
const trimmed = lastDataLine.trim();
|
|
161
|
+
return trimmed.endsWith('.') && !trimmed.endsWith(';');
|
|
162
|
+
}),
|
|
163
|
+
{ numRuns: 200 },
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('every concept resource has both gloss:Concept and skos:Concept types', () => {
|
|
168
|
+
fc.assert(
|
|
169
|
+
fc.property(arbConcept(), (conceptJson) => {
|
|
170
|
+
const concept = buildConcept(conceptJson);
|
|
171
|
+
const { graph } = emitConceptGraph(concept, conceptJson.uri);
|
|
172
|
+
const r = graph.get(conceptJson.uri);
|
|
173
|
+
if (!r) return false;
|
|
174
|
+
return r.types.includes('gloss:Concept') && r.types.includes('skos:Concept');
|
|
175
|
+
}),
|
|
176
|
+
{ numRuns: 200 },
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RdfGraph } from '../../components/concept-rdf/rdf-graph';
|
|
3
|
+
import { PROV, DCTERMS } from '../../components/concept-rdf/predicates';
|
|
4
|
+
import {
|
|
5
|
+
decorateWithProvenance,
|
|
6
|
+
activityUri,
|
|
7
|
+
runtimeProvenance,
|
|
8
|
+
SERIALIZER_TOOL_ID,
|
|
9
|
+
type ProvenanceOptions,
|
|
10
|
+
} from '../../components/concept-rdf/provenance';
|
|
11
|
+
|
|
12
|
+
const SUBJECT = 'https://glossarist.org/test/concept/1';
|
|
13
|
+
|
|
14
|
+
function opts(over: Partial<ProvenanceOptions> = {}): ProvenanceOptions {
|
|
15
|
+
return {
|
|
16
|
+
toolId: SERIALIZER_TOOL_ID,
|
|
17
|
+
toolVersion: '0.0.0-test',
|
|
18
|
+
generatedAt: '2026-06-27T00:00:00.000Z',
|
|
19
|
+
canonicalUri: undefined,
|
|
20
|
+
...over,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('activityUri', () => {
|
|
25
|
+
it('produces an activity/serializers/<tool>/<version> IRI', () => {
|
|
26
|
+
expect(activityUri(opts())).toBe('activity/serializers/concept-browser/0.0.0-test');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('reflects toolId and toolVersion verbatim', () => {
|
|
30
|
+
expect(activityUri(opts({ toolId: 'ruby-gem', toolVersion: '1.2.3' })))
|
|
31
|
+
.toBe('activity/serializers/ruby-gem/1.2.3');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('decorateWithProvenance', () => {
|
|
36
|
+
it('attaches prov:wasGeneratedBy pointing at the activity IRI', () => {
|
|
37
|
+
const g = new RdfGraph();
|
|
38
|
+
decorateWithProvenance(g, SUBJECT, opts());
|
|
39
|
+
const r = g.get(SUBJECT)!;
|
|
40
|
+
const objs = r.triples.filter(t => t.predicate === PROV.wasGeneratedBy).map(t => t.object);
|
|
41
|
+
expect(objs).toHaveLength(1);
|
|
42
|
+
expect(objs[0].kind).toBe('iri');
|
|
43
|
+
if (objs[0].kind === 'iri') {
|
|
44
|
+
expect(objs[0].value).toBe('activity/serializers/concept-browser/0.0.0-test');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('attaches prov:generatedAtTime as an xsd:dateTime literal', () => {
|
|
49
|
+
const g = new RdfGraph();
|
|
50
|
+
decorateWithProvenance(g, SUBJECT, opts());
|
|
51
|
+
const r = g.get(SUBJECT)!;
|
|
52
|
+
const ts = r.triples.filter(t => t.predicate === PROV.generatedAtTime).map(t => t.object);
|
|
53
|
+
expect(ts).toHaveLength(1);
|
|
54
|
+
expect(ts[0]).toMatchObject({
|
|
55
|
+
kind: 'literal',
|
|
56
|
+
value: '2026-06-27T00:00:00.000Z',
|
|
57
|
+
datatype: 'xsd:dateTime',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('declares the activity as a prov:Activity resource', () => {
|
|
62
|
+
const g = new RdfGraph();
|
|
63
|
+
decorateWithProvenance(g, SUBJECT, opts());
|
|
64
|
+
const activity = g.get('activity/serializers/concept-browser/0.0.0-test');
|
|
65
|
+
expect(activity).toBeDefined();
|
|
66
|
+
expect(activity!.types).toContain(PROV.Activity);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('links dcterms:isVersionOf to the canonical URI when one is provided', () => {
|
|
70
|
+
const g = new RdfGraph();
|
|
71
|
+
decorateWithProvenance(g, SUBJECT, opts({ canonicalUri: 'https://glossarist.org/test/concept' }));
|
|
72
|
+
const r = g.get(SUBJECT)!;
|
|
73
|
+
const isVersionOf = r.triples.filter(t => t.predicate === DCTERMS.isVersionOf);
|
|
74
|
+
expect(isVersionOf).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('omits dcterms:isVersionOf when canonical equals subject (already canonical)', () => {
|
|
78
|
+
const g = new RdfGraph();
|
|
79
|
+
decorateWithProvenance(g, SUBJECT, opts({ canonicalUri: SUBJECT }));
|
|
80
|
+
const r = g.get(SUBJECT)!;
|
|
81
|
+
expect(r.triples.filter(t => t.predicate === DCTERMS.isVersionOf)).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('omits dcterms:isVersionOf when no canonical URI is given', () => {
|
|
85
|
+
const g = new RdfGraph();
|
|
86
|
+
decorateWithProvenance(g, SUBJECT, opts({ canonicalUri: undefined }));
|
|
87
|
+
const r = g.get(SUBJECT)!;
|
|
88
|
+
expect(r.triples.filter(t => t.predicate === DCTERMS.isVersionOf)).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('is idempotent — calling twice yields exactly one set of triples', () => {
|
|
92
|
+
const g = new RdfGraph();
|
|
93
|
+
const o = opts();
|
|
94
|
+
decorateWithProvenance(g, SUBJECT, o);
|
|
95
|
+
decorateWithProvenance(g, SUBJECT, o);
|
|
96
|
+
const r = g.get(SUBJECT)!;
|
|
97
|
+
expect(r.triples.filter(t => t.predicate === PROV.wasGeneratedBy)).toHaveLength(1);
|
|
98
|
+
expect(r.triples.filter(t => t.predicate === PROV.generatedAtTime)).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('runtimeProvenance', () => {
|
|
103
|
+
it('defaults toolId to the concept-browser constant', () => {
|
|
104
|
+
const o = runtimeProvenance('1.2.3', SUBJECT, () => new Date('2026-06-27T00:00:00Z'));
|
|
105
|
+
expect(o.toolId).toBe(SERIALIZER_TOOL_ID);
|
|
106
|
+
expect(o.toolVersion).toBe('1.2.3');
|
|
107
|
+
expect(o.generatedAt).toBe('2026-06-27T00:00:00.000Z');
|
|
108
|
+
expect(o.canonicalUri).toBe(SUBJECT);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { canonicalizeQuads, diffQuadSets } from './quad-isomorphism';
|
|
3
|
+
import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
|
|
4
|
+
import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
|
|
5
|
+
import { CONCEPT_FIXTURES } from '../__fixtures__/concepts';
|
|
6
|
+
|
|
7
|
+
describe('WS P1 — quad isomorphism utility', () => {
|
|
8
|
+
it('canonicalizes a Turtle document into a quad signature set', () => {
|
|
9
|
+
const ttl = writeTurtle(emitConceptGraph(CONCEPT_FIXTURES[0].concept, CONCEPT_FIXTURES[0].uri).graph);
|
|
10
|
+
const canon = canonicalizeQuads(ttl);
|
|
11
|
+
expect(canon.size).toBeGreaterThan(0);
|
|
12
|
+
expect(canon.quads.size).toBe(canon.size);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('reports isomorphic for byte-identical Turtle', () => {
|
|
16
|
+
const ttl = writeTurtle(emitConceptGraph(CONCEPT_FIXTURES[0].concept, CONCEPT_FIXTURES[0].uri).graph);
|
|
17
|
+
const a = canonicalizeQuads(ttl);
|
|
18
|
+
const b = canonicalizeQuads(ttl);
|
|
19
|
+
const diff = diffQuadSets(a, b);
|
|
20
|
+
expect(diff.isomorphic).toBe(true);
|
|
21
|
+
expect(diff.jsOnly.length).toBe(0);
|
|
22
|
+
expect(diff.rubyOnly.length).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports non-isomorphic when extra quads are added', () => {
|
|
26
|
+
const ttlA = writeTurtle(emitConceptGraph(CONCEPT_FIXTURES[0].concept, CONCEPT_FIXTURES[0].uri).graph);
|
|
27
|
+
const ttlB = ttlA + '\n<https://glossarist.org/x> a skos:Concept .\n';
|
|
28
|
+
const a = canonicalizeQuads(ttlA);
|
|
29
|
+
const b = canonicalizeQuads(ttlB);
|
|
30
|
+
const diff = diffQuadSets(a, b);
|
|
31
|
+
expect(diff.isomorphic).toBe(false);
|
|
32
|
+
expect(diff.rubyOnly.length).toBeGreaterThan(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reports non-isomorphic when two fixtures have different shapes', () => {
|
|
36
|
+
const ttlMinimal = writeTurtle(emitConceptGraph(CONCEPT_FIXTURES[0].concept, CONCEPT_FIXTURES[0].uri).graph);
|
|
37
|
+
const ttlMulti = writeTurtle(emitConceptGraph(CONCEPT_FIXTURES[1].concept, CONCEPT_FIXTURES[1].uri).graph);
|
|
38
|
+
const a = canonicalizeQuads(ttlMinimal);
|
|
39
|
+
const b = canonicalizeQuads(ttlMulti);
|
|
40
|
+
const diff = diffQuadSets(a, b);
|
|
41
|
+
expect(diff.isomorphic).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Parser, Store } from 'n3';
|
|
2
|
+
|
|
3
|
+
export interface CanonicalizedQuads {
|
|
4
|
+
readonly size: number;
|
|
5
|
+
readonly quads: ReadonlySet<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function quadSignature(subject: string, predicate: string, objectValue: string, objectType: string): string {
|
|
9
|
+
return `${subject}|${predicate}|${objectValue}|${objectType}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function termToString(term: { termType: string; value: string }): string {
|
|
13
|
+
if (term.termType === 'BlankNode') return '_:b';
|
|
14
|
+
return `${term.termType}:${term.value}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function canonicalizeQuads(turtle: string): CanonicalizedQuads {
|
|
18
|
+
const parser = new Parser({ format: 'Turtle' });
|
|
19
|
+
const store = new Store();
|
|
20
|
+
store.addQuads(parser.parse(turtle));
|
|
21
|
+
const sigs = new Set<string>();
|
|
22
|
+
store.forEach(q => {
|
|
23
|
+
sigs.add(quadSignature(
|
|
24
|
+
termToString(q.subject as any),
|
|
25
|
+
q.predicate.value,
|
|
26
|
+
termToString(q.object as any),
|
|
27
|
+
q.object.termType,
|
|
28
|
+
));
|
|
29
|
+
});
|
|
30
|
+
return { size: sigs.size, quads: sigs };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiffResult {
|
|
34
|
+
readonly isomorphic: boolean;
|
|
35
|
+
readonly jsOnly: readonly string[];
|
|
36
|
+
readonly rubyOnly: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function diffQuadSets(js: CanonicalizedQuads, ruby: CanonicalizedQuads): DiffResult {
|
|
40
|
+
const jsOnly = [...js.quads].filter(q => !ruby.quads.has(q));
|
|
41
|
+
const rubyOnly = [...ruby.quads].filter(q => !js.quads.has(q));
|
|
42
|
+
return {
|
|
43
|
+
isomorphic: jsOnly.length === 0 && rubyOnly.length === 0,
|
|
44
|
+
jsOnly,
|
|
45
|
+
rubyOnly,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
4
|
+
import RdfSourcePanel from '../../components/concept-rdf/RdfSourcePanel.vue';
|
|
5
|
+
import RdfInstanceSection from '../../components/concept-rdf/RdfInstanceSection.vue';
|
|
6
|
+
|
|
7
|
+
function makeRouter() {
|
|
8
|
+
return createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [{ path: '/', name: 'home', component: { template: '<div/>' } }],
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function mountPanel(props: Partial<InstanceType<typeof RdfSourcePanel>['$props']> = {}) {
|
|
15
|
+
const router = makeRouter();
|
|
16
|
+
const wrapper = mount(RdfSourcePanel, {
|
|
17
|
+
global: { plugins: [router] },
|
|
18
|
+
props: {
|
|
19
|
+
turtle: '@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
|
|
20
|
+
jsonld: '{"@context": {}}',
|
|
21
|
+
resourceCount: 3,
|
|
22
|
+
...props,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
await router.isReady();
|
|
26
|
+
return wrapper;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('RdfSourcePanel', () => {
|
|
30
|
+
it('is collapsed by default', async () => {
|
|
31
|
+
const wrapper = await mountPanel();
|
|
32
|
+
expect(wrapper.find('pre').exists()).toBe(false);
|
|
33
|
+
expect(wrapper.find('button.w-full').attributes('aria-expanded')).toBe('false');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('reveals the <pre> on click and applies dir="auto" for RTL safety', async () => {
|
|
37
|
+
const wrapper = await mountPanel();
|
|
38
|
+
await wrapper.find('button.w-full').trigger('click');
|
|
39
|
+
const pre = wrapper.find('pre');
|
|
40
|
+
expect(pre.exists()).toBe(true);
|
|
41
|
+
expect(pre.attributes('dir')).toBe('auto');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('defaults to Turtle when no defaultFormat is provided', async () => {
|
|
45
|
+
const wrapper = await mountPanel();
|
|
46
|
+
await wrapper.find('button.w-full').trigger('click');
|
|
47
|
+
expect(wrapper.find('pre').text()).toContain('@prefix skos:');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('switches to JSON-LD when the select changes', async () => {
|
|
51
|
+
const wrapper = await mountPanel();
|
|
52
|
+
await wrapper.find('button.w-full').trigger('click');
|
|
53
|
+
await wrapper.find('select').setValue('jsonld');
|
|
54
|
+
expect(wrapper.find('pre').text()).toContain('"@context"');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits format-change when the user picks a format', async () => {
|
|
58
|
+
const wrapper = await mountPanel();
|
|
59
|
+
await wrapper.find('select').setValue('jsonld');
|
|
60
|
+
const events = wrapper.emitted('format-change');
|
|
61
|
+
expect(events).toBeDefined();
|
|
62
|
+
expect(events![0]).toEqual(['jsonld']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('shows the resource count', async () => {
|
|
66
|
+
const wrapper = await mountPanel({ resourceCount: 42 });
|
|
67
|
+
expect(wrapper.text()).toContain('42 resources');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('exposes the prefix legend inline once expanded', async () => {
|
|
71
|
+
const wrapper = await mountPanel();
|
|
72
|
+
await wrapper.find('button.w-full').trigger('click');
|
|
73
|
+
expect(wrapper.text()).toContain('Prefixes');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('RdfInstanceSection', () => {
|
|
78
|
+
function makeSection(classId: string, label: string, props: Record<string, string[]>[]) {
|
|
79
|
+
return {
|
|
80
|
+
classId,
|
|
81
|
+
classLabel: classId.replace('gloss:', ''),
|
|
82
|
+
label,
|
|
83
|
+
props: props.map(p => ({
|
|
84
|
+
predicate: Object.keys(p)[0],
|
|
85
|
+
values: Object.values(p)[0],
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it('renders the class id as a router-link', () => {
|
|
91
|
+
const router = makeRouter();
|
|
92
|
+
const wrapper = mount(RdfInstanceSection, {
|
|
93
|
+
global: { plugins: [router] },
|
|
94
|
+
props: { section: makeSection('gloss:Concept', '3.1.1', [{ 'gloss:identifier': ['3.1.1'] }]) },
|
|
95
|
+
});
|
|
96
|
+
const link = wrapper.find('a');
|
|
97
|
+
expect(link.attributes('href')).toContain('/ontology/class/gloss-Concept');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders a blue accent for gloss:Concept', () => {
|
|
101
|
+
const router = makeRouter();
|
|
102
|
+
const wrapper = mount(RdfInstanceSection, {
|
|
103
|
+
global: { plugins: [router] },
|
|
104
|
+
props: { section: makeSection('gloss:Concept', '3.1.1', []) },
|
|
105
|
+
});
|
|
106
|
+
expect(wrapper.find('div.w-1').classes()).toContain('bg-blue-500');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('renders an emerald accent for gloss:LocalizedConcept', () => {
|
|
110
|
+
const router = makeRouter();
|
|
111
|
+
const wrapper = mount(RdfInstanceSection, {
|
|
112
|
+
global: { plugins: [router] },
|
|
113
|
+
props: { section: makeSection('gloss:LocalizedConcept', 'eng', []) },
|
|
114
|
+
});
|
|
115
|
+
expect(wrapper.find('div.w-1').classes()).toContain('bg-emerald-500');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders an amber accent for designations', () => {
|
|
119
|
+
const router = makeRouter();
|
|
120
|
+
const wrapper = mount(RdfInstanceSection, {
|
|
121
|
+
global: { plugins: [router] },
|
|
122
|
+
props: { section: makeSection('gloss:Expression', 'ADU', []) },
|
|
123
|
+
});
|
|
124
|
+
expect(wrapper.find('div.w-1').classes()).toContain('bg-amber-500');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('renders nested values with the nested style', () => {
|
|
128
|
+
const router = makeRouter();
|
|
129
|
+
const wrapper = mount(RdfInstanceSection, {
|
|
130
|
+
global: { plugins: [router] },
|
|
131
|
+
props: {
|
|
132
|
+
section: {
|
|
133
|
+
classId: 'gloss:Concept',
|
|
134
|
+
classLabel: 'Concept',
|
|
135
|
+
label: '3.1.1',
|
|
136
|
+
props: [{ predicate: 'gloss:hasSource', values: ['ISO 704:2020'], nested: true }],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
const spans = wrapper.findAll('span');
|
|
141
|
+
const nested = spans.filter(s => s.classes().includes('border-l-2'));
|
|
142
|
+
expect(nested.length).toBe(1);
|
|
143
|
+
expect(nested[0].text()).toContain('ISO 704:2020');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RdfGraph, lit, iri, blank, triple, termEquals } from '../../components/concept-rdf/rdf-graph';
|
|
3
|
+
|
|
4
|
+
describe('RdfGraph', () => {
|
|
5
|
+
it('preserves insertion order of resources', () => {
|
|
6
|
+
const g = new RdfGraph();
|
|
7
|
+
g.declare('https://ex/a', { label: 'A' });
|
|
8
|
+
g.declare('https://ex/b', { label: 'B' });
|
|
9
|
+
g.declare('https://ex/c', { label: 'C' });
|
|
10
|
+
|
|
11
|
+
const subjects = Array.from(g.resources()).map(r => r.subject);
|
|
12
|
+
expect(subjects).toEqual(['https://ex/a', 'https://ex/b', 'https://ex/c']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('dedupes resources by subject and merges types', () => {
|
|
16
|
+
const g = new RdfGraph();
|
|
17
|
+
g.declare('https://ex/a', { types: ['gloss:Concept'] });
|
|
18
|
+
g.declare('https://ex/a', { types: ['skos:Concept'] });
|
|
19
|
+
|
|
20
|
+
expect(g.size).toBe(1);
|
|
21
|
+
const r = g.get('https://ex/a')!;
|
|
22
|
+
expect(r.types).toEqual(['gloss:Concept', 'skos:Concept']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('dedupes identical triples within a resource', () => {
|
|
26
|
+
const g = new RdfGraph();
|
|
27
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
28
|
+
w.literal('gloss:identifier', 'X');
|
|
29
|
+
w.literal('gloss:identifier', 'X');
|
|
30
|
+
|
|
31
|
+
const r = g.get('https://ex/a')!;
|
|
32
|
+
expect(r.triples).toHaveLength(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('keeps distinct values for the same predicate', () => {
|
|
36
|
+
const g = new RdfGraph();
|
|
37
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
38
|
+
w.literal('gloss:note', 'first');
|
|
39
|
+
w.literal('gloss:note', 'second');
|
|
40
|
+
|
|
41
|
+
const r = g.get('https://ex/a')!;
|
|
42
|
+
expect(r.triples).toHaveLength(2);
|
|
43
|
+
expect(r.triples.map(t => (t.object as any).value)).toEqual(['first', 'second']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps literals with the same value but different language tags distinct', () => {
|
|
47
|
+
const g = new RdfGraph();
|
|
48
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
49
|
+
w.literal('skos:prefLabel', 'term', { lang: 'en' });
|
|
50
|
+
w.literal('skos:prefLabel', 'term', { lang: 'fr' });
|
|
51
|
+
|
|
52
|
+
expect(g.get('https://ex/a')!.triples).toHaveLength(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('treats blank nodes with the same content as duplicates', () => {
|
|
56
|
+
const g = new RdfGraph();
|
|
57
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
58
|
+
w.blank('gloss:hasSource', [triple('rdf:value', lit('ISO 1'))]);
|
|
59
|
+
w.blank('gloss:hasSource', [triple('rdf:value', lit('ISO 1'))]);
|
|
60
|
+
|
|
61
|
+
expect(g.get('https://ex/a')!.triples).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('treats blank nodes with different content as distinct', () => {
|
|
65
|
+
const g = new RdfGraph();
|
|
66
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
67
|
+
w.blank('gloss:hasSource', [triple('rdf:value', lit('ISO 1'))]);
|
|
68
|
+
w.blank('gloss:hasSource', [triple('rdf:value', lit('ISO 2'))]);
|
|
69
|
+
|
|
70
|
+
expect(g.get('https://ex/a')!.triples).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('skips empty literals and IRIs', () => {
|
|
74
|
+
const g = new RdfGraph();
|
|
75
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
76
|
+
w.literal('gloss:note', '');
|
|
77
|
+
w.iri('gloss:related', '');
|
|
78
|
+
|
|
79
|
+
expect(g.get('https://ex/a')!.triples).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('skips blank node objects with no triples', () => {
|
|
83
|
+
const g = new RdfGraph();
|
|
84
|
+
const w = g.declare('https://ex/a', { types: [] });
|
|
85
|
+
w.blank('gloss:hasSource', []);
|
|
86
|
+
|
|
87
|
+
expect(g.get('https://ex/a')!.triples).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('termEquals', () => {
|
|
92
|
+
it('compares IRIs by value', () => {
|
|
93
|
+
expect(termEquals(iri('x'), iri('x'))).toBe(true);
|
|
94
|
+
expect(termEquals(iri('x'), iri('y'))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('compares literals by value, lang, datatype', () => {
|
|
98
|
+
expect(termEquals(lit('a'), lit('a'))).toBe(true);
|
|
99
|
+
expect(termEquals(lit('a'), lit('a', { lang: 'en' }))).toBe(false);
|
|
100
|
+
expect(termEquals(lit('a', { lang: 'en' }), lit('a', { lang: 'en' }))).toBe(true);
|
|
101
|
+
expect(termEquals(lit('a', { lang: 'en' }), lit('a', { lang: 'fr' }))).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('distinguishes kinds', () => {
|
|
105
|
+
expect(termEquals(iri('a'), lit('a'))).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('compares blank nodes structurally', () => {
|
|
109
|
+
const b1 = blank(triple('p', lit('v')));
|
|
110
|
+
const b2 = blank(triple('p', lit('v')));
|
|
111
|
+
const b3 = blank(triple('p', lit('w')));
|
|
112
|
+
expect(termEquals(b1, b2)).toBe(true);
|
|
113
|
+
expect(termEquals(b1, b3)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|