glossarist 0.4.39 → 0.4.40
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/designation.ts +297 -0
- package/src/models/gcr-metadata.ts +118 -0
- package/src/models/gcr-statistics.ts +76 -0
- package/src/models/hyperedge-colors.ts +87 -0
- package/src/models/register.ts +315 -0
- package/src/models/relation-categories.ts +167 -0
- package/src/models/{relation-colors.js → relation-colors.ts} +36 -23
- package/src/models/section.ts +61 -0
- package/src/models/designation.js +0 -213
- package/src/models/gcr-metadata.js +0 -68
- package/src/models/gcr-statistics.js +0 -51
- package/src/models/hyperedge-colors.js +0 -71
- package/src/models/register.js +0 -253
- package/src/models/relation-categories.js +0 -182
- package/src/models/section.js +0 -44
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { RegistrableModel } from './registrable.js';
|
|
2
|
-
import { ConceptSource } from './concept-source.js';
|
|
3
|
-
import { Pronunciation } from './pronunciation.js';
|
|
4
|
-
import { GrammarInfo } from './grammar-info.js';
|
|
5
|
-
import { RelatedConcept } from './related-concept.js';
|
|
6
|
-
import { DesignationRelationship, DESIGNATION_RELATIONSHIP_TYPES } from './designation-relationship.js';
|
|
7
|
-
|
|
8
|
-
// Maps a normative-status local-name to the matching SKOS/SKOS-XL label
|
|
9
|
-
// predicate local-name. Lives on the model so a new status requires a
|
|
10
|
-
// single edit here, not edits in every emitter.
|
|
11
|
-
const SKOS_LABEL_BY_NORMATIVE_STATUS = Object.freeze({
|
|
12
|
-
preferred: 'prefLabel',
|
|
13
|
-
deprecated: 'hiddenLabel',
|
|
14
|
-
admitted: 'altLabel',
|
|
15
|
-
deprecated_: 'altLabel',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export class Designation extends RegistrableModel {
|
|
19
|
-
constructor(data = {}) {
|
|
20
|
-
super();
|
|
21
|
-
this.designation = data.designation ?? '';
|
|
22
|
-
this.type = data.type ?? 'expression';
|
|
23
|
-
this.normativeStatus = data.normative_status ?? null;
|
|
24
|
-
this.absent = data.absent ?? null;
|
|
25
|
-
this.fieldOfApplication = data.field_of_application ?? null;
|
|
26
|
-
this.usageInfo = data.usage_info ?? null;
|
|
27
|
-
this.geographicalArea = data.geographical_area ?? null;
|
|
28
|
-
this.language = data.language ?? null;
|
|
29
|
-
this.script = data.script ?? null;
|
|
30
|
-
this.system = data.system ?? null;
|
|
31
|
-
this.international = data.international ?? null;
|
|
32
|
-
this.termType = data.term_type ?? null;
|
|
33
|
-
this.pronunciations = (data.pronunciation ?? []).map(
|
|
34
|
-
p => p instanceof Pronunciation ? p : new Pronunciation(p)
|
|
35
|
-
);
|
|
36
|
-
this.sources = (data.sources ?? []).map(
|
|
37
|
-
s => s instanceof ConceptSource ? s : new ConceptSource(s)
|
|
38
|
-
);
|
|
39
|
-
this.related = (data.related ?? []).map(
|
|
40
|
-
r => (r instanceof DesignationRelationship || r instanceof RelatedConcept)
|
|
41
|
-
? r
|
|
42
|
-
: DESIGNATION_RELATIONSHIP_TYPES.includes(r.type)
|
|
43
|
-
? DesignationRelationship.fromJSON(r)
|
|
44
|
-
: RelatedConcept.fromJSON(r)
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
toJSON() {
|
|
49
|
-
const obj = { type: this.type, designation: this.designation };
|
|
50
|
-
if (this.normativeStatus != null) obj.normative_status = this.normativeStatus;
|
|
51
|
-
if (this.absent != null) obj.absent = this.absent;
|
|
52
|
-
if (this.fieldOfApplication != null) obj.field_of_application = this.fieldOfApplication;
|
|
53
|
-
if (this.usageInfo != null) obj.usage_info = this.usageInfo;
|
|
54
|
-
if (this.geographicalArea != null) obj.geographical_area = this.geographicalArea;
|
|
55
|
-
if (this.language != null) obj.language = this.language;
|
|
56
|
-
if (this.script != null) obj.script = this.script;
|
|
57
|
-
if (this.system != null) obj.system = this.system;
|
|
58
|
-
if (this.international != null) obj.international = this.international;
|
|
59
|
-
if (this.termType != null) obj.term_type = this.termType;
|
|
60
|
-
if (this.pronunciations.length > 0) obj.pronunciation = this.pronunciations.map(p => p.toJSON());
|
|
61
|
-
if (this.sources.length > 0) obj.sources = this.sources.map(s => s.toJSON());
|
|
62
|
-
if (this.related.length > 0) obj.related = this.related.map(r => r.toJSON());
|
|
63
|
-
return obj;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// RDF class local-name for this designation subtype. Override in
|
|
67
|
-
// subclasses that map to a different ontology class.
|
|
68
|
-
rdfClass() {
|
|
69
|
-
return 'Expression';
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// SKOS label predicate URI (skos:prefLabel / skos:altLabel / skos:hiddenLabel)
|
|
73
|
-
// appropriate for this designation's normative status. Falls back to
|
|
74
|
-
// skos:altLabel for unknown statuses.
|
|
75
|
-
skosLabelPredicate(skosNs) {
|
|
76
|
-
const label = this._skosLabelLocalName();
|
|
77
|
-
return `${skosNs}${label}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// SKOS-XL label predicate URI (skosxl:prefLabel / etc.).
|
|
81
|
-
skosxlLabelPredicate(skosxlNs) {
|
|
82
|
-
const label = this._skosLabelLocalName();
|
|
83
|
-
return `${skosxlNs}${label}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
_skosLabelLocalName() {
|
|
87
|
-
const status = String(this.normativeStatus ?? '')
|
|
88
|
-
.split(/[/#]/)
|
|
89
|
-
.pop()
|
|
90
|
-
.trim();
|
|
91
|
-
return SKOS_LABEL_BY_NORMATIVE_STATUS[status] ?? 'altLabel';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Static identity works on a model instance OR its plain JSON form.
|
|
95
|
-
// The diff layer uses this so that patch-side identity (computed on
|
|
96
|
-
// plain JSON from toJSON) matches diff-side identity (computed on
|
|
97
|
-
// model instances).
|
|
98
|
-
static identityOf(value) {
|
|
99
|
-
const v = value ?? {};
|
|
100
|
-
const type = v.type ?? 'expression';
|
|
101
|
-
const text = String(v.designation ?? '').toLowerCase().trim();
|
|
102
|
-
return `${type}|${text}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
identity() {
|
|
106
|
-
return Designation.identityOf(this);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
static fromJSON(data) {
|
|
110
|
-
return Designation.fromData(data);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export class Expression extends Designation {
|
|
115
|
-
constructor(data = {}) {
|
|
116
|
-
super(data);
|
|
117
|
-
this.prefix = data.prefix ?? null;
|
|
118
|
-
this.grammarInfo = (data.grammar_info ?? []).map(
|
|
119
|
-
g => g instanceof GrammarInfo ? g : new GrammarInfo(g)
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
toJSON() {
|
|
124
|
-
const obj = super.toJSON();
|
|
125
|
-
if (this.prefix != null) obj.prefix = this.prefix;
|
|
126
|
-
if (this.grammarInfo.length > 0) obj.grammar_info = this.grammarInfo.map(g => g.toJSON());
|
|
127
|
-
return obj;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
static fromJSON(data) { return new Expression(data); }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
Designation.register('expression', Expression);
|
|
134
|
-
|
|
135
|
-
export class Abbreviation extends Expression {
|
|
136
|
-
constructor(data = {}) {
|
|
137
|
-
super(data);
|
|
138
|
-
this.acronym = data.acronym ?? false;
|
|
139
|
-
this.initialism = data.initialism ?? false;
|
|
140
|
-
this.truncation = data.truncation ?? false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
rdfClass() {
|
|
144
|
-
return 'Abbreviation';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
toJSON() {
|
|
148
|
-
const obj = super.toJSON();
|
|
149
|
-
if (this.acronym) obj.acronym = true;
|
|
150
|
-
if (this.initialism) obj.initialism = true;
|
|
151
|
-
if (this.truncation) obj.truncation = true;
|
|
152
|
-
return obj;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
static fromJSON(data) { return new Abbreviation(data); }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
Designation.register('abbreviation', Abbreviation);
|
|
159
|
-
|
|
160
|
-
export class Symbol extends Designation {
|
|
161
|
-
rdfClass() {
|
|
162
|
-
return 'Symbol';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
static fromJSON(data) { return new Symbol(data); }
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
Designation.register('symbol', Symbol);
|
|
169
|
-
|
|
170
|
-
export class LetterSymbol extends Symbol {
|
|
171
|
-
constructor(data = {}) {
|
|
172
|
-
super(data);
|
|
173
|
-
this.text = data.text ?? null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
rdfClass() {
|
|
177
|
-
return 'LetterSymbol';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
toJSON() {
|
|
181
|
-
const obj = super.toJSON();
|
|
182
|
-
if (this.text != null) obj.text = this.text;
|
|
183
|
-
return obj;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
static fromJSON(data) { return new LetterSymbol(data); }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
Designation.register('letter_symbol', LetterSymbol);
|
|
190
|
-
|
|
191
|
-
export class GraphicalSymbol extends Symbol {
|
|
192
|
-
constructor(data = {}) {
|
|
193
|
-
super(data);
|
|
194
|
-
this.text = data.text ?? null;
|
|
195
|
-
this.image = data.image ?? null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
rdfClass() {
|
|
199
|
-
return 'GraphicalSymbol';
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
toJSON() {
|
|
203
|
-
const obj = super.toJSON();
|
|
204
|
-
if (this.text != null) obj.text = this.text;
|
|
205
|
-
if (this.image != null) obj.image = this.image;
|
|
206
|
-
return obj;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
static fromJSON(data) { return new GraphicalSymbol(data); }
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
Designation.register('graphical symbol', GraphicalSymbol);
|
|
213
|
-
Designation.register('graphical_symbol', GraphicalSymbol);
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import * as yaml from 'js-yaml';
|
|
2
|
-
import { GlossaristModel } from './base.js';
|
|
3
|
-
import { GcrStatistics } from './gcr-statistics.js';
|
|
4
|
-
|
|
5
|
-
export class GcrMetadata extends GlossaristModel {
|
|
6
|
-
constructor(data = {}) {
|
|
7
|
-
super();
|
|
8
|
-
this.shortname = data.shortname ?? null;
|
|
9
|
-
this.version = data.version ?? null;
|
|
10
|
-
this.title = data.title ?? null;
|
|
11
|
-
this.description = data.description ?? null;
|
|
12
|
-
this.owner = data.owner ?? null;
|
|
13
|
-
this.tags = data.tags ?? [];
|
|
14
|
-
this.conceptCount = data.concept_count ?? data.conceptCount ?? 0;
|
|
15
|
-
this.languages = data.languages ?? [];
|
|
16
|
-
this.createdAt = data.created_at ?? data.createdAt ?? null;
|
|
17
|
-
this.glossaristVersion = data.glossarist_version ?? data.glossaristVersion ?? null;
|
|
18
|
-
this.schemaVersion = data.schema_version ?? data.schemaVersion ?? '1';
|
|
19
|
-
this.homepage = data.homepage ?? null;
|
|
20
|
-
this.repository = data.repository ?? null;
|
|
21
|
-
this.license = data.license ?? null;
|
|
22
|
-
this.uriPrefix = data.uri_prefix ?? data.uriPrefix ?? null;
|
|
23
|
-
this.conceptUriTemplate = data.concept_uri_template ?? data.conceptUriTemplate ?? null;
|
|
24
|
-
this.compiledFormats = data.compiled_formats ?? data.compiledFormats ?? [];
|
|
25
|
-
this.statistics = data.statistics
|
|
26
|
-
? (data.statistics instanceof GcrStatistics ? data.statistics : new GcrStatistics(data.statistics))
|
|
27
|
-
: null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get concept_count() { return this.conceptCount; }
|
|
31
|
-
get created_at() { return this.createdAt; }
|
|
32
|
-
get glossarist_version() { return this.glossaristVersion; }
|
|
33
|
-
get schema_version() { return this.schemaVersion; }
|
|
34
|
-
get uri_prefix() { return this.uriPrefix; }
|
|
35
|
-
get concept_uri_template() { return this.conceptUriTemplate; }
|
|
36
|
-
get compiled_formats() { return this.compiledFormats; }
|
|
37
|
-
|
|
38
|
-
toJSON() {
|
|
39
|
-
const obj = {};
|
|
40
|
-
if (this.shortname != null) obj.shortname = this.shortname;
|
|
41
|
-
if (this.version != null) obj.version = this.version;
|
|
42
|
-
if (this.title != null) obj.title = this.title;
|
|
43
|
-
if (this.description != null) obj.description = this.description;
|
|
44
|
-
if (this.owner != null) obj.owner = this.owner;
|
|
45
|
-
if (this.tags.length > 0) obj.tags = this.tags;
|
|
46
|
-
if (this.conceptCount > 0) obj.concept_count = this.conceptCount;
|
|
47
|
-
if (this.languages.length > 0) obj.languages = this.languages;
|
|
48
|
-
if (this.createdAt != null) obj.created_at = this.createdAt;
|
|
49
|
-
if (this.glossaristVersion != null) obj.glossarist_version = this.glossaristVersion;
|
|
50
|
-
if (this.schemaVersion != null) obj.schema_version = this.schemaVersion;
|
|
51
|
-
if (this.homepage != null) obj.homepage = this.homepage;
|
|
52
|
-
if (this.repository != null) obj.repository = this.repository;
|
|
53
|
-
if (this.license != null) obj.license = this.license;
|
|
54
|
-
if (this.uriPrefix != null) obj.uri_prefix = this.uriPrefix;
|
|
55
|
-
if (this.conceptUriTemplate != null) obj.concept_uri_template = this.conceptUriTemplate;
|
|
56
|
-
if (this.compiledFormats.length > 0) obj.compiled_formats = this.compiledFormats;
|
|
57
|
-
if (this.statistics != null) obj.statistics = this.statistics.toJSON();
|
|
58
|
-
return obj;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
static fromJSON(data) {
|
|
62
|
-
return new GcrMetadata(data);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
static fromYaml(yamlString) {
|
|
66
|
-
return new GcrMetadata(yaml.load(yamlString));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
|
|
3
|
-
export class GcrStatistics extends GlossaristModel {
|
|
4
|
-
constructor(data = {}) {
|
|
5
|
-
super();
|
|
6
|
-
this.totalConcepts = data.total_concepts ?? data.totalConcepts ?? 0;
|
|
7
|
-
this.conceptsWithDefinitions = data.concepts_with_definitions ?? data.conceptsWithDefinitions ?? 0;
|
|
8
|
-
this.conceptsByStatus = data.concepts_by_status ?? data.conceptsByStatus ?? {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get total_concepts() { return this.totalConcepts; }
|
|
12
|
-
get concepts_with_definitions() { return this.conceptsWithDefinitions; }
|
|
13
|
-
get concepts_by_status() { return this.conceptsByStatus; }
|
|
14
|
-
|
|
15
|
-
toJSON() {
|
|
16
|
-
const obj = { total_concepts: this.totalConcepts };
|
|
17
|
-
if (this.conceptsWithDefinitions > 0) {
|
|
18
|
-
obj.concepts_with_definitions = this.conceptsWithDefinitions;
|
|
19
|
-
}
|
|
20
|
-
if (Object.keys(this.conceptsByStatus).length > 0) {
|
|
21
|
-
obj.concepts_by_status = this.conceptsByStatus;
|
|
22
|
-
}
|
|
23
|
-
return obj;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
static fromJSON(data) {
|
|
27
|
-
return new GcrStatistics(data);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
static fromConcepts(concepts) {
|
|
31
|
-
const langs = new Set();
|
|
32
|
-
let withDefs = 0;
|
|
33
|
-
const byStatus = {};
|
|
34
|
-
|
|
35
|
-
for (const concept of concepts) {
|
|
36
|
-
for (const lang of concept.languages) {
|
|
37
|
-
langs.add(lang);
|
|
38
|
-
const lc = concept.localization(lang);
|
|
39
|
-
if (lc && lc.definitions.length > 0) withDefs++;
|
|
40
|
-
const status = lc?.entryStatus ?? 'unknown';
|
|
41
|
-
byStatus[status] = (byStatus[status] ?? 0) + 1;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return new GcrStatistics({
|
|
46
|
-
total_concepts: concepts.length,
|
|
47
|
-
concepts_with_definitions: withDefs,
|
|
48
|
-
concepts_by_status: byStatus,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Hyperedge color resolver — resolves display colors for a
|
|
2
|
-
// PartitiveHyperedge based on its enumeration and the carrying
|
|
3
|
-
// concept's category color.
|
|
4
|
-
//
|
|
5
|
-
// Closed hyperedges render at full intensity (the relationship is
|
|
6
|
-
// definitive — these are ALL the parts). Open hyperedges render at
|
|
7
|
-
// reduced intensity (the relationship is partial — other parts may
|
|
8
|
-
// exist).
|
|
9
|
-
//
|
|
10
|
-
// Marker-based styling (double = thicker stroke, dashed = dashed
|
|
11
|
-
// pattern) is the caller's responsibility; this resolver returns
|
|
12
|
-
// the color only, plus a `pattern` hint for marker-based styling.
|
|
13
|
-
|
|
14
|
-
import { categoryColorPair } from './relation-colors.js';
|
|
15
|
-
|
|
16
|
-
const ENUMERATION_INTENSITY = Object.freeze({
|
|
17
|
-
closed: 1.0,
|
|
18
|
-
open: 0.55,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const DEFAULT_CATEGORY = 'hierarchical';
|
|
22
|
-
|
|
23
|
-
export function resolveHyperedgeColor(hyperedge, options = {}) {
|
|
24
|
-
if (!hyperedge) return null;
|
|
25
|
-
|
|
26
|
-
const category = options.category ?? DEFAULT_CATEGORY;
|
|
27
|
-
const base = categoryColorPair(category, options.overrides);
|
|
28
|
-
if (!base) return null;
|
|
29
|
-
|
|
30
|
-
const intensity = ENUMERATION_INTENSITY[hyperedge.enumeration] ?? 1.0;
|
|
31
|
-
const pattern = derivePattern(hyperedge.markers);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
light: tint(base.light, intensity),
|
|
35
|
-
dark: tint(base.dark, intensity),
|
|
36
|
-
pattern,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function derivePattern(markers) {
|
|
41
|
-
const arr = markers ?? [];
|
|
42
|
-
if (arr.includes('dashed')) return 'dashed';
|
|
43
|
-
if (arr.includes('double')) return 'double';
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Linear interpolation between the base color and white (light mode)
|
|
48
|
-
// or between the base and a neutral (dark mode). Pure-string inputs
|
|
49
|
-
// return the original color when intensity === 1.0.
|
|
50
|
-
function tint(hex, ratio) {
|
|
51
|
-
if (ratio >= 1.0) return hex;
|
|
52
|
-
const rgb = parseHex(hex);
|
|
53
|
-
if (!rgb) return hex;
|
|
54
|
-
|
|
55
|
-
const target = [255, 255, 255];
|
|
56
|
-
const mixed = rgb.map((c, i) =>
|
|
57
|
-
Math.round(c + (target[i] - c) * (1 - ratio)));
|
|
58
|
-
return '#' + mixed.map(c => c.toString(16).padStart(2, '0')).join('');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseHex(s) {
|
|
62
|
-
if (typeof s !== 'string') return null;
|
|
63
|
-
const m = s.match(/^#?([0-9a-f]{6})$/i);
|
|
64
|
-
if (!m) return null;
|
|
65
|
-
const n = m[1];
|
|
66
|
-
return [
|
|
67
|
-
parseInt(n.slice(0, 2), 16),
|
|
68
|
-
parseInt(n.slice(2, 4), 16),
|
|
69
|
-
parseInt(n.slice(4, 6), 16),
|
|
70
|
-
];
|
|
71
|
-
}
|
package/src/models/register.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { GlossaristModel } from './base.js';
|
|
2
|
-
import { Section } from './section.js';
|
|
3
|
-
import { resolveColor } from './dataset-color.js';
|
|
4
|
-
|
|
5
|
-
export const REGISTER_STATUSES = Object.freeze([
|
|
6
|
-
'current',
|
|
7
|
-
'superseded',
|
|
8
|
-
'withdrawn',
|
|
9
|
-
'draft',
|
|
10
|
-
]);
|
|
11
|
-
|
|
12
|
-
export class Register extends GlossaristModel {
|
|
13
|
-
constructor(data = {}) {
|
|
14
|
-
super();
|
|
15
|
-
this.schemaVersion = data.schema_version ?? data.schemaVersion ?? '3';
|
|
16
|
-
this.id = data.id ?? null;
|
|
17
|
-
this.name = data.name ?? null;
|
|
18
|
-
this.ref = data.ref ?? null;
|
|
19
|
-
this.refAliases = data.refAliases ?? data.ref_aliases ?? [];
|
|
20
|
-
this.year = data.year ?? null;
|
|
21
|
-
this.urn = data.urn ?? null;
|
|
22
|
-
this.urnAliases = data.urnAliases ?? data.urn_aliases ?? [];
|
|
23
|
-
this.status = data.status ?? null;
|
|
24
|
-
this.supersedes = data.supersedes ?? null;
|
|
25
|
-
this.owner = data.owner ?? null;
|
|
26
|
-
this.sourceRepo = data.sourceRepo ?? data.source_repo ?? null;
|
|
27
|
-
this.tags = data.tags ?? [];
|
|
28
|
-
this.languages = data.languages ?? [];
|
|
29
|
-
this.languageOrder = data.languageOrder ?? data.language_order ?? [];
|
|
30
|
-
this.ordering = data.ordering ?? null;
|
|
31
|
-
this.logo = data.logo ?? null;
|
|
32
|
-
this.color = data.color ?? null;
|
|
33
|
-
this.description = data.description ?? {};
|
|
34
|
-
this.about = data.about ?? {};
|
|
35
|
-
this.provenance = data.provenance ?? [];
|
|
36
|
-
this.contributors = data.contributors ?? [];
|
|
37
|
-
this.sections = (data.sections ?? []).map(s =>
|
|
38
|
-
s instanceof Section ? s : new Section(s)
|
|
39
|
-
);
|
|
40
|
-
this._raw = this._extractRaw(data);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
_extractRaw(data) {
|
|
44
|
-
const known = new Set([
|
|
45
|
-
'schema_version', 'schemaVersion',
|
|
46
|
-
'id', 'name', 'ref', 'refAliases', 'ref_aliases',
|
|
47
|
-
'year', 'urn', 'urnAliases', 'urn_aliases',
|
|
48
|
-
'status', 'supersedes', 'owner',
|
|
49
|
-
'sourceRepo', 'source_repo', 'tags',
|
|
50
|
-
'languages', 'languageOrder', 'language_order',
|
|
51
|
-
'ordering', 'logo', 'color', 'description', 'about',
|
|
52
|
-
'provenance', 'contributors', 'sections',
|
|
53
|
-
]);
|
|
54
|
-
const extra = {};
|
|
55
|
-
for (const [k, v] of Object.entries(data)) {
|
|
56
|
-
if (!known.has(k)) extra[k] = v;
|
|
57
|
-
}
|
|
58
|
-
return extra;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
sectionById(id) {
|
|
62
|
-
for (const section of this.sections) {
|
|
63
|
-
if (section.id === id) return section;
|
|
64
|
-
const found = section.descendantById(id);
|
|
65
|
-
if (found) return found;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Walks the section tree upward from `sectionId`, returning the
|
|
71
|
-
// ancestor chain in immediate-parent-first order. The returned array
|
|
72
|
-
// does NOT include sectionId itself. Returns [] when sectionId is
|
|
73
|
-
// unknown or is a top-level section.
|
|
74
|
-
//
|
|
75
|
-
// Mirrors glossarist-ruby's section-cascading walk (commit 43dca6b)
|
|
76
|
-
// and the ontology's owl:TransitiveProperty declaration on
|
|
77
|
-
// gloss:hasParentSection.
|
|
78
|
-
sectionAncestorIds(sectionId) {
|
|
79
|
-
if (!sectionId) return [];
|
|
80
|
-
for (const root of this.sections) {
|
|
81
|
-
const chain = _ancestorChain(root, sectionId);
|
|
82
|
-
if (chain) return chain;
|
|
83
|
-
}
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Returns the closure of `sectionId`: the section plus all of its
|
|
88
|
-
// ancestors. Concept-section membership tests should intersect the
|
|
89
|
-
// concept's section list with this closure so a concept in section
|
|
90
|
-
// "3.1.1" matches a filter on "3.1" or "3".
|
|
91
|
-
sectionClosure(sectionId) {
|
|
92
|
-
return [sectionId, ...this.sectionAncestorIds(sectionId)];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Walks the section tree DOWNWARD from `sectionId`, returning all
|
|
96
|
-
// descendant section IDs in pre-order (immediate children first,
|
|
97
|
-
// then their children). The returned array does NOT include
|
|
98
|
-
// sectionId itself. Returns [] when sectionId is unknown or is a
|
|
99
|
-
// leaf section. Useful for filtering concepts by parent section
|
|
100
|
-
// (include descendants) — the inverse direction of sectionClosure.
|
|
101
|
-
sectionDescendantIds(sectionId) {
|
|
102
|
-
if (!sectionId) return [];
|
|
103
|
-
const section = this.sectionById(sectionId);
|
|
104
|
-
if (!section) return [];
|
|
105
|
-
const out = [];
|
|
106
|
-
for (const child of section.children ?? []) {
|
|
107
|
-
out.push(child.id);
|
|
108
|
-
out.push(...this.sectionDescendantIds(child.id));
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Returns the descendant closure of `sectionId`: the section plus
|
|
114
|
-
// all of its descendants. Useful for "show me everything in section
|
|
115
|
-
// X or below" queries.
|
|
116
|
-
sectionDescendantClosure(sectionId) {
|
|
117
|
-
return [sectionId, ...this.sectionDescendantIds(sectionId)];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Returns all section IDs the concept belongs to: the concept's own
|
|
121
|
-
// sections plus every ancestor of each. Concept-side section
|
|
122
|
-
// membership comes from `concept.sections` if set, otherwise from
|
|
123
|
-
// `concept.groups` (a flat list of section IDs as strings).
|
|
124
|
-
//
|
|
125
|
-
// Returns the union (deduped) so callers can do a single
|
|
126
|
-
// intersection test.
|
|
127
|
-
conceptSectionIds(concept) {
|
|
128
|
-
const own = conceptSectionIdList(concept);
|
|
129
|
-
if (own.length === 0) return [];
|
|
130
|
-
const closure = new Set();
|
|
131
|
-
for (const id of own) {
|
|
132
|
-
for (const ancestor of this.sectionClosure(id)) closure.add(ancestor);
|
|
133
|
-
}
|
|
134
|
-
return [...closure];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
sectionName(sectionId, lang) {
|
|
138
|
-
const section = this.sectionById(sectionId);
|
|
139
|
-
return section ? section.name(lang) : null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Localized display name. Returns the best-matching name string for
|
|
143
|
-
// the requested language, falling back to English, then to the first
|
|
144
|
-
// available language, then to null. Accepts three input shapes:
|
|
145
|
-
// - null/undefined → null
|
|
146
|
-
// - string → returned as-is for every language
|
|
147
|
-
// - { eng, fra, ... } → language-resolved
|
|
148
|
-
displayName(lang) {
|
|
149
|
-
if (this.name == null) return null;
|
|
150
|
-
if (typeof this.name === 'string') return this.name;
|
|
151
|
-
if (typeof this.name === 'object') {
|
|
152
|
-
return this.name[lang]
|
|
153
|
-
?? this.name.eng
|
|
154
|
-
?? Object.values(this.name)[0]
|
|
155
|
-
?? null;
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Returns the hex color for the requested UI mode, falling back to
|
|
161
|
-
// the single hex when the spec is a string. Returns null when no
|
|
162
|
-
// color is set or when the requested mode is missing.
|
|
163
|
-
resolvedColor(mode) {
|
|
164
|
-
return resolveColor(this.color, mode);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
toJSON() {
|
|
168
|
-
const obj = { ...this._raw, schema_version: this.schemaVersion };
|
|
169
|
-
if (this.id != null) obj.id = this.id;
|
|
170
|
-
if (this.name != null) obj.name = this.name;
|
|
171
|
-
if (this.ref != null) obj.ref = this.ref;
|
|
172
|
-
if (this.refAliases.length > 0) obj.refAliases = [...this.refAliases];
|
|
173
|
-
if (this.year != null) obj.year = this.year;
|
|
174
|
-
if (this.urn != null) obj.urn = this.urn;
|
|
175
|
-
if (this.urnAliases.length > 0) obj.urnAliases = [...this.urnAliases];
|
|
176
|
-
if (this.status != null) obj.status = this.status;
|
|
177
|
-
if (this.supersedes != null) obj.supersedes = this.supersedes;
|
|
178
|
-
if (this.owner != null) obj.owner = this.owner;
|
|
179
|
-
if (this.sourceRepo != null) obj.sourceRepo = this.sourceRepo;
|
|
180
|
-
if (this.tags.length > 0) obj.tags = [...this.tags];
|
|
181
|
-
if (this.languages.length > 0) obj.languages = [...this.languages];
|
|
182
|
-
if (this.languageOrder.length > 0) obj.languageOrder = [...this.languageOrder];
|
|
183
|
-
if (this.ordering != null) obj.ordering = this.ordering;
|
|
184
|
-
if (this.logo != null) obj.logo = { ...this.logo };
|
|
185
|
-
if (this.color != null) {
|
|
186
|
-
obj.color = (typeof this.color === 'object' && this.color !== null)
|
|
187
|
-
? { ...this.color }
|
|
188
|
-
: this.color;
|
|
189
|
-
}
|
|
190
|
-
if (Object.keys(this.description).length > 0) obj.description = { ...this.description };
|
|
191
|
-
if (Object.keys(this.about).length > 0) obj.about = { ...this.about };
|
|
192
|
-
if (this.provenance.length > 0) obj.provenance = this.provenance.map(p => ({ ...p }));
|
|
193
|
-
if (this.contributors.length > 0) obj.contributors = this.contributors.map(c => ({ ...c }));
|
|
194
|
-
if (this.sections.length > 0) obj.sections = this.sections.map(s => s.toJSON());
|
|
195
|
-
return obj;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
static fromJSON(data) {
|
|
199
|
-
const instance = new Register(data);
|
|
200
|
-
const snakeToCamel = k => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
201
|
-
return new Proxy(instance, {
|
|
202
|
-
get(target, prop) {
|
|
203
|
-
if (prop in target) return target[prop];
|
|
204
|
-
if (prop in target._raw) return target._raw[prop];
|
|
205
|
-
const camel = snakeToCamel(prop);
|
|
206
|
-
if (camel in target) return target[camel];
|
|
207
|
-
return undefined;
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Recursive walk: returns the ancestor chain of `targetId` within the
|
|
214
|
-
// subtree rooted at `section`, or null if targetId is not in this
|
|
215
|
-
// subtree. The chain is immediate-parent-first; the section itself is
|
|
216
|
-
// not included. Built root-first, then reversed for the documented order.
|
|
217
|
-
function _ancestorChain(section, targetId, ancestors = []) {
|
|
218
|
-
if (section.id === targetId) return [...ancestors].reverse();
|
|
219
|
-
for (const child of section.children) {
|
|
220
|
-
const found = _ancestorChain(child, targetId, [...ancestors, section.id]);
|
|
221
|
-
if (found) return found;
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Returns the list of section IDs a concept claims membership in.
|
|
227
|
-
// Concepts may carry section IDs via either `sections` (preferred) or
|
|
228
|
-
// `groups` (legacy). Each entry may be a string or an object with an
|
|
229
|
-
// `id` field; both forms are flattened to a string list.
|
|
230
|
-
function conceptSectionIdList(concept) {
|
|
231
|
-
if (!concept) return [];
|
|
232
|
-
const fromSections = concept.sections
|
|
233
|
-
? _flattenSectionIds(concept.sections)
|
|
234
|
-
: [];
|
|
235
|
-
const fromGroups = concept.groups
|
|
236
|
-
? _flattenSectionIds(concept.groups)
|
|
237
|
-
: [];
|
|
238
|
-
return [...fromSections, ...fromGroups];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function _flattenSectionIds(value) {
|
|
242
|
-
if (!Array.isArray(value)) return [];
|
|
243
|
-
const out = [];
|
|
244
|
-
for (const entry of value) {
|
|
245
|
-
if (typeof entry === 'string') {
|
|
246
|
-
out.push(entry);
|
|
247
|
-
} else if (entry && typeof entry === 'object') {
|
|
248
|
-
const id = entry.id ?? entry.sectionId ?? entry.ref?.id;
|
|
249
|
-
if (id) out.push(String(id));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return out;
|
|
253
|
-
}
|