glossarist 0.4.8 → 0.4.9
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/package.json +1 -1
- package/src/models/register.js +25 -0
- package/src/models/relation-categories.js +40 -15
- package/src/models/relation-colors.js +1 -2
- package/src/rdf/agents-emitter.js +130 -0
- package/src/rdf/bibliography-emitter.js +120 -0
- package/src/rdf/build-activity-emitter.js +127 -0
- package/src/rdf/dataset-emitter.js +179 -0
- package/src/rdf/group-emitter.js +132 -0
- package/src/rdf/image-variant-emitter.js +77 -0
- package/src/rdf/index.d.ts +152 -0
- package/src/rdf/index.js +12 -0
- package/src/rdf/version-emitter.js +96 -0
- package/src/rdf/vocabulary-emitter.js +87 -0
package/package.json
CHANGED
package/src/models/register.js
CHANGED
|
@@ -91,6 +91,31 @@ export class Register extends GlossaristModel {
|
|
|
91
91
|
return [sectionId, ...this.sectionAncestorIds(sectionId)];
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Walks the section tree DOWNWARD from `sectionId`, returning all
|
|
95
|
+
// descendant section IDs in pre-order (immediate children first,
|
|
96
|
+
// then their children). The returned array does NOT include
|
|
97
|
+
// sectionId itself. Returns [] when sectionId is unknown or is a
|
|
98
|
+
// leaf section. Useful for filtering concepts by parent section
|
|
99
|
+
// (include descendants) — the inverse direction of sectionClosure.
|
|
100
|
+
sectionDescendantIds(sectionId) {
|
|
101
|
+
if (!sectionId) return [];
|
|
102
|
+
const section = this.sectionById(sectionId);
|
|
103
|
+
if (!section) return [];
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const child of section.children ?? []) {
|
|
106
|
+
out.push(child.id);
|
|
107
|
+
out.push(...this.sectionDescendantIds(child.id));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Returns the descendant closure of `sectionId`: the section plus
|
|
113
|
+
// all of its descendants. Useful for "show me everything in section
|
|
114
|
+
// X or below" queries.
|
|
115
|
+
sectionDescendantClosure(sectionId) {
|
|
116
|
+
return [sectionId, ...this.sectionDescendantIds(sectionId)];
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
// Returns all section IDs the concept belongs to: the concept's own
|
|
95
120
|
// sections plus every ancestor of each. Concept-side section
|
|
96
121
|
// membership comes from `concept.sections` if set, otherwise from
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
// Categorization SSOT for the 52 glossarist relationship types.
|
|
2
2
|
//
|
|
3
3
|
// Each type in RELATIONSHIP_TYPES belongs to exactly one category. The
|
|
4
|
-
// categories
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// categories mirror the concept-model taxonomy
|
|
5
|
+
// (concept-browser:src/data/taxonomies.json) so JS consumers and the
|
|
6
|
+
// Vue UI see the same buckets. Adding a new type means adding a
|
|
7
|
+
// taxonomy entry — no code changes here.
|
|
8
|
+
//
|
|
9
|
+
// The 8 categories:
|
|
10
|
+
// lifecycle — supersession, deprecation, replacement
|
|
11
|
+
// hierarchical — SKOS + ISO 25964 broader/narrower (generic/partitive/instantial)
|
|
12
|
+
// associative — generic see-also references
|
|
13
|
+
// comparative — compare / contrast
|
|
14
|
+
// spatiotemporal — sequential, spatial, temporal
|
|
15
|
+
// lexical — homograph, false_friend (cross-language)
|
|
16
|
+
// mapping — SKOS mapping properties (cross-vocabulary)
|
|
17
|
+
// definitional — ISO 19135 concept-to-concept relations: definition, part,
|
|
18
|
+
// instance, inheritance, versioning
|
|
19
|
+
//
|
|
20
|
+
// Designation-level relationships (abbreviated_form_for, short_form_for)
|
|
21
|
+
// are a separate concern — see DESIGNATION_RELATIONSHIP_TYPES.
|
|
7
22
|
|
|
8
23
|
import { RELATIONSHIP_TYPES } from './related-concept.js';
|
|
9
24
|
|
|
@@ -75,22 +90,21 @@ export const RELATION_CATEGORIES = Object.freeze({
|
|
|
75
90
|
]),
|
|
76
91
|
}),
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
93
|
+
// ISO 19135 concept-to-concept relations. Replaces the previous
|
|
94
|
+
// `conceptInstance` and `versioning` split — concept-browser's
|
|
95
|
+
// taxonomy treats versioning as part of the definitional cluster
|
|
96
|
+
// (a version IS a definitional variant of the same concept identity).
|
|
97
|
+
// Renaming aligns glossarist-js with the canonical taxonomy at
|
|
98
|
+
// concept-browser:src/data/taxonomies.json.
|
|
99
|
+
definitional: Object.freeze({
|
|
100
|
+
label: 'Definitional',
|
|
101
|
+
description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance, versioning.',
|
|
81
102
|
types: Object.freeze([
|
|
82
103
|
'has_concept', 'is_concept_of',
|
|
83
104
|
'instance_of', 'has_instance',
|
|
84
105
|
'has_definition', 'definition_of',
|
|
85
106
|
'has_part', 'is_part_of',
|
|
86
107
|
'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
108
|
'has_version', 'version_of',
|
|
95
109
|
'current_version', 'current_version_of',
|
|
96
110
|
]),
|
|
@@ -106,6 +120,14 @@ const _CATEGORY_BY_TYPE = (() => {
|
|
|
106
120
|
return map;
|
|
107
121
|
})();
|
|
108
122
|
|
|
123
|
+
// Backward-compat aliases for callers that adopted the original
|
|
124
|
+
// glossarist-js naming (v0.4.6). Resolved at lookup time so any of
|
|
125
|
+
// the three names returns the definitional category.
|
|
126
|
+
const _CATEGORY_ALIASES = {
|
|
127
|
+
conceptInstance: 'definitional',
|
|
128
|
+
versioning: 'definitional',
|
|
129
|
+
};
|
|
130
|
+
|
|
109
131
|
/**
|
|
110
132
|
* Returns the category key for the given relationship type, or null
|
|
111
133
|
* when the type is not categorized (e.g. an unrecognized custom type).
|
|
@@ -116,10 +138,13 @@ export function categoryOf(type) {
|
|
|
116
138
|
|
|
117
139
|
/**
|
|
118
140
|
* Returns the category definition ({ label, description, types }) for
|
|
119
|
-
* the given category key
|
|
141
|
+
* the given category key. Accepts both canonical names (definitional)
|
|
142
|
+
* and aliases from earlier glossarist-js versions (conceptInstance,
|
|
143
|
+
* versioning) for backward compatibility.
|
|
120
144
|
*/
|
|
121
145
|
export function categoryDefinition(categoryKey) {
|
|
122
|
-
|
|
146
|
+
const canonical = _CATEGORY_ALIASES[categoryKey] ?? categoryKey;
|
|
147
|
+
return RELATION_CATEGORIES[canonical] ?? null;
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
/**
|
|
@@ -18,8 +18,7 @@ export const RELATION_COLOR_DEFAULTS = Object.freeze({
|
|
|
18
18
|
spatiotemporal: { light: '#0D9488', dark: '#5EEAD4' },
|
|
19
19
|
lexical: { light: '#D97706', dark: '#FBBF24' },
|
|
20
20
|
mapping: { light: '#2563EB', dark: '#93BBFD' },
|
|
21
|
-
|
|
22
|
-
versioning: { light: '#9333EA', dark: '#C4B5FD' },
|
|
21
|
+
definitional: { light: '#65A30D', dark: '#A3E635' },
|
|
23
22
|
}),
|
|
24
23
|
|
|
25
24
|
// Per-type overrides. Use the same { light, dark } shape. Keys are
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Agents emitter — emits foaf:Person / prov:Organization from
|
|
2
|
+
// contributor declarations. Mirrors concept-browser's agents-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// Each contributor becomes a foaf:Person + prov:Person + prov:Agent
|
|
5
|
+
// with foaf:name, optional foaf:mbox (mailto link), rdfs:seeAlso (url).
|
|
6
|
+
// Their organization (when provided) becomes a foaf:Organization +
|
|
7
|
+
// prov:Organization, and the person is linked via prov:actedOnBehalfOf.
|
|
8
|
+
|
|
9
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
10
|
+
|
|
11
|
+
const PROV_NS = 'http://www.w3.org/ns/prov#';
|
|
12
|
+
const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
|
|
13
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
14
|
+
const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
|
|
15
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
16
|
+
|
|
17
|
+
const PROV = {
|
|
18
|
+
Person: `${PROV_NS}Person`,
|
|
19
|
+
Organization: `${PROV_NS}Organization`,
|
|
20
|
+
Agent: `${PROV_NS}Agent`,
|
|
21
|
+
actedOnBehalfOf: `${PROV_NS}actedOnBehalfOf`,
|
|
22
|
+
};
|
|
23
|
+
const FOAF = {
|
|
24
|
+
Person: `${FOAF_NS}Person`,
|
|
25
|
+
Organization: `${FOAF_NS}Organization`,
|
|
26
|
+
name: `${FOAF_NS}name`,
|
|
27
|
+
mbox: `${FOAF_NS}mbox`,
|
|
28
|
+
};
|
|
29
|
+
const DCTERMS = {
|
|
30
|
+
description: `${DCTERMS_NS}description`,
|
|
31
|
+
};
|
|
32
|
+
const RDFS = {
|
|
33
|
+
seeAlso: `${RDFS_NS}seeAlso`,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_AGENT_BASE = 'https://glossarist.org/agent';
|
|
37
|
+
const DEFAULT_ORG_BASE = 'https://glossarist.org/org';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} Contributor
|
|
41
|
+
* @property {string} name
|
|
42
|
+
* @property {string} [role]
|
|
43
|
+
* @property {string} [organization]
|
|
44
|
+
* @property {string} [url]
|
|
45
|
+
* @property {string} [email]
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Contributor & { slug: string, agentIri: string }} AgentInput
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Slugify a name for IRIs. Lowercase, ASCII, hyphenated.
|
|
53
|
+
*/
|
|
54
|
+
export function slugify(input) {
|
|
55
|
+
return String(input)
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.normalize('NFKD')
|
|
58
|
+
.replace(/[̀-ͯ]/g, '')
|
|
59
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
60
|
+
.replace(/^-+|-+$/g, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Builds AgentInput records from raw contributor declarations.
|
|
65
|
+
* Useful for callers that want to inspect the IRIs before emission.
|
|
66
|
+
*/
|
|
67
|
+
export function agentsFromContributors(contributors, agentBase = DEFAULT_AGENT_BASE) {
|
|
68
|
+
return (contributors ?? []).map(c => {
|
|
69
|
+
const slug = slugify(c.name);
|
|
70
|
+
return {
|
|
71
|
+
slug,
|
|
72
|
+
name: c.name,
|
|
73
|
+
role: c.role,
|
|
74
|
+
organization: c.organization,
|
|
75
|
+
url: c.url,
|
|
76
|
+
email: c.email,
|
|
77
|
+
agentIri: `${agentBase}/${slug}`,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Emits foaf:Person + prov:Organization quads for a list of agents.
|
|
84
|
+
*
|
|
85
|
+
* Returns a generator of quads AND the set of unique person and
|
|
86
|
+
* organization IRIs emitted. Organizations are deduplicated — the
|
|
87
|
+
* same org across multiple contributors is emitted once.
|
|
88
|
+
*
|
|
89
|
+
* @param {readonly AgentInput[]} agents
|
|
90
|
+
* @param {Object} [options]
|
|
91
|
+
* @param {string} [options.orgBase] — default https://glossarist.org/org
|
|
92
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
93
|
+
*/
|
|
94
|
+
export function* agentsToQuads(agents, options = {}) {
|
|
95
|
+
const orgBase = options.orgBase ?? DEFAULT_ORG_BASE;
|
|
96
|
+
const emittedOrgs = new Set();
|
|
97
|
+
|
|
98
|
+
for (const a of agents ?? []) {
|
|
99
|
+
const person = namedNode(a.agentIri);
|
|
100
|
+
yield quad(person, namedNode(RDF_TYPE), namedNode(FOAF.Person));
|
|
101
|
+
yield quad(person, namedNode(RDF_TYPE), namedNode(PROV.Person));
|
|
102
|
+
yield quad(person, namedNode(RDF_TYPE), namedNode(PROV.Agent));
|
|
103
|
+
yield quad(person, namedNode(FOAF.name), literal(a.name));
|
|
104
|
+
|
|
105
|
+
if (a.email) {
|
|
106
|
+
yield quad(person, namedNode(FOAF.mbox), namedNode(`mailto:${a.email}`));
|
|
107
|
+
}
|
|
108
|
+
if (a.url) {
|
|
109
|
+
yield quad(person, namedNode(RDFS.seeAlso), namedNode(a.url));
|
|
110
|
+
}
|
|
111
|
+
if (a.role) {
|
|
112
|
+
yield quad(person, namedNode(DCTERMS.description), literal(a.role));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (a.organization) {
|
|
116
|
+
const orgSlug = slugify(a.organization);
|
|
117
|
+
const orgIri = `${orgBase}/${orgSlug}`;
|
|
118
|
+
yield quad(person, namedNode(PROV.actedOnBehalfOf), namedNode(orgIri));
|
|
119
|
+
|
|
120
|
+
if (!emittedOrgs.has(orgIri)) {
|
|
121
|
+
emittedOrgs.add(orgIri);
|
|
122
|
+
const org = namedNode(orgIri);
|
|
123
|
+
yield quad(org, namedNode(RDF_TYPE), namedNode(FOAF.Organization));
|
|
124
|
+
yield quad(org, namedNode(RDF_TYPE), namedNode(PROV.Organization));
|
|
125
|
+
yield quad(org, namedNode(RDF_TYPE), namedNode(PROV.Agent));
|
|
126
|
+
yield quad(org, namedNode(FOAF.name), literal(a.organization));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Bibliography emitter — emits dcterms:BibliographicResource per
|
|
2
|
+
// bibliography entry. Mirrors concept-browser's bibliography-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// Each entry becomes a typed resource with:
|
|
5
|
+
// - dcterms:identifier (the entry id)
|
|
6
|
+
// - dcterms:bibliographicCitation (the reference string)
|
|
7
|
+
// - dcterms:title (optional)
|
|
8
|
+
// - foaf:page (optional link)
|
|
9
|
+
// - dcterms:type (optional, value URIs from gloss:bibtype/*)
|
|
10
|
+
// - dcterms:isPartOf (parent dataset)
|
|
11
|
+
|
|
12
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
13
|
+
|
|
14
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
15
|
+
const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
|
|
16
|
+
const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
|
|
17
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
18
|
+
|
|
19
|
+
const DCTERMS = {
|
|
20
|
+
BibliographicResource: `${DCTERMS_NS}BibliographicResource`,
|
|
21
|
+
identifier: `${DCTERMS_NS}identifier`,
|
|
22
|
+
bibliographicCitation: `${DCTERMS_NS}bibliographicCitation`,
|
|
23
|
+
title: `${DCTERMS_NS}title`,
|
|
24
|
+
type: `${DCTERMS_NS}type`,
|
|
25
|
+
isPartOf: `${DCTERMS_NS}isPartOf`,
|
|
26
|
+
};
|
|
27
|
+
const FOAF = {
|
|
28
|
+
page: `${FOAF_NS}page`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const DEFAULT_BASE_URI = 'https://glossarist.org';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} BibliographyEntry
|
|
35
|
+
* @property {string} id
|
|
36
|
+
* @property {string} reference
|
|
37
|
+
* @property {string} [title]
|
|
38
|
+
* @property {string} [link]
|
|
39
|
+
* @property {string} [type]
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} BibliographyInput
|
|
43
|
+
* @property {string} registerId
|
|
44
|
+
* @property {readonly BibliographyEntry[]} entries
|
|
45
|
+
* @property {string} [baseUri]
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the canonical IRI for a bibliography entry.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} registerId
|
|
52
|
+
* @param {string} entryId
|
|
53
|
+
* @param {string} [baseUri]
|
|
54
|
+
*/
|
|
55
|
+
export function bibliographyEntryIri(registerId, entryId, baseUri = DEFAULT_BASE_URI) {
|
|
56
|
+
return `${baseUri}/${registerId}/bib/${entryId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Emits dcterms:BibliographicResource quads for each entry. Entries
|
|
61
|
+
* without an id or reference are skipped (matches concept-browser).
|
|
62
|
+
*
|
|
63
|
+
* @param {BibliographyInput} input
|
|
64
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
65
|
+
*/
|
|
66
|
+
export function* bibliographyToQuads(input) {
|
|
67
|
+
const baseUri = input.baseUri ?? DEFAULT_BASE_URI;
|
|
68
|
+
const parentIri = `${baseUri}/${input.registerId}/`;
|
|
69
|
+
|
|
70
|
+
for (const entry of input.entries ?? []) {
|
|
71
|
+
if (!entry.id || !entry.reference) continue;
|
|
72
|
+
const entryIri = bibliographyEntryIri(input.registerId, entry.id, baseUri);
|
|
73
|
+
const e = namedNode(entryIri);
|
|
74
|
+
|
|
75
|
+
yield quad(e, namedNode(RDF_TYPE), namedNode(DCTERMS.BibliographicResource));
|
|
76
|
+
yield quad(e, namedNode(DCTERMS.identifier), literal(entry.id));
|
|
77
|
+
yield quad(e, namedNode(DCTERMS.bibliographicCitation), literal(entry.reference));
|
|
78
|
+
if (entry.title) {
|
|
79
|
+
yield quad(e, namedNode(DCTERMS.title), literal(entry.title));
|
|
80
|
+
}
|
|
81
|
+
if (entry.link) {
|
|
82
|
+
yield quad(e, namedNode(FOAF.page), namedNode(entry.link));
|
|
83
|
+
}
|
|
84
|
+
if (entry.type) {
|
|
85
|
+
yield quad(e, namedNode(DCTERMS.type), namedNode(`${GLOSS_NS}bibtype/${entry.type}`));
|
|
86
|
+
}
|
|
87
|
+
yield quad(e, namedNode(DCTERMS.isPartOf), namedNode(parentIri));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes raw bibliography data from a v3 YAML bibliography file.
|
|
93
|
+
* Accepts both shapes:
|
|
94
|
+
* - { bibliography: [...] }
|
|
95
|
+
* - { <id>: { ... }, <id>: { ... } }
|
|
96
|
+
*
|
|
97
|
+
* Returns a flat list of BibliographyEntry objects with explicit ids.
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeBibliographyData(raw) {
|
|
100
|
+
if (!raw || typeof raw !== 'object') return [];
|
|
101
|
+
if (Array.isArray(raw.bibliography)) {
|
|
102
|
+
return raw.bibliography.map(e => entryFromV3(e));
|
|
103
|
+
}
|
|
104
|
+
const entries = [];
|
|
105
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
106
|
+
if (!value || typeof value !== 'object') continue;
|
|
107
|
+
entries.push(entryFromV3(value, id));
|
|
108
|
+
}
|
|
109
|
+
return entries;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function entryFromV3(e, fallbackId) {
|
|
113
|
+
return {
|
|
114
|
+
id: e.id ?? fallbackId ?? '',
|
|
115
|
+
reference: e.reference ?? '',
|
|
116
|
+
title: e.title,
|
|
117
|
+
link: e.link,
|
|
118
|
+
type: e.type,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Build activity emitter — emits prov:Activity per build run.
|
|
2
|
+
// Mirrors concept-browser's build-activity-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// Models a single CI build as a prov:Activity with:
|
|
5
|
+
// - prov:StartingPoint and prov:EndingPoint time bounds
|
|
6
|
+
// - prov:used links to the git commit, the build tool (prov:SoftwareAgent),
|
|
7
|
+
// and each dataset register
|
|
8
|
+
// - gloss:conceptCount for the build output
|
|
9
|
+
// - prov:wasAssociatedWith for the CI bot agent
|
|
10
|
+
|
|
11
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
12
|
+
|
|
13
|
+
const PROV_NS = 'http://www.w3.org/ns/prov#';
|
|
14
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
15
|
+
const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
|
|
16
|
+
const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
|
|
17
|
+
const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
|
|
18
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
19
|
+
|
|
20
|
+
const PROV = {
|
|
21
|
+
Activity: `${PROV_NS}Activity`,
|
|
22
|
+
StartingPoint: `${PROV_NS}StartingPoint`,
|
|
23
|
+
EndingPoint: `${PROV_NS}EndingPoint`,
|
|
24
|
+
Entity: `${PROV_NS}Entity`,
|
|
25
|
+
SoftwareAgent: `${PROV_NS}SoftwareAgent`,
|
|
26
|
+
Agent: `${PROV_NS}Agent`,
|
|
27
|
+
Person: `${PROV_NS}Person`,
|
|
28
|
+
generatedAtTime: `${PROV_NS}generatedAtTime`,
|
|
29
|
+
atTime: `${PROV_NS}atTime`,
|
|
30
|
+
used: `${PROV_NS}used`,
|
|
31
|
+
wasAssociatedWith: `${PROV_NS}wasAssociatedWith`,
|
|
32
|
+
version: `${PROV_NS}version`,
|
|
33
|
+
};
|
|
34
|
+
const DCTERMS = {
|
|
35
|
+
identifier: `${DCTERMS_NS}identifier`,
|
|
36
|
+
description: `${DCTERMS_NS}description`,
|
|
37
|
+
};
|
|
38
|
+
const FOAF = {
|
|
39
|
+
Person: `${FOAF_NS}Person`,
|
|
40
|
+
};
|
|
41
|
+
const GLOSS = {
|
|
42
|
+
conceptCount: `${GLOSS_NS}conceptCount`,
|
|
43
|
+
};
|
|
44
|
+
const XSD_DATE_TIME = `${XSD_NS}dateTime`;
|
|
45
|
+
const XSD_INTEGER = `${XSD_NS}integer`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} BuildActivityInput
|
|
49
|
+
* @property {string} runId
|
|
50
|
+
* @property {string} startedAt — ISO date-time
|
|
51
|
+
* @property {string} endedAt — ISO date-time
|
|
52
|
+
* @property {string} [gitSha]
|
|
53
|
+
* @property {string} [gitBranch]
|
|
54
|
+
* @property {string} toolId
|
|
55
|
+
* @property {string} toolVersion
|
|
56
|
+
* @property {readonly string[]} datasetRegisters
|
|
57
|
+
* @property {number} conceptCount
|
|
58
|
+
* @property {string} [associatedAgentIri]
|
|
59
|
+
* @property {string} [baseUri] — default https://glossarist.org
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns the canonical relative IRI for a build activity.
|
|
64
|
+
*/
|
|
65
|
+
export function buildActivityIri(input) {
|
|
66
|
+
return `activity/build/${input.runId}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Emits prov:Activity quads for one build run, including the
|
|
71
|
+
* starting/ending time bounds, the build tool as prov:SoftwareAgent,
|
|
72
|
+
* the git commit (when provided) as a prov:Entity, and each dataset
|
|
73
|
+
* register as a prov:used target.
|
|
74
|
+
*
|
|
75
|
+
* @param {BuildActivityInput} input
|
|
76
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
77
|
+
*/
|
|
78
|
+
export function* buildActivityToQuads(input) {
|
|
79
|
+
const baseUri = input.baseUri ?? 'https://glossarist.org';
|
|
80
|
+
const activityIri = `${baseUri}/${buildActivityIri(input)}`;
|
|
81
|
+
const activity = namedNode(activityIri);
|
|
82
|
+
|
|
83
|
+
yield quad(activity, namedNode(RDF_TYPE), namedNode(PROV.Activity));
|
|
84
|
+
yield quad(activity, namedNode(PROV.generatedAtTime), literal(input.endedAt, namedNode(XSD_DATE_TIME)));
|
|
85
|
+
|
|
86
|
+
// StartingPoint and EndingPoint are sub-types of prov:TimeBound.
|
|
87
|
+
// Modeling them as separate resources lets consumers query exact
|
|
88
|
+
// start vs end without parsing date ranges.
|
|
89
|
+
const start = namedNode(`${activityIri}/start`);
|
|
90
|
+
yield quad(start, namedNode(RDF_TYPE), namedNode(PROV.StartingPoint));
|
|
91
|
+
yield quad(start, namedNode(PROV.atTime), literal(input.startedAt, namedNode(XSD_DATE_TIME)));
|
|
92
|
+
|
|
93
|
+
const end = namedNode(`${activityIri}/end`);
|
|
94
|
+
yield quad(end, namedNode(RDF_TYPE), namedNode(PROV.EndingPoint));
|
|
95
|
+
yield quad(end, namedNode(PROV.atTime), literal(input.endedAt, namedNode(XSD_DATE_TIME)));
|
|
96
|
+
|
|
97
|
+
if (input.gitSha) {
|
|
98
|
+
const commitIri = `${baseUri}/commit/${input.gitSha}`;
|
|
99
|
+
yield quad(activity, namedNode(PROV.used), namedNode(commitIri));
|
|
100
|
+
const commit = namedNode(commitIri);
|
|
101
|
+
yield quad(commit, namedNode(RDF_TYPE), namedNode(PROV.Entity));
|
|
102
|
+
if (input.gitBranch) {
|
|
103
|
+
yield quad(commit, namedNode(DCTERMS.description), literal(`branch: ${input.gitBranch}`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const toolIri = `${baseUri}/tool/${input.toolId}/${input.toolVersion}`;
|
|
108
|
+
yield quad(activity, namedNode(PROV.used), namedNode(toolIri));
|
|
109
|
+
const tool = namedNode(toolIri);
|
|
110
|
+
yield quad(tool, namedNode(RDF_TYPE), namedNode(PROV.Entity));
|
|
111
|
+
yield quad(tool, namedNode(RDF_TYPE), namedNode(PROV.SoftwareAgent));
|
|
112
|
+
yield quad(tool, namedNode(DCTERMS.identifier), literal(input.toolVersion));
|
|
113
|
+
yield quad(tool, namedNode(PROV.version), literal(input.toolVersion));
|
|
114
|
+
|
|
115
|
+
for (const register of input.datasetRegisters ?? []) {
|
|
116
|
+
yield quad(activity, namedNode(PROV.used), namedNode(`${baseUri}/${register}/`));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
yield quad(activity, namedNode(GLOSS.conceptCount), literal(String(input.conceptCount), namedNode(XSD_INTEGER)));
|
|
120
|
+
|
|
121
|
+
if (input.associatedAgentIri) {
|
|
122
|
+
yield quad(activity, namedNode(PROV.wasAssociatedWith), namedNode(input.associatedAgentIri));
|
|
123
|
+
const agent = namedNode(input.associatedAgentIri);
|
|
124
|
+
yield quad(agent, namedNode(RDF_TYPE), namedNode(PROV.Agent));
|
|
125
|
+
yield quad(agent, namedNode(RDF_TYPE), namedNode(FOAF.Person));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Dataset emitter — emits dcat:Dataset + skos:ConceptScheme per
|
|
2
|
+
// register. Mirrors concept-browser's dataset-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// The dataset describes the vocabulary as a whole: title, description,
|
|
5
|
+
// modified date, languages, distributions (Turtle/JSON-LD/YAML), top
|
|
6
|
+
// concepts, sections, source repo, publisher, contact.
|
|
7
|
+
//
|
|
8
|
+
// Each section becomes a skos:Collection linked via skos:member to
|
|
9
|
+
// its concept URIs and via gloss:hasParentSection / gloss:hasChildSection
|
|
10
|
+
// to its relatives (the ontology declares these as owl:TransitiveProperty
|
|
11
|
+
// for cascading section membership).
|
|
12
|
+
|
|
13
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
14
|
+
|
|
15
|
+
// Prefix namespaces used by this emitter. Hard-coded (not via PREFIXES)
|
|
16
|
+
// because dcat and prov are absent from the JSON-LD context that
|
|
17
|
+
// PREFIXES is generated from — they appear in instance data only.
|
|
18
|
+
// See concept-model/prefixes.ttl for the canonical bindings.
|
|
19
|
+
const DCAT_NS = 'http://www.w3.org/ns/dcat#';
|
|
20
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
21
|
+
const SKOS_NS = 'http://www.w3.org/2004/02/skos/core#';
|
|
22
|
+
const PROV_NS = 'http://www.w3.org/ns/prov#';
|
|
23
|
+
const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
|
|
24
|
+
const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} DatasetDistribution
|
|
28
|
+
* @property {string} id
|
|
29
|
+
* @property {string} title
|
|
30
|
+
* @property {string} mediaType
|
|
31
|
+
* @property {string} downloadUrl
|
|
32
|
+
* @property {number} [byteSize]
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} DatasetSection
|
|
36
|
+
* @property {string} collectionIri
|
|
37
|
+
* @property {string} title
|
|
38
|
+
* @property {readonly string[]} memberUris
|
|
39
|
+
* @property {string} [parentCollectionIri]
|
|
40
|
+
* @property {readonly string[]} [childCollectionIris]
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} DatasetEmitterInput
|
|
44
|
+
* @property {string} datasetIri
|
|
45
|
+
* @property {string} registerId
|
|
46
|
+
* @property {string} title
|
|
47
|
+
* @property {string} [description]
|
|
48
|
+
* @property {string} modified — ISO date (YYYY-MM-DD)
|
|
49
|
+
* @property {readonly string[]} languages
|
|
50
|
+
* @property {readonly DatasetDistribution[]} distributions
|
|
51
|
+
* @property {readonly string[]} topConceptUris
|
|
52
|
+
* @property {readonly DatasetSection[]} sections
|
|
53
|
+
* @property {string} [sourceRepoUrl]
|
|
54
|
+
* @property {string} [publisherIri]
|
|
55
|
+
* @property {string} [contactIri]
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const DCAT = {
|
|
59
|
+
Dataset: `${DCAT_NS}Dataset`,
|
|
60
|
+
ConceptScheme: `${SKOS_NS}ConceptScheme`,
|
|
61
|
+
Distribution: `${DCAT_NS}Distribution`,
|
|
62
|
+
Collection: `${SKOS_NS}Collection`,
|
|
63
|
+
mediaType: `${DCAT_NS}mediaType`,
|
|
64
|
+
downloadURL: `${DCAT_NS}downloadURL`,
|
|
65
|
+
byteSize: `${DCAT_NS}byteSize`,
|
|
66
|
+
distribution: `${DCAT_NS}distribution`,
|
|
67
|
+
contactPoint: `${DCAT_NS}contactPoint`,
|
|
68
|
+
};
|
|
69
|
+
const DCTERMS = {
|
|
70
|
+
title: `${DCTERMS_NS}title`,
|
|
71
|
+
description: `${DCTERMS_NS}description`,
|
|
72
|
+
modified: `${DCTERMS_NS}modified`,
|
|
73
|
+
identifier: `${DCTERMS_NS}identifier`,
|
|
74
|
+
language: `${DCTERMS_NS}language`,
|
|
75
|
+
publisher: `${DCTERMS_NS}publisher`,
|
|
76
|
+
};
|
|
77
|
+
const SKOS = {
|
|
78
|
+
hasTopConcept: `${SKOS_NS}hasTopConcept`,
|
|
79
|
+
member: `${SKOS_NS}member`,
|
|
80
|
+
};
|
|
81
|
+
const PROV = {
|
|
82
|
+
wasDerivedFrom: `${PROV_NS}wasDerivedFrom`,
|
|
83
|
+
};
|
|
84
|
+
const GLOSS = {
|
|
85
|
+
hasParentSection: `${GLOSS_NS}hasParentSection`,
|
|
86
|
+
hasChildSection: `${GLOSS_NS}hasChildSection`,
|
|
87
|
+
};
|
|
88
|
+
const XSD_DATE = `${XSD_NS}date`;
|
|
89
|
+
const XSD_INTEGER = `${XSD_NS}integer`;
|
|
90
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Emits dcat:Dataset + skos:ConceptScheme quads for one register's
|
|
94
|
+
* metadata, plus its distributions, sections, and top concepts.
|
|
95
|
+
*
|
|
96
|
+
* @param {DatasetEmitterInput} input
|
|
97
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
98
|
+
*/
|
|
99
|
+
export function* datasetToQuads(input) {
|
|
100
|
+
const ds = namedNode(input.datasetIri);
|
|
101
|
+
|
|
102
|
+
yield quad(ds, namedNode(RDF_TYPE), namedNode(DCAT.Dataset));
|
|
103
|
+
yield quad(ds, namedNode(RDF_TYPE), namedNode(DCAT.ConceptScheme));
|
|
104
|
+
yield quad(ds, namedNode(DCTERMS.title), literal(input.title));
|
|
105
|
+
if (input.description) {
|
|
106
|
+
yield quad(ds, namedNode(DCTERMS.description), literal(input.description));
|
|
107
|
+
}
|
|
108
|
+
yield quad(ds, namedNode(DCTERMS.modified), literal(input.modified, namedNode(XSD_DATE)));
|
|
109
|
+
yield quad(ds, namedNode(DCTERMS.identifier), literal(input.registerId));
|
|
110
|
+
|
|
111
|
+
for (const lang of input.languages ?? []) {
|
|
112
|
+
const langIri = `http://id.loc.gov/vocabulary/iso639-1/${lang}`;
|
|
113
|
+
yield quad(ds, namedNode(DCTERMS.language), namedNode(langIri));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const dist of input.distributions ?? []) {
|
|
117
|
+
yield* distributionToQuads(ds, dist);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const conceptUri of input.topConceptUris ?? []) {
|
|
121
|
+
yield quad(ds, namedNode(SKOS.hasTopConcept), namedNode(conceptUri));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (input.sourceRepoUrl) {
|
|
125
|
+
yield quad(ds, namedNode(PROV.wasDerivedFrom), namedNode(input.sourceRepoUrl));
|
|
126
|
+
}
|
|
127
|
+
if (input.publisherIri) {
|
|
128
|
+
yield quad(ds, namedNode(DCTERMS.publisher), namedNode(input.publisherIri));
|
|
129
|
+
}
|
|
130
|
+
if (input.contactIri) {
|
|
131
|
+
yield quad(ds, namedNode(DCAT.contactPoint), namedNode(input.contactIri));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const section of input.sections ?? []) {
|
|
135
|
+
yield* sectionToQuads(section);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function* distributionToQuads(ds, dist) {
|
|
140
|
+
// Distributions are blank nodes — they don't have stable identity
|
|
141
|
+
// outside their parent dataset.
|
|
142
|
+
const distSubject = namedNode(`_:b${deterministicId('dist', dist.id, dist.downloadUrl)}`);
|
|
143
|
+
yield quad(ds, namedNode(DCAT.distribution), distSubject);
|
|
144
|
+
yield quad(distSubject, namedNode(RDF_TYPE), namedNode(DCAT.Distribution));
|
|
145
|
+
yield quad(distSubject, namedNode(DCTERMS.title), literal(dist.title));
|
|
146
|
+
yield quad(distSubject, namedNode(DCAT.mediaType), literal(dist.mediaType));
|
|
147
|
+
yield quad(distSubject, namedNode(DCAT.downloadURL), namedNode(dist.downloadUrl));
|
|
148
|
+
if (dist.byteSize != null) {
|
|
149
|
+
yield quad(distSubject, namedNode(DCAT.byteSize), literal(String(dist.byteSize), namedNode(XSD_INTEGER)));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function* sectionToQuads(section) {
|
|
154
|
+
const coll = namedNode(section.collectionIri);
|
|
155
|
+
yield quad(coll, namedNode(RDF_TYPE), namedNode(DCAT.Collection));
|
|
156
|
+
yield quad(coll, namedNode(DCTERMS.title), literal(section.title));
|
|
157
|
+
for (const member of section.memberUris ?? []) {
|
|
158
|
+
yield quad(coll, namedNode(SKOS.member), namedNode(member));
|
|
159
|
+
}
|
|
160
|
+
if (section.parentCollectionIri) {
|
|
161
|
+
yield quad(coll, namedNode(GLOSS.hasParentSection), namedNode(section.parentCollectionIri));
|
|
162
|
+
}
|
|
163
|
+
for (const child of section.childCollectionIris ?? []) {
|
|
164
|
+
yield quad(coll, namedNode(GLOSS.hasChildSection), namedNode(child));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Local deterministic-id helper (avoid pulling crypto-bearing module
|
|
169
|
+
// into this browser-safe file). Mirrors deterministicId's seed|join
|
|
170
|
+
// pattern using a small hash. Repeated calls within one process are
|
|
171
|
+
// stable; cross-process stability isn't required for blank node IDs.
|
|
172
|
+
function deterministicId(...parts) {
|
|
173
|
+
const seed = parts.filter(p => p != null).join('|');
|
|
174
|
+
let h = 0;
|
|
175
|
+
for (let i = 0; i < seed.length; i++) {
|
|
176
|
+
h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
|
|
177
|
+
}
|
|
178
|
+
return (h >>> 0).toString(16).padStart(8, '0');
|
|
179
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Group emitter — emits dcat:DatasetSeries (lineage) or dcat:Catalog
|
|
2
|
+
// (topic/family/collection) for dataset groups. Mirrors concept-browser's
|
|
3
|
+
// group-emitter.ts.
|
|
4
|
+
//
|
|
5
|
+
// A lineage group (editions of the same vocabulary) is rendered as
|
|
6
|
+
// dcat:DatasetSeries with hasVersion links to each member edition.
|
|
7
|
+
// The current edition is linked via hasCurrentVersion.
|
|
8
|
+
//
|
|
9
|
+
// Other group kinds (topic, family, collection) are rendered as
|
|
10
|
+
// dcat:Catalog with dcat:dataset links to each member.
|
|
11
|
+
//
|
|
12
|
+
// Both kinds are also typed as skos:ConceptScheme for SKOS interop.
|
|
13
|
+
|
|
14
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
15
|
+
|
|
16
|
+
// Hard-coded namespaces — see dataset-emitter.js for rationale.
|
|
17
|
+
const DCAT_NS = 'http://www.w3.org/ns/dcat#';
|
|
18
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
19
|
+
const SKOS_NS = 'http://www.w3.org/2004/02/skos/core#';
|
|
20
|
+
const PROV_NS = 'http://www.w3.org/ns/prov#';
|
|
21
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
22
|
+
|
|
23
|
+
const DCAT = {
|
|
24
|
+
DatasetSeries: `${DCAT_NS}DatasetSeries`,
|
|
25
|
+
Catalog: `${DCAT_NS}Catalog`,
|
|
26
|
+
ConceptScheme: `${SKOS_NS}ConceptScheme`,
|
|
27
|
+
hasVersion: `${DCAT_NS}hasVersion`,
|
|
28
|
+
hasCurrentVersion: `${DCAT_NS}hasCurrentVersion`,
|
|
29
|
+
dataset: `${DCAT_NS}dataset`,
|
|
30
|
+
theme: `${DCAT_NS}theme`,
|
|
31
|
+
keyword: `${DCAT_NS}keyword`,
|
|
32
|
+
contactPoint: `${DCAT_NS}contactPoint`,
|
|
33
|
+
};
|
|
34
|
+
const DCTERMS = {
|
|
35
|
+
title: `${DCTERMS_NS}title`,
|
|
36
|
+
description: `${DCTERMS_NS}description`,
|
|
37
|
+
identifier: `${DCTERMS_NS}identifier`,
|
|
38
|
+
subject: `${DCTERMS_NS}subject`,
|
|
39
|
+
publisher: `${DCTERMS_NS}publisher`,
|
|
40
|
+
};
|
|
41
|
+
const PROV = {
|
|
42
|
+
wasDerivedFrom: `${PROV_NS}wasDerivedFrom`,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {'lineage' | 'topic' | 'family' | 'collection' | 'default'} DatasetGroupKind
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} GroupEmitInput
|
|
50
|
+
* @property {string} groupId
|
|
51
|
+
* @property {string} groupIri
|
|
52
|
+
* @property {DatasetGroupKind} kind
|
|
53
|
+
* @property {string} title
|
|
54
|
+
* @property {string} [description]
|
|
55
|
+
* @property {readonly string[]} memberIris
|
|
56
|
+
* @property {string} [currentMemberIri]
|
|
57
|
+
* @property {string} [subject]
|
|
58
|
+
* @property {readonly string[]} [themes]
|
|
59
|
+
* @property {readonly string[]} [keywords]
|
|
60
|
+
* @property {string} [publisher]
|
|
61
|
+
* @property {string} [contact]
|
|
62
|
+
* @property {string} [sourceRepo]
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
// Returns the RDF class IRIs for a group kind. Empty for 'default'
|
|
66
|
+
// (no semantics — group is purely a presentation concern).
|
|
67
|
+
function rdfClassesForKind(kind) {
|
|
68
|
+
switch (kind) {
|
|
69
|
+
case 'lineage': return [DCAT.DatasetSeries, DCAT.ConceptScheme];
|
|
70
|
+
case 'topic':
|
|
71
|
+
case 'family':
|
|
72
|
+
case 'collection': return [DCAT.Catalog, DCAT.ConceptScheme];
|
|
73
|
+
default: return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Emits dcat:DatasetSeries or dcat:Catalog quads for one group.
|
|
79
|
+
*
|
|
80
|
+
* Returns an empty generator (no quads) for 'default' kind — those
|
|
81
|
+
* groups have no RDF semantics.
|
|
82
|
+
*
|
|
83
|
+
* @param {GroupEmitInput} input
|
|
84
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
85
|
+
*/
|
|
86
|
+
export function* groupToQuads(input) {
|
|
87
|
+
const classes = rdfClassesForKind(input.kind);
|
|
88
|
+
if (classes.length === 0) return;
|
|
89
|
+
|
|
90
|
+
const g = namedNode(input.groupIri);
|
|
91
|
+
|
|
92
|
+
for (const cls of classes) {
|
|
93
|
+
yield quad(g, namedNode(RDF_TYPE), namedNode(cls));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
yield quad(g, namedNode(DCTERMS.title), literal(input.title));
|
|
97
|
+
yield quad(g, namedNode(DCTERMS.identifier), literal(input.groupId));
|
|
98
|
+
if (input.description) {
|
|
99
|
+
yield quad(g, namedNode(DCTERMS.description), literal(input.description));
|
|
100
|
+
}
|
|
101
|
+
if (input.subject) {
|
|
102
|
+
yield quad(g, namedNode(DCTERMS.subject), literal(input.subject));
|
|
103
|
+
}
|
|
104
|
+
for (const theme of input.themes ?? []) {
|
|
105
|
+
yield quad(g, namedNode(DCAT.theme), namedNode(theme));
|
|
106
|
+
}
|
|
107
|
+
for (const kw of input.keywords ?? []) {
|
|
108
|
+
yield quad(g, namedNode(DCAT.keyword), literal(kw));
|
|
109
|
+
}
|
|
110
|
+
if (input.publisher) {
|
|
111
|
+
yield quad(g, namedNode(DCTERMS.publisher), namedNode(input.publisher));
|
|
112
|
+
}
|
|
113
|
+
if (input.contact) {
|
|
114
|
+
yield quad(g, namedNode(DCAT.contactPoint), namedNode(input.contact));
|
|
115
|
+
}
|
|
116
|
+
if (input.sourceRepo) {
|
|
117
|
+
yield quad(g, namedNode(PROV.wasDerivedFrom), namedNode(input.sourceRepo));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (input.kind === 'lineage') {
|
|
121
|
+
for (const memberIri of input.memberIris ?? []) {
|
|
122
|
+
yield quad(g, namedNode(DCAT.hasVersion), namedNode(memberIri));
|
|
123
|
+
}
|
|
124
|
+
if (input.currentMemberIri) {
|
|
125
|
+
yield quad(g, namedNode(DCAT.hasCurrentVersion), namedNode(input.currentMemberIri));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
for (const memberIri of input.memberIris ?? []) {
|
|
129
|
+
yield quad(g, namedNode(DCAT.dataset), namedNode(memberIri));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Image variant emitter — emits foaf:Image per format/language variant
|
|
2
|
+
// of a dataset figure. Mirrors concept-browser's image-variant-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// Each variant becomes a foaf:Image with:
|
|
5
|
+
// - dcterms:format (MIME type)
|
|
6
|
+
// - dcterms:language (when localized)
|
|
7
|
+
// - dcat:byteSize (when known)
|
|
8
|
+
// - dcat:downloadURL (canonical download)
|
|
9
|
+
|
|
10
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
11
|
+
|
|
12
|
+
const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
|
|
13
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
14
|
+
const DCAT_NS = 'http://www.w3.org/ns/dcat#';
|
|
15
|
+
const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
|
|
16
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
17
|
+
|
|
18
|
+
const FOAF = {
|
|
19
|
+
Image: `${FOAF_NS}Image`,
|
|
20
|
+
};
|
|
21
|
+
const DCTERMS = {
|
|
22
|
+
format: `${DCTERMS_NS}format`,
|
|
23
|
+
language: `${DCTERMS_NS}language`,
|
|
24
|
+
};
|
|
25
|
+
const DCAT = {
|
|
26
|
+
byteSize: `${DCAT_NS}byteSize`,
|
|
27
|
+
downloadURL: `${DCAT_NS}downloadURL`,
|
|
28
|
+
};
|
|
29
|
+
const XSD_INTEGER = `${XSD_NS}integer`;
|
|
30
|
+
|
|
31
|
+
const DEFAULT_BASE_URI = 'https://glossarist.org';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {'svg' | 'png' | 'webp' | 'jpg' | 'gif'} ImageFormat
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} ImageVariantInput
|
|
38
|
+
* @property {string} registerId
|
|
39
|
+
* @property {string} figureId
|
|
40
|
+
* @property {string} [lang]
|
|
41
|
+
* @property {ImageFormat} format
|
|
42
|
+
* @property {number} [byteSize]
|
|
43
|
+
* @property {string} downloadUrl
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const MIME_BY_FORMAT = Object.freeze({
|
|
47
|
+
svg: 'image/svg+xml',
|
|
48
|
+
png: 'image/png',
|
|
49
|
+
webp: 'image/webp',
|
|
50
|
+
jpg: 'image/jpeg',
|
|
51
|
+
gif: 'image/gif',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns the canonical IRI for an image variant. Includes the lang
|
|
56
|
+
* segment when the variant is localized, otherwise omits it.
|
|
57
|
+
*/
|
|
58
|
+
export function imageVariantIri(input, baseUri = DEFAULT_BASE_URI) {
|
|
59
|
+
const langSuffix = input.lang ? `${input.lang}.` : '';
|
|
60
|
+
return `${baseUri}/${input.registerId}/image/${input.figureId}/${langSuffix}${input.format}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emits foaf:Image quads for one image variant.
|
|
65
|
+
*/
|
|
66
|
+
export function* imageVariantToQuads(input, baseUri = DEFAULT_BASE_URI) {
|
|
67
|
+
const img = namedNode(imageVariantIri(input, baseUri));
|
|
68
|
+
yield quad(img, namedNode(RDF_TYPE), namedNode(FOAF.Image));
|
|
69
|
+
yield quad(img, namedNode(DCTERMS.format), literal(MIME_BY_FORMAT[input.format] ?? 'application/octet-stream'));
|
|
70
|
+
if (input.lang) {
|
|
71
|
+
yield quad(img, namedNode(DCTERMS.language), literal(input.lang));
|
|
72
|
+
}
|
|
73
|
+
if (input.byteSize != null) {
|
|
74
|
+
yield quad(img, namedNode(DCAT.byteSize), literal(String(input.byteSize), namedNode(XSD_INTEGER)));
|
|
75
|
+
}
|
|
76
|
+
yield quad(img, namedNode(DCAT.downloadURL), namedNode(input.downloadUrl));
|
|
77
|
+
}
|
package/src/rdf/index.d.ts
CHANGED
|
@@ -97,6 +97,158 @@ export type NonVerbalEntity = Figure | Table | Formula;
|
|
|
97
97
|
export declare function nonVerbalEntityUri(entity: NonVerbalEntity, options: NonVerbalEntityEmitOptions): string;
|
|
98
98
|
export declare function nonVerbalEntityToQuads(entity: NonVerbalEntity, options: NonVerbalEntityEmitOptions): Generator<Quad, void, unknown>;
|
|
99
99
|
|
|
100
|
+
// Vocabulary emitter (SKOS ConceptSchemes for enumeration IRIs)
|
|
101
|
+
export interface VocabTerm {
|
|
102
|
+
iri: string;
|
|
103
|
+
label: string;
|
|
104
|
+
}
|
|
105
|
+
export interface VocabScheme {
|
|
106
|
+
schemeIri: string;
|
|
107
|
+
label: string;
|
|
108
|
+
terms: readonly VocabTerm[];
|
|
109
|
+
}
|
|
110
|
+
export declare function resolveIri(iri: string): string;
|
|
111
|
+
export declare function vocabularySchemeToQuads(scheme: VocabScheme): Generator<Quad, void, unknown>;
|
|
112
|
+
export declare function vocabularyToQuads(schemes: readonly VocabScheme[]): Generator<Quad, void, unknown>;
|
|
113
|
+
|
|
114
|
+
// Dataset emitter (dcat:Dataset + skos:ConceptScheme per register)
|
|
115
|
+
export interface DatasetDistribution {
|
|
116
|
+
id: string;
|
|
117
|
+
title: string;
|
|
118
|
+
mediaType: string;
|
|
119
|
+
downloadUrl: string;
|
|
120
|
+
byteSize?: number;
|
|
121
|
+
}
|
|
122
|
+
export interface DatasetSection {
|
|
123
|
+
collectionIri: string;
|
|
124
|
+
title: string;
|
|
125
|
+
memberUris: readonly string[];
|
|
126
|
+
parentCollectionIri?: string;
|
|
127
|
+
childCollectionIris?: readonly string[];
|
|
128
|
+
}
|
|
129
|
+
export interface DatasetEmitterInput {
|
|
130
|
+
datasetIri: string;
|
|
131
|
+
registerId: string;
|
|
132
|
+
title: string;
|
|
133
|
+
description?: string;
|
|
134
|
+
modified: string;
|
|
135
|
+
languages: readonly string[];
|
|
136
|
+
distributions: readonly DatasetDistribution[];
|
|
137
|
+
topConceptUris: readonly string[];
|
|
138
|
+
sections: readonly DatasetSection[];
|
|
139
|
+
sourceRepoUrl?: string;
|
|
140
|
+
publisherIri?: string;
|
|
141
|
+
contactIri?: string;
|
|
142
|
+
}
|
|
143
|
+
export declare function datasetToQuads(input: DatasetEmitterInput): Generator<Quad, void, unknown>;
|
|
144
|
+
|
|
145
|
+
// Group emitter (dcat:DatasetSeries or dcat:Catalog)
|
|
146
|
+
export type DatasetGroupKind = 'lineage' | 'topic' | 'family' | 'collection' | 'default';
|
|
147
|
+
export interface GroupEmitInput {
|
|
148
|
+
groupId: string;
|
|
149
|
+
groupIri: string;
|
|
150
|
+
kind: DatasetGroupKind;
|
|
151
|
+
title: string;
|
|
152
|
+
description?: string;
|
|
153
|
+
memberIris: readonly string[];
|
|
154
|
+
currentMemberIri?: string;
|
|
155
|
+
subject?: string;
|
|
156
|
+
themes?: readonly string[];
|
|
157
|
+
keywords?: readonly string[];
|
|
158
|
+
publisher?: string;
|
|
159
|
+
contact?: string;
|
|
160
|
+
sourceRepo?: string;
|
|
161
|
+
}
|
|
162
|
+
export declare function groupToQuads(input: GroupEmitInput): Generator<Quad, void, unknown>;
|
|
163
|
+
|
|
164
|
+
// Bibliography emitter (dcterms:BibliographicResource)
|
|
165
|
+
export interface BibliographyEntry {
|
|
166
|
+
id: string;
|
|
167
|
+
reference: string;
|
|
168
|
+
title?: string;
|
|
169
|
+
link?: string;
|
|
170
|
+
type?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface BibliographyInput {
|
|
173
|
+
registerId: string;
|
|
174
|
+
entries: readonly BibliographyEntry[];
|
|
175
|
+
baseUri?: string;
|
|
176
|
+
}
|
|
177
|
+
export declare function bibliographyEntryIri(registerId: string, entryId: string, baseUri?: string): string;
|
|
178
|
+
export declare function bibliographyToQuads(input: BibliographyInput): Generator<Quad, void, unknown>;
|
|
179
|
+
export declare function normalizeBibliographyData(raw: unknown): BibliographyEntry[];
|
|
180
|
+
|
|
181
|
+
// Build activity emitter (prov:Activity per build run)
|
|
182
|
+
export interface BuildActivityInput {
|
|
183
|
+
runId: string;
|
|
184
|
+
startedAt: string;
|
|
185
|
+
endedAt: string;
|
|
186
|
+
gitSha?: string;
|
|
187
|
+
gitBranch?: string;
|
|
188
|
+
toolId: string;
|
|
189
|
+
toolVersion: string;
|
|
190
|
+
datasetRegisters: readonly string[];
|
|
191
|
+
conceptCount: number;
|
|
192
|
+
associatedAgentIri?: string;
|
|
193
|
+
baseUri?: string;
|
|
194
|
+
}
|
|
195
|
+
export declare function buildActivityIri(input: Pick<BuildActivityInput, 'runId'>): string;
|
|
196
|
+
export declare function buildActivityToQuads(input: BuildActivityInput): Generator<Quad, void, unknown>;
|
|
197
|
+
|
|
198
|
+
// Agents emitter (foaf:Person / prov:Organization)
|
|
199
|
+
export interface Contributor {
|
|
200
|
+
name: string;
|
|
201
|
+
role?: string;
|
|
202
|
+
organization?: string;
|
|
203
|
+
url?: string;
|
|
204
|
+
email?: string;
|
|
205
|
+
}
|
|
206
|
+
export interface AgentInput extends Contributor {
|
|
207
|
+
slug: string;
|
|
208
|
+
agentIri: string;
|
|
209
|
+
}
|
|
210
|
+
export declare function slugify(input: string): string;
|
|
211
|
+
export declare function agentsFromContributors(contributors: readonly Contributor[], agentBase?: string): AgentInput[];
|
|
212
|
+
export declare function agentsToQuads(agents: readonly AgentInput[], options?: { orgBase?: string }): Generator<Quad, void, unknown>;
|
|
213
|
+
|
|
214
|
+
// Version emitter (prov:Entity version chain)
|
|
215
|
+
export interface DatasetVersionInput {
|
|
216
|
+
registerId: string;
|
|
217
|
+
version: string;
|
|
218
|
+
versionIri: string;
|
|
219
|
+
datasetIri: string;
|
|
220
|
+
generatedAt: string;
|
|
221
|
+
previousVersionIri?: string;
|
|
222
|
+
changeSummary?: string;
|
|
223
|
+
associatedAgentIri?: string;
|
|
224
|
+
}
|
|
225
|
+
export interface VersionHistoryEntry {
|
|
226
|
+
version: string;
|
|
227
|
+
generatedAt: string;
|
|
228
|
+
changeSummary?: string;
|
|
229
|
+
}
|
|
230
|
+
export interface VersionEmitAllInput {
|
|
231
|
+
registerId: string;
|
|
232
|
+
datasetIri: string;
|
|
233
|
+
versions: readonly VersionHistoryEntry[];
|
|
234
|
+
associatedAgentIri?: string;
|
|
235
|
+
}
|
|
236
|
+
export declare function versionToQuads(input: DatasetVersionInput): Generator<Quad, void, unknown>;
|
|
237
|
+
export declare function versionHistoryToQuads(input: VersionEmitAllInput): Generator<Quad, void, unknown>;
|
|
238
|
+
|
|
239
|
+
// Image variant emitter (foaf:Image per format/language variant)
|
|
240
|
+
export type ImageFormat = 'svg' | 'png' | 'webp' | 'jpg' | 'gif';
|
|
241
|
+
export interface ImageVariantInput {
|
|
242
|
+
registerId: string;
|
|
243
|
+
figureId: string;
|
|
244
|
+
lang?: string;
|
|
245
|
+
format: ImageFormat;
|
|
246
|
+
byteSize?: number;
|
|
247
|
+
downloadUrl: string;
|
|
248
|
+
}
|
|
249
|
+
export declare function imageVariantIri(input: ImageVariantInput, baseUri?: string): string;
|
|
250
|
+
export declare function imageVariantToQuads(input: ImageVariantInput, baseUri?: string): Generator<Quad, void, unknown>;
|
|
251
|
+
|
|
100
252
|
export declare function collectQuads(quadsIterable: Iterable<Quad>): Quad[];
|
|
101
253
|
|
|
102
254
|
export interface WriteTurtleOptions {
|
package/src/rdf/index.js
CHANGED
|
@@ -22,6 +22,18 @@ export {
|
|
|
22
22
|
nonVerbalEntityToQuads,
|
|
23
23
|
nonVerbalEntityUri,
|
|
24
24
|
} from './gloss-non-verbal-rep.js';
|
|
25
|
+
export {
|
|
26
|
+
vocabularySchemeToQuads,
|
|
27
|
+
vocabularyToQuads,
|
|
28
|
+
resolveIri,
|
|
29
|
+
} from './vocabulary-emitter.js';
|
|
30
|
+
export { datasetToQuads } from './dataset-emitter.js';
|
|
31
|
+
export { groupToQuads } from './group-emitter.js';
|
|
32
|
+
export { bibliographyToQuads, bibliographyEntryIri, normalizeBibliographyData } from './bibliography-emitter.js';
|
|
33
|
+
export { buildActivityIri, buildActivityToQuads } from './build-activity-emitter.js';
|
|
34
|
+
export { agentsFromContributors, agentsToQuads, slugify } from './agents-emitter.js';
|
|
35
|
+
export { versionToQuads, versionHistoryToQuads } from './version-emitter.js';
|
|
36
|
+
export { imageVariantIri, imageVariantToQuads } from './image-variant-emitter.js';
|
|
25
37
|
export {
|
|
26
38
|
collectQuads,
|
|
27
39
|
writeTurtle,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Version emitter — emits prov:Entity version chain per dataset.
|
|
2
|
+
// Mirrors concept-browser's version-emitter.ts.
|
|
3
|
+
//
|
|
4
|
+
// Each version of a dataset becomes a prov:Entity typed resource with:
|
|
5
|
+
// - dcterms:isVersionOf → the dataset
|
|
6
|
+
// - prov:wasRevisionOf → previous version (when provided)
|
|
7
|
+
// - prov:generatedAtTime (xsd:dateTime)
|
|
8
|
+
// - dcterms:description (optional change summary)
|
|
9
|
+
// - prov:wasAssociatedWith (optional agent)
|
|
10
|
+
|
|
11
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
12
|
+
|
|
13
|
+
const PROV_NS = 'http://www.w3.org/ns/prov#';
|
|
14
|
+
const DCTERMS_NS = 'http://purl.org/dc/terms/';
|
|
15
|
+
const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
|
|
16
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
17
|
+
|
|
18
|
+
const PROV = {
|
|
19
|
+
Entity: `${PROV_NS}Entity`,
|
|
20
|
+
wasRevisionOf: `${PROV_NS}wasRevisionOf`,
|
|
21
|
+
generatedAtTime: `${PROV_NS}generatedAtTime`,
|
|
22
|
+
wasAssociatedWith: `${PROV_NS}wasAssociatedWith`,
|
|
23
|
+
};
|
|
24
|
+
const DCTERMS = {
|
|
25
|
+
isVersionOf: `${DCTERMS_NS}isVersionOf`,
|
|
26
|
+
description: `${DCTERMS_NS}description`,
|
|
27
|
+
};
|
|
28
|
+
const XSD_DATE_TIME = `${XSD_NS}dateTime`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} DatasetVersionInput
|
|
32
|
+
* @property {string} registerId
|
|
33
|
+
* @property {string} version
|
|
34
|
+
* @property {string} versionIri
|
|
35
|
+
* @property {string} datasetIri
|
|
36
|
+
* @property {string} generatedAt — ISO date-time
|
|
37
|
+
* @property {string} [previousVersionIri]
|
|
38
|
+
* @property {string} [changeSummary]
|
|
39
|
+
* @property {string} [associatedAgentIri]
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} VersionHistoryEntry
|
|
43
|
+
* @property {string} version
|
|
44
|
+
* @property {string} generatedAt
|
|
45
|
+
* @property {string} [changeSummary]
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {Object} VersionEmitAllInput
|
|
49
|
+
* @property {string} registerId
|
|
50
|
+
* @property {string} datasetIri
|
|
51
|
+
* @property {readonly VersionHistoryEntry[]} versions
|
|
52
|
+
* @property {string} [associatedAgentIri]
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emits a single prov:Entity version with its provenance chain.
|
|
57
|
+
*/
|
|
58
|
+
export function* versionToQuads(input) {
|
|
59
|
+
const v = namedNode(input.versionIri);
|
|
60
|
+
yield quad(v, namedNode(RDF_TYPE), namedNode(PROV.Entity));
|
|
61
|
+
yield quad(v, namedNode(DCTERMS.isVersionOf), namedNode(input.datasetIri));
|
|
62
|
+
if (input.previousVersionIri) {
|
|
63
|
+
yield quad(v, namedNode(PROV.wasRevisionOf), namedNode(input.previousVersionIri));
|
|
64
|
+
}
|
|
65
|
+
yield quad(v, namedNode(PROV.generatedAtTime), literal(input.generatedAt, namedNode(XSD_DATE_TIME)));
|
|
66
|
+
if (input.changeSummary) {
|
|
67
|
+
yield quad(v, namedNode(DCTERMS.description), literal(input.changeSummary));
|
|
68
|
+
}
|
|
69
|
+
if (input.associatedAgentIri) {
|
|
70
|
+
yield quad(v, namedNode(PROV.wasAssociatedWith), namedNode(input.associatedAgentIri));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Emits the full version history chain. Versions are linked in
|
|
76
|
+
* sequence via prov:wasRevisionOf (newest → previous → ... → oldest).
|
|
77
|
+
*
|
|
78
|
+
* The version IRI is constructed as `${datasetIri}versions/${version}`.
|
|
79
|
+
*/
|
|
80
|
+
export function* versionHistoryToQuads(input) {
|
|
81
|
+
let previousIri;
|
|
82
|
+
for (const v of input.versions ?? []) {
|
|
83
|
+
const versionIri = `${input.datasetIri}versions/${v.version}`;
|
|
84
|
+
yield* versionToQuads({
|
|
85
|
+
registerId: input.registerId,
|
|
86
|
+
version: v.version,
|
|
87
|
+
versionIri,
|
|
88
|
+
datasetIri: input.datasetIri,
|
|
89
|
+
generatedAt: v.generatedAt,
|
|
90
|
+
previousVersionIri: previousIri,
|
|
91
|
+
changeSummary: v.changeSummary,
|
|
92
|
+
associatedAgentIri: input.associatedAgentIri,
|
|
93
|
+
});
|
|
94
|
+
previousIri = versionIri;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Vocabulary graph emitter — emits SKOS ConceptSchemes for the
|
|
2
|
+
// enumeration IRIs that glossarist emits in instance data
|
|
3
|
+
// (gloss:status/*, gloss:entstatus/*, gloss:norm/*, gloss:srcstatus/*,
|
|
4
|
+
// gloss:srctype/*, gloss:datetype/*, gloss:rel/*, etc.).
|
|
5
|
+
//
|
|
6
|
+
// Each enumeration value becomes a skos:Concept in its corresponding
|
|
7
|
+
// skos:ConceptScheme. The scheme uses skos:hasTopConcept to link to
|
|
8
|
+
// its members; members use skos:inScheme to link back.
|
|
9
|
+
//
|
|
10
|
+
// Accepts the schemes data as a plain object — no fs deps, browser-safe.
|
|
11
|
+
// Callers can load the JSON file themselves (Node) or construct the
|
|
12
|
+
// data inline. CURIEs in the input are resolved to absolute IRIs via
|
|
13
|
+
// PREFIXES.
|
|
14
|
+
|
|
15
|
+
import { namedNode, literal, quad } from './terms.js';
|
|
16
|
+
import { PREFIXES } from './predicates.js';
|
|
17
|
+
|
|
18
|
+
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
19
|
+
const RDF_LABEL = 'http://www.w3.org/2000/01/rdf-schema#label';
|
|
20
|
+
const SKOS_NS = PREFIXES.skos;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} VocabTerm
|
|
24
|
+
* @property {string} iri — absolute IRI or CURIE (resolved via PREFIXES)
|
|
25
|
+
* @property {string} label — human-readable label (typically English)
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} VocabScheme
|
|
29
|
+
* @property {string} schemeIri — absolute IRI or CURIE
|
|
30
|
+
* @property {string} label
|
|
31
|
+
* @property {readonly VocabTerm[]} terms
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a CURIE (e.g. "gloss:status/valid") to an absolute IRI
|
|
36
|
+
* using PREFIXES. Pass-through if already absolute.
|
|
37
|
+
*/
|
|
38
|
+
function resolveIri(iri) {
|
|
39
|
+
if (iri.startsWith('http://') || iri.startsWith('https://') || iri.startsWith('urn:')) {
|
|
40
|
+
return iri;
|
|
41
|
+
}
|
|
42
|
+
const colonIdx = iri.indexOf(':');
|
|
43
|
+
if (colonIdx < 0) return iri;
|
|
44
|
+
const prefix = iri.slice(0, colonIdx);
|
|
45
|
+
const local = iri.slice(colonIdx + 1);
|
|
46
|
+
const base = PREFIXES[prefix];
|
|
47
|
+
return base ? `${base}${local}` : iri;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emits SKOS ConceptScheme + Concept quads for one scheme.
|
|
52
|
+
*
|
|
53
|
+
* @param {VocabScheme} scheme
|
|
54
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
55
|
+
*/
|
|
56
|
+
export function* vocabularySchemeToQuads(scheme) {
|
|
57
|
+
const schemeIri = resolveIri(scheme.schemeIri);
|
|
58
|
+
const schemeSubject = namedNode(schemeIri);
|
|
59
|
+
|
|
60
|
+
yield quad(schemeSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}ConceptScheme`));
|
|
61
|
+
yield quad(schemeSubject, namedNode(RDF_LABEL), literal(scheme.label, 'en'));
|
|
62
|
+
|
|
63
|
+
for (const term of scheme.terms ?? []) {
|
|
64
|
+
const termIri = resolveIri(term.iri);
|
|
65
|
+
const termSubject = namedNode(termIri);
|
|
66
|
+
|
|
67
|
+
yield quad(termSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}Concept`));
|
|
68
|
+
yield quad(termSubject, namedNode(RDF_LABEL), literal(term.label, 'en'));
|
|
69
|
+
yield quad(termSubject, namedNode(`${SKOS_NS}inScheme`), schemeSubject);
|
|
70
|
+
yield quad(schemeSubject, namedNode(`${SKOS_NS}hasTopConcept`), termSubject);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Emits SKOS ConceptScheme + Concept quads for many schemes.
|
|
76
|
+
*
|
|
77
|
+
* @param {readonly VocabScheme[]} schemes
|
|
78
|
+
* @returns {Generator<Quad, void, unknown>}
|
|
79
|
+
*/
|
|
80
|
+
export function* vocabularyToQuads(schemes) {
|
|
81
|
+
for (const scheme of schemes ?? []) {
|
|
82
|
+
yield* vocabularySchemeToQuads(scheme);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Re-exported for callers that want to resolve CURIEs themselves.
|
|
87
|
+
export { resolveIri };
|