From 644a4c2045b328b4cf2777e2087ccc94edbd8999 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:43:52 -0800 Subject: [PATCH 1/3] Implementing configurable fetch: https://github.com/DesignLiquido/xslt-processor/issues/172 --- README.md | 33 +++++++++++++++++++++++++++++++++ src/xslt/types.ts | 3 ++- src/xslt/xslt.ts | 34 ++++++++++++++++++---------------- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e1e11ef..26491ee 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,39 @@ const xslt = new Xslt(options); - `name`: the parameter name; - `namespaceUri` (optional): the namespace; - `value`: the value. +- `fetchFunction` (`(uri: string) => Promise`, optional): a custom function for loading external resources referenced by `` and ``. Receives the URI and must return the fetched content as a string. Defaults to the global `fetch` API. This is useful for: + - Denying external loading entirely; + - Loading from the local filesystem or other non-HTTP sources; + - Transforming or remapping URIs before fetching. + +**`fetchFunction` examples:** + +```js +import { readFileSync } from 'fs'; + +// Deny all external loading +const xslt = new Xslt({ + fetchFunction: async (uri) => { + throw new Error(`External loading is not allowed: ${uri}`); + } +}); + +// Load from local filesystem +const xslt = new Xslt({ + fetchFunction: async (uri) => { + return readFileSync(uri, 'utf-8'); + } +}); + +// Remap URIs before fetching +const xslt = new Xslt({ + fetchFunction: async (uri) => { + const remapped = uri.replace('https://example.com/', '/local/stylesheets/'); + const response = await fetch(remapped); + return response.text(); + } +}); +``` #### JSON Output Format diff --git a/src/xslt/types.ts b/src/xslt/types.ts index a031e4c..17330b1 100644 --- a/src/xslt/types.ts +++ b/src/xslt/types.ts @@ -35,5 +35,6 @@ export type XsltOptions = { escape: boolean, selfClosingTags: boolean, outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json' | 'adaptive', - parameters?: XsltParameter[] + parameters?: XsltParameter[], + fetchFunction?: (uri: string) => Promise } diff --git a/src/xslt/xslt.ts b/src/xslt/xslt.ts index 2b50528..4b2d9ad 100644 --- a/src/xslt/xslt.ts +++ b/src/xslt/xslt.ts @@ -141,6 +141,13 @@ export class Xslt { decimalFormatSettings: XsltDecimalFormatSettings; warningsCallback: (...args: any[]) => void; + /** + * Custom fetch function for loading external resources (e.g. xsl:import, xsl:include). + * Takes a URI and returns the fetched content as a string. + * Defaults to using the global `fetch` API. + */ + fetchFunction: (uri: string) => Promise; + outputDocument: XDocument; outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json' | 'adaptive'; outputOmitXmlDeclaration: string; @@ -307,6 +314,16 @@ export class Xslt { this.firstTemplateRan = false; this.forwardsCompatible = false; this.warningsCallback = console.warn.bind(console); + this.fetchFunction = options.fetchFunction || (async (uri: string) => { + if (!global.globalThis.fetch) { + global.globalThis.fetch = fetch as any; + global.globalThis.Headers = Headers as any; + global.globalThis.Request = Request as any; + global.globalThis.Response = Response as any; + } + const response = await global.globalThis.fetch(uri); + return response.text(); + }); this.streamingProcessor = new StreamingProcessor({ xPath: this.xPath, version: '' @@ -2126,20 +2143,6 @@ export class Xslt { */ protected async xsltImportOrInclude(context: ExprContext, template: XNode, output: XNode | undefined, isImport: boolean) { const elementName = isImport ? 'xsl:import' : 'xsl:include'; - const [major, minor] = process.versions.node.split('.').map(Number); - if (major <= 17 && minor < 5) { - throw new Error(`Your Node.js version does not support \`<${elementName}>\`. If possible, please update your Node.js version to at least version 17.5.0.`); - } - - // We need to test here whether `window.fetch` is available or not. - // If it is a browser environemnt, it should be. - // Otherwise, we will need to import an equivalent library, like 'node-fetch'. - if (!global.globalThis.fetch) { - global.globalThis.fetch = fetch as any; - global.globalThis.Headers = Headers as any; - global.globalThis.Request = Request as any; - global.globalThis.Response = Response as any; - } const hrefAttributeFind = template.childNodes.filter(n => n.nodeName === 'href'); if (hrefAttributeFind.length <= 0) { @@ -2155,8 +2158,7 @@ export class Xslt { return; } - const fetchTest = await global.globalThis.fetch(href); - const fetchResponse = await fetchTest.text(); + const fetchResponse = await this.fetchFunction(href); const includedXslt = this.xmlParser.xmlParse(fetchResponse); // Track stylesheet metadata for apply-imports From cbedc3b87dbfe4c6a001e768aeece84155c6b263 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:03:05 -0800 Subject: [PATCH 2/3] Update src/xslt/xslt.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/xslt/xslt.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/xslt/xslt.ts b/src/xslt/xslt.ts index 4b2d9ad..4893e25 100644 --- a/src/xslt/xslt.ts +++ b/src/xslt/xslt.ts @@ -315,13 +315,19 @@ export class Xslt { this.forwardsCompatible = false; this.warningsCallback = console.warn.bind(console); this.fetchFunction = options.fetchFunction || (async (uri: string) => { - if (!global.globalThis.fetch) { - global.globalThis.fetch = fetch as any; - global.globalThis.Headers = Headers as any; - global.globalThis.Request = Request as any; - global.globalThis.Response = Response as any; + const globalFetch = + typeof globalThis !== 'undefined' && typeof (globalThis as any).fetch === 'function' + ? (globalThis as any).fetch + : null; + + if (!globalFetch) { + throw new Error( + 'No global fetch implementation available. ' + + 'Please provide options.fetchFunction or use a runtime that exposes globalThis.fetch.' + ); } - const response = await global.globalThis.fetch(uri); + + const response = await globalFetch(uri); return response.text(); }); this.streamingProcessor = new StreamingProcessor({ From 9d147f864545c65d0f7230e6ea82508126e7cc51 Mon Sep 17 00:00:00 2001 From: Leonel Sanches da Silva <53848829+leonelsanchesdasilva@users.noreply.github.com> Date: Wed, 11 Feb 2026 09:05:19 -0800 Subject: [PATCH 3/3] Unit tests for the new `fetch` function. --- tests/xslt/fetch-function.test.tsx | 78 ++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/xslt/fetch-function.test.tsx diff --git a/tests/xslt/fetch-function.test.tsx b/tests/xslt/fetch-function.test.tsx new file mode 100644 index 0000000..e7c86d1 --- /dev/null +++ b/tests/xslt/fetch-function.test.tsx @@ -0,0 +1,78 @@ +import assert from 'assert'; + +import { XmlParser } from "../../src/dom"; +import { Xslt } from "../../src/xslt"; + +describe('fetchFunction option', () => { + const xmlSource = ``; + + const includedStylesheet = ` + + included + + `; + + it('is called with the href URI on xsl:include', async () => { + const calledWith: string[] = []; + + const xsltSource = ` + + `; + + const xsltClass = new Xslt({ + fetchFunction: async (uri) => { + calledWith.push(uri); + return includedStylesheet; + } + }); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlSource); + const xslt = xmlParser.xmlParse(xsltSource); + const result = await xsltClass.xsltProcess(xml, xslt); + + assert.deepStrictEqual(calledWith, ['https://example.com/my-stylesheet.xsl']); + assert.equal(result, 'included'); + }); + + it('is called with the href URI on xsl:import', async () => { + const calledWith: string[] = []; + + const xsltSource = ` + + `; + + const xsltClass = new Xslt({ + fetchFunction: async (uri) => { + calledWith.push(uri); + return includedStylesheet; + } + }); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlSource); + const xslt = xmlParser.xmlParse(xsltSource); + const result = await xsltClass.xsltProcess(xml, xslt); + + assert.deepStrictEqual(calledWith, ['https://example.com/imported.xsl']); + assert.equal(result, 'included'); + }); + + it('propagates errors thrown from fetchFunction', async () => { + const xsltSource = ` + + `; + + const xsltClass = new Xslt({ + fetchFunction: async (uri) => { + throw new Error(`External loading denied: ${uri}`); + } + }); + const xmlParser = new XmlParser(); + const xml = xmlParser.xmlParse(xmlSource); + const xslt = xmlParser.xmlParse(xsltSource); + + await assert.rejects( + async () => await xsltClass.xsltProcess(xml, xslt), + { message: 'External loading denied: https://example.com/denied.xsl' } + ); + }); +});