xslt 2.1.0
dotnet tool install --global xslt --version 2.1.0
dotnet new tool-manifest
dotnet tool install --local xslt --version 2.1.0
#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 | Versions 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. |
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 |
**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.