glossarist 0.4.2 → 0.4.4
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/data/concept-model/README.md +44 -0
- package/data/concept-model/shapes/glossarist.shacl.ttl +704 -0
- package/package.json +23 -3
- package/src/concept-collection.js +57 -0
- package/src/index.js +15 -0
- package/src/models/concept.js +6 -0
- package/src/models/dataset-color.js +79 -0
- package/src/models/designation.js +54 -0
- package/src/models/index.d.ts +119 -4
- package/src/models/index.js +13 -0
- package/src/models/register.js +99 -1
- package/src/models/relation-categories.js +151 -0
- package/src/models/relation-colors.js +78 -0
- package/src/rdf/deterministic-id.js +17 -0
- package/src/rdf/document-writer.js +87 -0
- package/src/rdf/gloss-concept.js +43 -0
- package/src/rdf/gloss-designation.js +72 -0
- package/src/rdf/gloss-detailed-definition.js +69 -0
- package/src/rdf/gloss-localized-concept.js +72 -0
- package/src/rdf/gloss-source.js +51 -0
- package/src/rdf/index.d.ts +107 -0
- package/src/rdf/index.js +27 -0
- package/src/rdf/normalize-enum.js +23 -0
- package/src/rdf/predicates.d.ts +443 -0
- package/src/rdf/predicates.js +245 -0
- package/src/rdf/prefixes.js +31 -0
- package/src/rdf/shacl.js +98 -0
- package/src/rdf/terms.js +15 -0
- package/src/transforms/concept-to-gloss.transform.js +75 -0
- package/src/transforms/index.d.ts +26 -0
- package/src/transforms/index.js +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Categorization SSOT for the 52 glossarist relationship types.
|
|
2
|
+
//
|
|
3
|
+
// Each type in RELATIONSHIP_TYPES belongs to exactly one category. The
|
|
4
|
+
// categories are semantic buckets used for filtering and visualization
|
|
5
|
+
// in concept-browser. Bringing the categorization into glossarist-js
|
|
6
|
+
// means all consumers see the same buckets.
|
|
7
|
+
|
|
8
|
+
import { RELATIONSHIP_TYPES } from './related-concept.js';
|
|
9
|
+
|
|
10
|
+
// Category definitions. The `types` array is the exhaustive list of
|
|
11
|
+
// relationship-type local names that fall into the category. Each type
|
|
12
|
+
// must appear in exactly one category (verified by MECE test).
|
|
13
|
+
export const RELATION_CATEGORIES = Object.freeze({
|
|
14
|
+
lifecycle: Object.freeze({
|
|
15
|
+
label: 'Lifecycle',
|
|
16
|
+
description: 'Concept lifecycle transitions: supersession, deprecation, replacement.',
|
|
17
|
+
types: Object.freeze([
|
|
18
|
+
'deprecates', 'deprecated_by',
|
|
19
|
+
'supersedes', 'superseded_by',
|
|
20
|
+
'replaces', 'replaced_by',
|
|
21
|
+
'invalidates', 'invalidated_by',
|
|
22
|
+
'retires', 'retired_by',
|
|
23
|
+
]),
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
hierarchical: Object.freeze({
|
|
27
|
+
label: 'Hierarchical',
|
|
28
|
+
description: 'Broader/narrower relations, including SKOS and ISO 25964 generic/partitive/instantial.',
|
|
29
|
+
types: Object.freeze([
|
|
30
|
+
'broader', 'narrower',
|
|
31
|
+
'broader_generic', 'narrower_generic',
|
|
32
|
+
'broader_partitive', 'narrower_partitive',
|
|
33
|
+
'broader_instantial', 'narrower_instantial',
|
|
34
|
+
]),
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
associative: Object.freeze({
|
|
38
|
+
label: 'Associative',
|
|
39
|
+
description: 'Generic see-also references between concepts.',
|
|
40
|
+
types: Object.freeze([
|
|
41
|
+
'see', 'related_concept',
|
|
42
|
+
'related_concept_broader', 'related_concept_narrower',
|
|
43
|
+
'references',
|
|
44
|
+
]),
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
comparative: Object.freeze({
|
|
48
|
+
label: 'Comparative',
|
|
49
|
+
description: 'Compare / contrast relations between concepts.',
|
|
50
|
+
types: Object.freeze(['compare', 'contrast']),
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
spatiotemporal: Object.freeze({
|
|
54
|
+
label: 'Spatiotemporal',
|
|
55
|
+
description: 'Sequential, spatial, and temporal relations.',
|
|
56
|
+
types: Object.freeze([
|
|
57
|
+
'sequentially_related_concept',
|
|
58
|
+
'spatially_related_concept',
|
|
59
|
+
'temporally_related_concept',
|
|
60
|
+
]),
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
lexical: Object.freeze({
|
|
64
|
+
label: 'Lexical',
|
|
65
|
+
description: 'Lexical relations between designations across languages.',
|
|
66
|
+
types: Object.freeze(['homograph', 'false_friend']),
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
mapping: Object.freeze({
|
|
70
|
+
label: 'Mapping',
|
|
71
|
+
description: 'SKOS mapping properties for cross-vocabulary alignment.',
|
|
72
|
+
types: Object.freeze([
|
|
73
|
+
'equivalent', 'close_match',
|
|
74
|
+
'broad_match', 'narrow_match', 'related_match',
|
|
75
|
+
]),
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
conceptInstance: Object.freeze({
|
|
79
|
+
label: 'Concept / Instance',
|
|
80
|
+
description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance.',
|
|
81
|
+
types: Object.freeze([
|
|
82
|
+
'has_concept', 'is_concept_of',
|
|
83
|
+
'instance_of', 'has_instance',
|
|
84
|
+
'has_definition', 'definition_of',
|
|
85
|
+
'has_part', 'is_part_of',
|
|
86
|
+
'inherits', 'inherited_by',
|
|
87
|
+
]),
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
versioning: Object.freeze({
|
|
91
|
+
label: 'Versioning',
|
|
92
|
+
description: 'Version lineage within the same concept identity.',
|
|
93
|
+
types: Object.freeze([
|
|
94
|
+
'has_version', 'version_of',
|
|
95
|
+
'current_version', 'current_version_of',
|
|
96
|
+
]),
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Reverse index built once. Maps each type local name to its category.
|
|
101
|
+
const _CATEGORY_BY_TYPE = (() => {
|
|
102
|
+
const map = new Map();
|
|
103
|
+
for (const [cat, def] of Object.entries(RELATION_CATEGORIES)) {
|
|
104
|
+
for (const t of def.types) map.set(t, cat);
|
|
105
|
+
}
|
|
106
|
+
return map;
|
|
107
|
+
})();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns the category key for the given relationship type, or null
|
|
111
|
+
* when the type is not categorized (e.g. an unrecognized custom type).
|
|
112
|
+
*/
|
|
113
|
+
export function categoryOf(type) {
|
|
114
|
+
return _CATEGORY_BY_TYPE.get(type) ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the category definition ({ label, description, types }) for
|
|
119
|
+
* the given category key, or null when the key is unknown.
|
|
120
|
+
*/
|
|
121
|
+
export function categoryDefinition(categoryKey) {
|
|
122
|
+
return RELATION_CATEGORIES[categoryKey] ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* MECE sanity check. Returns the list of relationship types that are
|
|
127
|
+
* NOT categorized (i.e. present in RELATIONSHIP_TYPES but missing from
|
|
128
|
+
* RELATION_CATEGORIES). Used by the test suite to verify SSOT drift.
|
|
129
|
+
*/
|
|
130
|
+
export function uncategorizedTypes() {
|
|
131
|
+
return RELATIONSHIP_TYPES.filter(t => !_CATEGORY_BY_TYPE.has(t));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* MECE sanity check. Returns a list of { type, categories } entries
|
|
136
|
+
* for any type that appears in MORE than one category. Should be
|
|
137
|
+
* empty when the categorization is mutually exclusive.
|
|
138
|
+
*/
|
|
139
|
+
export function duplicatedTypes() {
|
|
140
|
+
const counts = new Map();
|
|
141
|
+
for (const def of Object.values(RELATION_CATEGORIES)) {
|
|
142
|
+
for (const t of def.types) {
|
|
143
|
+
counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const [type, count] of counts) {
|
|
148
|
+
if (count > 1) out.push({ type, count });
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Default color palette for relation categories and individual types.
|
|
2
|
+
//
|
|
3
|
+
// The defaults mirror concept-browser/data/colors.json. Per-deployment
|
|
4
|
+
// overrides pass through resolveRelationColor(type, { overrides, mode })
|
|
5
|
+
// and take precedence over category defaults.
|
|
6
|
+
|
|
7
|
+
import { categoryOf, categoryDefinition } from './relation-categories.js';
|
|
8
|
+
|
|
9
|
+
// One { light, dark } pair per category. Keeping these in glossarist-js
|
|
10
|
+
// lets the defaults ship with the library; consumers only need to
|
|
11
|
+
// customize when they want to deviate.
|
|
12
|
+
export const RELATION_COLOR_DEFAULTS = Object.freeze({
|
|
13
|
+
byCategory: Object.freeze({
|
|
14
|
+
lifecycle: { light: '#B43A2E', dark: '#F87171' },
|
|
15
|
+
hierarchical: { light: '#1F6FEB', dark: '#6EA8FE' },
|
|
16
|
+
associative: { light: '#6B7280', dark: '#9CA3AF' },
|
|
17
|
+
comparative: { light: '#A855F7', dark: '#C4B5FD' },
|
|
18
|
+
spatiotemporal: { light: '#0D9488', dark: '#5EEAD4' },
|
|
19
|
+
lexical: { light: '#D97706', dark: '#FBBF24' },
|
|
20
|
+
mapping: { light: '#2563EB', dark: '#93BBFD' },
|
|
21
|
+
conceptInstance:{ light: '#65A30D', dark: '#A3E635' },
|
|
22
|
+
versioning: { light: '#9333EA', dark: '#C4B5FD' },
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
// Per-type overrides. Use the same { light, dark } shape. Keys are
|
|
26
|
+
// relationship-type local names. Empty by default — consumers may
|
|
27
|
+
// populate this through `overrides.byType` at call time.
|
|
28
|
+
byType: Object.freeze({}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the color for a relationship type.
|
|
33
|
+
*
|
|
34
|
+
* Resolution order:
|
|
35
|
+
* 1. overrides.byType[type]
|
|
36
|
+
* 2. RELATION_COLOR_DEFAULTS.byType[type]
|
|
37
|
+
* 3. overrides.byCategory[category]
|
|
38
|
+
* 4. RELATION_COLOR_DEFAULTS.byCategory[category]
|
|
39
|
+
* 5. null (uncategorized or unrecognized type)
|
|
40
|
+
*
|
|
41
|
+
* @param {string} type - relationship type local name
|
|
42
|
+
* @param {{ overrides?: { byType?: object, byCategory?: object }, mode?: 'light'|'dark' }} [options]
|
|
43
|
+
* @returns {string | null}
|
|
44
|
+
*/
|
|
45
|
+
export function resolveRelationColor(type, options = {}) {
|
|
46
|
+
const mode = options.mode ?? 'light';
|
|
47
|
+
const overrides = options.overrides ?? {};
|
|
48
|
+
|
|
49
|
+
const typeOverride = overrides.byType?.[type] ?? RELATION_COLOR_DEFAULTS.byType[type];
|
|
50
|
+
if (typeOverride) return pickMode(typeOverride, mode);
|
|
51
|
+
|
|
52
|
+
const category = categoryOf(type);
|
|
53
|
+
if (!category) return null;
|
|
54
|
+
|
|
55
|
+
const categoryOverride = overrides.byCategory?.[category] ?? RELATION_COLOR_DEFAULTS.byCategory[category];
|
|
56
|
+
if (!categoryOverride) return null;
|
|
57
|
+
|
|
58
|
+
return pickMode(categoryOverride, mode);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns the { light, dark } pair for a category, merging overrides
|
|
63
|
+
* over defaults. Useful for UIs that want to render a legend.
|
|
64
|
+
*/
|
|
65
|
+
export function categoryColorPair(categoryKey, overrides = {}) {
|
|
66
|
+
const def = categoryDefinition(categoryKey);
|
|
67
|
+
if (!def) return null;
|
|
68
|
+
const fromOverride = overrides.byCategory?.[categoryKey];
|
|
69
|
+
const fromDefault = RELATION_COLOR_DEFAULTS.byCategory[categoryKey];
|
|
70
|
+
if (!fromDefault && !fromOverride) return null;
|
|
71
|
+
return { ...(fromDefault ?? {}), ...(fromOverride ?? {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pickMode(pair, mode) {
|
|
75
|
+
if (pair == null) return null;
|
|
76
|
+
if (typeof pair === 'string') return pair;
|
|
77
|
+
return pair[mode] ?? pair.light ?? pair.dark ?? null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
// 12-char hex prefix, mirroring glossarist-ruby's
|
|
4
|
+
// `Digest::MD5.hexdigest(content)[0..11]`. Same content → same ID across
|
|
5
|
+
// processes, machines, and language runtimes (Ruby vs JS).
|
|
6
|
+
export function deterministicId(...parts) {
|
|
7
|
+
const seed = parts.filter(p => p !== null && p !== undefined).join('|');
|
|
8
|
+
return createHash('md5').update(seed).digest('hex').slice(0, 12);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Stable blank-node label for an RDF fragment identified by its parent
|
|
12
|
+
// subject + role + index. Useful for reified resources like DetailedDefinition,
|
|
13
|
+
// Designation, ConceptSource where the bnode ID must be deterministic to
|
|
14
|
+
// produce byte-equivalent Turtle across runs.
|
|
15
|
+
export function deterministicBnode(subject, role, index) {
|
|
16
|
+
return `_:b${deterministicId(subject, role, index)}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Quads → canonical Turtle / N-Triples / JSON-LD document.
|
|
2
|
+
//
|
|
3
|
+
// Turtle is produced via N3.Writer. JSON-LD via the `jsonld` package
|
|
4
|
+
// (fromRDF + compact against the canonical context). N-Triples is provided
|
|
5
|
+
// as a stable fallback that doesn't depend on prefix maps.
|
|
6
|
+
import { Writer as N3Writer } from 'n3';
|
|
7
|
+
import jsonld from 'jsonld';
|
|
8
|
+
import { PREFIXES } from './predicates.js';
|
|
9
|
+
|
|
10
|
+
// Collects all quads yielded by an emitter into an array. Useful for tests
|
|
11
|
+
// and for callers that want to inspect the quad stream directly.
|
|
12
|
+
export function collectQuads(quadsIterable) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const q of quadsIterable) out.push(q);
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Returns a Turtle serialization of `quads` using the canonical prefix map
|
|
19
|
+
// generated from the vendored concept-model JSON-LD context. Quads are
|
|
20
|
+
// sorted deterministically so the same input always produces byte-equivalent
|
|
21
|
+
// output across runs.
|
|
22
|
+
export function writeTurtle(quads, { prefixes = PREFIXES } = {}) {
|
|
23
|
+
const sorted = sortQuads(quads);
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const writer = new N3Writer({ prefixes, format: 'Turtle' });
|
|
26
|
+
for (const q of sorted) writer.addQuad(q);
|
|
27
|
+
writer.end((err, result) => {
|
|
28
|
+
if (err) reject(err);
|
|
29
|
+
else resolve(result);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Returns an N-Triples serialization of `quads` — no prefixes, no bnode
|
|
35
|
+
// shortening. Useful when consumers need a totally stable wire format.
|
|
36
|
+
export function writeNTriples(quads) {
|
|
37
|
+
const sorted = sortQuads(quads);
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const writer = new N3Writer({ format: 'N-Triples' });
|
|
40
|
+
for (const q of sorted) writer.addQuad(q);
|
|
41
|
+
writer.end((err, result) => {
|
|
42
|
+
if (err) reject(err);
|
|
43
|
+
else resolve(result);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Returns a compacted JSON-LD serialization of `quads`. The context defaults
|
|
49
|
+
// to the canonical glossarist JSON-LD context derived from the vendored
|
|
50
|
+
// PREFIXES. Quads are sorted first for deterministic output.
|
|
51
|
+
export async function writeJsonld(quads, { context = defaultJsonldContext() } = {}) {
|
|
52
|
+
const sorted = sortQuads(quads);
|
|
53
|
+
const nquads = await writeNTriples(sorted);
|
|
54
|
+
const expanded = await jsonld.fromRDF(nquads, { format: 'application/n-quads' });
|
|
55
|
+
const compacted = await jsonld.compact(expanded, context);
|
|
56
|
+
return JSON.stringify(compacted, null, 2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function defaultJsonldContext() {
|
|
60
|
+
const ctx = {};
|
|
61
|
+
for (const [prefix, uri] of Object.entries(PREFIXES)) {
|
|
62
|
+
ctx[prefix] = uri;
|
|
63
|
+
}
|
|
64
|
+
return ctx;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Sort by (subject.value, predicate.value, object.termType, object.value,
|
|
68
|
+
// graph.value). Bnodes sort by their generated ID which is deterministic
|
|
69
|
+
// because we use content-addressed IDs.
|
|
70
|
+
export function sortQuads(quads) {
|
|
71
|
+
return [...quads].sort(compareQuad);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compareQuad(a, b) {
|
|
75
|
+
let cmp = cmpTerm(a.subject, b.subject);
|
|
76
|
+
if (cmp !== 0) return cmp;
|
|
77
|
+
cmp = cmpTerm(a.predicate, b.predicate);
|
|
78
|
+
if (cmp !== 0) return cmp;
|
|
79
|
+
cmp = cmpTerm(a.object, b.object);
|
|
80
|
+
if (cmp !== 0) return cmp;
|
|
81
|
+
return cmpTerm(a.graph, b.graph);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cmpTerm(a, b) {
|
|
85
|
+
if (a.termType !== b.termType) return a.termType.localeCompare(b.termType);
|
|
86
|
+
return String(a.value).localeCompare(String(b.value));
|
|
87
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Concept → RDF quads. Mirrors glossarist-ruby's `Rdf::GlossConcept`.
|
|
2
|
+
//
|
|
3
|
+
// URI shape: `<uriBase>/<registerId>/concept/<conceptId>`.
|
|
4
|
+
// Localized concepts: `<uriBase>/<registerId>/concept/<conceptId>/<lang>`.
|
|
5
|
+
import { PRED } from './predicates.js';
|
|
6
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
7
|
+
import { localizedConceptToQuads } from './gloss-localized-concept.js';
|
|
8
|
+
import { conceptSourceToQuads } from './gloss-source.js';
|
|
9
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
10
|
+
|
|
11
|
+
export function conceptUri(concept, { registerId, uriBase }) {
|
|
12
|
+
const id = String(concept.id ?? concept.termid ?? '');
|
|
13
|
+
const base = String(uriBase ?? '').replace(/\/+$/, '');
|
|
14
|
+
return `${base}/${registerId}/concept/${id}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function* conceptToQuads(concept, options) {
|
|
18
|
+
const subjectUri = conceptUri(concept, options);
|
|
19
|
+
const s = namedNode(subjectUri);
|
|
20
|
+
|
|
21
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.Concept));
|
|
22
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(WELL_KNOWN.skosConcept));
|
|
23
|
+
|
|
24
|
+
yield quad(s, namedNode(PRED.gloss.identifier), literal(String(concept.id ?? concept.termid ?? '')));
|
|
25
|
+
|
|
26
|
+
if (concept.status) {
|
|
27
|
+
yield quad(s, namedNode(PRED.gloss.hasStatus), namedNode(`${PRED.gloss.$ns}status/${concept.status}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const lang of concept.languages) {
|
|
31
|
+
const lc = concept.localization(lang);
|
|
32
|
+
if (!lc) continue;
|
|
33
|
+
const lcUri = `${subjectUri}/${lang}`;
|
|
34
|
+
yield quad(s, namedNode(PRED.gloss.hasLocalization), namedNode(lcUri));
|
|
35
|
+
yield* localizedConceptToQuads(lc, { parentUri: subjectUri, language: lang });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let srcIndex = 0;
|
|
39
|
+
for (const source of concept.sources ?? []) {
|
|
40
|
+
yield* conceptSourceToQuads(source, { subjectUri, index: srcIndex });
|
|
41
|
+
srcIndex += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Designation → RDF quads. Mirrors glossarist-ruby's
|
|
2
|
+
// `Rdf::GlossDesignation` + the per-subtype classes (GlossExpression,
|
|
3
|
+
// GlossAbbreviation, etc.). The ontology class for each subtype is
|
|
4
|
+
// provided by the model itself via Designation#rdfClass (OCP: new
|
|
5
|
+
// subtypes register without editing this emitter).
|
|
6
|
+
import { PRED, PREFIXES } from './predicates.js';
|
|
7
|
+
import { SKOSXL, WELL_KNOWN } from './prefixes.js';
|
|
8
|
+
import { deterministicBnode } from './deterministic-id.js';
|
|
9
|
+
import { normalizeEnum } from './normalize-enum.js';
|
|
10
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
11
|
+
|
|
12
|
+
const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
|
|
13
|
+
|
|
14
|
+
// Boolean flag fields on designation subtypes. Each entry maps a model
|
|
15
|
+
// field name to its ontology predicate. Only emitted when the value is
|
|
16
|
+
// truthy, so legacy data without the fields round-trips unchanged.
|
|
17
|
+
const BOOLEAN_FLAGS = [
|
|
18
|
+
['acronym', PRED.gloss.isAcronym],
|
|
19
|
+
['initialism', PRED.gloss.isInitialism],
|
|
20
|
+
['truncation', PRED.gloss.isTruncation],
|
|
21
|
+
['international', PRED.gloss.isInternational],
|
|
22
|
+
['absent', PRED.gloss.isAbsent],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Returns the SKOS-XL predicate (as a string URI) appropriate for this
|
|
26
|
+
// designation's normative status. Mirrors `skosxl_label_for` in Ruby.
|
|
27
|
+
// The dispatch lives on the Designation model so a new status requires
|
|
28
|
+
// a single edit there, not here.
|
|
29
|
+
export function skosxlLabelPredicate(designation) {
|
|
30
|
+
return designation.skosxlLabelPredicate(PREFIXES.skosxl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Returns the matching plain-SKOS predicate URI (skos:prefLabel etc.).
|
|
34
|
+
// Used when emitting direct SKOS alongside reified SKOS-XL.
|
|
35
|
+
export function skosLabelPredicate(designation) {
|
|
36
|
+
return designation.skosLabelPredicate(PREFIXES.skos);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Emits quads for one designation. Yields them lazily so callers can
|
|
40
|
+
// compose many designations into a single stream without intermediate
|
|
41
|
+
// arrays.
|
|
42
|
+
//
|
|
43
|
+
// `subjectUri` is the URI of the LocalizedConcept that owns this designation.
|
|
44
|
+
// `index` is the position of the designation within its parent's terms list
|
|
45
|
+
// — used to make the bnode ID deterministic.
|
|
46
|
+
export function* designationToQuads(designation, { subjectUri, language, index }) {
|
|
47
|
+
const desigSubject = deterministicBnode(subjectUri, 'desig', index);
|
|
48
|
+
|
|
49
|
+
const labelPredicate = namedNode(skosxlLabelPredicate(designation));
|
|
50
|
+
yield quad(namedNode(subjectUri), labelPredicate, namedNode(desigSubject));
|
|
51
|
+
|
|
52
|
+
yield quad(namedNode(desigSubject), namedNode(WELL_KNOWN.rdfType), namedNode(`${PRED.gloss.$ns}${designation.rdfClass()}`));
|
|
53
|
+
yield quad(namedNode(desigSubject), namedNode(WELL_KNOWN.rdfType), namedNode(WELL_KNOWN.skosxlLabel));
|
|
54
|
+
yield quad(namedNode(desigSubject), namedNode(SKOSXL.literalForm), literal(designation.designation ?? '', language));
|
|
55
|
+
|
|
56
|
+
if (designation.normativeStatus) {
|
|
57
|
+
const statusToken = normalizeEnum(designation.normativeStatus);
|
|
58
|
+
if (statusToken) {
|
|
59
|
+
const statusUri = `${PRED.gloss.$ns}norm/${statusToken}`;
|
|
60
|
+
yield quad(namedNode(desigSubject), namedNode(PRED.gloss.normativeStatus), namedNode(statusUri));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Subtype-specific boolean flags. Skipping when falsy preserves
|
|
65
|
+
// backward compatibility with simpler designations.
|
|
66
|
+
const booleanType = namedNode(XSD_BOOLEAN);
|
|
67
|
+
for (const [field, predicate] of BOOLEAN_FLAGS) {
|
|
68
|
+
if (designation[field]) {
|
|
69
|
+
yield quad(namedNode(desigSubject), namedNode(predicate), literal('true', booleanType));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// DetailedDefinition → RDF quads. Mirrors glossarist-ruby's
|
|
2
|
+
// `Rdf::GlossDetailedDefinition`. Used for definitions, notes, examples,
|
|
3
|
+
// and annotations — they all share the same shape (content + optional
|
|
4
|
+
// sources + nested examples).
|
|
5
|
+
//
|
|
6
|
+
// The emitter takes a `linkPredicate` (e.g. PRED.gloss.hasDefinition,
|
|
7
|
+
// PRED.gloss.hasNote, PRED.gloss.hasExample) so callers can reuse it for
|
|
8
|
+
// any role.
|
|
9
|
+
import { PRED } from './predicates.js';
|
|
10
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
11
|
+
import { deterministicBnode } from './deterministic-id.js';
|
|
12
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
13
|
+
|
|
14
|
+
export function* detailedDefinitionToQuads(definition, {
|
|
15
|
+
subjectUri, language, index, role,
|
|
16
|
+
}) {
|
|
17
|
+
const linkPredicate = linkPredicateFor(role);
|
|
18
|
+
const defSubject = deterministicBnode(subjectUri, role, index);
|
|
19
|
+
|
|
20
|
+
yield quad(namedNode(subjectUri), namedNode(linkPredicate), namedNode(defSubject));
|
|
21
|
+
yield quad(namedNode(defSubject), namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.DetailedDefinition));
|
|
22
|
+
|
|
23
|
+
if (definition.content) {
|
|
24
|
+
yield quad(
|
|
25
|
+
namedNode(defSubject),
|
|
26
|
+
namedNode(WELL_KNOWN.rdfValue),
|
|
27
|
+
literal(definition.content, language),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Plain SKOS direct literal — so non-SKOS-XL consumers see definitions.
|
|
32
|
+
// The reified form above carries the same text with sources/examples;
|
|
33
|
+
// this direct literal makes SPARQL `?c skos:definition ?d` work.
|
|
34
|
+
const directPredicate = directSkosPredicateFor(role);
|
|
35
|
+
if (directPredicate && definition.content) {
|
|
36
|
+
yield quad(namedNode(subjectUri), namedNode(directPredicate), literal(definition.content, language));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let exampleIndex = 0;
|
|
40
|
+
for (const example of definition.examples ?? []) {
|
|
41
|
+
yield* detailedDefinitionToQuads(example, {
|
|
42
|
+
subjectUri: defSubject, language, index: exampleIndex, role: 'hasScopedExample',
|
|
43
|
+
});
|
|
44
|
+
exampleIndex += 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function linkPredicateFor(role) {
|
|
49
|
+
switch (role) {
|
|
50
|
+
case 'hasDefinition': return PRED.gloss.hasDefinition;
|
|
51
|
+
case 'hasNote': return PRED.gloss.hasNote;
|
|
52
|
+
case 'hasExample': return PRED.gloss.hasExample;
|
|
53
|
+
case 'hasScopedExample': return PRED.gloss.hasScopedExample;
|
|
54
|
+
case 'hasAnnotation': return PRED.gloss.hasAnnotation;
|
|
55
|
+
default: throw new Error(`Unknown detailed-definition role: ${role}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function directSkosPredicateFor(role) {
|
|
60
|
+
switch (role) {
|
|
61
|
+
case 'hasDefinition': return PRED.skos.definition;
|
|
62
|
+
case 'hasNote': return PRED.skos.scopeNote;
|
|
63
|
+
case 'hasExample': return PRED.skos.example;
|
|
64
|
+
case 'hasScopedExample': return PRED.skos.example;
|
|
65
|
+
// No direct SKOS counterpart for annotations.
|
|
66
|
+
case 'hasAnnotation': return null;
|
|
67
|
+
default: return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// LocalizedConcept → RDF quads. Mirrors glossarist-ruby's
|
|
2
|
+
// `Rdf::GlossLocalizedConcept` + the `EmitsExtraTriples` hook.
|
|
3
|
+
//
|
|
4
|
+
// Emits BOTH reified SKOS-XL labels AND direct SKOS literals, so consumers
|
|
5
|
+
// that don't implement SKOS-XL still see labels/definitions/notes/examples
|
|
6
|
+
// as plain literals. This is the same shape glossarist-ruby emits after
|
|
7
|
+
// WS-B Phase 1.
|
|
8
|
+
import { PRED, PREFIXES } from './predicates.js';
|
|
9
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
10
|
+
import { designationToQuads, skosLabelPredicate } from './gloss-designation.js';
|
|
11
|
+
import { detailedDefinitionToQuads } from './gloss-detailed-definition.js';
|
|
12
|
+
import { conceptSourceToQuads } from './gloss-source.js';
|
|
13
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
14
|
+
const DCTERMS_LANGUAGE = `${PREFIXES.dcterms}language`;
|
|
15
|
+
|
|
16
|
+
export function localizedConceptUri(parentUri, language) {
|
|
17
|
+
return `${parentUri}/${language}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function* localizedConceptToQuads(localizedConcept, { parentUri, language }) {
|
|
21
|
+
const subjectUri = localizedConceptUri(parentUri, language);
|
|
22
|
+
const s = namedNode(subjectUri);
|
|
23
|
+
|
|
24
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.LocalizedConcept));
|
|
25
|
+
yield quad(s, namedNode(WELL_KNOWN.rdfType), namedNode(WELL_KNOWN.skosConcept));
|
|
26
|
+
yield quad(s, namedNode(PRED.gloss.isLocalizationOf), namedNode(parentUri));
|
|
27
|
+
yield quad(s, namedNode(DCTERMS_LANGUAGE), literal(language));
|
|
28
|
+
|
|
29
|
+
if (localizedConcept.entryStatus) {
|
|
30
|
+
yield quad(s, namedNode(PRED.gloss.hasEntryStatus), namedNode(`${PRED.gloss.$ns}entstatus/${localizedConcept.entryStatus}`));
|
|
31
|
+
}
|
|
32
|
+
if (localizedConcept.domain) {
|
|
33
|
+
yield quad(s, namedNode(PRED.gloss.domain), literal(localizedConcept.domain));
|
|
34
|
+
}
|
|
35
|
+
if (localizedConcept.release) {
|
|
36
|
+
yield quad(s, namedNode(PRED.gloss.release), literal(localizedConcept.release));
|
|
37
|
+
}
|
|
38
|
+
if (localizedConcept.script) {
|
|
39
|
+
yield quad(s, namedNode(PRED.gloss.script), literal(localizedConcept.script));
|
|
40
|
+
}
|
|
41
|
+
if (localizedConcept.system) {
|
|
42
|
+
yield quad(s, namedNode(PRED.gloss.conversionSystem), literal(localizedConcept.system));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let desigIndex = 0;
|
|
46
|
+
for (const designation of localizedConcept.terms ?? []) {
|
|
47
|
+
yield* designationToQuads(designation, { subjectUri, language, index: desigIndex });
|
|
48
|
+
// Direct SKOS literal alongside the reified SKOS-XL form.
|
|
49
|
+
yield quad(s, namedNode(skosLabelPredicate(designation)), literal(designation.designation, language));
|
|
50
|
+
desigIndex += 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
yield* definitionsToQuads(localizedConcept.definitions, { subjectUri, language, role: 'hasDefinition' });
|
|
54
|
+
yield* definitionsToQuads(localizedConcept.notes, { subjectUri, language, role: 'hasNote' });
|
|
55
|
+
yield* definitionsToQuads(localizedConcept.examples, { subjectUri, language, role: 'hasExample' });
|
|
56
|
+
yield* definitionsToQuads(localizedConcept.annotations, { subjectUri, language, role: 'hasAnnotation' });
|
|
57
|
+
|
|
58
|
+
let srcIndex = 0;
|
|
59
|
+
for (const source of localizedConcept.sources ?? []) {
|
|
60
|
+
yield* conceptSourceToQuads(source, { subjectUri, index: srcIndex });
|
|
61
|
+
srcIndex += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function* definitionsToQuads(definitions, { subjectUri, language, role }) {
|
|
66
|
+
if (!definitions) return;
|
|
67
|
+
let i = 0;
|
|
68
|
+
for (const def of definitions) {
|
|
69
|
+
yield* detailedDefinitionToQuads(def, { subjectUri, language, index: i, role });
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ConceptSource → RDF quads. Mirrors glossarist-ruby's
|
|
2
|
+
// `Rdf::GlossConceptSource` + `Rdf::GlossCitation`. Sources are reified
|
|
3
|
+
// resources linked from their parent (concept or localized concept or
|
|
4
|
+
// definition) via `gloss:hasSource`.
|
|
5
|
+
import { PRED } from './predicates.js';
|
|
6
|
+
import { WELL_KNOWN } from './prefixes.js';
|
|
7
|
+
import { deterministicBnode } from './deterministic-id.js';
|
|
8
|
+
import { normalizeEnum } from './normalize-enum.js';
|
|
9
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
10
|
+
|
|
11
|
+
export function* conceptSourceToQuads(source, { subjectUri, index }) {
|
|
12
|
+
const srcSubject = deterministicBnode(subjectUri, 'source', index);
|
|
13
|
+
|
|
14
|
+
yield quad(namedNode(subjectUri), namedNode(PRED.gloss.hasSource), namedNode(srcSubject));
|
|
15
|
+
yield quad(namedNode(srcSubject), namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.ConceptSource));
|
|
16
|
+
|
|
17
|
+
const statusToken = normalizeEnum(source.status);
|
|
18
|
+
if (statusToken) {
|
|
19
|
+
yield quad(namedNode(srcSubject), namedNode(PRED.gloss.sourceStatus), namedNode(`${PRED.gloss.$ns}srcstatus/${statusToken}`));
|
|
20
|
+
}
|
|
21
|
+
const typeToken = normalizeEnum(source.type);
|
|
22
|
+
if (typeToken) {
|
|
23
|
+
yield quad(namedNode(srcSubject), namedNode(PRED.gloss.sourceType), namedNode(`${PRED.gloss.$ns}srctype/${typeToken}`));
|
|
24
|
+
}
|
|
25
|
+
if (source.modification) {
|
|
26
|
+
yield quad(namedNode(srcSubject), namedNode(PRED.gloss.modification), literal(source.modification));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (source.origin) {
|
|
30
|
+
yield* citationToQuads(source.origin, { subjectUri: srcSubject });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function* citationToQuads(citation, { subjectUri }) {
|
|
35
|
+
const citSubject = deterministicBnode(subjectUri, 'origin', 0);
|
|
36
|
+
yield quad(namedNode(subjectUri), namedNode(PRED.gloss.sourceOrigin), namedNode(citSubject));
|
|
37
|
+
yield quad(namedNode(citSubject), namedNode(WELL_KNOWN.rdfType), namedNode(PRED.gloss.Citation));
|
|
38
|
+
|
|
39
|
+
if (citation.original) {
|
|
40
|
+
yield quad(namedNode(citSubject), namedNode(PRED.gloss.citationOriginal), literal(citation.original));
|
|
41
|
+
}
|
|
42
|
+
if (citation.link) {
|
|
43
|
+
yield quad(namedNode(citSubject), namedNode(PRED.gloss.citationLink), literal(citation.link));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (citation.ref) {
|
|
47
|
+
if (citation.ref.source) yield quad(namedNode(citSubject), namedNode(PRED.gloss.citationRefSource), literal(citation.ref.source));
|
|
48
|
+
if (citation.ref.id) yield quad(namedNode(citSubject), namedNode(PRED.gloss.citationRefId), literal(citation.ref.id));
|
|
49
|
+
if (citation.ref.version) yield quad(namedNode(citSubject), namedNode(PRED.gloss.citationRefVersion), literal(citation.ref.version));
|
|
50
|
+
}
|
|
51
|
+
}
|