glossarist 0.4.39 → 0.4.41
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/concept.ts +499 -0
- 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/localized-concept.ts +284 -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/{relation-loader.js → relation-loader.ts} +50 -46
- package/src/models/section.ts +61 -0
- package/src/models/concept.js +0 -420
- 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/localized-concept.js +0 -187
- package/src/models/register.js +0 -253
- package/src/models/relation-categories.js +0 -182
- package/src/models/section.js +0 -44
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import { Designation } from './designation.js';
|
|
3
|
+
import { DetailedDefinition } from './detailed-definition.js';
|
|
4
|
+
import { ConceptSource } from './concept-source.js';
|
|
5
|
+
import { ConceptDate } from './concept-date.js';
|
|
6
|
+
import { NonVerbRep } from './non-verb-rep.js';
|
|
7
|
+
import { RelatedConcept } from './related-concept.js';
|
|
8
|
+
|
|
9
|
+
function wrapAs<T>(
|
|
10
|
+
Cls: new (...args: never[]) => T,
|
|
11
|
+
): (item: unknown) => T {
|
|
12
|
+
return (item: unknown): T =>
|
|
13
|
+
item instanceof Cls ? item : new Cls(item as never);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LOC_WIRE_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
17
|
+
entryStatus: 'entry_status',
|
|
18
|
+
reviewType: 'review_type',
|
|
19
|
+
lineageSourceSimilarity: 'lineage_source_similarity',
|
|
20
|
+
reviewDate: 'review_date',
|
|
21
|
+
reviewDecisionDate: 'review_decision_date',
|
|
22
|
+
reviewDecisionEvent: 'review_decision_event',
|
|
23
|
+
reviewStatus: 'review_status',
|
|
24
|
+
reviewDecision: 'review_decision',
|
|
25
|
+
reviewDecisionNotes: 'review_decision_notes',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export interface LocalizedConceptJson {
|
|
29
|
+
language_code?: string | null;
|
|
30
|
+
languageCode?: string | null;
|
|
31
|
+
script?: string | null;
|
|
32
|
+
system?: string | null;
|
|
33
|
+
entry_status?: string | null;
|
|
34
|
+
entryStatus?: string | null;
|
|
35
|
+
classification?: string | null;
|
|
36
|
+
review_type?: string | null;
|
|
37
|
+
reviewType?: string | null;
|
|
38
|
+
domain?: string | null;
|
|
39
|
+
release?: string | null;
|
|
40
|
+
lineage_source_similarity?: string | null;
|
|
41
|
+
lineageSourceSimilarity?: string | null;
|
|
42
|
+
review_date?: string | null;
|
|
43
|
+
reviewDate?: string | null;
|
|
44
|
+
review_decision_date?: string | null;
|
|
45
|
+
reviewDecisionDate?: string | null;
|
|
46
|
+
review_decision_event?: string | null;
|
|
47
|
+
reviewDecisionEvent?: string | null;
|
|
48
|
+
review_status?: string | null;
|
|
49
|
+
reviewStatus?: string | null;
|
|
50
|
+
review_decision?: string | null;
|
|
51
|
+
reviewDecision?: string | null;
|
|
52
|
+
review_decision_notes?: string | null;
|
|
53
|
+
reviewDecisionNotes?: string | null;
|
|
54
|
+
terms?: ReadonlyArray<unknown>;
|
|
55
|
+
definition?: ReadonlyArray<unknown>;
|
|
56
|
+
sources?: ReadonlyArray<unknown>;
|
|
57
|
+
notes?: ReadonlyArray<unknown>;
|
|
58
|
+
annotations?: ReadonlyArray<unknown>;
|
|
59
|
+
examples?: ReadonlyArray<unknown>;
|
|
60
|
+
dates?: ReadonlyArray<unknown>;
|
|
61
|
+
non_verbal_rep?: ReadonlyArray<unknown>;
|
|
62
|
+
non_verb?: ReadonlyArray<unknown>;
|
|
63
|
+
related?: ReadonlyArray<unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type SlotName = 'definitions' | 'notes' | 'examples' | 'annotations';
|
|
67
|
+
|
|
68
|
+
export class LocalizedConcept extends GlossaristModel {
|
|
69
|
+
static get DIFF_FIELDS(): ReadonlyArray<string> {
|
|
70
|
+
return Object.freeze([
|
|
71
|
+
'entryStatus', 'classification', 'reviewType', 'domain', 'release',
|
|
72
|
+
'lineageSourceSimilarity', 'script', 'system',
|
|
73
|
+
'reviewDate', 'reviewDecisionDate', 'reviewDecisionEvent',
|
|
74
|
+
'reviewStatus', 'reviewDecision', 'reviewDecisionNotes',
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static wireNameFor(field: string): string {
|
|
79
|
+
return LOC_WIRE_NAMES[field] ?? field;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
readonly languageCode: string | null;
|
|
83
|
+
readonly script: string | null;
|
|
84
|
+
readonly system: string | null;
|
|
85
|
+
readonly entryStatus: string | null;
|
|
86
|
+
readonly classification: string | null;
|
|
87
|
+
readonly reviewType: string | null;
|
|
88
|
+
readonly domain: string | null;
|
|
89
|
+
readonly release: string | null;
|
|
90
|
+
readonly lineageSourceSimilarity: string | null;
|
|
91
|
+
readonly reviewDate: string | null;
|
|
92
|
+
readonly reviewDecisionDate: string | null;
|
|
93
|
+
readonly reviewDecisionEvent: string | null;
|
|
94
|
+
readonly reviewStatus: string | null;
|
|
95
|
+
readonly reviewDecision: string | null;
|
|
96
|
+
readonly reviewDecisionNotes: string | null;
|
|
97
|
+
|
|
98
|
+
private readonly _rawTerms: ReadonlyArray<unknown>;
|
|
99
|
+
private readonly _rawDefinition: ReadonlyArray<unknown>;
|
|
100
|
+
private readonly _rawSources: ReadonlyArray<unknown>;
|
|
101
|
+
private readonly _rawNotes: ReadonlyArray<unknown>;
|
|
102
|
+
private readonly _rawAnnotations: ReadonlyArray<unknown>;
|
|
103
|
+
private readonly _rawExamples: ReadonlyArray<unknown>;
|
|
104
|
+
private readonly _rawDates: ReadonlyArray<unknown>;
|
|
105
|
+
private readonly _rawNonVerbal: ReadonlyArray<unknown>;
|
|
106
|
+
private readonly _rawRelated: ReadonlyArray<unknown>;
|
|
107
|
+
private _terms: ReadonlyArray<Designation> | null = null;
|
|
108
|
+
private _definitions: ReadonlyArray<DetailedDefinition> | null = null;
|
|
109
|
+
private _sources: ReadonlyArray<ConceptSource> | null = null;
|
|
110
|
+
private _notes: ReadonlyArray<DetailedDefinition> | null = null;
|
|
111
|
+
private _annotations: ReadonlyArray<DetailedDefinition> | null = null;
|
|
112
|
+
private _examples: ReadonlyArray<DetailedDefinition> | null = null;
|
|
113
|
+
private _dates: ReadonlyArray<ConceptDate> | null = null;
|
|
114
|
+
private _nonVerbal: ReadonlyArray<NonVerbRep> | null = null;
|
|
115
|
+
private _related: ReadonlyArray<RelatedConcept> | null = null;
|
|
116
|
+
|
|
117
|
+
constructor(data: LocalizedConceptJson = {}) {
|
|
118
|
+
super();
|
|
119
|
+
this.languageCode = data.language_code ?? data.languageCode ?? null;
|
|
120
|
+
this.script = data.script ?? null;
|
|
121
|
+
this.system = data.system ?? null;
|
|
122
|
+
this.entryStatus = data.entry_status ?? data.entryStatus ?? null;
|
|
123
|
+
this.classification = data.classification ?? null;
|
|
124
|
+
this.reviewType = data.review_type ?? data.reviewType ?? null;
|
|
125
|
+
this.domain = data.domain ?? null;
|
|
126
|
+
this.release = data.release ?? null;
|
|
127
|
+
this.lineageSourceSimilarity =
|
|
128
|
+
data.lineage_source_similarity ?? data.lineageSourceSimilarity ?? null;
|
|
129
|
+
this.reviewDate = data.review_date ?? data.reviewDate ?? null;
|
|
130
|
+
this.reviewDecisionDate = data.review_decision_date ?? data.reviewDecisionDate ?? null;
|
|
131
|
+
this.reviewDecisionEvent = data.review_decision_event ?? data.reviewDecisionEvent ?? null;
|
|
132
|
+
this.reviewStatus = data.review_status ?? data.reviewStatus ?? null;
|
|
133
|
+
this.reviewDecision = data.review_decision ?? data.reviewDecision ?? null;
|
|
134
|
+
this.reviewDecisionNotes = data.review_decision_notes ?? data.reviewDecisionNotes ?? null;
|
|
135
|
+
|
|
136
|
+
this._rawTerms = data.terms ?? [];
|
|
137
|
+
this._rawDefinition = data.definition ?? [];
|
|
138
|
+
this._rawSources = data.sources ?? [];
|
|
139
|
+
this._rawNotes = data.notes ?? [];
|
|
140
|
+
this._rawAnnotations = data.annotations ?? [];
|
|
141
|
+
this._rawExamples = data.examples ?? [];
|
|
142
|
+
this._rawDates = data.dates ?? [];
|
|
143
|
+
this._rawNonVerbal = data.non_verbal_rep ?? data.non_verb ?? [];
|
|
144
|
+
this._rawRelated = data.related ?? [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get terms(): ReadonlyArray<Designation> {
|
|
148
|
+
return this._lazy<Designation>(
|
|
149
|
+
'_terms',
|
|
150
|
+
'_rawTerms',
|
|
151
|
+
(t) => Designation.fromData(t),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get definitions(): ReadonlyArray<DetailedDefinition> {
|
|
156
|
+
return this._lazy<DetailedDefinition>(
|
|
157
|
+
'_definitions',
|
|
158
|
+
'_rawDefinition',
|
|
159
|
+
wrapAs(DetailedDefinition),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
get definition(): ReadonlyArray<DetailedDefinition> {
|
|
164
|
+
return this.definitions;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get sources(): ReadonlyArray<ConceptSource> {
|
|
168
|
+
return this._lazy<ConceptSource>(
|
|
169
|
+
'_sources',
|
|
170
|
+
'_rawSources',
|
|
171
|
+
wrapAs(ConceptSource),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get notes(): ReadonlyArray<DetailedDefinition> {
|
|
176
|
+
return this._lazy<DetailedDefinition>(
|
|
177
|
+
'_notes',
|
|
178
|
+
'_rawNotes',
|
|
179
|
+
wrapAs(DetailedDefinition),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get annotations(): ReadonlyArray<DetailedDefinition> {
|
|
184
|
+
return this._lazy<DetailedDefinition>(
|
|
185
|
+
'_annotations',
|
|
186
|
+
'_rawAnnotations',
|
|
187
|
+
wrapAs(DetailedDefinition),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get examples(): ReadonlyArray<DetailedDefinition> {
|
|
192
|
+
return this._lazy<DetailedDefinition>(
|
|
193
|
+
'_examples',
|
|
194
|
+
'_rawExamples',
|
|
195
|
+
wrapAs(DetailedDefinition),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get dates(): ReadonlyArray<ConceptDate> {
|
|
200
|
+
return this._lazy<ConceptDate>(
|
|
201
|
+
'_dates',
|
|
202
|
+
'_rawDates',
|
|
203
|
+
wrapAs(ConceptDate),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
get nonVerbalRep(): ReadonlyArray<NonVerbRep> {
|
|
208
|
+
return this._lazy<NonVerbRep>(
|
|
209
|
+
'_nonVerbal',
|
|
210
|
+
'_rawNonVerbal',
|
|
211
|
+
wrapAs(NonVerbRep),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get related(): ReadonlyArray<RelatedConcept> {
|
|
216
|
+
return this._lazy<RelatedConcept>(
|
|
217
|
+
'_related',
|
|
218
|
+
'_rawRelated',
|
|
219
|
+
wrapAs(RelatedConcept),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get primaryDesignation(): string | null {
|
|
224
|
+
return this.terms[0]?.designation ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get primaryDefinition(): string | null {
|
|
228
|
+
return this.definitions[0]?.content ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private *_textSlots(): Generator<[SlotName, ReadonlyArray<DetailedDefinition>]> {
|
|
232
|
+
yield ['definitions', this.definitions];
|
|
233
|
+
yield ['notes', this.notes];
|
|
234
|
+
yield ['examples', this.examples];
|
|
235
|
+
yield ['annotations', this.annotations];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
*walkTexts(basePath: string): Generator<{ text: string; source: string }> {
|
|
239
|
+
for (const [name, arr] of this._textSlots()) {
|
|
240
|
+
for (let i = 0; i < arr.length; i++) {
|
|
241
|
+
const item = arr[i];
|
|
242
|
+
if (!item) continue;
|
|
243
|
+
yield* item.walkTexts(`${basePath}.${name}[${i}]`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override toJSON(): LocalizedConceptJson {
|
|
249
|
+
const obj: LocalizedConceptJson = {};
|
|
250
|
+
if (this.languageCode) obj.language_code = this.languageCode;
|
|
251
|
+
if (this.script) obj.script = this.script;
|
|
252
|
+
if (this.system) obj.system = this.system;
|
|
253
|
+
if (this.entryStatus) obj.entry_status = this.entryStatus;
|
|
254
|
+
if (this.classification) obj.classification = this.classification;
|
|
255
|
+
if (this.reviewType) obj.review_type = this.reviewType;
|
|
256
|
+
if (this.domain) obj.domain = this.domain;
|
|
257
|
+
if (this.release) obj.release = this.release;
|
|
258
|
+
if (this.lineageSourceSimilarity != null) {
|
|
259
|
+
obj.lineage_source_similarity = this.lineageSourceSimilarity;
|
|
260
|
+
}
|
|
261
|
+
if (this.reviewDate) obj.review_date = this.reviewDate;
|
|
262
|
+
if (this.reviewDecisionDate) obj.review_decision_date = this.reviewDecisionDate;
|
|
263
|
+
if (this.reviewDecisionEvent) obj.review_decision_event = this.reviewDecisionEvent;
|
|
264
|
+
if (this.reviewStatus) obj.review_status = this.reviewStatus;
|
|
265
|
+
if (this.reviewDecision) obj.review_decision = this.reviewDecision;
|
|
266
|
+
if (this.reviewDecisionNotes) obj.review_decision_notes = this.reviewDecisionNotes;
|
|
267
|
+
|
|
268
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'terms', '_terms', '_rawTerms');
|
|
269
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'definition', '_definitions', '_rawDefinition');
|
|
270
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'notes', '_notes', '_rawNotes');
|
|
271
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'annotations', '_annotations', '_rawAnnotations');
|
|
272
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'examples', '_examples', '_rawExamples');
|
|
273
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'sources', '_sources', '_rawSources');
|
|
274
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'dates', '_dates', '_rawDates');
|
|
275
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'non_verbal_rep', '_nonVerbal', '_rawNonVerbal');
|
|
276
|
+
this._serialize(obj as unknown as Record<string, unknown>, 'related', '_related', '_rawRelated');
|
|
277
|
+
|
|
278
|
+
return obj;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static override fromJSON(data: LocalizedConceptJson): LocalizedConcept {
|
|
282
|
+
return new LocalizedConcept(data);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import { Section } from './section.js';
|
|
3
|
+
import type { SectionJson } from './section.js';
|
|
4
|
+
import { resolveColor } from './dataset-color.js';
|
|
5
|
+
import type { DatasetColor } from './dataset-color.js';
|
|
6
|
+
import type { LocalizedString } from './generic-member.js';
|
|
7
|
+
|
|
8
|
+
export const REGISTER_STATUSES: ReadonlyArray<string> = Object.freeze([
|
|
9
|
+
'current',
|
|
10
|
+
'superseded',
|
|
11
|
+
'withdrawn',
|
|
12
|
+
'draft',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export interface RegisterJson {
|
|
16
|
+
schema_version?: string;
|
|
17
|
+
schemaVersion?: string;
|
|
18
|
+
id?: string | null;
|
|
19
|
+
name?: string | LocalizedString | null;
|
|
20
|
+
ref?: string | null;
|
|
21
|
+
refAliases?: ReadonlyArray<string>;
|
|
22
|
+
ref_aliases?: ReadonlyArray<string>;
|
|
23
|
+
year?: number | string | null;
|
|
24
|
+
urn?: string | null;
|
|
25
|
+
urnAliases?: ReadonlyArray<string>;
|
|
26
|
+
urn_aliases?: ReadonlyArray<string>;
|
|
27
|
+
status?: string | null;
|
|
28
|
+
supersedes?: string | null;
|
|
29
|
+
owner?: string | null;
|
|
30
|
+
sourceRepo?: string | null;
|
|
31
|
+
source_repo?: string | null;
|
|
32
|
+
tags?: ReadonlyArray<string>;
|
|
33
|
+
languages?: ReadonlyArray<string>;
|
|
34
|
+
languageOrder?: ReadonlyArray<string>;
|
|
35
|
+
language_order?: ReadonlyArray<string>;
|
|
36
|
+
ordering?: string | null;
|
|
37
|
+
logo?: Record<string, unknown> | null;
|
|
38
|
+
color?: DatasetColor | null;
|
|
39
|
+
description?: LocalizedString;
|
|
40
|
+
about?: LocalizedString;
|
|
41
|
+
provenance?: ReadonlyArray<Record<string, unknown>>;
|
|
42
|
+
contributors?: ReadonlyArray<Record<string, unknown>>;
|
|
43
|
+
sections?: ReadonlyArray<SectionJson | Section>;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Register extends GlossaristModel {
|
|
48
|
+
readonly schemaVersion: string;
|
|
49
|
+
readonly id: string | null;
|
|
50
|
+
readonly name: string | LocalizedString | null;
|
|
51
|
+
readonly ref: string | null;
|
|
52
|
+
readonly refAliases: ReadonlyArray<string>;
|
|
53
|
+
readonly year: number | string | null;
|
|
54
|
+
readonly urn: string | null;
|
|
55
|
+
readonly urnAliases: ReadonlyArray<string>;
|
|
56
|
+
readonly status: string | null;
|
|
57
|
+
readonly supersedes: string | null;
|
|
58
|
+
readonly owner: string | null;
|
|
59
|
+
readonly sourceRepo: string | null;
|
|
60
|
+
readonly tags: ReadonlyArray<string>;
|
|
61
|
+
readonly languages: ReadonlyArray<string>;
|
|
62
|
+
readonly languageOrder: ReadonlyArray<string>;
|
|
63
|
+
readonly ordering: string | null;
|
|
64
|
+
readonly logo: Record<string, unknown> | null;
|
|
65
|
+
readonly color: DatasetColor | null;
|
|
66
|
+
readonly description: LocalizedString;
|
|
67
|
+
readonly about: LocalizedString;
|
|
68
|
+
readonly provenance: ReadonlyArray<Record<string, unknown>>;
|
|
69
|
+
readonly contributors: ReadonlyArray<Record<string, unknown>>;
|
|
70
|
+
readonly sections: ReadonlyArray<Section>;
|
|
71
|
+
private readonly _raw: Record<string, unknown>;
|
|
72
|
+
|
|
73
|
+
constructor(data: RegisterJson = {}) {
|
|
74
|
+
super();
|
|
75
|
+
this.schemaVersion = data.schema_version ?? data.schemaVersion ?? '3';
|
|
76
|
+
this.id = data.id ?? null;
|
|
77
|
+
this.name = (data.name as string | LocalizedString | null) ?? null;
|
|
78
|
+
this.ref = data.ref ?? null;
|
|
79
|
+
this.refAliases = data.refAliases ?? data.ref_aliases ?? [];
|
|
80
|
+
this.year = (data.year as number | string | null) ?? null;
|
|
81
|
+
this.urn = data.urn ?? null;
|
|
82
|
+
this.urnAliases = data.urnAliases ?? data.urn_aliases ?? [];
|
|
83
|
+
this.status = data.status ?? null;
|
|
84
|
+
this.supersedes = data.supersedes ?? null;
|
|
85
|
+
this.owner = data.owner ?? null;
|
|
86
|
+
this.sourceRepo = data.sourceRepo ?? data.source_repo ?? null;
|
|
87
|
+
this.tags = (data.tags as ReadonlyArray<string>) ?? [];
|
|
88
|
+
this.languages = (data.languages as ReadonlyArray<string>) ?? [];
|
|
89
|
+
this.languageOrder = data.languageOrder ?? data.language_order ?? [];
|
|
90
|
+
this.ordering = data.ordering ?? null;
|
|
91
|
+
this.logo = (data.logo as Record<string, unknown> | null) ?? null;
|
|
92
|
+
this.color = (data.color as DatasetColor | null) ?? null;
|
|
93
|
+
this.description = (data.description as LocalizedString) ?? {};
|
|
94
|
+
this.about = (data.about as LocalizedString) ?? {};
|
|
95
|
+
this.provenance = (data.provenance as ReadonlyArray<Record<string, unknown>>) ?? [];
|
|
96
|
+
this.contributors = (data.contributors as ReadonlyArray<Record<string, unknown>>) ?? [];
|
|
97
|
+
this.sections = (data.sections ?? []).map((s) =>
|
|
98
|
+
s instanceof Section ? s : new Section(s as SectionJson),
|
|
99
|
+
);
|
|
100
|
+
this._raw = this._extractRaw(data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private _extractRaw(data: RegisterJson): Record<string, unknown> {
|
|
104
|
+
const known = new Set([
|
|
105
|
+
'schema_version', 'schemaVersion',
|
|
106
|
+
'id', 'name', 'ref', 'refAliases', 'ref_aliases',
|
|
107
|
+
'year', 'urn', 'urnAliases', 'urn_aliases',
|
|
108
|
+
'status', 'supersedes', 'owner',
|
|
109
|
+
'sourceRepo', 'source_repo', 'tags',
|
|
110
|
+
'languages', 'languageOrder', 'language_order',
|
|
111
|
+
'ordering', 'logo', 'color', 'description', 'about',
|
|
112
|
+
'provenance', 'contributors', 'sections',
|
|
113
|
+
]);
|
|
114
|
+
const extra: Record<string, unknown> = {};
|
|
115
|
+
for (const [k, v] of Object.entries(data)) {
|
|
116
|
+
if (!known.has(k)) extra[k] = v;
|
|
117
|
+
}
|
|
118
|
+
return extra;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
sectionById(id: string): Section | null {
|
|
122
|
+
for (const section of this.sections) {
|
|
123
|
+
if (section.id === id) return section;
|
|
124
|
+
const found = section.descendantById(id);
|
|
125
|
+
if (found) return found;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Walks the section tree upward from `sectionId`, returning the
|
|
132
|
+
* ancestor chain in immediate-parent-first order. The returned array
|
|
133
|
+
* does NOT include sectionId itself. Returns [] when sectionId is
|
|
134
|
+
* unknown or is a top-level section.
|
|
135
|
+
*/
|
|
136
|
+
sectionAncestorIds(sectionId: string | null | undefined): string[] {
|
|
137
|
+
if (!sectionId) return [];
|
|
138
|
+
for (const root of this.sections) {
|
|
139
|
+
const chain = _ancestorChain(root, sectionId);
|
|
140
|
+
if (chain) return chain;
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns the closure of `sectionId`: the section plus all of its
|
|
147
|
+
* ancestors. Concept-section membership tests should intersect the
|
|
148
|
+
* concept's section list with this closure.
|
|
149
|
+
*/
|
|
150
|
+
sectionClosure(sectionId: string | null | undefined): string[] {
|
|
151
|
+
return sectionId ? [sectionId, ...this.sectionAncestorIds(sectionId)] : [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Walks the section tree DOWNWARD from `sectionId`, returning all
|
|
156
|
+
* descendant section IDs in pre-order.
|
|
157
|
+
*/
|
|
158
|
+
sectionDescendantIds(sectionId: string | null | undefined): string[] {
|
|
159
|
+
if (!sectionId) return [];
|
|
160
|
+
const section = this.sectionById(sectionId);
|
|
161
|
+
if (!section) return [];
|
|
162
|
+
const out: string[] = [];
|
|
163
|
+
for (const child of section.children) {
|
|
164
|
+
if (child.id) out.push(child.id);
|
|
165
|
+
out.push(...this.sectionDescendantIds(child.id));
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
sectionDescendantClosure(sectionId: string | null | undefined): string[] {
|
|
171
|
+
return sectionId ? [sectionId, ...this.sectionDescendantIds(sectionId)] : [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Returns all section IDs the concept belongs to: the concept's own
|
|
176
|
+
* sections plus every ancestor of each.
|
|
177
|
+
*/
|
|
178
|
+
conceptSectionIds(concept: { sections?: unknown; groups?: unknown }): string[] {
|
|
179
|
+
const own = conceptSectionIdList(concept);
|
|
180
|
+
if (own.length === 0) return [];
|
|
181
|
+
const closure = new Set<string>();
|
|
182
|
+
for (const id of own) {
|
|
183
|
+
for (const ancestor of this.sectionClosure(id)) closure.add(ancestor);
|
|
184
|
+
}
|
|
185
|
+
return [...closure];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
sectionName(sectionId: string, lang: string): string | null {
|
|
189
|
+
const section = this.sectionById(sectionId);
|
|
190
|
+
return section ? section.name(lang) : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Localized display name. Returns the best-matching name string for
|
|
195
|
+
* the requested language, falling back to English, then to the first
|
|
196
|
+
* available language, then to null.
|
|
197
|
+
*/
|
|
198
|
+
displayName(lang: string): string | null {
|
|
199
|
+
if (this.name == null) return null;
|
|
200
|
+
if (typeof this.name === 'string') return this.name;
|
|
201
|
+
if (typeof this.name === 'object') {
|
|
202
|
+
return (
|
|
203
|
+
this.name[lang] ??
|
|
204
|
+
this.name['eng'] ??
|
|
205
|
+
(Object.values(this.name)[0] as string | undefined) ??
|
|
206
|
+
null
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resolvedColor(mode: 'light' | 'dark'): string | null {
|
|
213
|
+
return resolveColor(this.color, mode);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override toJSON(): RegisterJson {
|
|
217
|
+
const obj = { ...this._raw, schema_version: this.schemaVersion } as RegisterJson;
|
|
218
|
+
if (this.id != null) obj.id = this.id;
|
|
219
|
+
if (this.name != null) obj.name = this.name;
|
|
220
|
+
if (this.ref != null) obj.ref = this.ref;
|
|
221
|
+
if (this.refAliases.length > 0) obj.refAliases = [...this.refAliases];
|
|
222
|
+
if (this.year != null) obj.year = this.year;
|
|
223
|
+
if (this.urn != null) obj.urn = this.urn;
|
|
224
|
+
if (this.urnAliases.length > 0) obj.urnAliases = [...this.urnAliases];
|
|
225
|
+
if (this.status != null) obj.status = this.status;
|
|
226
|
+
if (this.supersedes != null) obj.supersedes = this.supersedes;
|
|
227
|
+
if (this.owner != null) obj.owner = this.owner;
|
|
228
|
+
if (this.sourceRepo != null) obj.sourceRepo = this.sourceRepo;
|
|
229
|
+
if (this.tags.length > 0) obj.tags = [...this.tags];
|
|
230
|
+
if (this.languages.length > 0) obj.languages = [...this.languages];
|
|
231
|
+
if (this.languageOrder.length > 0) obj.languageOrder = [...this.languageOrder];
|
|
232
|
+
if (this.ordering != null) obj.ordering = this.ordering;
|
|
233
|
+
if (this.logo != null) obj.logo = { ...this.logo };
|
|
234
|
+
if (this.color != null) {
|
|
235
|
+
obj.color =
|
|
236
|
+
typeof this.color === 'object' && this.color !== null
|
|
237
|
+
? { ...this.color }
|
|
238
|
+
: this.color;
|
|
239
|
+
}
|
|
240
|
+
if (Object.keys(this.description).length > 0) obj.description = { ...this.description };
|
|
241
|
+
if (Object.keys(this.about).length > 0) obj.about = { ...this.about };
|
|
242
|
+
if (this.provenance.length > 0) obj.provenance = this.provenance.map((p) => ({ ...p }));
|
|
243
|
+
if (this.contributors.length > 0) obj.contributors = this.contributors.map((c) => ({ ...c }));
|
|
244
|
+
if (this.sections.length > 0) obj.sections = this.sections.map((s) => s.toJSON());
|
|
245
|
+
return obj;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static override fromJSON(data: RegisterJson): Register {
|
|
249
|
+
const instance = new Register(data);
|
|
250
|
+
const snakeToCamel = (k: string): string =>
|
|
251
|
+
k.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
252
|
+
return new Proxy(instance, {
|
|
253
|
+
get(target: Register, prop: string | symbol): unknown {
|
|
254
|
+
if (typeof prop === 'string') {
|
|
255
|
+
if (prop in target) return (target as unknown as Record<string, unknown>)[prop];
|
|
256
|
+
if (prop in target._raw) return target._raw[prop];
|
|
257
|
+
const camel = snakeToCamel(prop);
|
|
258
|
+
if (camel in target) return (target as unknown as Record<string, unknown>)[camel];
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Recursive walk: returns the ancestor chain of `targetId` within the
|
|
268
|
+
* subtree rooted at `section`, or null if targetId is not in this
|
|
269
|
+
* subtree.
|
|
270
|
+
*/
|
|
271
|
+
function _ancestorChain(
|
|
272
|
+
section: Section,
|
|
273
|
+
targetId: string,
|
|
274
|
+
ancestors: string[] = [],
|
|
275
|
+
): string[] | null {
|
|
276
|
+
if (section.id === targetId) return [...ancestors].reverse();
|
|
277
|
+
for (const child of section.children) {
|
|
278
|
+
const childId = child.id;
|
|
279
|
+
if (childId == null) continue;
|
|
280
|
+
const nextAncestors = section.id != null ? [...ancestors, section.id] : [...ancestors];
|
|
281
|
+
const found = _ancestorChain(child, targetId, nextAncestors);
|
|
282
|
+
if (found) return found;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Returns the list of section IDs a concept claims membership in.
|
|
289
|
+
* Concepts may carry section IDs via either `sections` (preferred) or
|
|
290
|
+
* `groups` (legacy). Each entry may be a string or an object with an
|
|
291
|
+
* `id` field; both forms are flattened to a string list.
|
|
292
|
+
*/
|
|
293
|
+
function conceptSectionIdList(concept: { sections?: unknown; groups?: unknown }): string[] {
|
|
294
|
+
if (!concept) return [];
|
|
295
|
+
const fromSections = concept.sections
|
|
296
|
+
? _flattenSectionIds(concept.sections)
|
|
297
|
+
: [];
|
|
298
|
+
const fromGroups = concept.groups ? _flattenSectionIds(concept.groups) : [];
|
|
299
|
+
return [...fromSections, ...fromGroups];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function _flattenSectionIds(value: unknown): string[] {
|
|
303
|
+
if (!Array.isArray(value)) return [];
|
|
304
|
+
const out: string[] = [];
|
|
305
|
+
for (const entry of value) {
|
|
306
|
+
if (typeof entry === 'string') {
|
|
307
|
+
out.push(entry);
|
|
308
|
+
} else if (entry && typeof entry === 'object') {
|
|
309
|
+
const e = entry as { id?: string; sectionId?: string; ref?: { id?: string } };
|
|
310
|
+
const id = e.id ?? e.sectionId ?? e.ref?.id;
|
|
311
|
+
if (id) out.push(String(id));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|