Skip to content

v1.1.0

Choose a tag to compare

@spagu spagu released this 10 Sep 22:27
· 35 commits to main since this release
739769f

Minor release: new public API (setStylesheetLoader, setDocumentLoader, transformToString, engine), the xsl:output serializer and the XSLT 1.0 conformance fixes below. Versions 1.0.4-1.0.8 were tagged but never published to npm, so this is the first npm release after 1.0.3.

Added

  • XSLTProcessor.setStylesheetLoader(loader) - public API for configuring the loader used to resolve xsl:import and xsl:include. It can be called before importStylesheet() (required, since the engine is created during import) or after it (the live engine is updated). Passing anything other than a function or null throws a TypeError. Returns the processor for chaining.

  • XSLTProcessor.engine - read-only getter exposing the underlying XsltEngine for advanced usage. Returns null until a stylesheet has been imported.

  • importStylesheet(style, stylesheetUri) - the optional second argument is now forwarded to the engine and used as the base URI when resolving relative xsl:import/xsl:include hrefs.

  • TypeScript declarations for the new API, including an exported StylesheetLoader type (synchronous: (href, baseUri?) => Document | string).

  • dist/xslt-processor.d.cts - CommonJS-flavoured declarations, wired through nested types conditions in package.json exports, so require() consumers under TypeScript node16/nodenext resolution no longer get the ESM declarations for the CommonJS bundle ("Masquerading as ESM" reported by @arethetypeswrong/cli). Verified with TypeScript 7.0.2 in strict mode under nodenext and bundler resolution.

  • Release workflow - publish job using npm Trusted Publishing (OIDC) with provenance; runs on v* tags after tests and build, and refuses to publish when the tag does not match package.json. Requires a one-time Trusted Publisher configuration on npmjs.com (documented in README).

  • XSLT-defined XPath functions - document(), key(), format-number(), current(), generate-id(), system-property(), function-available(), element-available() and unparsed-entity-uri(). Calling any of them previously raised Unknown function: X, which made transformToFragment()/transformToDocument() return null.

  • XSLTProcessor.setDocumentLoader(loader) (and XsltEngine.setDocumentLoader(loader), plus a documentLoader engine option) - synchronous loader for the XSLT document() function, returning a Document, an XML string or null. Same validation and chaining contract as setStylesheetLoader(). Missing loader or a null result yields an empty node-set instead of failing the transformation; document('') returns the stylesheet, node-set arguments are unioned, fragment identifiers are ignored and relative URIs resolve against the stylesheet URI.

  • XPathEvaluator.registerFunctions(map) - extension hook used to register the XSLT function library, keeping src/xpath a pure XPath 1.0 implementation. XPathContext gained an optional hostContext that is carried through predicate evaluation so host functions such as current() can reach the XSLT context.

  • xsl:apply-imports - previously reported as Unknown XSLT element. Applies only templates of lower import precedence in the same mode, falling back to the built-in rules.

  • xsl:strip-space / xsl:preserve-space - parsed since 1.0.0 but never applied. Whitespace-only text nodes are now removed from a copy of the source tree (the caller's document is never modified), honouring "most specific name test wins", xsl:preserve-space winning ties, and xml:space="preserve" on ancestors.

  • New focused modules with full test suites: src/xslt/functions.js, keys.js, formatNumber.js, number.js, numberFormat.js, whitespace.js, literalResult.js, resultTree.js, elements.js and uri.js.

  • CommonJS consumer smoke test (tests/cjs-smoke.cjs) exercising the built bundle through require() with jsdom, plus tests for the package entry point.

  • Output serializer (xsl:output, XSLT 1.0 section 16) - new src/xslt/serializer.js exporting serializeResult(node, outputSettings) plus the focused modules in src/xslt/serializer/ (baseWriter, xmlSerializer, htmlSerializer, textSerializer, escape, indent, namespaces, settings, rawText, constants).

    • method="xml" - XML declaration honoring encoding, version and standalone, omit-xml-declaration, doctype-public/doctype-system, minimal text and attribute escaping, <x/> for empty elements, namespace declarations emitted where first used and never twice, comments and processing instructions.
    • method="html" - no XML declaration, HTML doctype, void elements written as <br>, minimized boolean attributes, unescaped script/style content, >-terminated processing instructions, original element and attribute name case, no namespace declarations.
    • method="xhtml" - XML rules with void elements written as <br />.
    • method="text" - concatenation of all descendant text nodes, unescaped.
    • Automatic default method detection: html when the result document element is html in no namespace, xml otherwise.
    • indent="yes" - newline plus two-space indentation for element-only content; mixed content, cdata-section-elements and the HTML pre/script/style/textarea elements are left untouched.
    • cdata-section-elements - text children wrapped in <![CDATA[...]]>, split around any ]]> terminator.
    • disable-output-escaping="yes" on xsl:text and xsl:value-of is now honored; text nodes can also be marked explicitly with the exported markRawText() helper.
  • XSLTProcessor.transformToString(source) and XsltEngine.transformToString(sourceNode) - non-W3C convenience methods returning the serialized result. transformToFragment() and transformToDocument() are unchanged.

  • Public exports - serializeResult, markRawText, isRawText and resolveOutputSettings are exported from the package entry point, and transformToString/OutputSettings are declared in the generated TypeScript declarations.

  • CLI - bin/xslt.js now serializes through transformToString() instead of re-indenting with a regular expression, and gained --indent, --method <m> and --no-declaration flags that override the stylesheet xsl:output settings. Helpers were extracted to bin/lib/options.js and bin/lib/transform.js.

  • Tests - src/xslt/serializer.test.js, src/XSLTProcessor.serialization.test.js and src/cli.test.js (119 new tests, 560 in total), including the <xsl:output method="xml" indent="yes"/> regression from DesignLiquido/xslt-processor#219.

Fixed

  • TypeError: Cannot read properties of undefined (reading 'setStylesheetLoader') (#6) - the README documented processor.engine.setStylesheetLoader(...), but processor.engine was undefined and the engine did not exist before importStylesheet(). The documented workflow now works through processor.setStylesheetLoader(...).
  • README - rewrote the "Using xsl:import and xsl:include" section: the previous example used await inside a non-async callback and contained two unreachable "options". It now shows a correct synchronous loader, a browser pre-fetch pattern, and a Node.js filesystem example using path.resolve(path.dirname(baseUri), href).
  • xsl:copy lost attributes - the identity transform turned <i k="a">1</i> into <i>a1</i>. Attribute nodes were never matched by patterns such as @*|node() because attributes have no parentNode; patterns are now evaluated from the ownerElement, so <xsl:copy> on an attribute copies the attribute instead of falling back to the built-in text rule. The identity transform now round-trips elements, attributes, text, comments and processing instructions exactly.
  • CDATA sections were invisible - the string-value of an element containing a CDATA section was empty. CDATA nodes now count as text everywhere: string-value, the text() node test, xsl:value-of, xsl:copy-of and the built-in text template.
  • xsl:number level="any" always produced the same number (I, I instead of I, II). Counting was rewritten for single, multiple and any, including count, from and the 1, 01, a, A, i, I format tokens with prefixes, separators and suffixes.
  • xsl:namespace-alias produced wrong output (<ax:stylesheet xmlns:ax="xsl"/>). Aliases are now resolved against the namespace declarations in scope, so literal result elements and their attributes are emitted in the result namespace with the result prefix (or the default namespace for result-prefix="#default"), which makes stylesheet-generating stylesheets work.
  • xsl:use-attribute-sets on literal result elements was ignored. It now applies the same attribute sets as on xsl:element/xsl:copy (literal attributes still win), and xsl:* attributes (xsl:version, xsl:exclude-result-prefixes, xsl:extension-element-prefixes, xsl:use-attribute-sets) never leak into the result.
  • transformToFragment(xmlDoc, htmlDocument) lower-cased names and injected the XHTML namespace (<bar xmlns="http://www.w3.org/1999/xhtml"> for <BAR>). The result tree is now built in a neutral XML document and imported into the output document at the end, preserving names, namespaces and disable-output-escaping markers while keeping the W3C behaviour that the fragment is owned by the output document.
  • XPath function lookup no longer resolves inherited Object.prototype members, so expressions such as constructor() report Unknown function instead of invoking an object built-in.
  • setParameter() broke every transformation - the processor stored { value } in globalParameters, but the engine only understood { select } / { node } definitions and called processChildren(undefined), throwing Cannot read properties of undefined (reading 'childNodes') (so transformTo* returned null). A value set before importStylesheet() was silently overwritten by the xsl:param declaration. External values are now merged into the declaration and always win over the declared default. This also fixes the CLI -p name=value flag.
  • Union match patterns had the wrong default priority - match="@*|node()" was treated as one "complex" pattern with priority 0.5, so the identity template beat every match="name" template (priority 0). Per XSLT 1.0 section 5.5 a union pattern is now registered as one template rule per alternative, each with its own default priority; calculatePriority() moved to src/xslt/templatePriority.js and also recognises @name, @*, prefix:name, @prefix:*, child::/attribute:: axes and processing-instruction('literal').
  • transformToDocument()/transformToString() failed in Node.js without a global document (Document creation not available in this environment). The result document is now created from the DOM implementation of the source document when no global document exists, so jsdom/xmldom users no longer need to install a global.
  • xsl:output ignored version and standalone - both attributes are now parsed into outputSettings (version defaults to 1.0, standalone to null).

Changed

  • reset() keeps the configured stylesheet loader (it is processor configuration, not stylesheet state); pass null to setStylesheetLoader() to remove it. Documented in JSDoc and the README API table.

  • Updated VERSION in src/index.js to 1.1.0.

  • Fixed the package name in the generated declaration header (@tradik/xslt-processor).

  • xsl:include/xsl:import failures are rethrown with { cause } so the original loader/parser error and stack are preserved (ESLint 10 preserve-caught-error).

  • GitHub Actions bumped to actions/checkout@v7, actions/setup-node@v7, actions/upload-artifact@v7, docker/setup-buildx-action@v4; FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 enabled.

  • README "Publishing to npm" section rewritten to match the actual workflow (it previously claimed an NPM_TOKEN-based auto-publish that did not exist).

  • XsltEngine.namespaceAliases is now a NamespaceAliasMap keyed by namespace URI instead of a plain prefix-to-prefix object (internal API; the previous shape never produced correct output).

  • XsltEngine.countNumber() was replaced by countXsltNumber() in src/xslt/number.js; XsltEngine.formatNumber() and XsltEngine.toRoman() are kept as thin delegating wrappers.

  • XsltEngine.resolveUri() delegates to src/xslt/uri.js, which also recognises URIs with any scheme (not just http:/https:) as absolute.

  • system-property('xsl:version') returns the string "1" (previously the number 1); XPath converts it for arithmetic and comparisons, so output is unchanged.

  • Build targets raised from ES2020/Node 18 to ES2022/Node 20 (the code now uses Object.hasOwn and Array.prototype.at; browser bundle needs Chrome 92+, Firefox 92+, Safari 15.4+).

  • removeParameter() and clearParameters() now restore the xsl:param default of the stylesheet instead of deleting the declaration (which made $name an undefined variable). New engine helpers setParameterValue(), clearParameterValue() and clearParameterValues() back this.

  • -f, --format on the CLI is now an alias of --indent and drives the real serializer instead of the previous naive re-indentation.

Security

  • CLI path validation - input and output paths are resolved, canonicalized (realpathSync, so symbolic links cannot escape), confined to a trusted base directory (the working directory, or XSLT_BASE_DIR) and validated (regular file / existing parent directory, no NUL bytes) before any filesystem access (SonarCloud S8707). Paths outside the base directory are rejected with a hint.
  • Workflows - every GitHub Action is pinned to a full commit SHA; the publish job installs with npm ci --ignore-scripts.
  • Docker - the production image runs as the unprivileged node user.
  • js-yaml (transitive via eslint) - GHSA-5p4m-2wfm-xmqj, vulnerable >= 4.0.0, < 4.3.1. Resolved by upgrading eslint to 10.x, which no longer pulls @eslint/eslintrc/js-yaml at all; npm audit reports 0 vulnerabilities.
  • brace-expansion - GHSA-mh99-v99m-4gvg / GHSA-rgw5-rvv9-x895 (DoS), resolved via npm audit fix (now 5.0.9).

Dependencies

  • eslint ^9.0.0 -> ^10.10.0 (flat config unchanged; @eslint/js is now an explicit devDependency because ESLint 10 stopped bundling it).
  • jsdom ^25.0.0 -> ^29.1.1, esbuild ^0.28.0 -> ^0.28.2, prettier ^3.4.0 -> ^3.9.6.
  • Supported Node.js: engines.node raised from >=18.0.0 to >=20.19.0 (Node.js 18 and 20 are end-of-life; jsdom 29 needs 20.19+). CI matrix is now Node.js 22, 24 and 26; Docker images use node:26-alpine.