glossarist 0.3.3 → 0.3.5
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 +2 -2
- package/src/concept-collection.js +36 -28
- package/src/concept-parser.js +6 -11
- package/src/gcr-reader.js +32 -2
- package/src/index.d.ts +10 -0
- package/src/index.js +4 -2
- package/src/models/concept-ref.js +9 -2
- package/src/models/concept-source.js +2 -0
- package/src/models/concept.js +44 -0
- package/src/models/designation-relationship.js +27 -0
- package/src/models/designation.js +6 -1
- package/src/models/index.d.ts +11 -1
- package/src/models/index.js +1 -0
- package/src/models/localized-concept.js +38 -82
- package/src/models/related-concept.js +0 -2
- package/src/reference-mention.js +88 -0
- package/src/reference-resolver.js +285 -26
- package/src/render-classification.js +106 -0
- package/src/validators/concept-validator.js +51 -44
- package/src/validators/gcr-validator.js +6 -6
- package/src/validators/index.js +7 -1
- package/src/validators/register-validator.js +7 -11
- package/src/validators/relationship-type-rule.js +39 -0
- package/src/validators/v3-rules.js +171 -107
- package/src/validators/validation-error.js +4 -0
- package/src/validators/validation-result.js +29 -11
- package/src/validators/validation-rule.js +7 -5
|
@@ -12,7 +12,7 @@ export class GcrValidator {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
async _validateMetadata(pkg, result) {
|
|
15
|
-
const raw = await pkg.
|
|
15
|
+
const raw = await pkg.readText('metadata.yaml');
|
|
16
16
|
if (!raw) {
|
|
17
17
|
result.addError('metadata.yaml is missing');
|
|
18
18
|
return;
|
|
@@ -49,7 +49,7 @@ export class GcrValidator {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async _validateFileAsset(pkg, path, result) {
|
|
52
|
-
const raw = await pkg.
|
|
52
|
+
const raw = await pkg.readText(path);
|
|
53
53
|
if (!raw) return;
|
|
54
54
|
try {
|
|
55
55
|
yaml.load(raw);
|
|
@@ -58,15 +58,15 @@ export class GcrValidator {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
_validateDirectoryAsset(pkg, dirPath, result) {
|
|
62
62
|
let hasFiles = false;
|
|
63
63
|
let hasEntries = false;
|
|
64
|
-
pkg.
|
|
65
|
-
if (
|
|
64
|
+
for (const entry of pkg.entryPaths()) {
|
|
65
|
+
if (entry.path.startsWith(`${dirPath}/`)) {
|
|
66
66
|
hasEntries = true;
|
|
67
67
|
if (!entry.dir) hasFiles = true;
|
|
68
68
|
}
|
|
69
|
-
}
|
|
69
|
+
}
|
|
70
70
|
if (hasEntries && !hasFiles) {
|
|
71
71
|
result.addWarning(`${dirPath}/ directory exists but is empty`);
|
|
72
72
|
}
|
package/src/validators/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { ValidationResult } from './validation-result.js';
|
|
|
4
4
|
export { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
|
|
5
5
|
export { RegisterValidator } from './register-validator.js';
|
|
6
6
|
export { GcrValidator } from './gcr-validator.js';
|
|
7
|
+
export { RelationshipTypeRule } from './relationship-type-rule.js';
|
|
7
8
|
export {
|
|
8
9
|
RefShapeRule,
|
|
9
10
|
LocalityCompletenessRule,
|
|
@@ -12,11 +13,13 @@ export {
|
|
|
12
13
|
DomainRefRule,
|
|
13
14
|
UuidFormatRule,
|
|
14
15
|
SourceUrnFormatRule,
|
|
16
|
+
CiteRefIntegrityRule,
|
|
15
17
|
} from './v3-rules.js';
|
|
16
18
|
|
|
17
19
|
import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
|
|
18
20
|
import { RegisterValidator } from './register-validator.js';
|
|
19
21
|
import { GcrValidator } from './gcr-validator.js';
|
|
22
|
+
import { RelationshipTypeRule } from './relationship-type-rule.js';
|
|
20
23
|
import {
|
|
21
24
|
RefShapeRule,
|
|
22
25
|
LocalityCompletenessRule,
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
DomainRefRule,
|
|
26
29
|
UuidFormatRule,
|
|
27
30
|
SourceUrnFormatRule,
|
|
31
|
+
CiteRefIntegrityRule,
|
|
28
32
|
} from './v3-rules.js';
|
|
29
33
|
|
|
30
34
|
const _default = new ConceptValidator()
|
|
@@ -37,7 +41,9 @@ const _default = new ConceptValidator()
|
|
|
37
41
|
.addRule(new SchemaVersionRule())
|
|
38
42
|
.addRule(new DomainRefRule())
|
|
39
43
|
.addRule(new UuidFormatRule())
|
|
40
|
-
.addRule(new SourceUrnFormatRule())
|
|
44
|
+
.addRule(new SourceUrnFormatRule())
|
|
45
|
+
.addRule(new RelationshipTypeRule())
|
|
46
|
+
.addRule(new CiteRefIntegrityRule());
|
|
41
47
|
|
|
42
48
|
export function validateConcept(concept) {
|
|
43
49
|
return _default.validate(concept);
|
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ValidationResult } from './validation-result.js';
|
|
2
2
|
|
|
3
3
|
export class RegisterValidator {
|
|
4
4
|
validate(register) {
|
|
5
|
-
const
|
|
5
|
+
const result = new ValidationResult();
|
|
6
6
|
if (!register || typeof register !== 'object') {
|
|
7
|
-
|
|
8
|
-
return
|
|
7
|
+
result.addError('', 'Register must be a non-null object');
|
|
8
|
+
return result;
|
|
9
9
|
}
|
|
10
10
|
if (!register.schema_version) {
|
|
11
|
-
|
|
11
|
+
result.addWarning('schema_version', 'Register must have a schema_version');
|
|
12
12
|
}
|
|
13
13
|
if (!register.shortname) {
|
|
14
|
-
|
|
14
|
+
result.addWarning('shortname', 'Register should have a shortname');
|
|
15
15
|
}
|
|
16
|
-
return
|
|
17
|
-
valid: errors.filter(e => e.severity === 'error').length === 0,
|
|
18
|
-
errors: errors.filter(e => e.severity === 'error'),
|
|
19
|
-
warnings: errors.filter(e => e.severity === 'warning'),
|
|
20
|
-
};
|
|
16
|
+
return result;
|
|
21
17
|
}
|
|
22
18
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ValidationRule } from './validation-rule.js';
|
|
2
|
+
import { RELATIONSHIP_TYPES } from '../models/related-concept.js';
|
|
3
|
+
import { DESIGNATION_RELATIONSHIP_TYPES } from '../models/designation-relationship.js';
|
|
4
|
+
|
|
5
|
+
const KNOWN_CONCEPT_TYPES = new Set(RELATIONSHIP_TYPES);
|
|
6
|
+
const KNOWN_DESIGNATION_TYPES = new Set(DESIGNATION_RELATIONSHIP_TYPES);
|
|
7
|
+
|
|
8
|
+
export class RelationshipTypeRule extends ValidationRule {
|
|
9
|
+
constructor() { super('relationship-type', 'warning'); }
|
|
10
|
+
|
|
11
|
+
validate(concept, path, result) {
|
|
12
|
+
const related = concept.relatedConcepts ?? concept.related ?? [];
|
|
13
|
+
this._checkRelated(related, `${path}related`, KNOWN_CONCEPT_TYPES, result);
|
|
14
|
+
|
|
15
|
+
const langs = concept.languages ?? [];
|
|
16
|
+
for (const lang of langs) {
|
|
17
|
+
const lc = concept.localization?.(lang);
|
|
18
|
+
if (!lc) continue;
|
|
19
|
+
|
|
20
|
+
this._checkRelated(lc.related, `${path}localizations.${lang}.related`, KNOWN_CONCEPT_TYPES, result);
|
|
21
|
+
|
|
22
|
+
for (let ti = 0; ti < lc.terms.length; ti++) {
|
|
23
|
+
this._checkRelated(lc.terms[ti]?.related,
|
|
24
|
+
`${path}localizations.${lang}.terms[${ti}].related`, KNOWN_DESIGNATION_TYPES, result);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_checkRelated(arr, basePath, knownTypes, result) {
|
|
30
|
+
if (!arr) return;
|
|
31
|
+
for (let i = 0; i < arr.length; i++) {
|
|
32
|
+
const type = arr[i]?.type;
|
|
33
|
+
if (type && !knownTypes.has(type)) {
|
|
34
|
+
this.addIssue(result, `${basePath}[${i}].type`,
|
|
35
|
+
`Unknown relationship type '${type}'`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { ValidationRule } from './validation-rule.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const _langs = (concept) =>
|
|
4
|
+
concept.languages ?? (concept.localizations ? Object.keys(concept.localizations) : []);
|
|
5
|
+
|
|
6
|
+
const _loc = (concept, lang) =>
|
|
7
|
+
typeof concept.localization === 'function' ? concept.localization(lang) : concept.localizations?.[lang];
|
|
8
|
+
|
|
9
|
+
const _eachLocalization = (concept, fn) => {
|
|
10
|
+
for (const lang of _langs(concept)) {
|
|
11
|
+
const lc = _loc(concept, lang);
|
|
12
|
+
if (lc) fn(lang, lc);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
7
16
|
export class RefShapeRule extends ValidationRule {
|
|
8
17
|
constructor() { super('ref-shape'); }
|
|
9
18
|
|
|
10
|
-
validate(
|
|
11
|
-
const errors = [];
|
|
12
|
-
|
|
13
|
-
// Check sources in localizations
|
|
14
|
-
const localizations = value.localizations || {};
|
|
19
|
+
validate(concept, path, result) {
|
|
15
20
|
let sourceIdx = 0;
|
|
16
|
-
|
|
17
|
-
const sources = lc.sources
|
|
21
|
+
_eachLocalization(concept, (lang, lc) => {
|
|
22
|
+
const sources = lc.sources ?? [];
|
|
18
23
|
for (let i = 0; i < sources.length; i++) {
|
|
19
24
|
sourceIdx++;
|
|
20
25
|
const origin = sources[i].origin;
|
|
@@ -22,195 +27,254 @@ export class RefShapeRule extends ValidationRule {
|
|
|
22
27
|
|
|
23
28
|
const ref = origin.ref;
|
|
24
29
|
if (!ref) {
|
|
25
|
-
|
|
30
|
+
this.addIssue(result,
|
|
26
31
|
`${path}localizations.${lang}.sources[${i}].origin.ref`,
|
|
27
|
-
`source ${sourceIdx} origin has nil ref (expected Citation.Ref hash)
|
|
28
|
-
));
|
|
32
|
+
`source ${sourceIdx} origin has nil ref (expected Citation.Ref hash)`);
|
|
29
33
|
} else if (!ref.source && !ref.id) {
|
|
30
|
-
|
|
34
|
+
this.addIssue(result,
|
|
31
35
|
`${path}localizations.${lang}.sources[${i}].origin.ref`,
|
|
32
|
-
`source ${sourceIdx} origin.ref has neither source nor id
|
|
33
|
-
));
|
|
36
|
+
`source ${sourceIdx} origin.ref has neither source nor id`);
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
|
-
}
|
|
39
|
+
});
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
const related = value.related || [];
|
|
41
|
+
const related = concept.relatedConcepts ?? concept.related ?? [];
|
|
40
42
|
for (let i = 0; i < related.length; i++) {
|
|
41
43
|
const ref = related[i].ref;
|
|
42
44
|
if (!ref) continue;
|
|
43
45
|
if (!ref.source && !ref.id) {
|
|
44
|
-
|
|
46
|
+
this.addIssue(result,
|
|
45
47
|
`${path}related[${i}].ref`,
|
|
46
|
-
`related concept ${i + 1} has empty ref (no source or id)
|
|
47
|
-
));
|
|
48
|
+
`related concept ${i + 1} has empty ref (no source or id)`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
return errors;
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
/**
|
|
56
|
-
* GLS-308: Locality must have type and reference_from when present.
|
|
57
|
-
*/
|
|
58
54
|
export class LocalityCompletenessRule extends ValidationRule {
|
|
59
55
|
constructor() { super('locality-completeness', 'warning'); }
|
|
60
56
|
|
|
61
|
-
validate(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for (const [lang, lc] of Object.entries(localizations)) {
|
|
66
|
-
const sources = lc.sources || [];
|
|
57
|
+
validate(concept, path, result) {
|
|
58
|
+
_eachLocalization(concept, (lang, lc) => {
|
|
59
|
+
const sources = lc.sources ?? [];
|
|
67
60
|
for (let i = 0; i < sources.length; i++) {
|
|
68
61
|
const origin = sources[i].origin;
|
|
69
62
|
if (!origin || !origin.locality) continue;
|
|
70
63
|
|
|
71
64
|
const loc = origin.locality;
|
|
72
65
|
if (!loc.type) {
|
|
73
|
-
|
|
66
|
+
this.addIssue(result,
|
|
74
67
|
`${path}localizations.${lang}.sources[${i}].origin.locality.type`,
|
|
75
|
-
`source locality has no type
|
|
76
|
-
));
|
|
68
|
+
`source locality has no type`);
|
|
77
69
|
}
|
|
78
|
-
if (!loc.reference_from) {
|
|
79
|
-
|
|
70
|
+
if (!loc.reference_from && !loc.referenceFrom) {
|
|
71
|
+
this.addIssue(result,
|
|
80
72
|
`${path}localizations.${lang}.sources[${i}].origin.locality.reference_from`,
|
|
81
|
-
`source locality has no reference_from
|
|
82
|
-
));
|
|
73
|
+
`source locality has no reference_from`);
|
|
83
74
|
}
|
|
84
75
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return errors;
|
|
76
|
+
});
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
79
|
|
|
91
|
-
/**
|
|
92
|
-
* GLS-017: Localization map consistency.
|
|
93
|
-
*/
|
|
94
80
|
export class LocalizationConsistencyRule extends ValidationRule {
|
|
95
81
|
constructor() { super('localization-consistency'); }
|
|
96
82
|
|
|
97
|
-
validate(
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const data = value.raw?.data || value;
|
|
101
|
-
|
|
83
|
+
validate(concept, path, result) {
|
|
84
|
+
const langs = _langs(concept);
|
|
85
|
+
const data = concept.raw?.data || concept;
|
|
102
86
|
const declaredLangs = data.localized_concepts
|
|
103
87
|
? Object.keys(data.localized_concepts)
|
|
104
|
-
:
|
|
88
|
+
: langs;
|
|
105
89
|
|
|
106
90
|
for (const lang of declaredLangs) {
|
|
107
|
-
if (!localizations[lang]) {
|
|
108
|
-
|
|
91
|
+
if (!concept.hasLocalization?.(lang) && !(concept.localizations?.[lang])) {
|
|
92
|
+
this.addIssue(result,
|
|
109
93
|
`${path}localizations.${lang}`,
|
|
110
|
-
`localized_concepts map has '${lang}' but no localization loaded
|
|
111
|
-
));
|
|
94
|
+
`localized_concepts map has '${lang}' but no localization loaded`);
|
|
112
95
|
}
|
|
113
96
|
}
|
|
114
|
-
|
|
115
|
-
return errors;
|
|
116
97
|
}
|
|
117
98
|
}
|
|
118
99
|
|
|
119
|
-
/**
|
|
120
|
-
* GLS-010: Schema version should be 3.
|
|
121
|
-
*/
|
|
122
100
|
export class SchemaVersionRule extends ValidationRule {
|
|
123
101
|
constructor() { super('schema-version', 'warning'); }
|
|
124
102
|
|
|
125
|
-
validate(
|
|
126
|
-
const
|
|
127
|
-
const version = value.schemaVersion || value.schema_version;
|
|
103
|
+
validate(concept, path, result) {
|
|
104
|
+
const version = concept.schemaVersion ?? concept.schema_version;
|
|
128
105
|
|
|
129
106
|
if (version && String(version) !== '3') {
|
|
130
|
-
|
|
107
|
+
this.addIssue(result,
|
|
131
108
|
`${path}schema_version`,
|
|
132
|
-
`schema_version is '${version}', expected '3'
|
|
133
|
-
));
|
|
109
|
+
`schema_version is '${version}', expected '3'`);
|
|
134
110
|
}
|
|
135
|
-
|
|
136
|
-
return errors;
|
|
137
111
|
}
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
/**
|
|
141
|
-
* GLS-309: Domain references need concept_id or urn.
|
|
142
|
-
*/
|
|
143
114
|
export class DomainRefRule extends ValidationRule {
|
|
144
115
|
constructor() { super('domain-ref', 'warning'); }
|
|
145
116
|
|
|
146
|
-
validate(
|
|
147
|
-
const
|
|
148
|
-
const domains = value.domains || [];
|
|
117
|
+
validate(concept, path, result) {
|
|
118
|
+
const domains = concept.domains || [];
|
|
149
119
|
|
|
150
120
|
for (let i = 0; i < domains.length; i++) {
|
|
151
121
|
const domain = domains[i];
|
|
152
|
-
|
|
153
|
-
|
|
122
|
+
const json = typeof domain.toJSON === 'function' ? domain.toJSON() : domain;
|
|
123
|
+
if (!json.concept_id && !json.urn) {
|
|
124
|
+
this.addIssue(result,
|
|
154
125
|
`${path}domains[${i}]`,
|
|
155
|
-
`domain ${i + 1} has neither concept_id nor urn
|
|
156
|
-
));
|
|
126
|
+
`domain ${i + 1} has neither concept_id nor urn`);
|
|
157
127
|
}
|
|
158
128
|
}
|
|
159
|
-
|
|
160
|
-
return errors;
|
|
161
129
|
}
|
|
162
130
|
}
|
|
163
131
|
|
|
164
|
-
/**
|
|
165
|
-
* GLS-016: UUID format validation.
|
|
166
|
-
*/
|
|
167
132
|
export class UuidFormatRule extends ValidationRule {
|
|
168
133
|
constructor() { super('uuid-format'); }
|
|
169
134
|
|
|
170
|
-
validate(
|
|
171
|
-
const errors = [];
|
|
135
|
+
validate(concept, path, result) {
|
|
172
136
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
173
|
-
const id =
|
|
137
|
+
const id = concept.id || concept.uuid;
|
|
174
138
|
|
|
175
139
|
if (id && !UUID_RE.test(String(id))) {
|
|
176
|
-
// Only flag if it looks like it's supposed to be a UUID
|
|
177
140
|
if (String(id).includes('-') && String(id).length > 20) {
|
|
178
|
-
|
|
141
|
+
this.addIssue(result,
|
|
179
142
|
`${path}id`,
|
|
180
|
-
`concept ID '${id}' is not valid UUID format
|
|
181
|
-
));
|
|
143
|
+
`concept ID '${id}' is not valid UUID format`);
|
|
182
144
|
}
|
|
183
145
|
}
|
|
184
|
-
|
|
185
|
-
return errors;
|
|
186
146
|
}
|
|
187
147
|
}
|
|
188
148
|
|
|
189
|
-
/**
|
|
190
|
-
* GLS-310: URN format validation for sources.
|
|
191
|
-
*/
|
|
192
149
|
export class SourceUrnFormatRule extends ValidationRule {
|
|
193
150
|
constructor() { super('source-urn-format', 'warning'); }
|
|
194
151
|
|
|
195
|
-
validate(
|
|
196
|
-
const errors = [];
|
|
152
|
+
validate(concept, path, result) {
|
|
197
153
|
const URN_RE = /^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,\-.:=@;$_!*'%/?#]+$/i;
|
|
198
154
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const sources = lc.sources || [];
|
|
155
|
+
_eachLocalization(concept, (lang, lc) => {
|
|
156
|
+
const sources = lc.sources ?? [];
|
|
202
157
|
for (let i = 0; i < sources.length; i++) {
|
|
203
|
-
const source = sources[i].origin?.ref?.source;
|
|
158
|
+
const source = lc.sources[i].origin?.ref?.source;
|
|
204
159
|
if (!source || !source.startsWith('urn:')) continue;
|
|
205
160
|
if (!URN_RE.test(source)) {
|
|
206
|
-
|
|
161
|
+
this.addIssue(result,
|
|
207
162
|
`${path}localizations.${lang}.sources[${i}].origin.ref.source`,
|
|
208
|
-
`malformed URN '${source}'
|
|
209
|
-
));
|
|
163
|
+
`malformed URN '${source}'`);
|
|
210
164
|
}
|
|
211
165
|
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const CITE_MENTION_RE = /\{\{\s*cite:([^,}\s]+)[^}]*?\}\}/g;
|
|
171
|
+
|
|
172
|
+
function _findCiteMentions(concept) {
|
|
173
|
+
const mentions = [];
|
|
174
|
+
const walkText = (text, source) => {
|
|
175
|
+
if (typeof text !== 'string' || text.length === 0) return;
|
|
176
|
+
CITE_MENTION_RE.lastIndex = 0;
|
|
177
|
+
let m;
|
|
178
|
+
while ((m = CITE_MENTION_RE.exec(text)) !== null) {
|
|
179
|
+
mentions.push({ key: m[1].trim(), source });
|
|
212
180
|
}
|
|
181
|
+
};
|
|
213
182
|
|
|
214
|
-
|
|
183
|
+
for (const lang of _langs(concept)) {
|
|
184
|
+
const lc = _loc(concept, lang);
|
|
185
|
+
if (!lc) continue;
|
|
186
|
+
|
|
187
|
+
for (let i = 0; (lc.definitions ?? [])[i]; i++) {
|
|
188
|
+
walkText(lc.definitions[i]?.content, `localizations.${lang}.definitions[${i}].content`);
|
|
189
|
+
}
|
|
190
|
+
for (let i = 0; (lc.notes ?? [])[i]; i++) {
|
|
191
|
+
const content = typeof lc.notes[i] === 'object'
|
|
192
|
+
? (lc.notes[i]?.content ?? '')
|
|
193
|
+
: String(lc.notes[i] ?? '');
|
|
194
|
+
walkText(content, `localizations.${lang}.notes[${i}].content`);
|
|
195
|
+
}
|
|
196
|
+
for (let i = 0; (lc.examples ?? [])[i]; i++) {
|
|
197
|
+
walkText(lc.examples[i]?.content, `localizations.${lang}.examples[${i}].content`);
|
|
198
|
+
}
|
|
199
|
+
for (let i = 0; (lc.annotations ?? [])[i]; i++) {
|
|
200
|
+
walkText(lc.annotations[i]?.content, `localizations.${lang}.annotations[${i}].content`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return mentions;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function _findDuplicateSourceIds(concept) {
|
|
208
|
+
const seen = new Map();
|
|
209
|
+
const record = (source) => {
|
|
210
|
+
if (source?.id == null) return;
|
|
211
|
+
if (!seen.has(source.id)) seen.set(source.id, []);
|
|
212
|
+
seen.get(source.id).push(source);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
for (const source of (concept.sources ?? [])) record(source);
|
|
216
|
+
for (const lang of _langs(concept)) {
|
|
217
|
+
const lc = _loc(concept, lang);
|
|
218
|
+
if (!lc) continue;
|
|
219
|
+
for (const source of (lc.sources ?? [])) record(source);
|
|
220
|
+
for (const designation of (lc.terms ?? [])) {
|
|
221
|
+
for (const source of (designation.sources ?? [])) record(source);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const duplicates = new Map();
|
|
226
|
+
for (const [id, sources] of seen) {
|
|
227
|
+
if (sources.length > 1) duplicates.set(id, sources);
|
|
228
|
+
}
|
|
229
|
+
return duplicates;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _collectSourceIds(concept) {
|
|
233
|
+
const ids = new Set();
|
|
234
|
+
for (const source of (concept.sources ?? [])) {
|
|
235
|
+
if (source?.id != null) ids.add(source.id);
|
|
236
|
+
}
|
|
237
|
+
for (const lang of _langs(concept)) {
|
|
238
|
+
const lc = _loc(concept, lang);
|
|
239
|
+
if (!lc) continue;
|
|
240
|
+
for (const source of (lc.sources ?? [])) {
|
|
241
|
+
if (source?.id != null) ids.add(source.id);
|
|
242
|
+
}
|
|
243
|
+
for (const designation of (lc.terms ?? [])) {
|
|
244
|
+
for (const source of (designation.sources ?? [])) {
|
|
245
|
+
if (source?.id != null) ids.add(source.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return ids;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export class CiteRefIntegrityRule extends ValidationRule {
|
|
253
|
+
constructor() {
|
|
254
|
+
super('cite-ref-integrity', 'warning');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
validate(concept, path, result) {
|
|
258
|
+
// 1. Check unique source ids.
|
|
259
|
+
const duplicates = _findDuplicateSourceIds(concept);
|
|
260
|
+
for (const [id] of duplicates) {
|
|
261
|
+
this.addIssue(result,
|
|
262
|
+
`${path}sources`,
|
|
263
|
+
`duplicate source id "${id}" in concept "${concept.id ?? ''}"`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2. Check that every inline {{cite:<key>}} mention resolves.
|
|
267
|
+
const mentions = _findCiteMentions(concept);
|
|
268
|
+
if (mentions.length === 0) return;
|
|
269
|
+
|
|
270
|
+
const knownIds = _collectSourceIds(concept);
|
|
271
|
+
|
|
272
|
+
for (const { key, source } of mentions) {
|
|
273
|
+
if (!knownIds.has(key)) {
|
|
274
|
+
this.addIssue(result,
|
|
275
|
+
source,
|
|
276
|
+
`inline {{cite:${key}}} does not resolve to any source in concept "${concept.id ?? ''}"`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
215
279
|
}
|
|
216
280
|
}
|
|
@@ -1,34 +1,52 @@
|
|
|
1
|
+
import { ValidationError } from './validation-error.js';
|
|
2
|
+
|
|
1
3
|
export class ValidationResult {
|
|
2
4
|
constructor() {
|
|
3
|
-
this.
|
|
4
|
-
this.warnings = [];
|
|
5
|
+
this._issues = [];
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
get valid() {
|
|
8
|
-
return this.
|
|
9
|
+
return this._issues.filter(e => e.severity === 'error').length === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get errors() {
|
|
13
|
+
return this._issues.filter(e => e.severity === 'error');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get warnings() {
|
|
17
|
+
return this._issues.filter(e => e.severity === 'warning');
|
|
9
18
|
}
|
|
10
19
|
|
|
11
|
-
addError(message) {
|
|
12
|
-
|
|
20
|
+
addError(pathOrMessage, message) {
|
|
21
|
+
if (message === undefined) {
|
|
22
|
+
this._issues.push(new ValidationError('', pathOrMessage, 'error'));
|
|
23
|
+
} else {
|
|
24
|
+
this._issues.push(new ValidationError(pathOrMessage, message, 'error'));
|
|
25
|
+
}
|
|
13
26
|
return this;
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
addWarning(message) {
|
|
17
|
-
|
|
29
|
+
addWarning(pathOrMessage, message) {
|
|
30
|
+
if (message === undefined) {
|
|
31
|
+
this._issues.push(new ValidationError('', pathOrMessage, 'warning'));
|
|
32
|
+
} else {
|
|
33
|
+
this._issues.push(new ValidationError(pathOrMessage, message, 'warning'));
|
|
34
|
+
}
|
|
18
35
|
return this;
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
merge(other) {
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
if (other instanceof ValidationResult) {
|
|
40
|
+
for (const issue of other._issues) this._issues.push(issue);
|
|
41
|
+
}
|
|
24
42
|
return this;
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
toJSON() {
|
|
28
46
|
return {
|
|
29
47
|
valid: this.valid,
|
|
30
|
-
errors:
|
|
31
|
-
warnings:
|
|
48
|
+
errors: this.errors.map(e => e.toJSON ? e.toJSON() : e),
|
|
49
|
+
warnings: this.warnings.map(e => e.toJSON ? e.toJSON() : e),
|
|
32
50
|
};
|
|
33
51
|
}
|
|
34
52
|
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { ValidationError } from './validation-error.js';
|
|
2
|
-
|
|
3
1
|
export class ValidationRule {
|
|
4
2
|
constructor(name, severity = 'error') {
|
|
5
3
|
this.name = name;
|
|
6
4
|
this.severity = severity;
|
|
7
5
|
}
|
|
8
6
|
|
|
9
|
-
validate(_value, _path) {
|
|
7
|
+
validate(_value, _path, _result) {
|
|
10
8
|
throw new Error(`${this.constructor.name} must implement validate()`);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
addIssue(result, path, message) {
|
|
12
|
+
if (this.severity === 'warning') {
|
|
13
|
+
result.addWarning(path, message);
|
|
14
|
+
} else {
|
|
15
|
+
result.addError(path, message);
|
|
16
|
+
}
|
|
15
17
|
}
|
|
16
18
|
}
|