import * as plugins from '../../plugins.js'; import * as SaxonJS from 'saxon-js'; import type { ValidationResult } from './validation.types.js'; /** * Schematron validation options */ export interface SchematronOptions { phase?: string; // Schematron phase to activate parameters?: Record; // Parameters to pass to Schematron includeWarnings?: boolean; // Include warning-level messages maxErrors?: number; // Maximum errors before stopping } /** * Schematron validation engine using Saxon-JS * Provides official standards validation through Schematron rules */ export class SchematronValidator { private compiledStylesheet: any; private schematronRules: string; private isCompiled: boolean = false; constructor(schematronRules?: string) { this.schematronRules = schematronRules || ''; } /** * Load Schematron rules from file or string */ public async loadSchematron(source: string, isFilePath: boolean = true): Promise { if (isFilePath) { // Load from file const smartfile = await import('@push.rocks/smartfile'); this.schematronRules = await smartfile.SmartFile.fromFilePath(source).then(f => f.contentBuffer.toString()); } else { // Use provided string this.schematronRules = source; } // Reset compilation state this.isCompiled = false; } /** * Compile Schematron to XSLT using ISO Schematron skeleton */ private async compileSchematron(): Promise { if (this.isCompiled) return; // The Schematron to XSLT transformation requires the ISO Schematron skeleton // For now, we'll use a simplified approach with direct XSLT generation // In production, we would use the official ISO Schematron skeleton XSLTs try { // Convert Schematron to XSLT // This is a simplified version - in production we'd use the full ISO skeleton const xslt = this.generateXSLTFromSchematron(this.schematronRules); // Compile the XSLT with Saxon-JS this.compiledStylesheet = await SaxonJS.compile({ stylesheetText: xslt, warnings: 'silent' }); this.isCompiled = true; } catch (error) { throw new Error(`Failed to compile Schematron: ${error.message}`); } } /** * Validate an XML document against loaded Schematron rules */ public async validate( xmlContent: string, options: SchematronOptions = {} ): Promise { if (!this.schematronRules) { throw new Error('No Schematron rules loaded'); } // Ensure Schematron is compiled await this.compileSchematron(); const results: ValidationResult[] = []; try { // Transform the XML with the compiled Schematron XSLT const transformResult = await SaxonJS.transform({ stylesheetInternal: this.compiledStylesheet, sourceText: xmlContent, destination: 'serialized', stylesheetParams: options.parameters || {} }); // Parse the SVRL (Schematron Validation Report Language) output results.push(...this.parseSVRL(transformResult.principalResult)); // Apply options filters if (!options.includeWarnings) { return results.filter(r => r.severity !== 'warning'); } if (options.maxErrors && results.filter(r => r.severity === 'error').length > options.maxErrors) { return results.slice(0, options.maxErrors); } return results; } catch (error) { results.push({ ruleId: 'SCHEMATRON-ERROR', source: 'SCHEMATRON', severity: 'error', message: `Schematron validation failed: ${error.message}`, btReference: undefined, bgReference: undefined }); return results; } } /** * Parse SVRL output to ValidationResult array */ private parseSVRL(svrlXml: string): ValidationResult[] { const results: ValidationResult[] = []; // Parse SVRL XML const parser = new plugins.xmldom.DOMParser(); const doc = parser.parseFromString(svrlXml, 'text/xml'); // Get all failed assertions and successful reports const failedAsserts = doc.getElementsByTagName('svrl:failed-assert'); const successfulReports = doc.getElementsByTagName('svrl:successful-report'); // Process failed assertions (these are errors) for (let i = 0; i < failedAsserts.length; i++) { const assert = failedAsserts[i]; const result = this.extractValidationResult(assert, 'error'); if (result) results.push(result); } // Process successful reports (these can be warnings or info) for (let i = 0; i < successfulReports.length; i++) { const report = successfulReports[i]; const result = this.extractValidationResult(report, 'warning'); if (result) results.push(result); } return results; } /** * Extract ValidationResult from SVRL element */ private extractValidationResult( element: Element, defaultSeverity: 'error' | 'warning' ): ValidationResult | null { const text = element.getElementsByTagName('svrl:text')[0]?.textContent || ''; const location = element.getAttribute('location') || undefined; const test = element.getAttribute('test') || ''; const id = element.getAttribute('id') || element.getAttribute('role') || 'UNKNOWN'; const flag = element.getAttribute('flag') || defaultSeverity; // Determine severity from flag attribute let severity: 'error' | 'warning' | 'info' = defaultSeverity; if (flag.toLowerCase().includes('fatal') || flag.toLowerCase().includes('error')) { severity = 'error'; } else if (flag.toLowerCase().includes('warning')) { severity = 'warning'; } else if (flag.toLowerCase().includes('info')) { severity = 'info'; } // Extract BT/BG references if present const btMatch = text.match(/\[BT-(\d+)\]/); const bgMatch = text.match(/\[BG-(\d+)\]/); return { ruleId: id, source: 'EN16931', severity, message: text, syntaxPath: location, btReference: btMatch ? `BT-${btMatch[1]}` : undefined, bgReference: bgMatch ? `BG-${bgMatch[1]}` : undefined, profile: 'EN16931' }; } /** * Generate simplified XSLT from Schematron * This is a placeholder - in production, use ISO Schematron skeleton */ private generateXSLTFromSchematron(schematron: string): string { // This is a simplified transformation // In production, we would use the official ISO Schematron skeleton XSLTs // (iso_schematron_skeleton.xsl, iso_svrl_for_xslt2.xsl, etc.) // For now, return a basic XSLT that creates SVRL output return ` `; } /** * Check if validator has rules loaded */ public hasRules(): boolean { return !!this.schematronRules; } /** * Get list of available phases from Schematron */ public async getPhases(): Promise { if (!this.schematronRules) return []; const parser = new plugins.xmldom.DOMParser(); const doc = parser.parseFromString(this.schematronRules, 'text/xml'); const phases = doc.getElementsByTagName('sch:phase'); const phaseNames: string[] = []; for (let i = 0; i < phases.length; i++) { const id = phases[i].getAttribute('id'); if (id) phaseNames.push(id); } return phaseNames; } /** * Validate with specific phase activated */ public async validateWithPhase( xmlContent: string, phase: string, options: SchematronOptions = {} ): Promise { return this.validate(xmlContent, { ...options, phase }); } } /** * Factory function to create validator with standard Schematron packs */ export async function createStandardValidator( standard: 'EN16931' | 'XRECHNUNG' | 'PEPPOL' | 'FACTURX' ): Promise { const validator = new SchematronValidator(); // Load appropriate Schematron based on standard // These paths would point to actual Schematron files in production switch (standard) { case 'EN16931': // Would load from ConnectingEurope/eInvoicing-EN16931 await validator.loadSchematron('assets/schematron/en16931/EN16931-UBL-validation.sch'); break; case 'XRECHNUNG': // Would load from itplr-kosit/xrechnung-schematron await validator.loadSchematron('assets/schematron/xrechnung/XRechnung-UBL-validation.sch'); break; case 'PEPPOL': // Would load from OpenPEPPOL/peppol-bis-invoice-3 await validator.loadSchematron('assets/schematron/peppol/PEPPOL-EN16931-UBL.sch'); break; case 'FACTURX': // Would load from Factur-X specific Schematron await validator.loadSchematron('assets/schematron/facturx/Factur-X-EN16931-validation.sch'); break; } return validator; } /** * Hybrid validator that combines TypeScript and Schematron validation */ export class HybridValidator { private schematronValidator: SchematronValidator; private tsValidators: Array<{ validate: (xml: string) => ValidationResult[] }> = []; constructor(schematronValidator?: SchematronValidator) { this.schematronValidator = schematronValidator || new SchematronValidator(); } /** * Add a TypeScript validator to the pipeline */ public addTSValidator(validator: { validate: (xml: string) => ValidationResult[] }): void { this.tsValidators.push(validator); } /** * Run all validators and merge results */ public async validate( xmlContent: string, options: SchematronOptions = {} ): Promise { const results: ValidationResult[] = []; // Run TypeScript validators first (faster, better UX) for (const validator of this.tsValidators) { try { results.push(...validator.validate(xmlContent)); } catch (error) { console.warn(`TS validator failed: ${error.message}`); } } // Run Schematron validation if available if (this.schematronValidator.hasRules()) { try { const schematronResults = await this.schematronValidator.validate(xmlContent, options); results.push(...schematronResults); } catch (error) { console.warn(`Schematron validation failed: ${error.message}`); } } // Deduplicate results by ruleId const seen = new Set(); return results.filter(r => { if (seen.has(r.ruleId)) return false; seen.add(r.ruleId); return true; }); } }