xslt 2.9.0

dotnet tool install --global xslt --version 2.9.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.9.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=xslt&version=2.9.0
                    
nuke :add-package xslt --version 2.9.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.

Requires PhoenixmlDb.XQuery 2.9.0 and PhoenixmlDb.Core 2.5.0. The processor is schema-aware by
default; the complete streamability analysis of the specification; streamed results that were
wrong are right; validation in variables and the XSLT error codes.

**W3C XSLT 3.0: 216 failing of 11,539.** From this release the conformance runner measures a
schema-aware processor, so the suite includes about 700 schema-aware cases that the run for 2.8.0
(131 failing of 10,839, as a basic processor) skipped. When the runner first measured the
schema-aware suite it was 437 failing; the schema work in this release brought it to 216. The two
figures are not comparable.

### Dates and times

- **The implicit timezone is a setting of the transformation**: `XsltTransformer.ImplicitTimezone`
 and `XsltTransformOptions.ImplicitTimezone`. It is the timezone a date, time or dateTime with
 none of its own is taken to be in (comparison, `xsl:sort`, `xsl:for-each-group`, `key()`),
 and the timezone of `current-dateTime()` and `implicit-timezone()`. Unset, it is the timezone
 of the machine as before; `TimeSpan.Zero` gives the same result on every machine.
 Transformations that run at the same time each have their own. Static expressions are
 evaluated when the stylesheet is compiled, in the timezone in effect then.
- **`key()` finds dates and times as `eq` compares them.** A dateTime with no timezone was
 taken to be in UTC, and a date or a time matched only one written with the same timezone.

### Schemas

- **A pattern that names a type matches by the type.** `element(*, T)`, `element(N, T)`,
 `attribute(N, T)`, `schema-element(N)` and `schema-attribute(N)` in a match pattern were
 parsed and the type not used, so the rule matched every node of the name. The type is now
 tested as `instance of` tests it; the test does not count as a predicate for the default
 priority.
- **`as="xs:int"`, `as="xs:NMTOKEN"` and the other derived built-in types are checked and
 cast as that type**: they were reduced to their base type, so a parameter `as="xs:int"` held
 a plain `xs:integer` and `$p instance of xs:int` was false.
- **`xsl:source-document validation="strict"` or `"lax"` validates the document it reads**,
 which then has the type annotations of the schema; the attribute was ignored. Such a
 document is read into a tree and not streamed.
- **`xsl:with-param select=` with an atomic `as=` type atomizes the nodes it selects** to
 their typed values; the nodes were passed on, so `$p instance of xs:int` was false.
- **`as=` may name an atomic type that a schema defines** (`as="my:partNumber"`); it was
 taken for `item()`, and the value kept the node it was selected as.
- **`xsl:copy-of validation="strict"` of an element from another document, in a variable**,
 validates the copy; such a copy was kept as it was.
- **A pattern may start with `element-with-id()`, or with `id()` given a second argument**
 (XSLT 3.0 §5.5.2); `element-with-id` was XTSE0340, and the second argument of `id` was
 read as part of the value.
- **`id()` finds an element of type `xs:ID` by the ID it holds**; only ID attributes were
 looked at.
- **A global `as="document-node()"` made by a validated `xsl:document` keeps its type
 annotations**; it was read again from its text, untyped.
- **`fn:copy-of`, `fn:snapshot` and `xsl:copy-of validation="preserve"` keep the type
 annotations of what they copy**; the copies were untyped, and a copy of a nilled element
 was not nilled.
- **An element validated as it is built in a typed variable keeps its annotations** when part
 of its content was copied in (`xsl:copy-of select="@*"`); the variable was read again from
 its text, untyped.
- **`xsl:result-document validation=` with no `href` validates the principal result as a
 document** (one element child, ID uniqueness); only a secondary result was validated.
- **`json-to-xml` with `validate: true()` gives a typed tree**, validated against the built-in
 schema for the XML representation of JSON; it was FOJS0004, as for a processor that is not
 schema-aware.
- **`XsltTransformer.PrincipalResultTree`** (new): the principal result as a tree with the
 type annotations that validation gave it, when the whole result was validated as it was
 built; the result was available only as text, and the text read again is untyped.
- **`xsl:attribute name="xml:lang"` (and `xml:space`, `xml:id`) with validation** is validated
 against the schema of the XML namespace; the `xml` prefix was not resolved (XTTE1512).
 `id()` finds `xml:id` in any tree, a temporary one included.
- **A strictly validated element with no declaration but an `xsi:type`** is validated against
 that type; it was XTTE1512.
- **A source whose DTD declares the entities its `xs:ENTITY` attributes name** is valid; each
 such value was "Reference to an undeclared entity".
- The base-URI marks a temporary tree's text carries are no longer seen by validation, which
 refused them as undeclared attributes.
- **`xsl:copy-of` of a document node with validation validates it as a document**: duplicate
 IDs and dangling IDREFs are XTTE1555; its content was validated as a fragment.
- **`lang()` is streamable** in a template of a streamable mode; under strict streamability
 it was XTSE3430.
- **`xsl:for-each select="head(//x)"`** (or `exactly-one`, `zero-or-one`) with a consuming body
 is streamable: one node at most is striding. Under strict streamability it was XTSE3430.
- **`xsl:evaluate schema-aware="yes"` may name a schema type with a prefix of the stylesheet**;
 the prefix was unbound (XPST0081). Without `schema-aware` such a type is XTDE3160.
- **The errors of the `type` attribute**: `type` with `validation` is XTSE1505; a `type` that
 is not an EQName or uses an undeclared prefix is XTSE1520 (it was XTSE0280);
 `xsl:attribute type=` naming a complex type is XTSE1530; a new attribute validated against
 `xs:QName` or `xs:NOTATION` is XTTE1545.
- **Typed modes**: a mode with `typed="no"` refuses a typed element or attribute (XTTE3110);
 a mode with `typed="strict"` refuses a rule whose pattern names an undeclared element
 (XTSE3105).
- **The name in `attribute(name, T)` of a pattern is not in `xpath-default-namespace`**; it
 was put there, so the rule matched the attribute of that namespace.
- **A `group-by` key that is a validated node is its typed value**; its string value was used,
 so `1.0` and `1` of type `xs:decimal`, or NOTATION values written with different prefixes,
 were separate groups.
- **`type="xs:untyped"` asks for no validation**; it was validated against `xs:untyped` and
 refused. An element typed `xs:untypedAtomic` may not have element children (XTTE1540).
- **`default-validation` other than preserve or strip is XTSE0020**; `strict` was XTSE1660.
- **An invalid `xsi:type` is XTTE1510** under lax validation too; it was XTTE1515.
- **A variable or parameter `as="schema-attribute(N)"` (or `schema-element(N)`) holds the node
 its body makes**; it was built as a temporary tree, and a document node cannot hold an
 attribute (XTDE0420).
- **A global variable with a schema-defined atomic `as=` type casts an untyped value to it**;
 it held the string.
- **`xsl:copy-of validation="preserve"` keeps annotations when it copies into a tree** too.
- **`fn:nilled` is true for a validated element with `xsi:nil="true"`**; it was always false.
- **`xsl:copy-of` of a document node with `validation="strict"` or `"lax"`, in a variable,
 validates the copy as a document**; it was kept untyped.
- **`xsl:value-of` of a validated node gives its typed value**: an element of type
 `xs:decimal` with the text `  1.10  ` gives `1.1`.
- **`input-type-annotations="strip"`** takes the type annotations away from a validated
 source; it was ignored.
- **An attribute made with `type=` or `validation="strict"` has its type annotation**, in a
 variable; it was validated and left untyped.
- **A global variable with no `as` keeps the annotations of what was validated in it**; it was
 built from its text, which is untyped.
- **A pattern may name an element or attribute called `union`** (`my:union`, `@union`): the
 name was taken for the union operator.
- **An inline schema with no target namespace is loaded.** An `xs:schema` child of
 `xsl:import-schema` with no `namespace` attribute was taken for the empty form of the
 declaration and skipped: validation against it found no declaration, and strict validation
 passed with nothing checked.
- **What a failing global variable wrote is not in the result.** A global variable whose body
 failed after it had written something, and that nothing used, left that output ahead of the
 result.
- **The processor is schema-aware by default.** A `type` attribute and
 `validation="strict"` are accepted in a stylesheet with no `xsl:import-schema`, and validate
 against the built-in types and the schemas of the schema provider; they were refused with
 XTSE1660. `system-property('xsl:is-schema-aware')` is `yes`. **A stylesheet that relied on
 XTSE1660 now compiles.** `XsltTransformer.SchemaAware = false` gives the behaviour of a basic
 processor again.
- **A validated element is not held to the ID constraints of a document.** An element with
 `[xsl:]validation` that had the same ID twice, or an IDREF to no ID, was refused. Those
 constraints are applied to a document that is validated (XTTE1555), and not to an element
 (XSLT 3.0 §25.4.1).
- **A mode with `typed="yes"` processes typed nodes** (XTTE3100). The check was in the built-in
 template rule only, and refused every element and document node, a validated one too. It is
 now made where `xsl:apply-templates` selects a node, and is about the type of the node: an
 element or attribute of type `xs:untyped` or `xs:untypedAtomic` is refused, whatever rule
 would match it.
- **A typed node bound with an atomic `as` type keeps its type.** With a validated source,
 `<xsl:variable name="v" select="elem" as="xs:int"/>` for an element of type `xs:int` held a
 plain `xs:integer`: the string value of the node was converted, not its typed value, so
 `$v instance of xs:int` was false.
- **The source document can be validated**: `XsltTransformer.SourceValidation` and
 `XsltTransformOptions.SourceValidation` (`Strict` or `Lax`). The source is validated against
 the schemas of the transformer's schema provider before the transformation; a source that
 is not valid fails it, and the stylesheet sees typed nodes
 (`. instance of schema-element(o:order)`, `data(@n) instance of xs:integer`, a mode with
 `typed="yes"`). Unset, the source is untyped as before. A validated source is built into a
 tree and not streamed.
- **A type of a schema with no target namespace can be named** (#359). A type name with no
 prefix is a type in the default namespace for elements and types (`xpath-default-namespace`,
 or no namespace): `'7' castable as small`, `cast as`, `instance of`, `treat as` and `as="small"`
 were XPST0051 for a type `small` of an imported schema with no target namespace.
- **An imported schema is compiled once for all transformers** (#355). The default schema
 provider of `XsltTransformer` took nothing from the cache of compiled schemas, because it
 holds the schema of the XML namespace; each `XsltTransformer` read and compiled every
 `xsl:import-schema` again. A schema from files is now compiled once and used until one of
 its documents changes. Needs PhoenixmlDb.XQuery with `XsdSchemaProvider.KnowsXmlNamespace`.
- **`xsl:with-param` with a body and a declared type of one node passes that node** (#358).
 `<xsl:with-param name="p" as="element()"><e/></xsl:with-param>` passed a temporary tree, so
 the receiver had a document node and `$p instance of element()` was false. Character content
 with `as="text()"` or `as="node()"` is passed as a text node. W3C XSLT 3.0: 122 failing (was
 123).
- **An element built in a variable, a parameter or a function is validated** (#353). An
 element with `[xsl:]validation="strict"` or `"lax"` was validated only on its way to the
 principal result. Built in a variable it was not validated: invalid content passed, and the
 element had no type annotation, so `$v instance of schema-element(o:order)` and
 `$v/o:qty instance of element(*, xs:integer)` were false for valid content. It is now
 validated where it is built, and the variable holds the validated element. Needs
 PhoenixmlDb.XQuery with host prefixes in kind tests (`schema-element(o:order)`).
- **The `type` attribute validates against the type** (#354). `type` on `xsl:element`,
 `xsl:document` and `xsl:result-document`, and `xsl:type` on a literal result element, were
 read and dropped. The node is now validated against the named type, whatever is declared
 for its name; a node that is not valid is XTTE1540, and a type that no imported schema
 declares is XTSE1520. In a variable the element has the type, and its content is typed.
- **`type` on `xsl:attribute`, `xsl:copy` and `xsl:copy-of`** (#364). The value of a
 constructed or copied attribute is checked against the simple type (XTTE1540). A copied
 element, and each of several, is validated against the type. An attribute that is copied
 with a complex type is XTTE1535. A stylesheet with `type` on `xsl:copy` or `xsl:copy-of` was
 refused when it was compiled.
- **A document node that is validated must have one element child** (XTTE1550), and must
 satisfy the constraints on ID and IDREF (XTTE1555), for `xsl:document` and
 `xsl:result-document` with `validation` or `type` (XSLT 3.0 §25.4.2). A validated document
 with two elements, or with text beside its element, was accepted.
- **A validation failure has the error code of XSLT** (#354): XTTE1510 for
 `validation="strict"` and a node that is not valid, XTTE1512 for `validation="strict"` and
 no top-level declaration, XTTE1515 for `validation="lax"` and a node that is not valid
 (XSLT 3.0 §25.4.1). The code was XQDY0027, which belongs to XQuery's `validate` expression,
 so `xsl:catch errors="err:XTTE1510"` did not catch the error. **A stylesheet or a host that
 tests for XQDY0027 must test for the new codes.**

### Streaming

- **A template rule of a streamable mode gave wrong output for ordinary bodies, with no error**
 (#340). A body that reads the children of the matched element with no instruction to walk
 them (a variable, a conditional expression, `xsl:attribute select`, `xsl:try`, a `for` or `let`
 expression) lost the end tags of the elements around it, or its values. Such a body now runs
 on a buffered copy of the matched element and gives the result the tree gives. Bodies with
 `xsl:apply-templates`, `xsl:for-each`, `xsl:iterate` and the like stream as before.
- **The live reader is used only for body shapes shown to be right on it; every other body is
 buffered.** Until now a body streamed unless a check found a reason to buffer it, and a shape
 no check knew gave a wrong result with no error (#340, #343). The rule is now the other way
 round: a body streams when one walking instruction, registered loop or set of aggregates does
 all of its reading and the rest reads nothing below the context node; the README lists the
 shapes. Any other body runs on a buffered copy (the matched element for a template rule, the
 whole input for a body at the document node) and gives the tree result. Every shape on the
 list is run against the tree result in every position the list allows (more than 1,100 cases).
 Effect on memory: of about 600 distinct streamed bodies in the W3C streaming sets, 28 now
 buffer the whole input, all of them bodies that read the input with nothing to read it for
 them. W3C XSLT 3.0: 123 failing (was 130); seven streamed cases that were wrong are right.
- **Rules reached through `xsl:apply-templates` in a streamed rule.** In the rule of a child that
 `xsl:apply-templates` had handed over, `xsl:iterate` and `xsl:for-each-group` read the
 following siblings of the child as its children, and a body that needed a buffer left the
 element after it one end tag short. `xsl:apply-templates select="node()"` and
 `xsl:copy-of select="."` in a rule for the document node selected nothing.
- **`xsl:iterate` and `xsl:for-each-group` under the document node read the input record by
 record.** With the streamed document node as the context and a path of child steps as the
 select (`<xsl:iterate select="root/item">`, in a rule for `/` or in `xsl:source-document`),
 each selected element is in memory for its own iteration, or for its own group with
 `group-adjacent`, `group-starting-with` and `group-ending-with`. The whole input used to be
 read into memory first. `xsl:break` now stops the reading: of a generated input of two
 million records, a loop that breaks at the third reads less than one megabyte. `group-by`
 still needs the whole input.
- **`TransformAsync(Stream, TextWriter)` gave wrong output for many stylesheets, with no
 error.** This overload sent every stylesheet to the streaming pass with none of the checks
 the other overloads make, and sent output to the writer at each input event, also from inside
 an element the rule for the document node still had open: `<out>1;2;</out>` came out as
 `1;2;<out/>`. Of 261 rules for the document node in the shape tests, 142 were wrong through
 this overload and right through the others. It now makes the same checks (and reads the
 input whole where the streaming pass cannot answer), and holds the output of the rule for
 the document node until the rule ends. Every shape is now tested through this overload too.
- **`xsl:on-completion` is not run after `xsl:break` in a streamed `xsl:iterate`.** It was, in
 the rule of an element; the unstreamed loop and the specification do not.
- **A variable with a grounded value is no longer refused for reading the descendants.**
 `<xsl:variable name="n" select="count(//item)"/>` in a template of a streamable mode raised
 XTSE3430, though the same expression in `xsl:value-of` was accepted. The value is a number,
 not a streamed node. A variable that would hold streamed nodes (`select="//item"`) is refused
 as before.
- **`StrictStreamability` reports three more shapes** (#298): a step down from a parent or an
 ancestor (`../item`, `ancestor::a/b`); a leading `/` in the rule of an element (`//item`,
 `/root/item`), which is streamable only where the context item is the document node; and a
 comma with two consuming operands in one instruction (`<xsl:value-of select="foo, bar"/>`).
 Without the option nothing changes: these run on a tree or a buffered copy, as before.
- **Three more streamed results that were wrong with no error** (#343).
 A streamed `xsl:for-each select="foo"` ran its body for every child element, not only for
 `foo`. An aggregate in a text value template, such as `{count(root/item)}`, in a rule for the
 document node or in `xsl:source-document streamable="yes"`, was evaluated against an empty
 document and gave 0 or nothing; the same expression in `xsl:value-of` was right. A rule for
 the document node lost the element it wrote around a streamed `xsl:for-each`.
- **`StrictStreamability` is now the streamability analysis of the specification** (#337). It
 was one check (two readers of the children of the matched node). It now applies the posture
 and sweep rules of XSLT 3.0 §19.8 to every template rule of a streamable mode and to the body
 of every `xsl:source-document streamable="yes"`, and reports XTSE3430 for what is not
 guaranteed-streamable: a `preceding-sibling` or other roaming axis, `reverse()`, sorting
 streamed nodes, and so on. Off by default as before; without it such a construct runs on a
 buffered copy and gives the right result. A stylesheet that passed the old check can fail the
 new one. W3C XSLT 3.0: 130 failing (was 131), and no test that expects a result is refused.

### API

- **A host can turn `fn:transform` off** with `ResourcePolicyBuilder.AllowTransformFunction(false)`
 (from PhoenixmlDb.XQuery). It is on unless turned off, so nothing changes for a host that
 does not use it. Turned off, every way a stylesheet can call the function (by name, through a
 function item, `fn:apply`, `xsl:evaluate`, a static expression) fails with FOXT0001 before the
 nested stylesheet is read, and `function-available` and `function-lookup` do not report the
 function, at run time or in `use-when`.

### Imports and includes

- **`xsl:import` and `xsl:include` are resolved against the base URI in effect at the element**,
 which `xml:base` sets, and the host's resolver is asked for that location once. In 2.8.0 the
 resolver was asked twice, the first time for a location the stylesheet does not name.
- **A module that `use-when` excludes is not read**, and neither is anything it imports.
- **A relative schema location in a stylesheet with no base URI is an error**, not a location
 resolved against the working directory.
- **The requests the compiler makes name the module that makes them** (#335).
 `ResourceRequest.ModuleUri` was null for `xsl:import`, `xsl:include`, the schema documents of
 `xsl:import-schema` and the `parameter-document` of `xsl:output`. It is now the URI the module
 that holds the element was loaded at, which `xml:base` does not change. A host's resolver can
 so tell which module imports, as it already could for a load a module makes when it runs.

### How conformance is measured

- **The W3C streaming sets run with `StrictStreamability` on.** A conformance run measures what
 the specification requires of a streaming processor: a construct that is not
 guaranteed-streamable is XTSE3430 (XSLT 3.0 §19.10), not a silent fall back to a buffered copy.
 Nothing changes for users: the option stays off by default. The figure did not move when this
 was switched on (131 failing before and after, the same cases).