@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,62 @@
|
|
|
1
|
+
import type { RdfGraph, RdfResource, RdfTerm } from './rdf-graph';
|
|
2
|
+
|
|
3
|
+
export interface PropValue {
|
|
4
|
+
predicate: string;
|
|
5
|
+
values: string[];
|
|
6
|
+
nested?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClassInstance {
|
|
10
|
+
classId: string;
|
|
11
|
+
classLabel: string;
|
|
12
|
+
label: string;
|
|
13
|
+
props: PropValue[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildSections(graph: RdfGraph): ClassInstance[] {
|
|
17
|
+
const out: ClassInstance[] = [];
|
|
18
|
+
for (const r of graph.resources()) {
|
|
19
|
+
out.push(resourceToSection(r));
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resourceToSection(r: RdfResource): ClassInstance {
|
|
25
|
+
const props: PropValue[] = [];
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
|
|
28
|
+
for (const t of r.triples) {
|
|
29
|
+
const value = formatTerm(t.object);
|
|
30
|
+
if (!value) continue;
|
|
31
|
+
const isNested = t.object.kind === 'blank';
|
|
32
|
+
const key = `${t.predicate}#${isNested ? 'n' : 'f'}#${value}`;
|
|
33
|
+
if (seen.has(key)) continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
const prop: PropValue = {
|
|
36
|
+
predicate: t.predicate,
|
|
37
|
+
values: [value],
|
|
38
|
+
};
|
|
39
|
+
if (isNested) prop.nested = true;
|
|
40
|
+
props.push(prop);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
classId: r.classId,
|
|
45
|
+
classLabel: r.classLabel,
|
|
46
|
+
label: r.label,
|
|
47
|
+
props,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatTerm(term: RdfTerm): string {
|
|
52
|
+
switch (term.kind) {
|
|
53
|
+
case 'iri':
|
|
54
|
+
return term.value;
|
|
55
|
+
case 'literal':
|
|
56
|
+
return term.value;
|
|
57
|
+
case 'blank': {
|
|
58
|
+
const parts = term.triples.map(t => `${t.predicate}: ${formatTerm(t.object)}`);
|
|
59
|
+
return parts.join('; ');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { GLOSS, DCTERMS, RDF, XSD } from './predicates';
|
|
2
|
+
import { RdfGraph, lit, iri, blank, triple } from './rdf-graph';
|
|
3
|
+
import type { RdfTriple } from './rdf-graph';
|
|
4
|
+
|
|
5
|
+
export interface FormulaInput {
|
|
6
|
+
readonly registerId: string;
|
|
7
|
+
readonly formulaId: string;
|
|
8
|
+
readonly expression: string;
|
|
9
|
+
readonly latexForm?: string;
|
|
10
|
+
readonly description?: string;
|
|
11
|
+
readonly lang?: string;
|
|
12
|
+
readonly sourceIri?: string;
|
|
13
|
+
readonly baseUri?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TableHeaderCell {
|
|
17
|
+
readonly value: string;
|
|
18
|
+
readonly lang?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TableRow {
|
|
22
|
+
readonly cells: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TableInput {
|
|
26
|
+
readonly registerId: string;
|
|
27
|
+
readonly tableId: string;
|
|
28
|
+
readonly title?: string;
|
|
29
|
+
readonly caption?: string;
|
|
30
|
+
readonly headers?: readonly TableHeaderCell[];
|
|
31
|
+
readonly rows?: readonly TableRow[];
|
|
32
|
+
readonly markup?: string;
|
|
33
|
+
readonly markupFormat?: string;
|
|
34
|
+
readonly lang?: string;
|
|
35
|
+
readonly baseUri?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function base(input: { baseUri?: string }): string {
|
|
39
|
+
return input.baseUri ?? 'https://glossarist.org';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formulaIri(input: FormulaInput): string {
|
|
43
|
+
return `${base(input)}/${input.registerId}/formula/${input.formulaId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function tableIri(input: TableInput): string {
|
|
47
|
+
return `${base(input)}/${input.registerId}/table/${input.tableId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function emitFormulaGraph(input: FormulaInput): RdfGraph {
|
|
51
|
+
const graph = new RdfGraph();
|
|
52
|
+
const iriStr = formulaIri(input);
|
|
53
|
+
const w = graph.declare(iriStr, {
|
|
54
|
+
types: [GLOSS.Formula],
|
|
55
|
+
label: input.expression,
|
|
56
|
+
classLabel: 'Formula',
|
|
57
|
+
classId: GLOSS.Formula,
|
|
58
|
+
});
|
|
59
|
+
w.literal(GLOSS.expression, input.expression);
|
|
60
|
+
if (input.latexForm) w.literal(GLOSS.latexForm, input.latexForm);
|
|
61
|
+
if (input.description) w.literal(DCTERMS.description, input.description, { lang: input.lang });
|
|
62
|
+
if (input.sourceIri) w.iri('prov:wasDerivedFrom', input.sourceIri);
|
|
63
|
+
return graph;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function emitTableGraph(input: TableInput): RdfGraph {
|
|
67
|
+
const graph = new RdfGraph();
|
|
68
|
+
const iriStr = tableIri(input);
|
|
69
|
+
const w = graph.declare(iriStr, {
|
|
70
|
+
types: [GLOSS.Table],
|
|
71
|
+
label: input.title ?? input.caption ?? input.tableId,
|
|
72
|
+
classLabel: 'Table',
|
|
73
|
+
classId: GLOSS.Table,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (input.title) w.literal(DCTERMS.title, input.title, { lang: input.lang });
|
|
77
|
+
if (input.caption) w.literal(GLOSS.caption, input.caption, { lang: input.lang });
|
|
78
|
+
|
|
79
|
+
if (input.markup != null) {
|
|
80
|
+
const contentTriples: RdfTriple[] = [
|
|
81
|
+
triple(RDF.type, iri('gloss:MarkupTable')),
|
|
82
|
+
triple(GLOSS.content, lit(input.markup)),
|
|
83
|
+
];
|
|
84
|
+
if (input.markupFormat) contentTriples.push(triple(DCTERMS.format, lit(input.markupFormat)));
|
|
85
|
+
w.blank(GLOSS.content, contentTriples);
|
|
86
|
+
} else if (input.headers && input.headers.length > 0) {
|
|
87
|
+
const contentTriples: RdfTriple[] = [
|
|
88
|
+
triple(RDF.type, iri('gloss:StructuredTable')),
|
|
89
|
+
];
|
|
90
|
+
for (const h of input.headers) {
|
|
91
|
+
contentTriples.push(triple(GLOSS.hasHeader, lit(h.value, { lang: h.lang ?? input.lang })));
|
|
92
|
+
}
|
|
93
|
+
for (const row of input.rows ?? []) {
|
|
94
|
+
const rowTriples = row.cells.map(c => triple(GLOSS.content, lit(c)));
|
|
95
|
+
contentTriples.push(triple(GLOSS.hasRow, blank(...rowTriples)));
|
|
96
|
+
}
|
|
97
|
+
w.blank(GLOSS.content, contentTriples);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return graph;
|
|
101
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RDF_PREFIXES } from './rdf-prefixes';
|
|
2
|
+
import type { RdfGraph, RdfResource, RdfTerm, RdfTriple } from './rdf-graph';
|
|
3
|
+
|
|
4
|
+
const INDENT = ' ';
|
|
5
|
+
|
|
6
|
+
const ABSOLUTE_SCHEMES = new Set(['http', 'https', 'urn', 'file', 'mailto', 'ftp']);
|
|
7
|
+
|
|
8
|
+
function isPrefixedName(s: string): boolean {
|
|
9
|
+
const colonIdx = s.indexOf(':');
|
|
10
|
+
if (colonIdx < 1) return false;
|
|
11
|
+
const prefix = s.slice(0, colonIdx);
|
|
12
|
+
if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(prefix)) return false;
|
|
13
|
+
if (ABSOLUTE_SCHEMES.has(prefix)) return false;
|
|
14
|
+
const local = s.slice(colonIdx + 1);
|
|
15
|
+
return !local.startsWith('//');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatIri(value: string): string {
|
|
19
|
+
if (!isPrefixedName(value)) return `<${value}>`;
|
|
20
|
+
const colonIdx = value.indexOf(':');
|
|
21
|
+
const prefix = value.slice(0, colonIdx + 1);
|
|
22
|
+
const local = value.slice(colonIdx + 1);
|
|
23
|
+
const escaped = local.replace(/([/])/g, '\\$1');
|
|
24
|
+
return prefix + escaped;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeTurtle(graph: RdfGraph): string {
|
|
28
|
+
const lines: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (const p of RDF_PREFIXES) {
|
|
31
|
+
lines.push(`@prefix ${p.prefix}: <${p.iri}> .`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let first = true;
|
|
35
|
+
for (const r of graph.resources()) {
|
|
36
|
+
if (first) {
|
|
37
|
+
lines.push('');
|
|
38
|
+
first = false;
|
|
39
|
+
} else {
|
|
40
|
+
lines.push('');
|
|
41
|
+
}
|
|
42
|
+
writeResource(lines, r);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeResource(lines: string[], r: RdfResource): void {
|
|
49
|
+
const subjectForm = formatSubject(r.subject);
|
|
50
|
+
|
|
51
|
+
if (r.types.length === 0 && r.triples.length === 0) {
|
|
52
|
+
lines.push(`${subjectForm} .`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (r.types.length === 0) {
|
|
57
|
+
const [first, ...rest] = r.triples;
|
|
58
|
+
lines.push(`${subjectForm} ${first.predicate} ${formatObject(first.object)} ;`);
|
|
59
|
+
for (const t of rest) {
|
|
60
|
+
lines.push(`${INDENT}${t.predicate} ${formatObject(t.object)} ;`);
|
|
61
|
+
}
|
|
62
|
+
const last = lines.length - 1;
|
|
63
|
+
lines[last] = lines[last].replace(/ ;$/, ' .');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const head = `${subjectForm} a ${r.types.join(', ')}`;
|
|
68
|
+
if (r.triples.length === 0) {
|
|
69
|
+
lines.push(`${head} .`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
lines.push(`${head} ;`);
|
|
73
|
+
for (const t of r.triples) {
|
|
74
|
+
lines.push(`${INDENT}${t.predicate} ${formatObject(t.object)} ;`);
|
|
75
|
+
}
|
|
76
|
+
const last = lines.length - 1;
|
|
77
|
+
lines[last] = lines[last].replace(/ ;$/, ' .');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatSubject(subject: string): string {
|
|
81
|
+
return formatIri(subject);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatObject(term: RdfTerm): string {
|
|
85
|
+
switch (term.kind) {
|
|
86
|
+
case 'iri':
|
|
87
|
+
return formatIri(term.value);
|
|
88
|
+
case 'literal':
|
|
89
|
+
return formatLiteral(term.value, term.lang, term.datatype);
|
|
90
|
+
case 'blank':
|
|
91
|
+
return formatBlankNode(term.triples);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatLiteral(value: string, lang?: string, datatype?: string): string {
|
|
96
|
+
const escaped = escapeLiteral(value);
|
|
97
|
+
let s = `"${escaped}"`;
|
|
98
|
+
if (lang) s += `@${lang}`;
|
|
99
|
+
else if (datatype) s += `^^${datatype}`;
|
|
100
|
+
return s;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatBlankNode(triples: readonly RdfTriple[]): string {
|
|
104
|
+
if (triples.length === 0) return '[]';
|
|
105
|
+
const parts = triples.map(t => `${t.predicate} ${formatObject(t.object)}`);
|
|
106
|
+
return `[ ${parts.join(' ; ')} ]`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function escapeLiteral(s: string): string {
|
|
110
|
+
return s
|
|
111
|
+
.replace(/\\/g, '\\\\')
|
|
112
|
+
.replace(/"/g, '\\"')
|
|
113
|
+
.replace(/\n/g, '\\n')
|
|
114
|
+
.replace(/\r/g, '\\r')
|
|
115
|
+
.replace(/\t/g, '\\t');
|
|
116
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { Concept } from 'glossarist';
|
|
4
|
+
import { getClass } from '../../adapters/ontology-schema';
|
|
5
|
+
import { ConceptIdentity } from '../../adapters/concept-identity';
|
|
6
|
+
import { emitConceptGraph } from './concept-emitter';
|
|
7
|
+
import { writeTurtle } from './turtle-writer';
|
|
8
|
+
import { writeJsonLd } from './jsonld-writer';
|
|
9
|
+
import { buildSections } from './sections-builder';
|
|
10
|
+
import type { ClassInstance, PropValue } from './sections-builder';
|
|
11
|
+
import { decorateWithProvenance, runtimeProvenance } from './provenance';
|
|
12
|
+
|
|
13
|
+
export type { ClassInstance, PropValue } from './sections-builder';
|
|
14
|
+
|
|
15
|
+
export interface RdfDocument {
|
|
16
|
+
sections: ComputedRef<ClassInstance[]>;
|
|
17
|
+
turtle: ComputedRef<string>;
|
|
18
|
+
jsonld: ComputedRef<string>;
|
|
19
|
+
typeChain: ComputedRef<string[]>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseRdfDocumentOptions {
|
|
23
|
+
readonly lazy?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SERIALIZER_VERSION =
|
|
27
|
+
typeof __CONCEPT_BROWSER_VERSION__ !== 'undefined'
|
|
28
|
+
? __CONCEPT_BROWSER_VERSION__
|
|
29
|
+
: 'dev';
|
|
30
|
+
|
|
31
|
+
export function useRdfDocument(
|
|
32
|
+
getConcept: () => Concept,
|
|
33
|
+
getConceptUri: () => string,
|
|
34
|
+
_options: UseRdfDocumentOptions = {},
|
|
35
|
+
): RdfDocument {
|
|
36
|
+
const identity = computed(() => {
|
|
37
|
+
const concept = getConcept();
|
|
38
|
+
const uri = getConceptUri();
|
|
39
|
+
if (ConceptIdentity.isConceptUri(uri)) {
|
|
40
|
+
const parsed = ConceptIdentity.fromUri(uri);
|
|
41
|
+
if (parsed.localId === concept.id) return parsed;
|
|
42
|
+
}
|
|
43
|
+
return new ConceptIdentity(concept.id, '', '');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const safeUri = computed(() => {
|
|
47
|
+
const uri = getConceptUri();
|
|
48
|
+
return uri || identity.value.uri;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const emission = computed(() => {
|
|
52
|
+
const result = emitConceptGraph(getConcept(), safeUri.value);
|
|
53
|
+
decorateWithProvenance(
|
|
54
|
+
result.graph,
|
|
55
|
+
safeUri.value,
|
|
56
|
+
runtimeProvenance(SERIALIZER_VERSION, safeUri.value),
|
|
57
|
+
);
|
|
58
|
+
return result;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const sections = computed(() => buildSections(emission.value.graph));
|
|
62
|
+
const turtle = computed(() => writeTurtle(emission.value.graph));
|
|
63
|
+
const jsonld = computed(() => writeJsonLd(emission.value.graph));
|
|
64
|
+
|
|
65
|
+
const typeChain = computed(() => {
|
|
66
|
+
const conceptCls = getClass('gloss:Concept');
|
|
67
|
+
if (!conceptCls) return ['owl:Thing', 'skos:Concept', 'gloss:Concept'];
|
|
68
|
+
return ['owl:Thing', ...conceptCls.ancestors, 'gloss:Concept'];
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { sections, turtle, jsonld, typeChain };
|
|
72
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PROV, DCTERMS, XSD } from './predicates';
|
|
2
|
+
import { RdfGraph, lit, iri, triple } from './rdf-graph';
|
|
3
|
+
|
|
4
|
+
export interface DatasetVersionInput {
|
|
5
|
+
readonly registerId: string;
|
|
6
|
+
readonly version: string;
|
|
7
|
+
readonly versionIri: string;
|
|
8
|
+
readonly datasetIri: string;
|
|
9
|
+
readonly generatedAt: string;
|
|
10
|
+
readonly previousVersionIri?: string;
|
|
11
|
+
readonly changeSummary?: string;
|
|
12
|
+
readonly associatedAgentIri?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emitVersionGraph(input: DatasetVersionInput): RdfGraph {
|
|
16
|
+
const graph = new RdfGraph();
|
|
17
|
+
const w = graph.declare(input.versionIri, {
|
|
18
|
+
types: [PROV.Entity],
|
|
19
|
+
label: `${input.registerId} version ${input.version}`,
|
|
20
|
+
classLabel: 'Version',
|
|
21
|
+
classId: PROV.Entity,
|
|
22
|
+
});
|
|
23
|
+
w.literal(DCTERMS.isVersionOf, input.datasetIri);
|
|
24
|
+
if (input.previousVersionIri) {
|
|
25
|
+
w.iri(PROV.wasRevisionOf, input.previousVersionIri);
|
|
26
|
+
}
|
|
27
|
+
w.literal(PROV.generatedAtTime, input.generatedAt, { datatype: XSD.dateTime });
|
|
28
|
+
if (input.changeSummary) w.literal(DCTERMS.description, input.changeSummary);
|
|
29
|
+
if (input.associatedAgentIri) w.iri(PROV.wasAssociatedWith, input.associatedAgentIri);
|
|
30
|
+
return graph;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface VersionHistoryEntry {
|
|
34
|
+
readonly version: string;
|
|
35
|
+
readonly generatedAt: string;
|
|
36
|
+
readonly changeSummary?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface VersionEmitAllInput {
|
|
40
|
+
readonly registerId: string;
|
|
41
|
+
readonly datasetIri: string;
|
|
42
|
+
readonly versions: readonly VersionHistoryEntry[];
|
|
43
|
+
readonly associatedAgentIri?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function emitVersionHistory(input: VersionEmitAllInput): RdfGraph {
|
|
47
|
+
const graph = new RdfGraph();
|
|
48
|
+
let previousIri: string | undefined;
|
|
49
|
+
for (const v of input.versions) {
|
|
50
|
+
const versionIri = `${input.datasetIri}versions/${v.version}`;
|
|
51
|
+
const single = emitVersionGraph({
|
|
52
|
+
registerId: input.registerId,
|
|
53
|
+
version: v.version,
|
|
54
|
+
versionIri,
|
|
55
|
+
datasetIri: input.datasetIri,
|
|
56
|
+
generatedAt: v.generatedAt,
|
|
57
|
+
previousVersionIri: previousIri,
|
|
58
|
+
changeSummary: v.changeSummary,
|
|
59
|
+
associatedAgentIri: input.associatedAgentIri,
|
|
60
|
+
});
|
|
61
|
+
graph.merge(single);
|
|
62
|
+
previousIri = versionIri;
|
|
63
|
+
}
|
|
64
|
+
return graph;
|
|
65
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { SKOS } from './predicates';
|
|
5
|
+
import { RdfGraph } from './rdf-graph';
|
|
6
|
+
import type { RdfGraph as RdfGraphType } from './rdf-graph';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const VOCAB_PATH = join(__dirname, '..', '..', '..', 'data', 'glossarist-vocab.json');
|
|
10
|
+
|
|
11
|
+
export interface VocabTerm {
|
|
12
|
+
readonly iri: string;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly group?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface VocabScheme {
|
|
18
|
+
readonly schemeIri: string;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly terms: readonly VocabTerm[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface VocabFile {
|
|
24
|
+
readonly schemes: readonly VocabScheme[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let cached: VocabFile | undefined;
|
|
28
|
+
|
|
29
|
+
export function loadVocabulary(path: string = VOCAB_PATH): VocabFile {
|
|
30
|
+
if (cached && path === VOCAB_PATH) return cached;
|
|
31
|
+
const text = readFileSync(path, 'utf8');
|
|
32
|
+
cached = JSON.parse(text) as VocabFile;
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const VOCAB_SCHEMES: readonly VocabScheme[] = loadVocabulary().schemes;
|
|
37
|
+
|
|
38
|
+
export function emitVocabularyGraph(graph: RdfGraphType = new RdfGraph()): RdfGraphType {
|
|
39
|
+
for (const scheme of VOCAB_SCHEMES) {
|
|
40
|
+
const schemeW = graph.declare(scheme.schemeIri, {
|
|
41
|
+
types: [SKOS.ConceptScheme],
|
|
42
|
+
label: scheme.label,
|
|
43
|
+
classLabel: 'ConceptScheme',
|
|
44
|
+
classId: SKOS.ConceptScheme,
|
|
45
|
+
});
|
|
46
|
+
for (const term of scheme.terms) {
|
|
47
|
+
const termW = graph.declare(term.iri, {
|
|
48
|
+
types: [SKOS.Concept],
|
|
49
|
+
label: term.label,
|
|
50
|
+
classLabel: 'Concept',
|
|
51
|
+
classId: SKOS.Concept,
|
|
52
|
+
});
|
|
53
|
+
termW.iri(SKOS.inScheme, scheme.schemeIri);
|
|
54
|
+
schemeW.iri(SKOS.hasTopConcept, term.iri);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return graph;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function listVocabSchemes(): readonly VocabScheme[] {
|
|
61
|
+
return VOCAB_SCHEMES;
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DatasetGroupRenderer — OCP dispatcher. Maps group.kind to the
|
|
4
|
+
* appropriate sidebar renderer component. New kinds: add entry to
|
|
5
|
+
* GROUP_RENDERERS + new component. Zero edits to existing code.
|
|
6
|
+
*/
|
|
7
|
+
import { computed } from 'vue';
|
|
8
|
+
import { groupRendererFor } from '../../config/group-renderers';
|
|
9
|
+
import { resolveGroupKind } from '../../config/group-types';
|
|
10
|
+
import type { DatasetGroupKind } from '../../config/types';
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
kind: DatasetGroupKind;
|
|
14
|
+
entries: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
ref?: string;
|
|
18
|
+
loaded: boolean;
|
|
19
|
+
conceptCount: number;
|
|
20
|
+
year?: number;
|
|
21
|
+
status?: string;
|
|
22
|
+
isCurrent?: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
currentDataset: string;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const renderer = computed(() => groupRendererFor(resolveGroupKind({ kind: props.kind })).sidebar);
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<component :is="renderer" :entries="entries" :current-dataset="currentDataset" />
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DefaultGroupSidebar — flat list of dataset entries with expansion.
|
|
4
|
+
* Used for topic, family, collection, and default group kinds.
|
|
5
|
+
* Replaces the inline v-else template in AppSidebar.
|
|
6
|
+
*/
|
|
7
|
+
import { useRouter } from 'vue-router';
|
|
8
|
+
import { useI18n } from '../../i18n';
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
entries: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
loaded: boolean;
|
|
15
|
+
conceptCount: number;
|
|
16
|
+
}>;
|
|
17
|
+
currentDataset: string;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const { t } = useI18n();
|
|
22
|
+
|
|
23
|
+
function navigate(id: string) {
|
|
24
|
+
if (id === props.currentDataset) return;
|
|
25
|
+
router.push({ name: 'dataset', params: { registerId: id } });
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div
|
|
31
|
+
v-for="ds in entries"
|
|
32
|
+
:key="ds.id"
|
|
33
|
+
class="rounded-lg transition-all duration-150"
|
|
34
|
+
:class="currentDataset === ds.id ? 'bg-surface' : ''"
|
|
35
|
+
>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm border-l-2"
|
|
39
|
+
:class="currentDataset === ds.id
|
|
40
|
+
? 'text-ink-800 dark:text-ink-50'
|
|
41
|
+
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700 hover:text-ink-800 dark:hover:text-ink-50'"
|
|
42
|
+
@click="navigate(ds.id)"
|
|
43
|
+
>
|
|
44
|
+
<div class="font-medium truncate leading-snug">{{ ds.title }}</div>
|
|
45
|
+
<div v-if="ds.loaded" class="text-xs mt-0.5 text-ink-300 dark:text-ink-400">
|
|
46
|
+
{{ ds.conceptCount.toLocaleString() }} {{ t('home.concepts').toLowerCase() }}
|
|
47
|
+
</div>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* LineageGroupSidebar — timeline-style entries for edition series.
|
|
4
|
+
* Replaces the inline v-if="group.kind === 'lineage'" template in
|
|
5
|
+
* AppSidebar. Open/closed: new group kinds get their own component.
|
|
6
|
+
*/
|
|
7
|
+
import { useRouter } from 'vue-router';
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
entries: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
ref?: string;
|
|
14
|
+
loaded: boolean;
|
|
15
|
+
conceptCount: number;
|
|
16
|
+
year?: number;
|
|
17
|
+
status?: string;
|
|
18
|
+
isCurrent?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
currentDataset: string;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
|
|
25
|
+
function navigate(id: string) {
|
|
26
|
+
if (id === props.currentDataset) return;
|
|
27
|
+
router.push({ name: 'dataset', params: { registerId: id } });
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="series-timeline">
|
|
33
|
+
<button
|
|
34
|
+
v-for="ds in entries"
|
|
35
|
+
:key="ds.id"
|
|
36
|
+
type="button"
|
|
37
|
+
class="series-entry w-full text-left flex items-center gap-2 pl-6 pr-3 py-1.5 rounded-md text-sm border-l-2 transition-all duration-150"
|
|
38
|
+
:class="currentDataset === ds.id
|
|
39
|
+
? 'bg-amber-50/70 dark:bg-amber-400/10 border-l-[3px] text-ink-900 dark:text-ink-50 font-semibold'
|
|
40
|
+
: 'border-transparent text-ink-600 dark:text-ink-300 hover:bg-ink-50 dark:hover:bg-ink-700/40 hover:text-ink-900 dark:hover:text-ink-50'"
|
|
41
|
+
:style="currentDataset === ds.id ? { borderLeftColor: 'var(--group-kind-lineage-light, #B8935A)' } : {}"
|
|
42
|
+
@click="navigate(ds.id)"
|
|
43
|
+
>
|
|
44
|
+
<span class="flex-1 truncate text-[13.5px] font-medium leading-snug">{{ ds.ref || ds.title || ds.id }}</span>
|
|
45
|
+
<span
|
|
46
|
+
v-if="ds.status && ds.status !== 'valid'"
|
|
47
|
+
class="text-[9px] uppercase tracking-wide italic text-ink-400 dark:text-ink-400"
|
|
48
|
+
>{{ ds.status }}</span>
|
|
49
|
+
<span
|
|
50
|
+
v-if="ds.isCurrent"
|
|
51
|
+
class="current-star flex-shrink-0"
|
|
52
|
+
title="Current edition"
|
|
53
|
+
>✦</span>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<style scoped>
|
|
59
|
+
.series-entry {
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
.current-star {
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
font-size: 16px;
|
|
67
|
+
line-height: 1;
|
|
68
|
+
color: var(--group-kind-lineage-light, #B8935A);
|
|
69
|
+
filter: drop-shadow(0 0 4px rgba(184, 147, 90, 0.45));
|
|
70
|
+
}
|
|
71
|
+
:global(.dark) .current-star {
|
|
72
|
+
color: var(--group-kind-lineage-dark, #D4AF6E);
|
|
73
|
+
filter: drop-shadow(0 0 4px rgba(212, 175, 110, 0.55));
|
|
74
|
+
}
|
|
75
|
+
</style>
|