######
XSLT
######
turbohtml ships an XSLT 1.0 processor in :mod:`turbohtml.transform`. This page explains how it is built on the existing
XPath engine and why it stops where it does.
************************
One evaluator, not two
************************
An XSLT processor is mostly an XPath processor with a template-dispatch loop wrapped around it. turbohtml already has a
compiled XPath 1.0 engine (:doc:`queries`), so the transform reuses it wholesale: every ``select`` expression, every
``test``, every ``use`` and ``value`` is compiled and evaluated by the same engine that backs
:meth:`~turbohtml.Node.xpath`. The XSLT layer adds only what XPath cannot express -- template matching, conflict
resolution, the instruction walk, and the result-tree serializer. This is why the whole engine is a single C file
alongside the XPath sources rather than a parallel path evaluator.
The XSLT-only functions ride the same seam. ``current()``, ``key()``, ``generate-id()``, ``format-number()``, and
``system-property()`` are not XPath core functions, so they are dispatched through the XPath engine's extension hook:
the evaluator, on an unknown function name, calls back into the XSLT layer, which resolves them against the current
node, the key tables, and the decimal-format rules. The engine never learns about XSLT; XSLT never re-implements
function dispatch.
************************
Patterns by membership
************************
XSLT matches a node against a pattern like ``chapter/para`` or ``para[1]``; XPath selects a node-set from a context. The
two meet through a definition in the spec: a node matches a pattern exactly when it is a member of the node-set the
pattern selects from some ancestor. turbohtml uses that directly. A pattern is rewritten to an equivalent absolute
expression (a relative pattern gains a leading ``//``), evaluated once against the source root, and the selected nodes
are recorded in a hash set. Testing whether a node matches is then an O(1) set membership test, and each pattern is
evaluated over the document only once regardless of how many nodes are dispatched against it. Because the source tree is
immutable for the duration of a transform, and XSLT 1.0 forbids variables and ``current()`` inside a pattern, the match
set is stable once built.
Conflict resolution (`section 5.5 `_) then reduces to ordering: rules are
sorted once by descending import precedence, then priority, then document position, so the first matching rule in that
order is the winner. Default priorities follow the spec -- ``0`` for a lone name test, ``-0.5`` for an unqualified node
test, ``0.5`` for anything with a predicate or more than one step -- computed from the pattern at compile time.
***********************
Stripping and imports
***********************
Two features reach outside the single-pass instruction walk. ``xsl:strip-space``/``xsl:preserve-space`` (`section 3.4
`_) remove whitespace-only text nodes from the *source* before any query runs, so
that ``position()``, ``last()``, and ``text()`` see the stripped tree the way the spec requires. turbohtml does this by
detaching the strippable text nodes from the source at the start of the transform and re-attaching them at the end, so
the caller's tree is a read-only participant that survives the call unchanged -- the same tree can drive many transforms
concurrently. A strip/preserve conflict on one element is resolved by import precedence then NameTest specificity, and
``xml:space="preserve"`` on an ancestor overrides both.
``xsl:import`` (`section 2.6.2 `_) does load other files, so the thin Python shim
resolves each ``href`` against the stylesheet's ``base_url`` and parses the imported sheets; the C engine then
deep-copies every sheet -- principal and imported -- into one private tree so all their declarations share a single atom
table, and walks them lowest import precedence first. Import precedence becomes the first key of the section 5.5
conflict resolution above. ``document()`` still loads nothing, to stay free of an arbitrary-URL fetch surface.
****************
Where it stops
****************
Two spec features stay out of reach for want of a supporting layer, not by design. Locale-aware ``xsl:sort`` collation
(``lang="de"``) needs a Unicode collation/ICU layer turbohtml does not carry, so sorting is Unicode-codepoint order.
``id()`` over DTD-declared ID attributes needs a DTD layer the XML parser does not have. Both are marked ``xfail`` in
the conformance suite with that reason. Everything else in XSLT 1.0 is modeled: whitespace stripping, ``xsl:import``
with import precedence, ``xsl:attribute-set``, ``xsl:namespace-alias``, multi-level ``xsl:number``,
``cdata-section-elements``, the auto-selected html output method, simplified stylesheets, and ``xsl:fallback``. A result
tree fragment converts to its string value when used through XPath (the strict XSLT 1.0 rule), and ``xsl:copy-of`` of a
fragment variable copies its nodes.
*******************
Conformance basis
*******************
The processor is validated against libxslt's own XSLT 1.0 Recommendation corpus -- the ``REC`` and ``REC2``
stylesheet/source/expected-output triples it ships, which are the worked examples from the spec run through the
reference implementation. ``tests/conformance/test_xslt_conformance.py`` runs turbohtml over every triple and asserts
byte-equal output (whitespace normalized per output method). Of the 79 cases, 76 pass exactly; the three that remain use
the locale-collation, DTD-id, and XPath namespace-axis layers turbohtml does not carry, and are marked ``xfail`` with
that reason. The corpus is the pinned ``tests/conformance/libxslt`` submodule.
For the XPath engine underneath, see :doc:`queries`; for a port from lxml, see :doc:`/migration/lxml`.