xslt 2.1.0

dotnet tool install --global xslt --version 2.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local xslt --version 2.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=xslt&version=2.1.0
                    
nuke :add-package xslt --version 2.1.0
                    

xslt

Command-line XSLT 3.0/4.0 processor for .NET. Transform XML documents from the terminal using the PhoenixmlDb XSLT engine.

Installation

dotnet tool install -g xslt

Usage

# Transform XML with a stylesheet
xslt stylesheet.xsl input.xml

# Write output to a file
xslt -o result.html report.xsl data.xml

# Start from a named template (no source needed)
xslt -it main generate.xsl

# Pass parameters
xslt -p year=2026 -p title="Report" style.xsl data.xml

# Read source from stdin
cat data.xml | xslt transform.xsl

# Show timing breakdown
xslt --timing style.xsl large-input.xml

# Validate a stylesheet without running
xslt --dry-run style.xsl

# Stream large files (lower memory)
xslt --stream style.xsl large-input.xml

Features

  • XSLT 3.0/4.0 — packages, streaming, maps/arrays, higher-order functions, JSON output
  • Multiple output methods — XML, HTML, XHTML, text, JSON, adaptive
  • Streaming — process large files without loading into memory
  • xsl:result-document — generate multiple output files in one transform
  • Parameters — pass values from the command line
  • Timing — built-in performance profiling
  • Tracing — log template matching, function calls, and built-in rules

Documentation

Full documentation at phoenixml.dev

License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
2.1.0 12 9/17/2026
2.0.0 51 9/15/2026
1.8.0 52 9/14/2026
1.7.0 60 9/11/2026
1.6.15 66 9/9/2026
1.6.14 76 9/7/2026
1.6.13 86 9/2/2026
1.6.12 84 8/30/2026
1.6.11 73 8/29/2026
1.6.10 80 8/27/2026
1.6.9 249 8/26/2026
1.6.8 72 8/26/2026
1.6.7 82 8/24/2026
1.6.6 86 8/23/2026
1.6.5 74 8/22/2026
1.6.4 103 8/16/2026
1.6.3 82 8/14/2026
1.6.2 77 8/13/2026
1.6.1 84 8/4/2026
1.6.0 82 7/31/2026
Loading failed

**Minor, but it breaks an API and it changes output.** Both are listed before the fixes, because a
consumer needs to read those two sections and can skip the rest.

Takes **PhoenixmlDb.XQuery 2.1.0** and **PhoenixmlDb.Core 2.0.0**. XQuery moves with this
release; Core runs its own cadence and did not.

### Breaking — one public API

`XsltStylesheet.StripSpace` and `XsltStylesheet.PreserveSpace` change type:

```csharp
public List<NameTest> StripSpace              // 2.0.0
public List<WhitespaceDeclaration> StripSpace // 2.1.0
```

`WhitespaceDeclaration` is `(NameTest Test, int ImportPrecedence)`. The precedence had nowhere to
live before, which is the defect below. **Only code that builds or reads the stylesheet AST
directly is affected** — callers that load a stylesheet and transform with it see no change.

### Breaking — behaviour

These change the output or the errors of stylesheets that previously appeared to work. In every
case the old behaviour was non-conformant.

- **A typed variable whose body produces the wrong number of items now raises `XTTE0570`, at all
 four binding seams** (#135). Three of the four were silent before. The worst: a **global**
 declared with a node type bound the **empty sequence** — so `empty()` was true, `xsl:if` took
 the other branch, `for-each` never ran its body, and nothing was raised. A stylesheet relying on
 that will now fail at the binding rather than produce quietly wrong output.
- **A content-bound global declared with an atomic type no longer leaks its items into the
 accumulator of the typed variable that first references it** (#132). Globals bind lazily on first
 reference, and that reference can occur inside another variable's body. Disclosed consequence: a
 global `as="xs:integer"` whose body yields two items used to bind the single value `"1 2"` and
 now binds two items. Neither form is conformant — #135 makes it an error.
- **`xsl:strip-space` and `xsl:preserve-space` in an imported module are now applied** (#137).
 Previously only the principal module's declarations and those of modules it `xsl:include`d took
 effect, so whitespace-only text nodes survived into the source tree and flowed into output, and
 emptiness tests (`xsl:where-populated`, `empty()`, `normalize-space()`) saw content that should
 not have been there. **DocBook xslTNG output changes**: the empty `<div class="db-bfs">` that
 Saxon does not emit is gone.
- **Conflicting whitespace declarations are resolved by import precedence first, then by pattern
 specificity** (#141), per §4.4. Previously specificity alone decided, so a more specific
 declaration in an imported module could beat a less specific one in the principal.
- **`xsl:map-entry` keys are atomized** (#131), per §11.6. The key expression's *node* was stored
 as the key, which made the entry **unreachable by any key at all** — a string lookup atomizes
 and cannot match a node, and passing the same node atomizes it too. `map:size()`, `map:keys()`
 and `map:for-each` all reported the map as correct while every lookup missed. Consequence:
 `map:for-each` now hands the callback an **atomic value** as `$k` where it previously handed a
 node, so `name($k)` or `$k/..` inside one will break.

### Fixed

- **A function with an atomic return type no longer returns `()` when the caller has an element
 open** (#129). This is **issue #4 reopening**, not a new regression: the March fix handled the
 flat shape, and the nested one — an `as="xs:string?"` body inside an open `xsl:copy` — was never
 in scope. It fails identically on 1.8.0, 2.0.0 and every build between, so no release regressed.

 **This is what makes SchXslt2 transpilation work at all.** Before it, no `.sch` could be
 transpiled on any build from 1.8.0 onward, the NEMSIS national rules included. Reported by
 Martin Honnen.
- **Martin's second report** — the DocBook xslTNG `XPTY0004` on `$hierarchical-uri` — is fixed by
 #132 above. Also pre-existing on 2.0.0.
- **`xsl:expose` raises `XTSE3010` and `XTSE3025`** for invalid visibility changes, and
 `names="f:abstract#0"` now matches: the arity suffix was being handed to the QName matcher (#127).
- **Attribute-set state is restored when expanding one fails** (#120).
- Earlier in this cycle: `xsl:where-populated` discards every item deemed empty rather than only
 zero-length strings (#118); `xsl:assert/@error-code` is evaluated as an AVT (#115); `XNode` and
 `XmlNode` parameters bind as navigable XDM nodes (#114); `XTTE0590` for a context item of the
 wrong type whatever `xsl:context-item/@use` says (#113); `current-dateTime`/`current-date`/
 `current-time` stay stable per transformation (#111); `XPST0081` for undeclared prefixes in
 XPath names (#110).

### Performance

**Per-call-site and per-context setup is paid once instead of per evaluation** (#116). Measured on
the Schematron corpus: the include step **3.81 → 2.70 minutes**, and the full compile
**5.93 → 4.68 minutes**.

**Both halves ship together.** The figures above are the XSLT evaluator alone. `PhoenixmlDb.XQuery`
2.1.0 (#67) applies the same change to the XQuery evaluator, and this release pins it, so what a
consumer actually installs is the pair:

| | Total compile | Include step |
|---|---|---|
| 2.0.0 | 5.93 min | 3.81 min |
| XSLT half alone | 4.68 min | 2.70 min |
| **2.1.0 as shipped** | **3.97 min** | **2.06 min** |

**46% off the include step.** Quoting either half on its own understates it by roughly half, which
is why the two engines are tagged as one train. All 231 compile-step outputs are byte-identical to
2.0.0's.

One behaviour change rides along from the XQuery side: `fn:implicit-timezone` is now stable within
a query and agrees with `fn:current-dateTime`, instead of reading the clock per call. See that
engine's 2.1.0 notes for its four error-code corrections, which apply to XPath evaluated here.

### Conformance

**10,347 / 10,839 W3C XSLT 3.0 cases (95.46%)**, measured on this commit, all eleven chunks.
Gains against the committed gate and no losses: `decl/expose` +6, `insn/call-template` +3,
`strm/si-map` +1, `type/maps` +1.

**Re-measured against the XQuery this release actually pins.** The conformance suite compiles
XQuery from source, not from the package, so the first run measured an engine pairing that was
never going to ship. Repeating it against XQuery 2.1.0 gives the same 10,347 / 10,839, case for
case — the XQuery half changes XSLT conformance by nothing, which is worth stating precisely
because it is not the same claim as "we did not look".

**The denominator moved, so this figure is not comparable with 2.0.0's** (#123). The harness ran
two sets the W3C catalog does not declare and skipped cases in one it does; set lists now match at
**260**. `fn/system-property-gen` contributes **166 cases, none passing** — a real feature gap
(compile-time XPath evaluation for shadow attributes, BUGS #47) that was previously invisible
rather than absent. Seven `si-map` skips matched by case name across two catalogs and were hiding
cases in both; that set now runs 12 rather than 5.

### Known limitations

- **`use-package` whitespace precedence is approximated** one level below the spec, with nested
 composition unmodelled (#142).
- **The conformance harness cannot check whitespace-only element content** — `assert-xml` parses
 at default `LoadOptions`, which discards it. About 22-24 assertions across 11-13 sets are
 unchecked for whitespace as a result (#140). Making the comparison whitespace-sensitive globally
 is not the fix: measured, it produces roughly a hundred false failures from indentation.
- **`insn/call-template-1003` returns different verdicts on identical code**, depending on
 available stack (BUGS #105). Its set is pinned at a floor rather than its measured value.