PureXML/Tests/XSLTDoctypeOutputTests.swift
Mihaela Mihaljevic 445412b105
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.
2026-06-24 02:29:26 +02:00

74 lines
3.1 KiB
Swift

import Testing
@testable import PureXML
@Suite("XSLT xsl:output doctype-public / doctype-system")
struct XSLTDoctypeOutputTests {
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("doctype-system emits a SYSTEM doctype before the root")
func test_systemOnly() 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 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="/"><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("<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")
func test_none() throws {
let style = """
<xsl:stylesheet version="1.0" \(xsl)>
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="/"><note/></xsl:template>
</xsl:stylesheet>
"""
#expect(try transform(style, "<x/>") == "<note/>")
}
}