XSLT 2.0 and 3.0: design decision
Status: released, @tradik/xslt3 1.0.0 alongside
@tradik/xslt-processor 1.3.0. This page records how XSLT 2.0, XSLT 3.0 and
XPath 3.1 were added, why, and how far the engine conforms.
Decision
| Package | Covers | Dependencies |
|---|---|---|
@tradik/xslt-processor (this package) | XSLT 1.0, XPath 1.0, EXSLT: the browser XSLTProcessor with Chrome (libxslt) behaviour | none |
@tradik/xslt3 (new, packages/xslt3/ in this repository) | XSLT 3.0 and XPath 3.1, which also run version="2.0" stylesheets and version="1.0" ones in backwards-compatible mode | none at run time |
- One engine for 2.0 and 3.0. XSLT 3.0 is a superset of 2.0, and a 3.0 processor must accept 2.0 stylesheets (XSLT 3.0 section 3.9). A separate 2.0 engine would duplicate the data model, the function library and most instructions.
- 1.0 stays separate and unchanged. Pages and applications that replace
the browser's native
XSLTProcessorneed libxslt's XSLT 1.0 behaviour, not a 3.0 processor's backwards-compatible mode, which differs in details (errors,xsl:number, serialization). The 1.0 package keeps its zero dependencies, and nobody who uses 1.0 downloads or loads 2.0/3.0 code: the 1.0 bundles carry only the bridge that hands a 2.0/3.0 stylesheet over (about 4 kB of the minified browser bundle). - Opt-in bridge (done).
XSLTProcessortakesxsltVersion: "auto": a stylesheet that declares version 2.0 or 3.0 is then run by@tradik/xslt3, loaded withimport()only at that moment and only when it is installed (optional peer dependency). The asynchronous API loads it by itself; the synchronous W3C API needsawait XSLTProcessor.preload()first. The CLI flag is--xslt-version auto. Without the option nothing changes: aversion="2.0"stylesheet runs in XSLT 1.0 forwards-compatible mode, as in Chrome. The bridge lives insrc/bridge/of the 1.0 package; the API mapping is in API Reference. The standalonexsltexecutables embed@tradik/xslt3, so--xslt-version autoworks there without installing anything. - Inside
@tradik/xslt3, rarely used parts (regular expressions, date/number formatting, JSON) live in their own modules so bundlers andimport()keep them out of programs that do not use them.
Scope
Target: a basic XSLT 3.0 processor (XSLT 3.0 section 27) with XPath 3.1, maps, arrays, higher-order functions and JSON, and Serialization 3.1.
Not planned: schema awareness (xsl:import-schema with a schema), streaming
(xsl:mode streamable="yes" runs, without streaming guarantees), static
typing and XQuery.
Implementation-defined limits
| Item | Value |
|---|---|
xs:decimal and xs:integer | exact, unbounded (BigInt) |
Decimal division (div) | at least 18 fraction digits, more when an operand has more, rounded half-down |
xs:double, xs:float | IEEE 754 (JavaScript numbers; float through Math.fround) |
| Years | -999,999,999 to 999,999,999 (XSD 1.1: year 0 exists) |
| Implicit timezone | an option, default UTC |
xs:anyURI | lexical form not validated |
Milestones
| Milestone | Content |
|---|---|
| 0.1 | XPath 3.1 on its own: parser, data model (sequences, atomic types, casting), path expressions over DOM nodes, FLWOR, maps, arrays, inline and dynamic functions, the core function library |
| 0.2 | XSLT 2.0 instructions: grouping, xsl:analyze-string, xsl:function, typed variables, tunnel parameters, xsl:result-document, character maps |
| 0.3 | XSLT 3.0: xsl:iterate, xsl:try, text value templates, xsl:evaluate, accumulators, xsl:merge, static parameters and use-when, JSON and adaptive output |
| 1.0 | Conformance against the W3C test suites published, the bridge in @tradik/xslt-processor, benchmarks in BENCHMARKS.md: the 1.0 package against the new engine on the same stylesheets, 2.0/3.0-only scenarios, and Saxon-JS as the reference 3.0 processor in JavaScript |
Conformance
Measured with the W3C suites, fetched at test time and not committed: qt3tests for XPath 3.1 and its functions, and xslt30-test for XSLT 3.0 and 2.0. Pass rates per feature are published here as they come in.
Current results
| Suite | Stage | Applicable | Pass | Rate |
|---|---|---|---|---|
| qt3tests (XPath 3.1) | parsing and static analysis | 21,787 | 21,759 | 99.9% |
| qt3tests (XPath 3.1) | evaluation, all families | 21,787 | 21,770 | 99.9% |
| xslt30-test (XSLT 3.0 and 2.0) | transformation, all families | 7,914 | 7,772 | 98.2% |
The 20 remaining evaluation failures are collation-key with UCA collations
(JavaScript's Intl exposes no sort keys), fn:transform and
load-xquery-module (not offered by a basic processor), and three cases where
the DOM implementation used by the test runner (@xmldom/xmldom) does not
normalize xml:id, apply DTD default attributes or resolve external entities.
The parse stage misses only XQuery-only errors and static typing (XPST0005).
XSLT per family:
| Family | Applicable | Pass | Rate |
|---|---|---|---|
| fn (XSLT functions) | 1,117 | 1,108 | 99.2% |
| misc | 1,884 | 1,854 | 98.4% |
| expr (expressions in stylesheets) | 659 | 650 | 98.6% |
| attr (attributes and AVTs) | 1,002 | 981 | 97.9% |
| insn (instructions) | 1,412 | 1,381 | 97.8% |
| decl (declarations, packages) | 1,053 | 1,019 | 96.8% |
| type (types and conversions) | 787 | 779 | 99.0% |
Streaming (2,542 tests) and schema awareness are out of scope; stylesheets
that ask for streaming run without it. Of the 142 failures, about 20 expect
whitespace the test driver of the reference processor drops, some need what
the DOM cannot do (external entities, XInclude, inherit-namespaces="no"),
and about a dozen contradict the current specification (listed in
tasks of the repository, not counted as bugs). Five tests are not run: they
check warnings (assert-warning).
Using XSLT 3.0
import { compileStylesheet, serialize } from "@tradik/xslt3";
const parseXml = (text) =>
new DOMParser().parseFromString(text, "application/xml");
const stylesheet = compileStylesheet(
`<xsl:stylesheet version="3.0" expand-text="yes"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" html-version="5"/>
<xsl:template match="orders">
<ul>
<xsl:for-each-group select="order" group-by="@city">
<xsl:sort select="current-grouping-key()"/>
<li>{current-grouping-key()}: {sum(current-group()/@total)}</li>
</xsl:for-each-group>
</ul>
</xsl:template>
</xsl:stylesheet>`,
{ parseXml },
);
const result = stylesheet.transform({ source: parseXml(ordersXml) });
serialize([result.principal].flat(), result.output);
// <ul><li>Gdańsk: 80</li><li>Kraków: 162.75</li></ul>
transform() returns the principal result, the secondary results of
xsl:result-document (a Map by URI), the xsl:message output and the
serialization parameters of xsl:output. Recursion runs on an explicit work
stack, so templates nested 10,000 deep work (maxDepth sets the limit). An
XSLTProcessor class with the browser's method names is also exported.
The website's playground
has an XSLT 3.0 mode that runs compileStylesheet, transform and
serialize in the browser. Its examples cover xsl:for-each-group
(group-by, group-adjacent), xsl:analyze-string, typed xsl:functions,
text value templates, xsl:iterate, maps written as JSON, xsl:try,
xsl:merge, accumulators, xsl:result-document and a Muenchian 1.0
grouping rewritten; a link runs the same stylesheet with the 1.0 package for
comparison.
Two XSLT 3.0 features are on by default, as the specification intends, and can be turned off per transformation:
xsl:evaluatecompiles and runs XPath expressions built at run time, often from the source document. For untrusted input passtransform({ dynamicEvaluation: false }):xsl:evaluatethen runs itsxsl:fallbackchildren or raises XTDE3175.xsl:assert:transform({ assertions: false })skips the assertions.
More transform() options: textLoader, xmlParser and collections (as
for XPath: unparsed-text(), json-doc(), parse-xml(), collection()
read nothing unless you pass them), buildTree: false or xsl:output method="json"/"adaptive" to get the raw result sequence (a map, an array)
instead of a tree, and paramsAsUntyped: true to pass string parameters as
xs:untypedAtomic so that as="xs:integer" converts them, as on a command
line (untypedAtomic(value) is exported for single values). The
loadStylesheet and documentLoader options receive (uri, baseUri).
Errors carry the W3C code in code and, when known, location ({ module, line, column }), also appended to the message. fn:transform runs another
stylesheet from inside one, and xsl:number/format-integer write English
and German words and ordinals.
Without a source document (an initialTemplate run), results are built with
the DOM of the stylesheet document, so createDocument is needed only when
there is no DOM at all. For method="html" the default HTML version is
implementation-defined (XSLT 3.0 section 26.1): stylesheets declaring
version 1.0 or 2.0 get HTML 4.01 (no doctype, HTML 4 empty elements), as with
@tradik/xslt-processor and XSLT 1.0/2.0 processors; version 3.0 stylesheets
get HTML5 (<!DOCTYPE html>). Set html-version on xsl:output to choose.
Packages (xsl:use-package) are found through a resolver you pass to
compileStylesheet; it returns the package as a document, a string or
{ source, baseUri }, and versionMatches(version, range) (exported) helps it
pick a version:
const stylesheet = compileStylesheet(text, {
parseXml,
resolvePackage: (name, versionRange) => packages.get(name),
});
Using XPath 3.1
import { compileXPath, evaluateXPath } from "@tradik/xslt3";
evaluateXPath("sum((1, 2, 3)) * 2", null); // [xs:integer 12]
const query = compileXPath("//item[@price > $min]/@id ! string()", {
variables: ["min"],
});
query.evaluate(document, { variables: { min: 10 } }); // strings
Expressions cannot reach outside the data you give them unless you allow it:
doc() needs a documentLoader, unparsed-text() and json-doc() a
textLoader, collection() a collections option. To let trusted
expressions read local files in Node.js, pass the exported readFileUri:
import { evaluateXPath, readFileUri } from "@tradik/xslt3";
evaluateXPath("json-doc('file:///srv/data/config.json')?name", null, {
textLoader: readFileUri,
});
The website's playground
has an XPath 3.1 mode that runs this function in the browser, with examples
of for/let, sort, maps, regular expressions, formatting, => and
fold-left.
evaluateXPath keeps the 64 most recently compiled expressions (keyed by the
expression text and the variable names; used only when no other static option
is given), so calling it in a loop does not parse the expression each time.
Use compileXPath to control compilation yourself.
An evaluation assumes the trees it reads do not change while it runs: results
of //x and the document order are computed once per evaluation. Built-in
functions only create new trees; an extension function written in JavaScript
must not modify the DOM it is evaluated over.
Values are returned as data model items ({ type, value } for atomic values,
DOM nodes, maps, arrays and functions). JavaScript values passed as
variables are converted: string to xs:string, number to xs:double,
BigInt to xs:integer, boolean, Date to xs:dateTime, arrays to
sequences, plain objects and Maps to maps.