type SefNode = Record & { N?: string; C?: SefNode[]; firstChild?: unknown; parentNode?: SefNode; }; type SaxonXdmMap = { inSituPut: (key: unknown, value: unknown[]) => void; }; let saxonLoadPromise: Promise | null = null; export function resolveSaxonScriptUrl( baseUrl = import.meta.env.BASE_URL, pageUrl = window.location.href ): string { const appBase = new URL(baseUrl, pageUrl); return new URL('vendor/saxon/SaxonJS2.js', appBase).href; } type SaxonPrivateRuntime = { getPlatform?: () => { resource?: (name: string) => unknown; parseXmlFromString?: (text: string) => unknown; }; checkOptions?: (options: Record) => Record; internalTransform?: ( stylesheetInternal: unknown, source: unknown, checkedOptions: Record ) => void; getResource?: (options: { file?: string; location?: string; text?: string; type?: 'xml' | 'json' | string; }) => Promise; XPath?: { sefToJSON?: (node: unknown, keepDebug: boolean) => unknown; }; XS?: { QName?: { fromParts: (prefix: string, uri: string, local: string) => unknown; }; }; XdmMap?: new () => SaxonXdmMap; transform?: ( options: Record, execution?: 'sync' | 'async' ) => Promise<{ principalResult?: unknown }>; }; async function getSaxon(): Promise { await ensureSaxonLoaded(); const saxon = window.SaxonJS as SaxonPrivateRuntime | undefined; if (!saxon) { throw new Error('window.SaxonJS is not loaded.'); } return saxon; } async function ensureSaxonLoaded(): Promise { if (window.SaxonJS) return; saxonLoadPromise ??= new Promise((resolve, reject) => { const saxonScriptUrl = resolveSaxonScriptUrl(); const existingScript = document.querySelector( `script[src="${saxonScriptUrl}"]` ); if (existingScript) { existingScript.addEventListener('load', () => resolve(), { once: true }); existingScript.addEventListener( 'error', () => reject(new Error('Failed to load SaxonJS2.js.')), { once: true } ); return; } const script = document.createElement('script'); script.src = saxonScriptUrl; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load SaxonJS2.js.')); document.head.appendChild(script); }); await saxonLoadPromise; } function addParentPointers(node: SefNode): void { node.C?.forEach((child) => { child.parentNode = node; addParentPointers(child); }); } function checksum(sef: SefNode): void { function hashString(value: string, seed: number): number { let current = seed << 8; for (let index = 0; index < value.length; index += 1) { current = (current << 1) + value.charCodeAt(index); } return current; } function hashPair(name: string, uri: string, seed: number): number { return hashString(name, seed) ^ hashString(uri, seed); } let hash = 0; let counter = 0; function visit(node: SefNode): void { hash ^= hashPair( String(node.N ?? ''), 'http://ns.saxonica.com/xslt/export', counter++ ); for (const [key, value] of Object.entries(node)) { if (key !== 'N' && key !== 'C' && key !== String.fromCharCode(931)) { hash ^= hashPair(key, '', counter); hash ^= hashString(String(value), counter); } } node.C?.forEach((child) => visit(child)); hash ^= 1; } visit(sef); sef[String.fromCharCode(931)] = ( hash < 0 ? 4294967295 + hash + 1 : hash ).toString(16); } function getFirstPrincipalNode(principalResult: unknown): SefNode { const value = Array.isArray(principalResult) ? principalResult[0] : principalResult; if (!value) { throw new Error('The SaxonJS compiler returned no principal result.'); } return value as SefNode; } function getSyntheticStylesheetBaseUri(): string { return new URL( '/__xsl-tools__/in-memory-stylesheet.xsl', window.location.href ).href; } function serializeSaxonResult(value: unknown): string { if (typeof value === 'string') { return value; } if (value instanceof XMLDocument || value instanceof Document) { return new XMLSerializer().serializeToString(value); } if (value instanceof Element) { return new XMLSerializer().serializeToString(value); } if (value instanceof DocumentFragment) { const container = document.createElement('div'); container.append( ...Array.from(value.childNodes).map((node) => node.cloneNode(true)) ); return container.innerHTML; } return String(value ?? ''); } async function parseXmlForSaxon( saxon: SaxonPrivateRuntime, xmlText: string ): Promise> { const baseUri = getSyntheticStylesheetBaseUri(); if (saxon.getResource) { try { const parsed = (await saxon.getResource({ text: xmlText, type: 'xml', })) as Record; parsed._saxonBaseUri = baseUri; parsed._saxonDocUri = baseUri; return parsed; } catch { // Some browser/runtime combinations only support parsing through the platform adapter. } } const parsed = saxon.getPlatform?.().parseXmlFromString?.(xmlText) as | Record | undefined; if (!parsed) { throw new Error( 'Could not parse XML for SaxonJS: neither getResource({ text }) nor platform.parseXmlFromString(...) is available.' ); } parsed._saxonBaseUri = baseUri; parsed._saxonDocUri = baseUri; return parsed; } export async function compileXsltTextToSefJson( xsltText: string, compilerOptions: Record = {} ): Promise { const saxon = await getSaxon(); const platform = saxon.getPlatform?.(); if (!platform?.resource) { throw new Error('SaxonJS platform.resource(...) is not available.'); } if (!saxon.checkOptions || !saxon.internalTransform) { throw new Error( 'SaxonJS internal compiler APIs are not available: checkOptions/internalTransform missing.' ); } if (!saxon.XPath?.sefToJSON) { throw new Error('SaxonJS.XPath.sefToJSON(...) is not available.'); } if (!saxon.XdmMap || !saxon.XS?.QName?.fromParts) { throw new Error('SaxonJS XdmMap/QName APIs are not available.'); } const compiler = platform.resource('compiler') as SefNode | undefined; if (!compiler || compiler.N !== 'package') { throw new Error( 'SaxonJS compiler resource is not available. You are probably loading SaxonJS2.rt.js, which contains only the runtime. Use the full SaxonJS2.js browser file for dynamic XSLT-to-SEF compilation.' ); } addParentPointers(compiler); const stylesheetParams = new saxon.XdmMap(); const staticParameters = new saxon.XdmMap(); stylesheetParams.inSituPut( saxon.XS.QName.fromParts('', '', 'staticParameters'), [staticParameters] ); const source = await parseXmlForSaxon(saxon, xsltText); const options = { destination: 'application', initialMode: 'compile-complete', isDynamicStylesheet: true, templateParams: { 'Q{}stylesheet-base-uri': getSyntheticStylesheetBaseUri(), 'Q{}options': { noXPath: false, ...compilerOptions, }, }, stylesheetParams, stylesheetInternal: compiler, outputProperties: {}, }; if (compiler.relocatable === 'true') { (options as Record).isRelocatableStylesheet = true; } const checkedOptions = saxon.checkOptions(options); saxon.internalTransform(compiler, source, checkedOptions); const sefXml = getFirstPrincipalNode(checkedOptions.principalResult); const sefJson = saxon.XPath.sefToJSON( sefXml.firstChild ?? sefXml, false ) as SefNode; checksum(sefJson); return JSON.stringify(sefJson); } export async function transformXmlWithDynamicSaxon( xmlText: string, xsltText: string ): Promise { const saxon = await getSaxon(); if (!saxon.transform) { throw new Error('SaxonJS transform API is not available.'); } const generatedSef = await compileXsltTextToSefJson(xsltText); const result = await saxon.transform( { stylesheetText: generatedSef, sourceText: xmlText, destination: 'serialized', }, 'async' ); return serializeSaxonResult(result.principalResult); }