v0.3.1 with Saxon fully working

This commit is contained in:
2026-06-05 03:19:04 +02:00
parent fc828363f0
commit 71f2f3d44b
51 changed files with 12683 additions and 1 deletions

View 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');
}

View 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`;
}

View 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;
}