v0.3.1 with Saxon fully working
This commit is contained in:
18
src/validation/validationTypes.ts
Normal file
18
src/validation/validationTypes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type DiagnosticSeverity = 'info' | 'warning' | 'error';
|
||||
|
||||
export interface DiagnosticMessage {
|
||||
severity: DiagnosticSeverity;
|
||||
message: string;
|
||||
source: string;
|
||||
from?: number;
|
||||
to?: number;
|
||||
}
|
||||
|
||||
export interface ParsedXmlDocument {
|
||||
document: Document;
|
||||
diagnostics: DiagnosticMessage[];
|
||||
}
|
||||
|
||||
export function hasErrors(diagnostics: DiagnosticMessage[]): boolean {
|
||||
return diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
||||
}
|
||||
93
src/validation/xmlValidation.ts
Normal file
93
src/validation/xmlValidation.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { DiagnosticMessage, ParsedXmlDocument } from './validationTypes';
|
||||
|
||||
function extractParserError(document: Document): string | null {
|
||||
const parserError = document.querySelector('parsererror');
|
||||
if (!parserError) return null;
|
||||
|
||||
return parserError.textContent?.trim().replace(/\s+/g, ' ') || 'Invalid XML.';
|
||||
}
|
||||
|
||||
export function parseXmlDocument(
|
||||
text: string,
|
||||
source = 'XML'
|
||||
): ParsedXmlDocument {
|
||||
const diagnostics: DiagnosticMessage[] = [];
|
||||
|
||||
if (!text.trim()) {
|
||||
diagnostics.push({
|
||||
severity: 'error',
|
||||
source,
|
||||
message: `${source} is empty.`,
|
||||
from: 0,
|
||||
to: 0,
|
||||
});
|
||||
|
||||
const emptyDocument = document.implementation.createDocument(null, null);
|
||||
return { document: emptyDocument, diagnostics };
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const parsed = parser.parseFromString(text, 'application/xml');
|
||||
const parserError = extractParserError(parsed);
|
||||
|
||||
if (parserError) {
|
||||
diagnostics.push({
|
||||
severity: 'error',
|
||||
source,
|
||||
message: parserError,
|
||||
from: 0,
|
||||
to: Math.min(text.length, 1),
|
||||
});
|
||||
}
|
||||
|
||||
return { document: parsed, diagnostics };
|
||||
}
|
||||
|
||||
export function validateXml(text: string, source = 'XML'): DiagnosticMessage[] {
|
||||
return parseXmlDocument(text, source).diagnostics;
|
||||
}
|
||||
|
||||
export function formatXml(text: string): string {
|
||||
const { document: parsed, diagnostics } = parseXmlDocument(text);
|
||||
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
||||
throw new Error(diagnostics[0]?.message ?? 'Cannot format invalid XML.');
|
||||
}
|
||||
|
||||
const serialized = new XMLSerializer().serializeToString(parsed);
|
||||
return prettyPrintXml(serialized);
|
||||
}
|
||||
|
||||
export function prettyPrintXml(xml: string): string {
|
||||
const compact = xml.replace(/>\s*</g, '><').trim();
|
||||
const withBreaks = compact.replace(/></g, '>\n<');
|
||||
const lines = withBreaks.split('\n');
|
||||
let indent = 0;
|
||||
|
||||
return `${lines
|
||||
.map((line) => {
|
||||
const trimmed = line.trim();
|
||||
const isClosing = /^<\//.test(trimmed);
|
||||
const isDeclaration = /^<\?/.test(trimmed);
|
||||
const isComment = /^<!--/.test(trimmed);
|
||||
const isSelfClosing = /\/\s*>$/.test(trimmed);
|
||||
const opensAndClosesSameLine = /^<[^!?/][\s\S]*>.*<\//.test(trimmed);
|
||||
|
||||
if (isClosing) indent = Math.max(indent - 1, 0);
|
||||
|
||||
const result = `${' '.repeat(indent)}${trimmed}`;
|
||||
|
||||
if (
|
||||
!isClosing &&
|
||||
!isDeclaration &&
|
||||
!isComment &&
|
||||
!isSelfClosing &&
|
||||
!opensAndClosesSameLine &&
|
||||
/^<[^!?/]/.test(trimmed)
|
||||
) {
|
||||
indent += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.join('\n')}\n`;
|
||||
}
|
||||
63
src/validation/xsltValidation.ts
Normal file
63
src/validation/xsltValidation.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { TransformEngineId } from '../transform/transformTypes';
|
||||
import { parseXmlDocument } from './xmlValidation';
|
||||
import type { DiagnosticMessage } from './validationTypes';
|
||||
|
||||
const XSLT_NAMESPACE = 'http://www.w3.org/1999/XSL/Transform';
|
||||
|
||||
export function validateXslt(
|
||||
text: string,
|
||||
engine: TransformEngineId = 'native-xsltprocessor'
|
||||
): DiagnosticMessage[] {
|
||||
const { document: stylesheet, diagnostics } = parseXmlDocument(
|
||||
text,
|
||||
'XSLT stylesheet'
|
||||
);
|
||||
|
||||
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
const root = stylesheet.documentElement;
|
||||
if (!root) {
|
||||
return [
|
||||
...diagnostics,
|
||||
{
|
||||
severity: 'error',
|
||||
source: 'XSLT stylesheet',
|
||||
message: 'The stylesheet has no document element.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
root.namespaceURI !== XSLT_NAMESPACE ||
|
||||
!['stylesheet', 'transform'].includes(root.localName)
|
||||
) {
|
||||
diagnostics.push({
|
||||
severity: 'error',
|
||||
source: 'XSLT stylesheet',
|
||||
message:
|
||||
'The root element should be xsl:stylesheet or xsl:transform in the XSLT namespace.',
|
||||
});
|
||||
}
|
||||
|
||||
const version = root.getAttribute('version');
|
||||
if (!version) {
|
||||
diagnostics.push({
|
||||
severity: 'warning',
|
||||
source: 'XSLT stylesheet',
|
||||
message: 'The stylesheet root should declare a version attribute.',
|
||||
});
|
||||
}
|
||||
|
||||
if (engine === 'native-xsltprocessor' && version && version !== '1.0') {
|
||||
diagnostics.push({
|
||||
severity: 'warning',
|
||||
source: 'XSLT stylesheet',
|
||||
message:
|
||||
'The native browser engine usually supports XSLT 1.0 only. XSLT 2.0/3.0 features may fail.',
|
||||
});
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
Reference in New Issue
Block a user