diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f571d6..070475f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.) diff --git a/Sources/Emitting/Escaping.swift b/Sources/Emitting/Escaping.swift index 60d0b9f..b31a566 100644 --- a/Sources/Emitting/Escaping.swift +++ b/Sources/Emitting/Escaping.swift @@ -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 + } } } diff --git a/Sources/Emitting/Serializer.swift b/Sources/Emitting/Serializer.swift index 0ca841a..07e8674 100644 --- a/Sources/Emitting/Serializer.swift +++ b/Sources/Emitting/Serializer.swift @@ -87,7 +87,7 @@ public extension PureXML.Emitting { case let .comment(value): output += "" case let .processingInstruction(target, data): - output += data.isEmpty ? "" : "" + output += data.isEmpty ? "" : "" } } diff --git a/Sources/Emitting/Writer.swift b/Sources/Emitting/Writer.swift index 534e1b5..84f91ae 100644 --- a/Sources/Emitting/Writer.swift +++ b/Sources/Emitting/Writer.swift @@ -109,7 +109,7 @@ public extension PureXML.Emitting { if options.prettyPrint, !open.isEmpty { output += options.lineEnding + pad(open.count) } - output += data.isEmpty ? "" : "" + output += data.isEmpty ? "" : "" markParentHasElements() } diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index ad4f890..c6f5139 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -30,7 +30,6 @@ output/output26 output/output32 output/output63 output/output70 -output/output72 output/output73 output/output75 output/output86 diff --git a/Tests/SerializerTests.swift b/Tests/SerializerTests.swift index 0fca65c..02d3067 100644 --- a/Tests/SerializerTests.swift +++ b/Tests/SerializerTests.swift @@ -9,6 +9,14 @@ struct SerializerTests { #expect(PureXML.serialize(node, options: .compact) == "
") } + @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) == " b ??>") + } + @Test("Attributes and text are escaped") func test_escapesAttributesAndText() { let element = PureXML.Model.Element(