fix(xslt): serialize a namespaced element as XML under the html output method (16.2) (#260)
* fix(xslt): serialize a namespaced element as XML under the html output method XSLT 1.0 16.2: the html output method outputs an element whose expanded-name has a non-null namespace URI as XML, not HTML. A namespaced document element's tree was serialized as HTML, leaving empty namespaced elements unclosed (e.g. a void name like br in a namespace) and repeating xmlns on every element, which is not well-formed XML. Such a tree now serializes through the XML path (empty elements self-close, the namespace is declared once and inherited); the html method still omits the XML declaration. Null-namespace trees are unchanged. Grounded in XSLT 1.0 16.2 and the libxml2 HTML serializer study; reimplemented in Swift. Apache Xalan output63 exercises this rule but stays baselined because its gold differs further only in non-normative attribute order. * docs(xslt): disclose the document-element granularity of the 16.2 namespace rule Make the whole-tree approximation explicit in the serializedBody comment and the changelog: the html-outputs-namespaced-elements-as-XML rule is applied at document-element granularity, not per element, so a mixed-namespace tree follows its document element's path.
This commit is contained in:
parent
445412b105
commit
1ba6bf86e2
@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- The `html` output method now serializes an element whose expanded-name has a non-null namespace URI as XML rather than HTML (XSLT 1.0 16.2). When the result's document element is namespaced, the tree is serialized through the XML path (empty elements self-close, the namespace is declared once and inherited) while the html method still omits the XML declaration; previously such a tree was emitted as HTML, leaving empty namespaced elements unclosed and repeating the `xmlns` declaration on every element, which is not well-formed XML. A null-namespace tree is unchanged. The rule is applied at document-element granularity rather than per element, so a tree mixing null- and non-null-namespace elements follows its document element's path (the realistic result is all-null or all-namespaced). (Apache Xalan `output63` exercises this rule but remains baselined: its gold differs further only in attribute order, which is not normative.)
|
||||
|
||||
- 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 `<` there. The `xml` output method is unchanged (it still escapes both). Closes Apache Xalan conformance cases `output49` and `output74`.
|
||||
|
||||
@ -43,16 +43,7 @@ public extension PureXML.XSLT {
|
||||
// never "unescaped".
|
||||
return sheet.usesRawText ? RawText.stripped(textValue(of: result)) : textValue(of: result)
|
||||
}
|
||||
let body: String
|
||||
if method == "html" {
|
||||
body = PureXML.HTML.serialize(withContentTypeMeta(result, encoding: sheet.output.encoding ?? "UTF-8"))
|
||||
} else {
|
||||
let fixed = XSLTNamespaceFixup.apply(result)
|
||||
let prepared = withCDATASections(fixed, sheet.output.cdataSectionElements)
|
||||
var emitOptions = options.applying(sheet.output)
|
||||
if sheet.output.omitXMLDeclaration == nil { emitOptions.includeXMLDeclaration = true }
|
||||
body = PureXML.serialize(prepared, options: emitOptions)
|
||||
}
|
||||
let body = serializedBody(result, sheet: sheet, options: options, method: method)
|
||||
// The XML declaration stays first; the doctype follows it.
|
||||
let resolved = sheet.usesRawText ? RawText.resolve(body) : body
|
||||
let documentType = doctype(for: result, sheet.output, method: method)
|
||||
@ -213,6 +204,40 @@ public extension PureXML.XSLT {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes the result tree by output method. 16.2 outputs a
|
||||
/// non-null-namespace element as XML rather than HTML; this is applied at
|
||||
/// document-element granularity, not per element: a namespaced document
|
||||
/// element serializes the whole tree as XML (the html method still omitting
|
||||
/// the XML declaration), so a tree mixing null- and non-null-namespace
|
||||
/// elements follows its document element's path. The realistic result is
|
||||
/// all-null (HTML) or all-namespaced (XML); the html null-namespace path is
|
||||
/// unchanged.
|
||||
private static func serializedBody(_ result: PureXML.Model.Node, sheet: Stylesheet, options: PureXML.Emitting.Options, method: String) -> String {
|
||||
if method == "html", !documentElementIsNamespaced(result) {
|
||||
return PureXML.HTML.serialize(withContentTypeMeta(result, encoding: sheet.output.encoding ?? "UTF-8"))
|
||||
}
|
||||
let fixed = XSLTNamespaceFixup.apply(result)
|
||||
let prepared = withCDATASections(fixed, sheet.output.cdataSectionElements)
|
||||
var emitOptions = options.applying(sheet.output)
|
||||
if method == "html" {
|
||||
emitOptions.includeXMLDeclaration = false
|
||||
} else if sheet.output.omitXMLDeclaration == nil {
|
||||
emitOptions.includeXMLDeclaration = true
|
||||
}
|
||||
return PureXML.serialize(prepared, options: emitOptions)
|
||||
}
|
||||
|
||||
/// Whether the result's document element has a non-null namespace URI, which
|
||||
/// the html output method serializes as XML rather than HTML (16.2).
|
||||
private static func documentElementIsNamespaced(_ node: PureXML.Model.Node) -> Bool {
|
||||
let element: PureXML.Model.Element? = switch node {
|
||||
case let .element(element): element
|
||||
case let .document(children): children.compactMap(\.element).first
|
||||
default: nil
|
||||
}
|
||||
return !((element?.name.namespaceURI) ?? "").isEmpty
|
||||
}
|
||||
|
||||
/// Transforms `source` with `stylesheet`, returning the result tree.
|
||||
static func transformToNode(
|
||||
stylesheet: String,
|
||||
|
||||
@ -57,6 +57,17 @@ struct XSLTHTMLOutputTests {
|
||||
#expect(try transform(style, "<x/>") == "<a title=\"<x>\">t</a>")
|
||||
}
|
||||
|
||||
@Test("the html method serializes a namespaced element as XML, not HTML")
|
||||
func test_namespacedElementAsXML() throws {
|
||||
// XSLT 1.0 16.2: an element whose expanded-name has a non-null namespace
|
||||
// URI is output as XML (empty elements self-close, the namespace is
|
||||
// declared once), not HTML; a null-namespace element stays HTML (see
|
||||
// test_voidElement). The html serializer otherwise left empty namespaced
|
||||
// elements unclosed and repeated xmlns on every element.
|
||||
let out = try transform(htmlStyle("<svg xmlns=\"urn:svg\"><rect/><g><circle/></g></svg>"), "<x/>")
|
||||
#expect(out == "<svg xmlns=\"urn:svg\"><rect/><g><circle/></g></svg>")
|
||||
}
|
||||
|
||||
@Test("The XML output method still self-closes empty elements")
|
||||
func test_xmlStillSelfCloses() throws {
|
||||
let style = """
|
||||
|
||||
Loading…
Reference in New Issue
Block a user