fix(xslt): name the html output method doctype HTML, not the element name (closes output40/48/60) (#259)

* fix(xslt): name the html output method doctype HTML, not the element name

XSLT 1.0 16.2: for the html output method the name following <!DOCTYPE should
be HTML or html, not the document element name (which is the xml method's rule,
16.1). The serializer used the document element name for both methods, so
method=html with a non-html root element (e.g. <root>) emitted
<!DOCTYPE root PUBLIC ...> instead of <!DOCTYPE HTML PUBLIC ...>.

Closes Apache Xalan conformance cases output40, output48, output60.

* fix(xslt): emit no html doctype when the result has no element

Addresses the doctype critic finding: a doctype precedes the first element
(16.2), so a text-only result must emit none even for the html method. The
fixed HTML name made the existing no-element guard unreachable; restore it by
guarding on the document element before choosing the name.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 02:29:26 +02:00 committed by GitHub
parent f3d9de2c86
commit 445412b105
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 39 additions and 8 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 now names the document type declaration `HTML` rather than the document element name (XSLT 1.0 16.2: "The name following the `<!DOCTYPE` should be HTML or html"). With `method="html"` and a `doctype-public` or `doctype-system`, the emitted declaration is `<!DOCTYPE HTML PUBLIC "...">` regardless of the result's root element; the `xml` method still uses the document element name (16.1). Closes Apache Xalan conformance cases `output40`, `output48`, and `output60`.
- 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`.

View File

@ -186,7 +186,12 @@ public extension PureXML.XSLT {
/// The `<!DOCTYPE …>` prologue for the result's root element when the output
/// declares `doctype-system` (optionally with `doctype-public`), else empty.
private static func doctype(for result: PureXML.Model.Node, _ output: Output, method: String) -> String {
guard let name = rootName(of: result) else { return "" }
// A doctype precedes the first element, so emit none without one. The
// html output method then names it HTML (16.2: "The name following
// <!DOCTYPE should be HTML or html"); the xml method uses the document
// element name (16.1).
guard let elementName = rootName(of: result) else { return "" }
let name = method == "html" ? "HTML" : elementName
if let system = output.doctypeSystem {
let external = output.doctypePublic.map { "PUBLIC \"\($0)\" \"\(system)\"" } ?? "SYSTEM \"\(system)\""
return "<!DOCTYPE \(name) \(external)>\n"

View File

@ -29,9 +29,6 @@ numbering/numbering63
output/output22
output/output26
output/output32
output/output40
output/output48
output/output60
output/output63
output/output70
output/output72

View File

@ -20,17 +20,44 @@ struct XSLTDoctypeOutputTests {
#expect(try transform(style, "<x/>") == "<!DOCTYPE note SYSTEM \"note.dtd\">\n<note/>")
}
@Test("doctype-public and doctype-system emit a PUBLIC doctype")
@Test("the html method names the doctype HTML regardless of the document element")
func test_publicAndSystem() throws {
// XSLT 1.0 16.2: the html output method's doctype name is HTML, not the
// document element name (here `root`), unlike the xml method (Apache
// Xalan output40, output48, output60).
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="html" doctype-public="-//W3C//DTD HTML 4.01//EN" doctype-system="http://www.w3.org/TR/html4/strict.dtd"/>
<xsl:template match="/"><html/></xsl:template>
<xsl:template match="/"><root/></xsl:template>
</xsl:stylesheet>
"""
let out = try transform(style, "<x/>")
#expect(out.hasPrefix("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n"))
#expect(out.contains("<html></html>"))
#expect(out.hasPrefix("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n"))
#expect(out.contains("<root></root>"))
}
@Test("the xml method still uses the document element name for the doctype")
func test_xmlMethodUsesElementName() throws {
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="xml" omit-xml-declaration="yes" doctype-system="note.dtd"/>
<xsl:template match="/"><note/></xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "<!DOCTYPE note SYSTEM \"note.dtd\">\n<note/>")
}
@Test("the html method emits no doctype when the result has no element")
func test_htmlNoElementNoDoctype() throws {
// A doctype precedes the first element (16.2); a text-only result has
// none, so even the html method with doctype-public emits nothing.
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="html" doctype-public="-//W3C//DTD HTML 4.01//EN"/>
<xsl:template match="/">just text</xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "just text")
}
@Test("Without doctype-system no doctype is emitted")