fix(emit): split ?> in processing-instruction data with a space (#262)

PI data may not contain ?>, which would terminate the instruction early. The
serializer emitted it raw, producing malformed output. Insert a space between
an offending ? and > (XSLT 1.0 7.3 recovery: a processor recovers from a
generated ?> by inserting a space after the ?), via a shared Escaping helper
applied at both XML PI serialization sites. A trailing ? is left alone.

Closes Apache Xalan conformance case output72.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 03:14:01 +02:00 committed by GitHub
parent c355daa00e
commit f1bc46e99b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 26 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
- Serializing a processing instruction whose data contains `?>` now inserts a space (`? >`) so the instruction is not terminated early (XSLT 1.0 7.3: a processor recovers from a generated `?>` by inserting a space after the `?`). The serializer emitted the `?>` raw, producing a malformed instruction. Closes Apache Xalan conformance case `output72`.
- The XSLT conformance harness now decodes source and stylesheet files through the library's own encoding detection (a BOM or the XML declaration) rather than assuming UTF-8, so a non-UTF-8 corpus input is fed to the transform instead of refused. This confirms the library round-trips a UTF-16 source correctly and closes Apache Xalan conformance cases `output80` (UTF-16) and `attribvaltemplate08`, which were unreadable by the harness, not library failures.
- 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.)

View File

@ -64,5 +64,19 @@ extension PureXML.Emitting {
if previousHyphen { result.append(" ") }
return result
}
/// Makes `data` safe inside a processing instruction: PI data may not
/// contain `?>`, so a space is inserted between an offending `?` and `>`
/// (the XSLT 1.0 7.3 recovery, so `a?>b` becomes `a? >b`).
static func processingInstruction(_ data: String) -> String {
var result = ""
var previousQuestion = false
for character in data {
if character == ">", previousQuestion { result.append(" ") }
result.append(character)
previousQuestion = character == "?"
}
return result
}
}
}

View File

@ -87,7 +87,7 @@ public extension PureXML.Emitting {
case let .comment(value):
output += "<!--\(Escaping.comment(value))-->"
case let .processingInstruction(target, data):
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(Escaping.processingInstruction(data))?>"
}
}

View File

@ -109,7 +109,7 @@ public extension PureXML.Emitting {
if options.prettyPrint, !open.isEmpty {
output += options.lineEnding + pad(open.count)
}
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(data)?>"
output += data.isEmpty ? "<?\(target)?>" : "<?\(target) \(Escaping.processingInstruction(data))?>"
markParentHasElements()
}

View File

@ -30,7 +30,6 @@ output/output26
output/output32
output/output63
output/output70
output/output72
output/output73
output/output75
output/output86

View File

@ -9,6 +9,14 @@ struct SerializerTests {
#expect(PureXML.serialize(node, options: .compact) == "<br/>")
}
@Test("A ?> inside processing-instruction data is split by a space")
func test_processingInstructionEscapesTerminator() {
// PI data may not contain ?>; the XSLT 1.0 7.3 recovery inserts a space
// (Apache Xalan output72). A trailing ? is safe and is left alone.
let instruction = PureXML.Model.Node.processingInstruction(target: "p", data: "a ?> b ?")
#expect(PureXML.serialize(instruction, options: .compact) == "<?p a ? > b ??>")
}
@Test("Attributes and text are escaped")
func test_escapesAttributesAndText() {
let element = PureXML.Model.Element(