fix(html): do not escape < in an attribute value in the html output method (#258)

XSLT 1.0 16.2: the html output method escapes & and " in attribute values
but, unlike the xml method, leaves < (and >) literal. The serializer was
escaping < to &lt; in attribute values. Removing that case lets it pass
through; the xml method is unchanged and still escapes both.

Closes Apache Xalan conformance cases output49 and output74.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 02:16:20 +02:00 committed by GitHub
parent 37ea8fcbfa
commit f3d9de2c86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 24 additions and 3 deletions

View File

@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- The `html` output method no longer escapes a `<` character in an attribute value (XSLT 1.0 16.2). The HTML output method escapes `&` and `"` in attribute values but leaves `<` and `>` literal, unlike the `xml` method; the serializer was escaping `<` to `&lt;` there. The `xml` output method is unchanged (it still escapes both). Closes Apache Xalan conformance cases `output49` and `output74`.
- A string-form `document()` reference now resolves against the base URI of the stylesheet element holding it (XSLT 1.0 12.1), which for a declaration in an `xsl:include`d or `xsl:import`ed file is that file's URI rather than the top stylesheet's. The parser already tracked each top-level declaration's source-file base; that base is now carried onto the compiled templates and global variables and threaded into expression evaluation, so a relative `document('data.xml')` (or `document('')`, the self-reference) in an included file resolves against the included file, not the importing one. Closes Apache Xalan conformance cases `mdocs12` and `mdocs13`.
- The `document()` function now resolves a relative URI reference against the correct base URI (XSLT 1.0 12.1). The two-argument form resolves the first argument against the base URI of the second argument's first node, and the node-set form resolves each reference against the base URI of the node that supplied it; previously every reference resolved against the single global base, so a reference relative to a document that `document()` itself had loaded from a subdirectory could not be found. Each loaded document now records the URI it came from as its base URI, and a relative reference is resolved against that base before the loader applies its own source-document base. Closes Apache Xalan conformance cases `reluri09`, `reluri10`, and `reluri11`.

View File

@ -85,7 +85,8 @@ extension PureXML.HTML {
case "&":
previousWasAmpersand = true
case "\"": result += "&quot;"
case "<": result += "&lt;"
// The html output method does not escape `<` (or `>`) in an
// attribute value, unlike the xml method (XSLT 1.0 16.2).
default: result += latin1Entity(scalar) ?? String(Character(scalar))
}
}

View File

@ -31,13 +31,11 @@ output/output26
output/output32
output/output40
output/output48
output/output49
output/output60
output/output63
output/output70
output/output72
output/output73
output/output74
output/output75
output/output80
output/output86

View File

@ -37,6 +37,26 @@ struct XSLTHTMLOutputTests {
#expect(!out.contains("&lt;"))
}
@Test("The html method leaves < and > literal in an attribute value")
func test_attributeAngleBrackets() throws {
// XSLT 1.0 16.2: the html output method does not escape `<` in an
// attribute value (nor `>`), unlike the xml method. `&` and `"` still
// escape (Apache Xalan output49, output74).
let out = try transform(htmlStyle("<a title=\"&lt;x>&amp;&quot;\">t</a>"), "<x/>")
#expect(out == "<a title=\"<x>&amp;&quot;\">t</a>")
}
@Test("The xml method still escapes < in an attribute value")
func test_xmlAttributeStillEscapes() throws {
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="/"><a title="&lt;x>">t</a></xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "<a title=\"&lt;x&gt;\">t</a>")
}
@Test("The XML output method still self-closes empty elements")
func test_xmlStillSelfCloses() throws {
let style = """