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,167 @@
|
|
|
1
|
+
// Categorization SSOT for the 52 glossarist relationship types.
|
|
2
|
+
|
|
3
|
+
import { RELATIONSHIP_TYPES } from './related-concept.js';
|
|
4
|
+
|
|
5
|
+
export interface CategoryDefinition {
|
|
6
|
+
label: string;
|
|
7
|
+
description: string;
|
|
8
|
+
types: ReadonlyArray<string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type CategoryKey =
|
|
12
|
+
| 'lifecycle'
|
|
13
|
+
| 'hierarchical'
|
|
14
|
+
| 'associative'
|
|
15
|
+
| 'comparative'
|
|
16
|
+
| 'spatiotemporal'
|
|
17
|
+
| 'lexical'
|
|
18
|
+
| 'mapping'
|
|
19
|
+
| 'definitional';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Each type in RELATIONSHIP_TYPES belongs to exactly one category. The
|
|
23
|
+
* categories mirror the concept-model taxonomy. Adding a new type
|
|
24
|
+
* means adding a taxonomy entry — no code changes here.
|
|
25
|
+
*/
|
|
26
|
+
export const RELATION_CATEGORIES: Readonly<Record<CategoryKey, CategoryDefinition>> = Object.freeze({
|
|
27
|
+
lifecycle: Object.freeze({
|
|
28
|
+
label: 'Lifecycle',
|
|
29
|
+
description: 'Concept lifecycle transitions: supersession, deprecation, replacement.',
|
|
30
|
+
types: Object.freeze([
|
|
31
|
+
'deprecates', 'deprecated_by',
|
|
32
|
+
'supersedes', 'superseded_by',
|
|
33
|
+
'replaces', 'replaced_by',
|
|
34
|
+
'invalidates', 'invalidated_by',
|
|
35
|
+
'retires', 'retired_by',
|
|
36
|
+
]),
|
|
37
|
+
}) as CategoryDefinition,
|
|
38
|
+
|
|
39
|
+
hierarchical: Object.freeze({
|
|
40
|
+
label: 'Hierarchical',
|
|
41
|
+
description: 'Broader/narrower relations, including SKOS and ISO 25964 generic/partitive/instantial.',
|
|
42
|
+
types: Object.freeze([
|
|
43
|
+
'broader', 'narrower',
|
|
44
|
+
'broader_generic', 'narrower_generic',
|
|
45
|
+
'broader_partitive', 'narrower_partitive',
|
|
46
|
+
'broader_instantial', 'narrower_instantial',
|
|
47
|
+
]),
|
|
48
|
+
}) as CategoryDefinition,
|
|
49
|
+
|
|
50
|
+
associative: Object.freeze({
|
|
51
|
+
label: 'Associative',
|
|
52
|
+
description: 'Generic see-also references between concepts.',
|
|
53
|
+
types: Object.freeze([
|
|
54
|
+
'see', 'related_concept',
|
|
55
|
+
'related_concept_broader', 'related_concept_narrower',
|
|
56
|
+
'references',
|
|
57
|
+
]),
|
|
58
|
+
}) as CategoryDefinition,
|
|
59
|
+
|
|
60
|
+
comparative: Object.freeze({
|
|
61
|
+
label: 'Comparative',
|
|
62
|
+
description: 'Compare / contrast relations between concepts.',
|
|
63
|
+
types: Object.freeze(['compare', 'contrast']),
|
|
64
|
+
}) as CategoryDefinition,
|
|
65
|
+
|
|
66
|
+
spatiotemporal: Object.freeze({
|
|
67
|
+
label: 'Spatiotemporal',
|
|
68
|
+
description: 'Sequential, spatial, and temporal relations.',
|
|
69
|
+
types: Object.freeze([
|
|
70
|
+
'sequentially_related_concept',
|
|
71
|
+
'spatially_related_concept',
|
|
72
|
+
'temporally_related_concept',
|
|
73
|
+
]),
|
|
74
|
+
}) as CategoryDefinition,
|
|
75
|
+
|
|
76
|
+
lexical: Object.freeze({
|
|
77
|
+
label: 'Lexical',
|
|
78
|
+
description: 'Lexical relations between designations across languages.',
|
|
79
|
+
types: Object.freeze(['homograph', 'false_friend']),
|
|
80
|
+
}) as CategoryDefinition,
|
|
81
|
+
|
|
82
|
+
mapping: Object.freeze({
|
|
83
|
+
label: 'Mapping',
|
|
84
|
+
description: 'SKOS mapping properties for cross-vocabulary alignment.',
|
|
85
|
+
types: Object.freeze([
|
|
86
|
+
'equivalent', 'close_match',
|
|
87
|
+
'broad_match', 'narrow_match', 'related_match',
|
|
88
|
+
]),
|
|
89
|
+
}) as CategoryDefinition,
|
|
90
|
+
|
|
91
|
+
definitional: Object.freeze({
|
|
92
|
+
label: 'Definitional',
|
|
93
|
+
description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance, versioning.',
|
|
94
|
+
types: Object.freeze([
|
|
95
|
+
'has_concept', 'is_concept_of',
|
|
96
|
+
'instance_of', 'has_instance',
|
|
97
|
+
'has_definition', 'definition_of',
|
|
98
|
+
'has_part', 'is_part_of',
|
|
99
|
+
'inherits', 'inherited_by',
|
|
100
|
+
'has_version', 'version_of',
|
|
101
|
+
'current_version', 'current_version_of',
|
|
102
|
+
'provides', 'provided_by',
|
|
103
|
+
]),
|
|
104
|
+
}) as CategoryDefinition,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const _CATEGORY_BY_TYPE: ReadonlyMap<string, string> = (() => {
|
|
108
|
+
const map = new Map<string, string>();
|
|
109
|
+
for (const [cat, def] of Object.entries(RELATION_CATEGORIES)) {
|
|
110
|
+
for (const t of def.types) map.set(t, cat);
|
|
111
|
+
}
|
|
112
|
+
return map;
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
const _CATEGORY_ALIASES: Readonly<Record<string, string>> = {
|
|
116
|
+
conceptInstance: 'definitional',
|
|
117
|
+
versioning: 'definitional',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Returns the category key for the given relationship type, or null
|
|
122
|
+
* when the type is not categorized.
|
|
123
|
+
*/
|
|
124
|
+
export function categoryOf(type: string): string | null {
|
|
125
|
+
return _CATEGORY_BY_TYPE.get(type) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns the category definition for the given category key.
|
|
130
|
+
* Accepts both canonical names (definitional) and aliases from
|
|
131
|
+
* earlier glossarist-js versions.
|
|
132
|
+
*/
|
|
133
|
+
export function categoryDefinition(categoryKey: string): CategoryDefinition | null {
|
|
134
|
+
const canonical = _CATEGORY_ALIASES[categoryKey] ?? categoryKey;
|
|
135
|
+
return (RELATION_CATEGORIES as Readonly<Record<string, CategoryDefinition>>)[canonical] ?? null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* MECE sanity check. Returns the list of relationship types that are
|
|
140
|
+
* NOT categorized.
|
|
141
|
+
*/
|
|
142
|
+
export function uncategorizedTypes(): string[] {
|
|
143
|
+
return RELATIONSHIP_TYPES.filter((t) => !_CATEGORY_BY_TYPE.has(t));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* MECE sanity check. Returns a list of { type, count } entries for any
|
|
148
|
+
* type that appears in MORE than one category.
|
|
149
|
+
*/
|
|
150
|
+
export function duplicatedTypes(): ReadonlyArray<{ type: string; count: number }> {
|
|
151
|
+
const counts = new Map<string, number>();
|
|
152
|
+
for (const def of Object.values(RELATION_CATEGORIES)) {
|
|
153
|
+
for (const t of def.types) {
|
|
154
|
+
counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const out: { type: string; count: number }[] = [];
|
|
158
|
+
for (const [type, count] of counts) {
|
|
159
|
+
if (count > 1) out.push({ type, count });
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
RELATION_CATEGORIES as RELATION_CATEGORIES_READONLY,
|
|
166
|
+
type CategoryKey as RelationCategoryKey,
|
|
167
|
+
};
|
|
@@ -6,10 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
import { categoryOf, categoryDefinition } from './relation-categories.js';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export interface ColorPair {
|
|
10
|
+
light: string;
|
|
11
|
+
dark: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RelationColorPalette {
|
|
15
|
+
byCategory: Readonly<Record<string, ColorPair>>;
|
|
16
|
+
byType: Readonly<Record<string, ColorPair>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResolveRelationColorOptions {
|
|
20
|
+
overrides?: Partial<RelationColorPalette>;
|
|
21
|
+
mode?: 'light' | 'dark';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One { light, dark } pair per category. Keeping these in glossarist-js
|
|
26
|
+
* lets the defaults ship with the library; consumers only need to
|
|
27
|
+
* customize when they want to deviate.
|
|
28
|
+
*/
|
|
29
|
+
export const RELATION_COLOR_DEFAULTS: RelationColorPalette = Object.freeze({
|
|
13
30
|
byCategory: Object.freeze({
|
|
14
31
|
lifecycle: { light: '#B43A2E', dark: '#F87171' },
|
|
15
32
|
hierarchical: { light: '#1F6FEB', dark: '#6EA8FE' },
|
|
@@ -19,39 +36,32 @@ export const RELATION_COLOR_DEFAULTS = Object.freeze({
|
|
|
19
36
|
lexical: { light: '#D97706', dark: '#FBBF24' },
|
|
20
37
|
mapping: { light: '#2563EB', dark: '#93BBFD' },
|
|
21
38
|
definitional: { light: '#65A30D', dark: '#A3E635' },
|
|
22
|
-
}),
|
|
23
|
-
|
|
24
|
-
// Per-type overrides. Use the same { light, dark } shape. Keys are
|
|
25
|
-
// relationship-type local names. Empty by default — consumers may
|
|
26
|
-
// populate this through `overrides.byType` at call time.
|
|
27
|
-
byType: Object.freeze({}),
|
|
39
|
+
}) as Readonly<Record<string, ColorPair>>,
|
|
40
|
+
byType: Object.freeze({}) as Readonly<Record<string, ColorPair>>,
|
|
28
41
|
});
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
|
-
* Resolves the color for a relationship type.
|
|
32
|
-
*
|
|
33
|
-
* Resolution order:
|
|
44
|
+
* Resolves the color for a relationship type. Resolution order:
|
|
34
45
|
* 1. overrides.byType[type]
|
|
35
46
|
* 2. RELATION_COLOR_DEFAULTS.byType[type]
|
|
36
47
|
* 3. overrides.byCategory[category]
|
|
37
48
|
* 4. RELATION_COLOR_DEFAULTS.byCategory[category]
|
|
38
49
|
* 5. null (uncategorized or unrecognized type)
|
|
39
|
-
*
|
|
40
|
-
* @param {string} type - relationship type local name
|
|
41
|
-
* @param {{ overrides?: { byType?: object, byCategory?: object }, mode?: 'light'|'dark' }} [options]
|
|
42
|
-
* @returns {string | null}
|
|
43
50
|
*/
|
|
44
|
-
export function resolveRelationColor(
|
|
51
|
+
export function resolveRelationColor(
|
|
52
|
+
type: string,
|
|
53
|
+
options: ResolveRelationColorOptions = {},
|
|
54
|
+
): string | null {
|
|
45
55
|
const mode = options.mode ?? 'light';
|
|
46
56
|
const overrides = options.overrides ?? {};
|
|
47
|
-
|
|
48
57
|
const typeOverride = overrides.byType?.[type] ?? RELATION_COLOR_DEFAULTS.byType[type];
|
|
49
58
|
if (typeOverride) return pickMode(typeOverride, mode);
|
|
50
59
|
|
|
51
60
|
const category = categoryOf(type);
|
|
52
61
|
if (!category) return null;
|
|
53
62
|
|
|
54
|
-
const categoryOverride =
|
|
63
|
+
const categoryOverride =
|
|
64
|
+
overrides.byCategory?.[category] ?? RELATION_COLOR_DEFAULTS.byCategory[category];
|
|
55
65
|
if (!categoryOverride) return null;
|
|
56
66
|
|
|
57
67
|
return pickMode(categoryOverride, mode);
|
|
@@ -61,16 +71,19 @@ export function resolveRelationColor(type, options = {}) {
|
|
|
61
71
|
* Returns the { light, dark } pair for a category, merging overrides
|
|
62
72
|
* over defaults. Useful for UIs that want to render a legend.
|
|
63
73
|
*/
|
|
64
|
-
export function categoryColorPair(
|
|
74
|
+
export function categoryColorPair(
|
|
75
|
+
categoryKey: string,
|
|
76
|
+
overrides: { byCategory?: Record<string, Partial<ColorPair>> } = {},
|
|
77
|
+
): ColorPair | null {
|
|
65
78
|
const def = categoryDefinition(categoryKey);
|
|
66
79
|
if (!def) return null;
|
|
67
80
|
const fromOverride = overrides.byCategory?.[categoryKey];
|
|
68
81
|
const fromDefault = RELATION_COLOR_DEFAULTS.byCategory[categoryKey];
|
|
69
82
|
if (!fromDefault && !fromOverride) return null;
|
|
70
|
-
return { ...(fromDefault ?? {}), ...(fromOverride ?? {}) };
|
|
83
|
+
return { ...(fromDefault ?? { light: '', dark: '' }), ...(fromOverride ?? {}) } as ColorPair;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
function pickMode(pair, mode) {
|
|
86
|
+
function pickMode(pair: ColorPair | string | null, mode: 'light' | 'dark'): string | null {
|
|
74
87
|
if (pair == null) return null;
|
|
75
88
|
if (typeof pair === 'string') return pair;
|
|
76
89
|
return pair[mode] ?? pair.light ?? pair.dark ?? null;
|
|
@@ -4,43 +4,52 @@
|
|
|
4
4
|
// dispatches each file to the appropriate class (PartitiveHyperedge,
|
|
5
5
|
// GenericHyperedge, future leaves) based on its `type` field, via
|
|
6
6
|
// HyperedgeRegistry.
|
|
7
|
-
//
|
|
8
|
-
// The wire-shape contract is owned by the JSON Schema at
|
|
9
|
-
// `schemas/v3/relation.yaml` (concept-model repo) — this loader
|
|
10
|
-
// produces the corresponding JS model instances. See
|
|
11
|
-
// docs/design/relations-as-files.md (concept-model).
|
|
12
|
-
//
|
|
13
|
-
// Adding a new hyperedge type means: declare the leaf class with the
|
|
14
|
-
// metadata block, register it. The loader picks it up automatically —
|
|
15
|
-
// no edits here.
|
|
16
7
|
|
|
17
8
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import type { Dirent } from 'node:fs';
|
|
18
10
|
import * as yaml from 'js-yaml';
|
|
19
11
|
import { HyperedgeRegistry } from './hyperedge-registry.js';
|
|
12
|
+
import type { HyperedgeClass } from './hyperedge-registry.js';
|
|
13
|
+
import type { AbstractHyperedge } from './abstract-hyperedge.js';
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
interface HyperedgeDocument {
|
|
16
|
+
type: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HyperedgeClassWithFromJson extends HyperedgeClass {
|
|
21
|
+
fromJSON(data: unknown): AbstractHyperedge;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Backward-compat: snapshot of registered classes keyed by typeTag.
|
|
26
|
+
* Prefer HyperedgeRegistry.forTypeTag() for new code — this snapshot
|
|
27
|
+
* is frozen at module load and won't pick up runtime registrations
|
|
28
|
+
* from tests (the OCP test in Phase 11 registers a mock type after
|
|
29
|
+
* import).
|
|
30
|
+
*/
|
|
31
|
+
export const TYPE_TO_CLASS = new Proxy(
|
|
32
|
+
{} as Record<string, HyperedgeClass>,
|
|
33
|
+
{
|
|
34
|
+
get(_target: unknown, prop: string | symbol): HyperedgeClass | null {
|
|
35
|
+
return typeof prop === 'string' ? HyperedgeRegistry.forTypeTag(prop) : null;
|
|
36
|
+
},
|
|
37
|
+
ownKeys(): string[] {
|
|
38
|
+
return HyperedgeRegistry.allTypeTags();
|
|
39
|
+
},
|
|
40
|
+
getOwnPropertyDescriptor(): PropertyDescriptor {
|
|
41
|
+
return { enumerable: true, configurable: true };
|
|
42
|
+
},
|
|
35
43
|
},
|
|
36
|
-
|
|
44
|
+
);
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* Thrown by `loadAll` when a relation file has an unknown `type`,
|
|
40
48
|
* malformed structure, or a class that fails to instantiate.
|
|
41
49
|
*/
|
|
42
50
|
export class RelationLoadError extends Error {
|
|
43
|
-
|
|
51
|
+
readonly path: string;
|
|
52
|
+
constructor(path: string, message: string) {
|
|
44
53
|
super(`relation-loader: ${path}: ${message}`);
|
|
45
54
|
this.path = path;
|
|
46
55
|
}
|
|
@@ -51,32 +60,27 @@ export class RelationLoadError extends Error {
|
|
|
51
60
|
* returning a Map keyed by comprehensive-id.
|
|
52
61
|
*
|
|
53
62
|
* Returns an empty Map if `relationsDir` doesn't exist or is empty.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} relationsDir — path to a `relations/` directory
|
|
56
|
-
* containing `<comprehensive-id>/<slug>.yaml` files (per the
|
|
57
|
-
* schema in schemas/v3/relation.yaml).
|
|
58
|
-
* @returns {Map<string, Array<AbstractHyperedge>>} — for each
|
|
59
|
-
* comprehensive id, the list of relations that have it as their
|
|
60
|
-
* comprehensive.
|
|
61
63
|
*/
|
|
62
|
-
export function loadAll(
|
|
63
|
-
|
|
64
|
+
export function loadAll(
|
|
65
|
+
relationsDir: string | null | undefined,
|
|
66
|
+
): Map<string, AbstractHyperedge[]> {
|
|
67
|
+
const result = new Map<string, AbstractHyperedge[]>();
|
|
64
68
|
if (!relationsDir) return result;
|
|
65
69
|
if (!existsSync(relationsDir)) return result;
|
|
66
70
|
|
|
67
71
|
const entries = readdirSync(relationsDir, { withFileTypes: true });
|
|
68
|
-
for (const compEntry of entries) {
|
|
72
|
+
for (const compEntry of entries as Dirent[]) {
|
|
69
73
|
if (!compEntry.isDirectory()) continue;
|
|
70
74
|
const compId = compEntry.name;
|
|
71
75
|
const compDir = `${relationsDir}/${compId}`;
|
|
72
76
|
if (!result.has(compId)) result.set(compId, []);
|
|
73
77
|
const files = readdirSync(compDir)
|
|
74
|
-
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
78
|
+
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
75
79
|
.sort();
|
|
76
80
|
for (const file of files) {
|
|
77
81
|
const path = `${compDir}/${file}`;
|
|
78
82
|
const rel = loadOne(path);
|
|
79
|
-
if (rel) result.get(compId)
|
|
83
|
+
if (rel) result.get(compId)?.push(rel);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
return result;
|
|
@@ -93,19 +97,19 @@ export const RelationLoader = Object.freeze({ loadAll, loadOne });
|
|
|
93
97
|
/**
|
|
94
98
|
* Load a single relation file. Returns null if the file is missing,
|
|
95
99
|
* unreadable, or doesn't have a recognized `type` field. Throws
|
|
96
|
-
* `RelationLoadError` on malformed content
|
|
97
|
-
* count, wrong comprehensive shape, YAML parse error).
|
|
100
|
+
* `RelationLoadError` on malformed content.
|
|
98
101
|
*/
|
|
99
|
-
export function loadOne(path) {
|
|
100
|
-
let doc;
|
|
102
|
+
export function loadOne(path: string): AbstractHyperedge | null {
|
|
103
|
+
let doc: unknown;
|
|
101
104
|
try {
|
|
102
105
|
doc = yaml.load(readFileSync(path, 'utf8'));
|
|
103
106
|
} catch (e) {
|
|
104
|
-
throw new RelationLoadError(path, `YAML parse failed: ${e.message}`);
|
|
107
|
+
throw new RelationLoadError(path, `YAML parse failed: ${(e as Error).message}`);
|
|
105
108
|
}
|
|
106
109
|
if (!doc || typeof doc !== 'object') return null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
const d = doc as HyperedgeDocument;
|
|
111
|
+
if (!d.type) return null;
|
|
112
|
+
const Cls = HyperedgeRegistry.forTypeTag(d.type) as HyperedgeClassWithFromJson | null;
|
|
113
|
+
if (!Cls) return null;
|
|
114
|
+
return Cls.fromJSON(d);
|
|
111
115
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
import type { LocalizedString } from './generic-member.js';
|
|
3
|
+
|
|
4
|
+
export const ORDERING_METHODS: ReadonlyArray<string> = Object.freeze([
|
|
5
|
+
'systematic',
|
|
6
|
+
'mixed',
|
|
7
|
+
'alphabetical',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export type OrderingMethod = string;
|
|
11
|
+
|
|
12
|
+
export interface SectionJson {
|
|
13
|
+
id?: string | null;
|
|
14
|
+
names?: LocalizedString;
|
|
15
|
+
ordering?: OrderingMethod | null;
|
|
16
|
+
children?: ReadonlyArray<SectionJson | Section>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Section extends GlossaristModel {
|
|
20
|
+
readonly id: string | null;
|
|
21
|
+
readonly names: LocalizedString;
|
|
22
|
+
readonly ordering: OrderingMethod | null;
|
|
23
|
+
readonly children: ReadonlyArray<Section>;
|
|
24
|
+
|
|
25
|
+
constructor(data: SectionJson = {}) {
|
|
26
|
+
super();
|
|
27
|
+
this.id = data.id ?? null;
|
|
28
|
+
this.names = (data.names as LocalizedString) ?? {};
|
|
29
|
+
this.ordering = data.ordering ?? null;
|
|
30
|
+
this.children = (data.children ?? []).map((c) =>
|
|
31
|
+
c instanceof Section ? c : new Section(c as SectionJson),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
name(lang: string): string | null {
|
|
36
|
+
return this.names[lang] ?? this.names['eng'] ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
descendantById(id: string): Section | null {
|
|
40
|
+
for (const child of this.children) {
|
|
41
|
+
if (child.id === id) return child;
|
|
42
|
+
const found = child.descendantById(id);
|
|
43
|
+
if (found) return found;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override toJSON(): SectionJson {
|
|
49
|
+
const obj: SectionJson = { id: this.id };
|
|
50
|
+
if (Object.keys(this.names).length > 0) obj.names = { ...this.names };
|
|
51
|
+
if (this.ordering != null) obj.ordering = this.ordering;
|
|
52
|
+
if (this.children.length > 0) {
|
|
53
|
+
obj.children = this.children.map((c) => c.toJSON());
|
|
54
|
+
}
|
|
55
|
+
return obj;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static override fromJSON(data: SectionJson): Section {
|
|
59
|
+
return new Section(data);
|
|
60
|
+
}
|
|
61
|
+
}
|