@fin.cx/einvoice 6.2.0 → 7.0.1
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/einvoice.js +3 -2
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.d.ts +7 -0
- package/dist_ts/formats/ubl/xrechnung/xrechnung.encoder.js +19 -3
- package/dist_ts/formats/validation/conformance.harness.js +1 -1
- package/dist_ts/formats/validation/schematron.downloader.d.ts +43 -15
- package/dist_ts/formats/validation/schematron.downloader.js +122 -77
- package/dist_ts/formats/validation/schematron.integration.d.ts +5 -1
- package/dist_ts/formats/validation/schematron.integration.js +16 -40
- package/dist_ts/formats/validation/schematron.validator.d.ts +35 -15
- package/dist_ts/formats/validation/schematron.validator.js +136 -138
- package/dist_ts/formats/validation/schematron.worker.d.ts +12 -9
- package/dist_ts/formats/validation/schematron.worker.js +148 -108
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +7 -2
- package/dist_ts_install/download-schematron.js +6 -3
- package/dist_ts_install/index.js +15 -11
- package/package.json +6 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/einvoice.ts +2 -1
- package/ts/formats/ubl/xrechnung/xrechnung.encoder.ts +19 -2
- package/ts/formats/validation/conformance.harness.ts +1 -1
- package/ts/formats/validation/schematron.downloader.ts +146 -97
- package/ts/formats/validation/schematron.integration.ts +18 -44
- package/ts/formats/validation/schematron.validator.ts +181 -181
- package/ts/formats/validation/schematron.worker.ts +191 -135
- package/ts/plugins.ts +8 -0
- package/ts/vendor/modules.d.ts +0 -19
- package/dist_ts/vendor/saxonjs.d.ts +0 -23
- package/dist_ts/vendor/saxonjs.js +0 -9
- package/readme.hints.md +0 -1134
- package/readme.howtofixtests.md +0 -38
- package/readme.literature.md +0 -1
- package/readme.plan.md +0 -497
- package/ts/vendor/saxonjs.ts +0 -36
|
@@ -1,190 +1,207 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { ValidationResult } from './validation.types.js';
|
|
3
|
-
import { loadSaxonJS, type ISaxonJSTransformOptions, type ISaxonJSTransformResult } from '../../vendor/saxonjs.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Schematron validation options
|
|
7
6
|
*/
|
|
8
7
|
export interface SchematronOptions {
|
|
9
8
|
phase?: string; // Schematron phase to activate
|
|
10
|
-
parameters?: Record<string, any>; // Parameters to pass to Schematron
|
|
11
9
|
includeWarnings?: boolean; // Include warning-level messages
|
|
12
10
|
maxErrors?: number; // Maximum errors before stopping
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Turns one failed assertion or fired report into a ValidationResult.
|
|
15
|
+
*/
|
|
16
|
+
export function schematronFailureToValidationResult(
|
|
17
|
+
failure: plugins.smartxmlSchematron.ISchematronFailure,
|
|
18
|
+
): ValidationResult {
|
|
19
|
+
const flag = (failure.flag || failure.role || '').toLowerCase();
|
|
20
|
+
let severity: 'error' | 'warning' | 'info' = failure.kind === 'assert' ? 'error' : 'warning';
|
|
21
|
+
if (flag.includes('fatal') || flag.includes('error')) {
|
|
22
|
+
severity = 'error';
|
|
23
|
+
} else if (flag.includes('warning')) {
|
|
24
|
+
severity = 'warning';
|
|
25
|
+
} else if (flag.includes('info')) {
|
|
26
|
+
severity = 'info';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const btMatch = failure.message.match(/\[BT-(\d+)\]/);
|
|
30
|
+
const bgMatch = failure.message.match(/\[BG-(\d+)\]/);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
ruleId: failure.id || failure.patternId || 'UNKNOWN',
|
|
34
|
+
source: 'SCHEMATRON',
|
|
35
|
+
severity,
|
|
36
|
+
message: failure.message,
|
|
37
|
+
syntaxPath: failure.location,
|
|
38
|
+
expected: failure.test,
|
|
39
|
+
btReference: btMatch ? `BT-${btMatch[1]}` : undefined,
|
|
40
|
+
bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Turns a whole report into results. An expression the engine could not evaluate has
|
|
46
|
+
* proven nothing, so it is reported as an error rather than passing silently.
|
|
47
|
+
*/
|
|
48
|
+
export function schematronReportToValidationResults(
|
|
49
|
+
report: plugins.smartxmlSchematron.ISchematronReport,
|
|
50
|
+
): ValidationResult[] {
|
|
51
|
+
return [
|
|
52
|
+
...report.failures.map(schematronFailureToValidationResult),
|
|
53
|
+
...report.engineErrors.map((engineError) => ({
|
|
54
|
+
ruleId: 'SCHEMATRON-ENGINE-ERROR',
|
|
55
|
+
source: 'SCHEMATRON',
|
|
56
|
+
severity: 'error' as const,
|
|
57
|
+
message: `${engineError.message} (at ${engineError.origin})`,
|
|
58
|
+
expected: engineError.expression,
|
|
59
|
+
})),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
16
62
|
|
|
17
63
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
64
|
+
* Applies the reporting options of a validation call to a result list.
|
|
65
|
+
*/
|
|
66
|
+
export function applySchematronOptions(
|
|
67
|
+
results: ValidationResult[],
|
|
68
|
+
options: SchematronOptions,
|
|
69
|
+
): ValidationResult[] {
|
|
70
|
+
const filtered = options.includeWarnings
|
|
71
|
+
? results
|
|
72
|
+
: results.filter((result) => result.severity !== 'warning');
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
options.maxErrors &&
|
|
76
|
+
filtered.filter((result) => result.severity === 'error').length > options.maxErrors
|
|
77
|
+
) {
|
|
78
|
+
return filtered.slice(0, options.maxErrors);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return filtered;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Schematron validation engine.
|
|
86
|
+
*
|
|
87
|
+
* The rules are interpreted on the XPath engine of `@push.rocks/smartxml`, so the
|
|
88
|
+
* published `.sch` files of CEN, KoSIT and PEPPOL run as they are downloaded; no
|
|
89
|
+
* XSLT processor and no precompiled stylesheet is involved.
|
|
20
90
|
*/
|
|
21
91
|
export class SchematronValidator {
|
|
22
92
|
private schematronRules: string;
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
|
|
93
|
+
private schema?: plugins.smartxmlSchematron.Schematron;
|
|
94
|
+
private schemaError?: string;
|
|
95
|
+
private baseUri?: string;
|
|
96
|
+
/** one compiled schema per activated phase; a phase selects patterns at compile time */
|
|
97
|
+
private schemasByPhase = new Map<string, plugins.smartxmlSchematron.Schematron>();
|
|
98
|
+
|
|
27
99
|
constructor(schematronRules?: string) {
|
|
28
100
|
this.schematronRules = schematronRules || '';
|
|
101
|
+
if (this.schematronRules) {
|
|
102
|
+
this.compile();
|
|
103
|
+
}
|
|
29
104
|
}
|
|
30
|
-
|
|
105
|
+
|
|
31
106
|
/**
|
|
32
107
|
* Load Schematron rules from file or string
|
|
33
108
|
*/
|
|
34
109
|
public async loadSchematron(source: string, isFilePath: boolean = true): Promise<void> {
|
|
35
|
-
|
|
110
|
+
this.schematronRules = isFilePath ? await plugins.fs.readFile(source, 'utf-8') : source;
|
|
111
|
+
this.baseUri = isFilePath ? source : undefined;
|
|
112
|
+
this.compile();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compiles the loaded rules. A schema that does not compile keeps its reason, so
|
|
117
|
+
* validate() can report it instead of load failing far from the cause.
|
|
118
|
+
*/
|
|
119
|
+
private compile(): void {
|
|
120
|
+
this.schema = undefined;
|
|
121
|
+
this.schemaError = undefined;
|
|
122
|
+
this.schemasByPhase.clear();
|
|
36
123
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
124
|
+
const trimmed = this.schematronRules.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (trimmed.startsWith('{')) {
|
|
129
|
+
this.schemaError =
|
|
130
|
+
'the source is a compiled XSLT stylesheet (SEF); load the Schematron .sch file instead, it is executed directly';
|
|
131
|
+
return;
|
|
41
132
|
}
|
|
42
133
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
134
|
+
try {
|
|
135
|
+
this.schema = this.compileForPhase(undefined);
|
|
136
|
+
this.schemasByPhase.set('', this.schema);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.schemaError = error instanceof Error ? error.message : String(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
47
141
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
142
|
+
private compileForPhase(phase: string | undefined): plugins.smartxmlSchematron.Schematron {
|
|
143
|
+
return plugins.smartxmlSchematron.Schematron.fromString(this.schematronRules, {
|
|
144
|
+
phase,
|
|
145
|
+
baseUri: this.baseUri,
|
|
146
|
+
// the published rule sets split their patterns over sch:include files
|
|
147
|
+
resolveInclude: (href, base) =>
|
|
148
|
+
plugins.readFileSync(
|
|
149
|
+
plugins.path.resolve(plugins.path.dirname(base ?? this.baseUri ?? '.'), href),
|
|
150
|
+
'utf-8',
|
|
151
|
+
),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The schema for one phase. A phase activates a subset of the patterns, which the
|
|
157
|
+
* engine resolves while compiling, so each phase gets its own compiled schema.
|
|
158
|
+
*/
|
|
159
|
+
private schemaForPhase(phase?: string): plugins.smartxmlSchematron.Schematron {
|
|
160
|
+
const key = phase ?? '';
|
|
161
|
+
const cached = this.schemasByPhase.get(key);
|
|
162
|
+
if (cached) {
|
|
163
|
+
return cached;
|
|
54
164
|
}
|
|
165
|
+
const compiled = this.compileForPhase(phase);
|
|
166
|
+
this.schemasByPhase.set(key, compiled);
|
|
167
|
+
return compiled;
|
|
55
168
|
}
|
|
56
|
-
|
|
169
|
+
|
|
57
170
|
/**
|
|
58
171
|
* Validate an XML document against loaded Schematron rules
|
|
59
172
|
*/
|
|
60
173
|
public async validate(
|
|
61
174
|
xmlContent: string,
|
|
62
|
-
options: SchematronOptions = {}
|
|
175
|
+
options: SchematronOptions = {},
|
|
63
176
|
): Promise<ValidationResult[]> {
|
|
64
177
|
if (!this.schematronRules) {
|
|
65
178
|
throw new Error('No Schematron rules loaded');
|
|
66
179
|
}
|
|
67
|
-
|
|
68
|
-
const results: ValidationResult[] = [];
|
|
69
180
|
|
|
70
|
-
if (!this.
|
|
71
|
-
return [this.
|
|
181
|
+
if (!this.schema) {
|
|
182
|
+
return [this.createUnusableResult()];
|
|
72
183
|
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const transformOptions: ISaxonJSTransformOptions = {
|
|
76
|
-
sourceText: xmlContent,
|
|
77
|
-
destination: 'serialized',
|
|
78
|
-
stylesheetParams: options.parameters || {}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (this.stylesheetFileName) {
|
|
82
|
-
transformOptions.stylesheetFileName = this.stylesheetFileName;
|
|
83
|
-
} else {
|
|
84
|
-
transformOptions.stylesheetInternal = this.stylesheetInternal;
|
|
85
|
-
}
|
|
86
184
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
results.push(...this.parseSVRL(transformResult.principalResult));
|
|
93
|
-
|
|
94
|
-
// Apply options filters
|
|
95
|
-
if (!options.includeWarnings) {
|
|
96
|
-
return results.filter(r => r.severity !== 'warning');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (options.maxErrors && results.filter(r => r.severity === 'error').length > options.maxErrors) {
|
|
100
|
-
return results.slice(0, options.maxErrors);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return results;
|
|
185
|
+
let results: ValidationResult[] = [];
|
|
186
|
+
try {
|
|
187
|
+
results = schematronReportToValidationResults(
|
|
188
|
+
this.schemaForPhase(options.phase).validate(xmlContent),
|
|
189
|
+
);
|
|
104
190
|
} catch (error) {
|
|
105
191
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return results;
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
ruleId: 'SCHEMATRON-ERROR',
|
|
195
|
+
source: 'SCHEMATRON',
|
|
196
|
+
severity: 'error',
|
|
197
|
+
message: `Schematron validation failed: ${errorMessage}`,
|
|
198
|
+
},
|
|
199
|
+
];
|
|
115
200
|
}
|
|
201
|
+
|
|
202
|
+
return applySchematronOptions(results, options);
|
|
116
203
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Parse SVRL output to ValidationResult array
|
|
120
|
-
*/
|
|
121
|
-
private parseSVRL(svrlXml: string): ValidationResult[] {
|
|
122
|
-
const results: ValidationResult[] = [];
|
|
123
|
-
|
|
124
|
-
// Parse SVRL XML
|
|
125
|
-
const parser = new plugins.xmldom.DOMParser();
|
|
126
|
-
const doc = parser.parseFromString(svrlXml, 'text/xml');
|
|
127
|
-
|
|
128
|
-
// Get all failed assertions and successful reports
|
|
129
|
-
const failedAsserts = doc.getElementsByTagName('svrl:failed-assert');
|
|
130
|
-
const successfulReports = doc.getElementsByTagName('svrl:successful-report');
|
|
131
|
-
|
|
132
|
-
// Process failed assertions (these are errors)
|
|
133
|
-
for (let i = 0; i < failedAsserts.length; i++) {
|
|
134
|
-
const assert = failedAsserts[i];
|
|
135
|
-
const result = this.extractValidationResult(assert, 'error');
|
|
136
|
-
if (result) results.push(result);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Process successful reports (these can be warnings or info)
|
|
140
|
-
for (let i = 0; i < successfulReports.length; i++) {
|
|
141
|
-
const report = successfulReports[i];
|
|
142
|
-
const result = this.extractValidationResult(report, 'warning');
|
|
143
|
-
if (result) results.push(result);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return results;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Extract ValidationResult from SVRL element
|
|
151
|
-
*/
|
|
152
|
-
private extractValidationResult(
|
|
153
|
-
element: Element,
|
|
154
|
-
defaultSeverity: 'error' | 'warning'
|
|
155
|
-
): ValidationResult | null {
|
|
156
|
-
const text = element.getElementsByTagName('svrl:text')[0]?.textContent || '';
|
|
157
|
-
const location = element.getAttribute('location') || undefined;
|
|
158
|
-
const test = element.getAttribute('test') || '';
|
|
159
|
-
const id = element.getAttribute('id') || element.getAttribute('role') || 'UNKNOWN';
|
|
160
|
-
const flag = element.getAttribute('flag') || defaultSeverity;
|
|
161
|
-
|
|
162
|
-
// Determine severity from flag attribute
|
|
163
|
-
let severity: 'error' | 'warning' | 'info' = defaultSeverity;
|
|
164
|
-
if (flag.toLowerCase().includes('fatal') || flag.toLowerCase().includes('error')) {
|
|
165
|
-
severity = 'error';
|
|
166
|
-
} else if (flag.toLowerCase().includes('warning')) {
|
|
167
|
-
severity = 'warning';
|
|
168
|
-
} else if (flag.toLowerCase().includes('info')) {
|
|
169
|
-
severity = 'info';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Extract BT/BG references if present
|
|
173
|
-
const btMatch = text.match(/\[BT-(\d+)\]/);
|
|
174
|
-
const bgMatch = text.match(/\[BG-(\d+)\]/);
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
ruleId: id,
|
|
178
|
-
source: 'EN16931',
|
|
179
|
-
severity,
|
|
180
|
-
message: text,
|
|
181
|
-
syntaxPath: location,
|
|
182
|
-
btReference: btMatch ? `BT-${btMatch[1]}` : undefined,
|
|
183
|
-
bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined,
|
|
184
|
-
profile: 'EN16931'
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
204
|
+
|
|
188
205
|
/**
|
|
189
206
|
* Check if validator has rules loaded
|
|
190
207
|
*/
|
|
@@ -193,65 +210,48 @@ export class SchematronValidator {
|
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
/**
|
|
196
|
-
* Check
|
|
213
|
+
* Check whether the loaded rules compiled and can be executed.
|
|
197
214
|
*/
|
|
198
215
|
public canValidate(): boolean {
|
|
199
|
-
return
|
|
216
|
+
return Boolean(this.schema);
|
|
200
217
|
}
|
|
201
218
|
|
|
202
|
-
private
|
|
203
|
-
const trimmed = sourceContent.trim();
|
|
204
|
-
if (!trimmed) {
|
|
205
|
-
return 'none';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (isFilePath && /\.sef\.json$/i.test(source)) {
|
|
209
|
-
return 'sef';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (trimmed.startsWith('{')) {
|
|
213
|
-
return 'sef';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return 'schematron';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
private createUnavailableResult(): ValidationResult {
|
|
219
|
+
private createUnusableResult(): ValidationResult {
|
|
220
220
|
return {
|
|
221
|
-
ruleId: 'SCHEMATRON-
|
|
221
|
+
ruleId: 'SCHEMATRON-UNUSABLE',
|
|
222
222
|
source: 'SCHEMATRON',
|
|
223
223
|
severity: 'error',
|
|
224
|
-
message:
|
|
225
|
-
remediation: '
|
|
224
|
+
message: `Schematron rules could not be compiled: ${this.schemaError ?? 'the source is empty'}`,
|
|
225
|
+
remediation: 'Load a Schematron schema whose sch:include files sit next to it.',
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
/**
|
|
230
230
|
* Get list of available phases from Schematron
|
|
231
231
|
*/
|
|
232
232
|
public async getPhases(): Promise<string[]> {
|
|
233
233
|
if (!this.schematronRules) return [];
|
|
234
|
-
|
|
234
|
+
|
|
235
235
|
const parser = new plugins.xmldom.DOMParser();
|
|
236
236
|
const doc = parser.parseFromString(this.schematronRules, 'text/xml');
|
|
237
237
|
const phases = doc.getElementsByTagName('sch:phase');
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
const phaseNames: string[] = [];
|
|
240
240
|
for (let i = 0; i < phases.length; i++) {
|
|
241
241
|
const id = phases[i].getAttribute('id');
|
|
242
242
|
if (id) phaseNames.push(id);
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
return phaseNames;
|
|
246
246
|
}
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
/**
|
|
249
249
|
* Validate with specific phase activated
|
|
250
250
|
*/
|
|
251
251
|
public async validateWithPhase(
|
|
252
252
|
xmlContent: string,
|
|
253
253
|
phase: string,
|
|
254
|
-
options: SchematronOptions = {}
|
|
254
|
+
options: SchematronOptions = {},
|
|
255
255
|
): Promise<ValidationResult[]> {
|
|
256
256
|
return this.validate(xmlContent, { ...options, phase });
|
|
257
257
|
}
|
|
@@ -261,10 +261,10 @@ export class SchematronValidator {
|
|
|
261
261
|
* Factory function to create validator with standard Schematron packs
|
|
262
262
|
*/
|
|
263
263
|
export async function createStandardValidator(
|
|
264
|
-
standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX'
|
|
264
|
+
standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX',
|
|
265
265
|
): Promise<SchematronValidator> {
|
|
266
266
|
const validator = new SchematronValidator();
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
// Load appropriate Schematron based on standard
|
|
269
269
|
// These paths would point to actual Schematron files in production
|
|
270
270
|
switch (standard) {
|
|
@@ -285,7 +285,7 @@ export async function createStandardValidator(
|
|
|
285
285
|
await validator.loadSchematron('assets_downloaded/schematron/facturx/Factur-X-EN16931-validation.sch');
|
|
286
286
|
break;
|
|
287
287
|
}
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
return validator;
|
|
290
290
|
}
|
|
291
291
|
|
|
@@ -295,27 +295,27 @@ export async function createStandardValidator(
|
|
|
295
295
|
export class HybridValidator {
|
|
296
296
|
private schematronValidator: SchematronValidator;
|
|
297
297
|
private tsValidators: Array<{ validate: (xml: string) => ValidationResult[] }> = [];
|
|
298
|
-
|
|
298
|
+
|
|
299
299
|
constructor(schematronValidator?: SchematronValidator) {
|
|
300
300
|
this.schematronValidator = schematronValidator || new SchematronValidator();
|
|
301
301
|
}
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
/**
|
|
304
304
|
* Add a TypeScript validator to the pipeline
|
|
305
305
|
*/
|
|
306
306
|
public addTSValidator(validator: { validate: (xml: string) => ValidationResult[] }): void {
|
|
307
307
|
this.tsValidators.push(validator);
|
|
308
308
|
}
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
/**
|
|
311
311
|
* Run all validators and merge results
|
|
312
312
|
*/
|
|
313
313
|
public async validate(
|
|
314
314
|
xmlContent: string,
|
|
315
|
-
options: SchematronOptions = {}
|
|
315
|
+
options: SchematronOptions = {},
|
|
316
316
|
): Promise<ValidationResult[]> {
|
|
317
317
|
const results: ValidationResult[] = [];
|
|
318
|
-
|
|
318
|
+
|
|
319
319
|
// Run TypeScript validators first (faster, better UX)
|
|
320
320
|
for (const validator of this.tsValidators) {
|
|
321
321
|
try {
|
|
@@ -325,7 +325,7 @@ export class HybridValidator {
|
|
|
325
325
|
console.warn(`TS validator failed: ${errorMessage}`);
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
// Run Schematron validation if available
|
|
330
330
|
if (this.schematronValidator.hasRules()) {
|
|
331
331
|
try {
|
|
@@ -336,10 +336,10 @@ export class HybridValidator {
|
|
|
336
336
|
console.warn(`Schematron validation failed: ${errorMessage}`);
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
|
|
340
340
|
// Deduplicate results by ruleId
|
|
341
341
|
const seen = new Set<string>();
|
|
342
|
-
return results.filter(r => {
|
|
342
|
+
return results.filter((r) => {
|
|
343
343
|
if (seen.has(r.ruleId)) return false;
|
|
344
344
|
seen.add(r.ruleId);
|
|
345
345
|
return true;
|