Commit Graph

43 Commits

Author SHA1 Message Date
6c0afa3749 fix(xslt): composition cycles terminate; id() gains a per-document index
An include or import cycle recursed without bound and crashed the
host with a stack overflow, reachable from stylesheet data. The
compile chain tracks the active load chain by resolved href and drops
re-entry, while diamond composition keeps loading both copies with
their own import precedences per 2.6.2. id() builds a per-document
index on first use instead of re-walking the document per call,
matching key(). Cycle and diamond regression tests added.
2026-06-10 18:07:02 +02:00
3ea4e6f37d feat(xslt): top-level stylesheet parameters
PureXML.XSLT.transform accepts parameters overriding xsl:param globals
by name; xsl:variable globals are never overridable, with parameter
names recorded across imports and includes. The conformance runner
honors the xalan harness contract, reading <case>.param files (CRLF
is one Character in Swift, so the runner splits on newline graphemes,
which the param files exposed). Closes #141. Xalan baseline
128 -> 127.
2026-06-10 17:35:11 +02:00
c96933df89 feat(xslt): space-ignorable sort collation
xsl:sort's default text order treats spaces as ignorable at the
primary level with a later-first-space tie-break, the Java Collator
semantics the conformance golds encode, derived from the gold
orderings. Xalan baseline 129 -> 128.
2026-06-10 17:24:06 +02:00
62db18f5f6 feat(xslt): DTD-typed id() and node-kind generate-id()
The XSLT layer's id() follows the XPath definition exactly: only
attributes the source's DTD declares as type ID identify elements
(internal or external subset, via the transform's resolver), and a
document without ID declarations yields the empty set. generate-id()
produces non-empty, distinct identifiers for attribute and namespace
nodes through their identity hash. Lookup implementations move to
XSLTLookups.swift for the file caps. Xalan baseline 131 -> 129.
2026-06-10 17:03:39 +02:00
6174c7bde1 feat(xslt): global variable scope and precedence, NCName:* name tests
A top-level variable evaluates with every previously evaluated global
in scope, so referencing an earlier top-level param works, directly or
via call-template; same-name globals resolve by import precedence with
the last fold winning, so a later sibling import beats an earlier one;
the stylesheet fold uniformly favors later contributions, matching the
post-order precedence model; and the XPath compiler accepts NCName:*
name tests, matching any name in the prefix's namespace at the -0.25
default priority, so @ped:* outranks @*. Xalan baseline 144 -> 134
(variable 10 -> 5, conflictres 8 -> 5).
2026-06-10 16:38:03 +02:00
ef4e7afa7a feat(xslt): post-order import precedence, scoped apply-imports, relative hrefs
Import precedences are assigned in post-order over the import tree, so
sibling imports are distinct and every unit sits above its own
imports; apply-imports searches only the current template's
stylesheet's own import subtree per 5.6, reverting to the built-in
rule when that unit imports nothing; an included stylesheet's
declarations join the including unit at its precedence while its
imports join the import list; and include/import hrefs resolve against
the including stylesheet's URI. Included documents' roots stay
retained until their declarations are parsed (tree parents are weak,
so the in-scope xmlns chain could vanish mid-compile). Xalan baseline
159 -> 144 (reluri 11 -> 3, impincl 9 -> 2).
2026-06-10 16:27:06 +02:00
cff26b255d feat(xslt): attribute-set merging, copied namespaces, html booleans, loader entities
Same-name xsl:attribute-set declarations merge as ordered definitions
per 7.1.4, each expanding its used sets before its own attributes with
later definitions winning; xsl:copy carries the source element's
in-scope namespace nodes per 7.5; the namespace fixup lets a carried
attribute prefix shadow an inherited binding unless the element's own
name relies on it; the html method minimizes boolean attributes whose
value repeats their name; and external DTDs and entities in the
stylesheet or source resolve through the transform's document loader.
Four unit tests pin the behaviors. Xalan baseline 174 -> 159
(attribset 13 -> 4, copy 12 -> 7).
2026-06-10 16:11:02 +02:00
b84fdd00ef fix(xslt): no fatal traps on extreme format-number and xsl:number values
format-number of a value at or beyond 2^53 ran an unguarded
Double-to-Int conversion, a fatal trap reachable from user data; a
fraction picture beyond 15 places overflowed the scale conversion the
same way; and xsl:number value= beyond 2^53 trapped identically.
Digit extraction clamps fraction places to 15 (the 2^53 exactness
bound), pads back to the picture's minimum, renders out-of-range
integers through the exponent expander, and xsl:number falls back to
plain number rendering outside [1, 2^53). Sort-key rows always carry
one key per sort. Regression tests cover all three trap paths.
2026-06-10 15:35:36 +02:00
2b5080e30e fix(xslt): gate raw-output markers, cache pattern table, add CI tests
Raw-output marker resolution and 16.4 stripping run only when the
stylesheet mentions disable-output-escaping, so source text containing
U+E000/U+E001 survives every other transform untouched, and the text
method removes markers without unescaping content that was never
escaped. The pattern-matching function table is built once per
transform instead of per (template, node) membership test, and a dead
variable left in the numbering engine is gone. Eight focused unit
tests cover d-o-e (with the fragment rule and marker safety), CDATA
terminator splitting, simplified syntax, the html default method with
META injection, per-document key(), and attribute-start axes, so the
burn-down behaviors are CI-visible without the opt-in corpus.
2026-06-10 15:01:07 +02:00
7b4887b7ad feat(xslt): per-document key indexes in document order
key() resolves its index against the current node's own document,
built lazily per document root, so keys work inside document() trees;
a node-set use expression indexes the matched node under every
member's string value; and key() results return in document order
regardless of lookup value order. Xalan baseline 187 -> 182.
2026-06-10 14:01:28 +02:00
40605ff6e5 feat(xslt): contextual sort keys and cross-document template matching
Sort keys evaluate once per node with the node's own position, size,
variables, and bindings, so position()-based keys work, and NaN keys
order before numbers under data-type=number. Match patterns evaluate
over the matched node's own document, so templates match nodes loaded
through document(); the match cache keys include the document
identity. Xalan baseline 204 -> 187 (sort 18 -> 11, mdocs 14 -> 6).
2026-06-10 13:54:37 +02:00
9945fb74a5 feat(xslt): prolog ordering, html doctype, simplified syntax, CDATA splitting
The XML declaration precedes the document type declaration; a
public-only doctype is emitted by the html method and ignored by the
xml method, which requires doctype-system; a literal result element
carrying xsl:version compiles as a single match="/" template (the 2.3
simplified syntax); and cdata-section text containing ]]> splits
across section boundaries. Parser entry and Parts move to
XSLTParserEntry.swift for the file caps. Xalan baseline 218 -> 204
(output 34 -> 21).
2026-06-10 13:48:03 +02:00
1646128b35 feat(xslt): stylesheet namespace resolution for patterns and expressions
Templates carry their in-scope xmlns bindings into match patterns
(match cache keyed on pattern plus bindings) and instantiated
expressions, resolving prefixed name tests by namespace URI; with
bindings supplied an unprefixed test selects only the null namespace,
the XPath 1.0 rule. Instruction-scope capture includes the default
declaration: xsl:element honors in-scope xmlns="uri" for unprefixed
names, xmlns="" keeps them namespace-free, and an unusable created
name drops the wrapper, keeping content. Xalan baseline 263 -> 218
(namespace 41 -> 13).
2026-06-10 13:36:59 +02:00
da8bb6b92a feat(xslt): key() over attribute nodes, key/id match patterns, doctyped sources
key() and document() read the string value of attribute and namespace
node arguments instead of dropping them, so the Muenchian grouping
idiom works; match patterns evaluate with the XSLT function table,
making key('k','v') and id('x') patterns match; and the XSLT entry
points parse sources, stylesheets, and document() loads with doctypes
allowed, as the conformance corpora require. Xalan baseline
292 -> 263 (idkey 39 -> 16).
2026-06-10 13:29:27 +02:00
b41678064e feat(xslt): templates over attribute and namespace nodes
apply-templates and for-each keep attribute and namespace nodes in
their selections; the context carries the non-tree current node with
its owner element backing the tree-only machinery, XPath evaluation
and current() start from it, match patterns select attribute nodes
through attribute identities in the match cache, the built-in rules
copy an attribute's or namespace's value as text, xsl:copy copies the
focused attribute or declaration itself, and an attribute result
arriving after element content is ignored (the XSLT recovery).
Sorting, for-each, and choose move to XSLTSorting.swift for the file
caps. Xalan baseline 352 -> 292 (axes 35 -> 16, copy 29 -> 16).
2026-06-10 13:24:31 +02:00
5a0c2ec7e4 feat(xslt): output methods in depth
disable-output-escaping on xsl:value-of and xsl:text (sentinel-marked
raw text, unescaped after serialization; string extraction from a
fragment drops the markers per 16.4), the html default method for a
null-namespace html root (16.1), the spec-default XML declaration for
the xml method, META Content-Type injection into head (16.2),
case-insensitive raw-text and void element classes, and HTML 4.01
Latin-1 named entities on html output. Whitespace-only stripping now
means XML whitespace, so NBSP-only text survives. Conf comparison
treats tag-adjacent whitespace as layout outside the canonical path.
Xalan baseline 371 -> 352 (output 49 -> 30).
2026-06-10 13:10:50 +02:00
255f7508fd feat(xslt): literal result element namespace copying
A literal result element copies its in-scope namespace declarations to
the result per 7.1.1, minus the XSLT namespace and the namespaces of
prefixes listed in exclude-result-prefixes and
extension-element-prefixes in scope (#default included); an aliased
stylesheet namespace declares its result prefix and URI instead. The
fixup pass drops declarations that repeat an inherited binding so the
copied nodes do not redeclare per descendant, and created names reuse
stylesheet prefixes. xsl:output indent=yes switches to the Xalan
indentation form (own lines, no leading spaces). Xalan baseline
446 -> 371 (namespace 89 -> 42).
2026-06-10 12:55:48 +02:00
38bbb349c5 feat(xslt): full format-number picture parsing
The picture parses into affixes, number part, and the negative
subpattern: literal prefixes and suffixes pass through, the negative
subpattern's affixes apply (with the default minus when it has no
distinguishing ones), per-mille and percent multiply and stay literal,
the grouping size derives from the pattern's last grouping separator,
colliding decimal/grouping separators read as grouping, and U+00A4
renders as the en-US currency symbol. Digit extraction now uses one
scaled rounding and integer arithmetic, eliminating per-digit float
drift. Xalan numberformat 30 -> 1; baseline 475 -> 446.
2026-06-10 12:45:57 +02:00
2134a23c41 feat(xslt): full xsl:number engine and stable document() identity
xsl:number implements all of XSLT 1.0 section 7.7: level
single/multiple/any with count and from matched as patterns through the
match cache, the value expression with the below-one bypass, and the
format-token engine (punctuation, separator and token reuse, zero
padding, alphabetic/roman/Greek sequences, digit grouping). An
unmatched single/multiple count renders empty; level=any always yields
a number. document() now caches per transform so repeated loads of the
same URI return the identical document and generate-id() stays unique
against live nodes (the idkey49 flake). Xalan baseline 557 -> 475
(numbering 84 -> 7).
2026-06-10 12:37:03 +02:00
e5402f8bf5 feat(xslt): namespaced name creation, copy attribute sets, result fixup
xsl:attribute/xsl:element honor the namespace attribute and resolve
prefixed names against the instruction's in-scope stylesheet
declarations (captured at parse); xsl:copy applies use-attribute-sets;
and a namespace-fixup pass before serialization declares every
namespace a created name carries, reusing same-URI bindings or
generating ns0.. prefixes, and undeclares an inherited default that
would capture an unqualified name. Xalan baseline 604 to 557.
2026-06-10 12:10:48 +02:00
7d3ef26558 perf(xpath): compile patterns once per run, not per node
XSLT: template selection resolves each match pattern once per transform via
a per-run MatchCache (the pattern's node set is computed once over the source
tree; selection becomes an identity lookup), replacing per-(node, template)
recompilation and full-tree re-walks. XSD identity validation: a per-run query
cache compiles each selector/field XPath once instead of at every visited
element. Schematron was verified to already store compiled queries. Behavior
unchanged (all suites green); a multi-template shape test over 300 nodes
guards the optimized path. MatchCache lives in its own file to keep the
transformer under the length cap.

Closes #112
2026-06-10 03:24:38 +02:00
7c24ada92b feat(xslt): document() fragments and base-URI resolution, completing #82
A document() reference with a #fragment selects a subset of the loaded
document via XPointer; a node-set first argument unions the documents
named by each node's string value; and a relative URI is resolved against
a caller-supplied baseURI (the loader is wrapped so xsl:include/xsl:import
resolve the same way). Closes the XSLT function/output audit.
2026-06-09 03:12:49 +02:00
4ea84b3d58 feat(xslt): make result-tree fragments usable as node-sets
An xsl:variable with a body now binds a queryable document fragment (its
instantiated result converted to a TreeNode document) instead of a plain
string, so count($rtf/*), $rtf/child, and for-each over $rtf/... work.
It still has its concatenated text as a string value, so existing string
uses are unchanged. Part of #82.
2026-06-09 03:04:56 +02:00
3fec78da51 feat(xslt): implement xsl:output cdata-section-elements
The text content of the named result elements is emitted in CDATA
sections rather than escaped: the result tree's text children of those
elements are converted to CDATA nodes before serialization, leaving
nested and other elements unaffected. Part of #82.
2026-06-09 02:58:34 +02:00
2a40300479 feat(xslt): emit xsl:output doctype-public/doctype-system
When doctype-system (optionally with doctype-public) is set on xsl:output,
a <!DOCTYPE> for the result's root element is emitted before the
serialized output: PUBLIC when both identifiers are given, SYSTEM
otherwise. Works for the XML and HTML output methods. Part of #82.
2026-06-09 02:53:50 +02:00
47b88f19b2 feat(xslt): key() over a node-set second argument
key(name, node-set), including key('k', .), now unions the indexed nodes
for every value node's string value and de-duplicates, instead of using
only the first node's string value. A string value still works directly.
Part of #82.
2026-06-09 02:50:24 +02:00
41a48d3130 feat(xslt): serialize method=html output by the HTML rules
An xsl:output method=html result is serialized through PureXML.HTML
(void elements emitted without a self-closing slash, raw-text elements
like script/style left unescaped) instead of as XML. Part of #82.
2026-06-09 02:48:00 +02:00
e30672d72b feat(xslt): implement xsl:sort case-order
case-order=upper-first/lower-first makes the string comparison
case-insensitive, with case used only to break ties among otherwise-equal
strings; without it the default codepoint order is unchanged. Part of #82.
2026-06-09 02:37:43 +02:00
322fd013e9 feat(xslt): add generate-id, system-property, *-available functions
generate-id returns a stable per-node identifier (the context node when
no argument is given, the empty string for an empty node-set);
system-property reports the XSLT version and vendor; element-available and
function-available answer against the implemented instruction and function
sets; unparsed-entity-uri is a stub. Part of #82.
2026-06-09 02:20:36 +02:00
610b7d0ca0 feat(xslt): implement xsl:namespace-alias, completing #81
A literal result element and its attributes in a stylesheet namespace
bound by stylesheet-prefix are rewritten on output to the namespace and
prefix bound by result-prefix (#default supported), so a stylesheet can
emit literal xsl: elements. Prefixes resolve through the in-scope xmlns
declarations; aliases fold across include/import. Splits AVT parsing into
XSLTAVT.swift and trims the transformer to stay within length budgets.
2026-06-09 02:10:16 +02:00
2d2572e265 feat(xslt): implement xsl:decimal-format for format-number
format-number now honors named and default decimal formats: the decimal
separator, grouping separator, percent, minus sign, zero-digit (for
non-Latin digit sets), NaN, and infinity symbols come from the chosen
xsl:decimal-format, in both the picture and the output. FormatNumber is
parameterized over the symbol set and the formats thread through the
function table; folded across include/import.
2026-06-09 01:59:37 +02:00
61ab8ce8f9 feat(xslt): implement xsl:fallback for forwards-compatible processing
An unrecognized XSLT element instantiates its xsl:fallback children in its
place, or is dropped when it has none; a fallback under a supported
instruction is ignored. The fallback content is handled in the instantiate
loop alongside variables, so the evaluate dispatch stays within budget.
2026-06-09 01:52:21 +02:00
4f11bafa28 feat(xslt): implement xsl:apply-imports
Re-applies templates to the current node in the current mode, considering
only templates of lower import precedence than the one being instantiated,
falling back to the built-in rule when none match. Threads the current
mode and import precedence through the evaluation context, and moves the
transformer runtime types into XSLTRuntime.swift (namespaced, via private
aliases) to stay within the file-length and namespacing gates.
2026-06-09 01:49:33 +02:00
21ed15b1ec refactor(xslt): keep ResultItem/XSLTContext file-private for namespacing
Only the static deduplicated helper moves to XSLTElements.swift (it needs
no private types); attributeSetAttributes stays in the transformer so the
evaluation context and result-item types remain file-private, satisfying
the one-file-scope-type namespacing gate.
2026-06-09 01:41:48 +02:00
22cd88f8ba feat(xslt): implement xsl:attribute-set and use-attribute-sets
Named attribute sets apply to a literal result element or xsl:element via
use-attribute-sets; sets may include other sets recursively with a cycle
guard, set attributes are lowest precedence so the element's own
attributes override them, and same-named attributes collapse to the last.
Folded across xsl:include/xsl:import. Splits the element-building helpers
into XSLTElements.swift to stay within the file-length budget.
2026-06-09 01:39:45 +02:00
367b5b9425 feat(xslt): implement xsl:message with terminate
xsl:message instantiates its body as diagnostic text and produces no
result-tree output; terminate=yes records the text and aborts the
transform with an XSLTError.terminated, halting further instantiation.
Splits the transform entry points into XSLTTransform.swift and moves the
message case to keep within the complexity and file-length budgets.
2026-06-09 01:29:37 +02:00
10d9525e3e feat(xslt): implement xsl:strip-space and xsl:preserve-space
Whitespace-only text nodes are removed from the source tree for elements
named by strip-space before transformation, with preserve-space and a
more specific name test winning over *, and xml:space=preserve on a
source element or ancestor overriding the stripping. The name sets fold
across xsl:include/xsl:import. Refactors the stylesheet compile loop
through a Parts accumulator to absorb the new top-level elements within
the complexity budget.
2026-06-09 01:22:59 +02:00
23db1289ed feat(xslt): add xsl:include/import, xsl:output, comment, and processing-instruction
Round out the XSLT 1.0 stylesheet and output controls:

- xsl:include folds another stylesheet's templates, keys, globals, and output
  at the same import precedence; xsl:import folds them one precedence lower,
  so the importing stylesheet's rules win conflicts. Both resolve their href
  through the injected document loader, never the filesystem.
- xsl:output drives serialization: method (xml or text), indent,
  omit-xml-declaration, encoding, version, and standalone, layered over the
  caller's emitter options. The text method emits character data only.
- xsl:comment and xsl:processing-instruction produce comment and PI nodes
  from their instantiated bodies.

Templates now carry an import precedence that the matcher ranks ahead of
priority and document order.

Closes #64
2026-06-08 08:08:38 +02:00
1286659c62 feat(xslt): add xsl:key/key(), format-number(), xsl:number, and document()
Reach full XSLT 1.0 key, numbering, and document parity:

- xsl:key declarations are compiled into an index over the source tree;
  the key() function looks nodes up by key name and value.
- format-number() renders a number against a picture string (digit places,
  decimal point, grouping, percent), Foundation-free.
- xsl:number generates the context node's position rendered as arabic,
  alphabetic, or roman numerals.
- document() resolves an external source through an injected loader so the
  transform never reaches the filesystem or network by default.

These hang off an XPath function-table injection mechanism: the evaluator
merges an engine-supplied FunctionTable, letting XSLT register current(),
key(), format-number(), and document() per evaluation.

Closes #63
2026-06-08 08:01:17 +02:00
7ce6b4da73 feat(xslt): template parameters and modes
Honor xsl:param defaults, xsl:with-param on apply-templates and
call-template, and mode on templates and apply-templates. A passed parameter
overrides the declared default; a mode routes nodes only to templates of that
mode, and the built-in rules recurse in the same mode. Add the parity-
completion epic to the roadmap.

Closes #62
2026-06-08 07:48:05 +02:00
2f86b326cc chore: silence clean-build warnings
Use let for never-mutated bindings in DateTimeParser, HTMLEntities, and the
XSLT parser, and drop the redundant Unicode.Scalar Comparable conformance
that the standard library already provides. No behavior change.
2026-06-08 07:32:28 +02:00
75e511481c feat(xslt): transformation engine completing XSLT 1.0
Add the XSLT execution engine: template matching against the source tree
(highest priority, then last declared, with the built-in rules), the
apply-templates processing model, and instantiation of the sequence
constructor. Implements apply-templates with sort, value-of, for-each, if,
choose, element, attribute, copy, copy-of, call-template, variable, literal
result elements, and attribute value templates, over the XPath engine. The
identity transform round-trips a document including attributes. Collapse the
completed Transformation roadmap block into the shipped summaries.

Closes #15
Closes #16
Closes #17
Closes #18
Closes #3
2026-06-08 07:06:49 +02:00
96eb85f1fc feat(xslt): stylesheet model and parser
Add the PureXML.XSLT namespace with a stylesheet model (template rules with
match patterns and computed default priorities, the instruction sequence
constructor, attribute value templates) and a parser compiling an
xsl:stylesheet document into it. Recognizes the XSLT vocabulary by namespace
or xsl prefix; other elements become literal result elements. Covers
apply-templates, value-of, for-each, if, choose, element, attribute, copy,
copy-of, call-template, variable, text, and sort.

Closes #14
2026-06-08 06:57:11 +02:00