diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8b31fd..2be1781 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- A template match pattern written with an explicit `child::` or `attribute::` axis now gets the default priority of its node test (XSLT 1.0 5.5), not 0. `attribute::node()` is a `node()` node test, so its default priority is -0.5 (the same as `@*`), but it was treated as a name test and given priority 0, which made it wrongly outrank `@*`. The axis is now stripped before the node test is classified. Closes Apache Xalan conformance cases `conflictres29` and `conflictres30`.
+
- `xsl:strip-space` and `xsl:preserve-space` now match their `elements` name tests by namespace rather than by prefix (XSLT 1.0 3.4). A specifier `prefix:*` or `prefix:local` resolves the prefix to its namespace URI at parse time, and a source element is matched on its expanded name in order of specificity (an exact `{uri}local` over a namespace wildcard `{uri}*` over `*`), so a stylesheet that strips `ns1:*` strips a source element in that namespace whatever prefix the source bound it to, while leaving another namespace untouched. The tests previously compared the raw prefixed name strings, so a differing prefix did not match. Closes Apache Xalan conformance cases `whitespace06` and `whitespace07`.
- `disable-output-escaping` is now ignored when an `xsl:text` or `xsl:value-of` produces the string value of an attribute, comment, or processing-instruction node (XSLT 1.0 16.4: disabling escaping for a value used as something other than a text node is an error, with the recovery being to ignore it). The attribute value built from such content kept its raw-output markers, so the serializer left `<` and `>` unescaped in the attribute; the markers are now stripped for these three node kinds (only when the stylesheet uses the feature, so a private-use character in ordinary data is untouched). Closes Apache Xalan conformance case `output75`.
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index d0bdcd5..ef7f76c 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -221,11 +221,18 @@ extension PureXML.XSLT {
XSLTNode.children(node, named: "with-param").map(binding)
}
- /// The XSLT default-priority rules for a match pattern.
+ /// The XSLT default-priority rules for a match pattern. The priority of a
+ /// single-step pattern is set by its node test, regardless of an explicit
+ /// `child::` or `attribute::` axis (XSLT 1.0 5.5), so the axis is stripped
+ /// before the node test is classified (`attribute::node()` is -0.5, like
+ /// `node()`, not 0).
static func defaultPriority(_ pattern: String) -> Double {
if pattern.contains("/") || pattern.contains("[") { return 0.5 }
- if ["*", "@*", "node()", "text()", "comment()"].contains(pattern) { return -0.5 }
- if pattern.hasSuffix(":*") { return -0.25 }
+ let test = pattern.hasPrefix("attribute::") ? String(pattern.dropFirst(11))
+ : pattern.hasPrefix("child::") ? String(pattern.dropFirst(7))
+ : pattern
+ if ["*", "@*", "node()", "text()", "comment()", "processing-instruction()"].contains(test) { return -0.5 }
+ if test.hasSuffix(":*") { return -0.25 }
return 0
}
diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt
index bbbe92c..0f51ce1 100644
--- a/Tests/Fixtures/xalan-baseline.txt
+++ b/Tests/Fixtures/xalan-baseline.txt
@@ -3,8 +3,6 @@
# must leave this list and a regression shows up as a new failure.
attribset/attribset44
axes/axes116
-conflictres/conflictres29
-conflictres/conflictres30
extend/extend01
extend/extend02
extend/extend04
diff --git a/Tests/XSLTPatternPriorityTests.swift b/Tests/XSLTPatternPriorityTests.swift
new file mode 100644
index 0000000..2302524
--- /dev/null
+++ b/Tests/XSLTPatternPriorityTests.swift
@@ -0,0 +1,29 @@
+import Testing
+@testable import PureXML
+
+/// XSLT 1.0 5.5 default-priority rules for template match patterns.
+@Suite("XSLT pattern priority")
+struct XSLTPatternPriorityTests {
+ private let xsl = "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\""
+
+ private func transform(_ stylesheet: String, _ source: String) throws -> String {
+ try PureXML.XSLT.transform(stylesheet: stylesheet, source: source)
+ }
+
+ @Test("an axis-prefixed node test ties with @*, not outranks it (5.5)")
+ func test_axisPatternPriority() throws {
+ // attribute::node() is a node() node test, default priority -0.5 like @*,
+ // not a name test (0). On the resulting tie the last template in document
+ // order wins, so @* (declared last) applies, not attribute::node() which
+ // was wrongly given priority 0 (Apache Xalan conflictres29, conflictres30).
+ let style = """
+
+
+
+ node
+ star
+
+ """
+ #expect(try transform(style, "") == "star")
+ }
+}