PhoenixmlDb.Xslt 1.6.14

There is a newer version of this package available.
See the version list below for details.
dotnet add package PhoenixmlDb.Xslt --version 1.6.14
                    
NuGet\Install-Package PhoenixmlDb.Xslt -Version 1.6.14
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PhoenixmlDb.Xslt" Version="1.6.14" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PhoenixmlDb.Xslt" Version="1.6.14" />
                    
Directory.Packages.props
<PackageReference Include="PhoenixmlDb.Xslt" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PhoenixmlDb.Xslt --version 1.6.14
                    
#r "nuget: PhoenixmlDb.Xslt, 1.6.14"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PhoenixmlDb.Xslt@1.6.14
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PhoenixmlDb.Xslt&version=1.6.14
                    
Install as a Cake Addin
#tool nuget:?package=PhoenixmlDb.Xslt&version=1.6.14
                    
Install as a Cake Tool

PhoenixmlDb.Xslt

XSLT 4.0 transformation engine for PhoenixmlDb — transform XML documents into HTML, JSON, CSV, text, or other XML formats.

Features

  • XSLT 3.0/4.0 — template matching, streaming, accumulators, packages, maps/arrays
  • All output methods — HTML5, XML, XHTML, JSON, text, CSV, adaptive
  • Multiple outputsxsl:result-document for multi-file generation
  • Full XPath 4.0 — 240+ built-in functions available in all expressions
  • Packages — reusable stylesheet libraries with visibility control

Quick example

using PhoenixmlDb.Xslt;

var transformer = new XsltTransformer();
await transformer.LoadStylesheetAsync(
    File.ReadAllText("style.xsl"),
    new Uri(Path.GetFullPath("style.xsl")));

transformer.SetParameter("title", "My Report");
var html = await transformer.TransformAsync(
    File.ReadAllText("data.xml"));

// Handle secondary outputs (xsl:result-document)
foreach (var (href, content) in transformer.SecondaryResultDocuments)
    File.WriteAllText(Path.Combine(outputDir, href), content);

See the full API overview in the README.

Package Description
PhoenixmlDb.Core Core types and XDM data model (dependency)
PhoenixmlDb.XQuery XQuery 4.0 query engine (dependency)
PhoenixmlDb.Xslt.Cli xslt command-line tool

Documentation

Full documentation at phoenixml.dev

License

Apache 2.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 64 9/17/2026
2.0.0 121 9/15/2026
1.8.0 206 9/14/2026
1.7.0 98 9/11/2026
1.6.15 172 9/9/2026
1.6.14 121 9/7/2026
1.6.13 160 9/2/2026
1.6.12 125 8/30/2026
1.6.11 110 8/29/2026
1.6.10 177 8/27/2026
1.6.9 106 8/26/2026
1.6.8 105 8/26/2026
1.6.7 109 8/24/2026
1.6.6 116 8/23/2026
1.6.5 132 8/22/2026
1.6.4 134 8/16/2026
1.6.3 105 8/14/2026
1.6.2 105 8/13/2026
1.6.1 126 8/4/2026
1.6.0 124 7/31/2026
Loading failed

### fn:current-output-uri() never had an implementation

Reported by Martin Honnen. The function returned the empty sequence unconditionally — no output,
no error — and the source said why:

```csharp
// Since we don't track output URIs for result-document (inline output),
return ValueTask.FromResult<object?>(Array.Empty<object>());
```

The tell was in the registration: every other context-dependent XSLT function is constructed with
the execution context, and this one was not, so it structurally could not know the answer.

Worth being precise about what was and was not broken. Writing to stdout, the empty sequence is
CORRECT — XSLT 3.0 §20.3.7 says the property is absent when no destination URI is known. It was
wrong the moment a destination existed: `xslt … -o result.xml` names one, and an
`xsl:result-document/@href` names another.

The host now supplies the base output URI (`XsltTransformer.SetBaseOutputUri`, set by the `xslt`
tool from `-o`), `xsl:result-document` reports its own destination for the duration of its body
with a relative `@href` resolved against that base, and the previous value is restored afterwards
so a following instruction still sees the principal destination. Absent a host-supplied URI the
behaviour is unchanged, because there the empty sequence is the right answer.

### A missing required attribute crashed instead of being diagnosed

`<xsl:key match="p" use=".//term"/>` — no `name` — produced
`Object reference not set to an instance of an object.` The parser reached for the attribute with
the null-forgiving `!`, which asserts a required attribute is present: an assertion that only
holds when there is nothing to report.

The same shape appeared 25 times in the parser. Searching the conformance logs for the runtime
message found eight reachable, five of them this cause — `xsl:key/@name`, `xsl:key/@match`,
`xsl:attribute-set/@name`, `xsl:call-template/@name` and `xsl:variable/@name`. All five now raise
XTSE0010 and name the attribute. The remaining dereferences are deliberately untouched: no test
reaches them, and several of those attributes are optional in context, so a blind sweep would risk
rejecting valid stylesheets.

### The conformance figures this project published were overstated

Not an engine change. A defect in our own test harness, described here because the numbers went
out under our name.

Tests that expect a specific error code were scored as passes whenever the transform threw
**anything at all**. The W3C corpora write the expected code as an attribute —
`<error code="XTSE0010"/>` — and both conformance runners read it from the element's text content,
which is empty for an empty element. The comparison was therefore always against `""`, and empty
matched everything. `fn:load-xquery-module`, which this engine does not implement, scored 4/4 on
the strength of throwing four times.

Fixing it cost 4.6 points. Reading the code from the right place gave most of that back — many
errors carry their code in a structured property rather than in the message text, so a
message-only comparison had been under-crediting correct behaviour just as hard as it
over-credited wrong behaviour.

| | published 2026-09-02 | corrected |
|---|---|---|
| W3C XSLT 3.0 | 10,224/10,630 (96.2%) | **10,034/10,630 (94.4%)** |
| W3C XQTS (XQuery) | 95.11% | **29,205/31,414 (92.97%)** |

Every XSLT conformance figure published before 2026-09-04 is overstated by roughly two points.
Historical entries in this file are left as written, with a correction banner at the top — they
record what was believed at the time. Runs now also report how many failures are "raised an error
with the wrong code", because that is a different and generally shallower defect than a wrong
result, and conflating them is what hid this in the first place.

### Casting and constructor functions leaked .NET exceptions

`xs:int('abc')` reported *"The input string 'abc' was not in a correct format."* — a raw
`FormatException`, not an XQuery error, and unmatchable against any expected code. The same
applied to numeric overflow and to casting from a type with no conversion at all.

A constructor function is defined by the spec as equivalent to a cast, so the two must report the
same codes; only the cast half had been wrapped. Both now translate: an invalid lexical form is
FORG0001, a value out of range is FOCA0002, an operand type with no conversion is XPTY0004.

Also fixed: casting to a gregorian type from an operand the spec does not permit — `xs:time` or a
different gregorian type — diagnosed a malformed lexical form for a cast that was never legal;
`fn:avg` reported XPTY0004 for boolean and anyURI operands where `fn:sum`, three lines away in the
same file, had always correctly reported FORG0006; and `fn:load-xquery-module` flattened every
failure to FOQM0002, discarding the analyzer's own classification.

Arrives via PhoenixmlDb.XQuery, so it reaches XSLT through the engine dependency.

### Source layout: one type per file

No behaviour change, and worth stating plainly because the diff is large. 37 files carrying five
or more top-level types are now one type per file, and the two files that resisted that —
`DefaultXsltExecutionContext` at 24,638 lines and `StylesheetParser` at 13,009 — are split into
partial classes grouped by concern rather than chopped by size. Largest file in either is now
about 5,000 lines.

The split was done with Roslyn rather than text matching, deliberately: this is an XSLT engine, so
string literals are full of AVT braces and brace-counting mis-locates member boundaries. It was
verified by comparing the member inventory before and after — 587 members, none missing, none
added, each matched on whitespace-normalised text — and by confirming that every field kept its
original relative order, which is the one way a partial split can change semantics.

One deliberate exception: a `file`-scoped helper became `internal`, because a file-scoped type
cannot survive being split away from its callers. It remains assembly-internal; no public surface
changed.


Thirteen more defects found by running XSpec's corpus. Stated in full, because a bare "129 to
139" reads better than the position warrants:

| | before | after |
|---|---|---|
| Suites run to completion | 129 | **139** of 284 (162 the runner can drive; 122 are XQuery or Schematron suites it does not) |
| Assertions passing | 1077 | **1152** of 1364 |
| Assertions failing | 212 | **209** |
| Suites not reaching completion | 33 | **23** |

So roughly **half** the corpus completes and **one assertion in seven still fails**. No suite lost
ground at any measured step. Measured with `phxspec --census` over the 284 top-level
`test/*.xspec` suites; a directory sweep picks up 783 and is not comparable.

### xsl:catch exposed almost none of fn:error()'s detail

`$err:value` was bound to a literal null, so the error object supplied as fn:error()'s third
argument simply vanished. Nothing ever failed loudly, because an empty sequence is a legal value
for the 1- and 2-argument forms — the wrong answer was indistinguishable from a right one. The
XQuery engine's own try/catch had bound it from the exception all along.

`$err:description` carried the engine's diagnostic rendering rather than the author's string: a
`[module:line] ` prefix and a trailing expression snippet, useful in a log and wrong in a
variable a stylesheet compares against the text it just passed to fn:error().

`$err:module` and `$err:line-number` were empty for every error a stylesheet raised itself,
because the location was read only from `XsltException` while fn:error() raises an
`XQueryException`. The evaluator already resolved that location to build its diagnostic prefix;
it now records it structurally as well.

### A failed XPath parse named neither the expression nor where it was

The message was ANTLR's raw output — "mismatched input '<EOF>' expecting {...}" followed by every
token the grammar would accept, well over a hundred of them. Against generated XSLT that is one
line hundreds of kilobytes wide, that identifies nothing. All four parse sites now append the
expression text and its origin. The first failure it was pointed at resolved in a single run,
after several that had not.

### The empty sequence has two representations, and coercion knew one

A typed `xsl:with-param` whose sequence-constructor body produced nothing bound **one empty
string**. `CoerceToType`'s per-item branch was guarded on `Length > 1`, so a zero-length array
fell through to the scalar arms where `StringValueOf(array)` manufactured an item. Which
representation a caller sees depends on how the parameter is spelled, not what it means:
`select="()"` yields null and always worked; a constructor body yields an empty array and did not.

### Nothing checked that a typed variable had ENOUGH items

Cardinality was enforced only in the "too many" direction. `as="xs:string+"` bound to a filter
that matched nothing simply succeeded, where XSLT 3.0 requires XTTE0570. The check is
type-independent — how many items there are is settled before what type they are — and a
zero-length string is explicitly one item, not an empty sequence.

### static-base-uri() inside xsl:function reported the wrong module

It returned the principal stylesheet's URI rather than the module the function is written in.
`xsl:template` pushed its module's base URI onto the static-base-uri stack; `xsl:function`
carried none and pushed nothing. Surfaced as XTSE0150 from an `fn:transform` that used
`static-base-uri()` to load itself and got a different document.

### Namespace nodes were judged against an element they would never join

XTDE0440 and XTDE0430 both describe "the element being constructed", but ran before the branch
that emits a free-standing namespace node — so a node merely being RETURNED, from a function
declared `as="namespace-node()*"`, was validated against whatever element happened to enclose the
call. Two such calls covering elements with different default namespaces were rejected as a
duplicate declaration.

### xsl:switch is not xsl:choose with a subject

Each `xsl:when/@test` supplies a sequence of candidate VALUES, and the branch is taken when the
switch operand equals one of them. The engine took the effective boolean value instead. That
raised FORG0006 on any `when` listing alternatives — and, far worse because it did not error, made
the FIRST branch match whatever the operand was, since the EBV of a non-empty string is true. The
existing coverage passed for exactly that reason and would have returned "Active" for an inactive
status. The `@select` shorthand on a branch was also parsed and ignored, so a branch matched and
then produced nothing.

### fn:transform's global-context-item arrived too late to be the focus

It was applied only as the focus for the initial template, which is established after global
variables have already been built. A stylesheet declaring `xsl:global-context-item` and reading
"." in a global raised XPDY0002 despite fn:transform having been handed the item. The two are
different things — one belongs to the template invocation, the other to the whole transformation —
and cannot share a field.

Relatedly, that condition raised XPDY0002 rather than the XTDE3086 the spec names. The check
existed in two of four entry points, and both of those tested only for a source document, so a
supplied global-context-item neither satisfied `use="required"` nor tripped `use="absent"`. All
four now share one implementation.

### xsl:result-document rejected a valid URI

`href="file:/dev/null"` — RFC 8089's minimal no-authority form, which Saxon accepts — aborted the
transform. The XTDE1500 read/write-overlap check combined the href against the base URI with the
throwing `Uri` constructor and turned any parse failure into XTDE1400. That check is overlap
detection, not a validity gate; it now parses with TryCreate throughout and reports XTDE1400 only
when the href is not a URI reference at all.

### xsl:include/@href ignored xml:base on the element carrying it

An href resolves against the effective base URI of its element, which `xml:base` overrides. The
parser used `XElement.BaseUri`, which reports the base of the DOCUMENT the element was read from
and knows nothing about `xml:base`. `ResolveEffectiveBaseUri` — already used for templates and
functions — simply was not called on this path.