@testable import PureXML
import Testing
@Suite("Output encoding")
struct OutputEncodingTests {
private func rootText(_ node: PureXML.Model.Node) -> String? {
guard case let .document(children) = node else { return nil }
for child in children {
if case let .element(element) = child { return element.text }
}
return nil
}
private func serialized(_ xml: String, _ encoding: PureXML.Parsing.InputEncoding) throws -> [UInt8] {
try PureXML.serialize(PureXML.parse(xml), encoding: encoding)
}
@Test("ISO-8859-1 emits one byte per Latin-1 scalar and round-trips")
func test_latin1() throws {
let bytes = try serialized("caf\u{E9}", .latin1) // café
#expect(bytes.contains(0xE9)) // é is a single 0xE9 byte
#expect(!bytes.contains(0xC3)) // not the UTF-8 lead byte of é
try #expect(rootText(PureXML.parse(bytes: bytes)) == "caf\u{E9}")
}
@Test("ISO-8859-7 round-trips Greek and windows-1252 round-trips its punctuation")
func test_greekAndWindows() throws {
try #expect(rootText(PureXML.parse(bytes: serialized("\u{0391}", .greek))) == "\u{0391}") // Α
try #expect(rootText(PureXML.parse(bytes: serialized("\u{20AC}\u{201C}", .windows1252))) == "\u{20AC}\u{201C}")
}
@Test("An unrepresentable scalar becomes a numeric character reference")
func test_characterReferenceFallback() throws {
// U+4E2D (中) is not in Latin-1, so it is written as 中 (decimal).
let bytes = try serialized("\u{4E2D}", .latin1)
let text = String(decoding: bytes, as: UTF8.self)
#expect(text.contains("中"))
// The reference bytes are ASCII, so they re-parse to the original scalar.
try #expect(rootText(PureXML.parse(bytes: bytes)) == "\u{4E2D}")
}
@Test("The declaration carries the canonical encoding name")
func test_declarationName() throws {
let text = try String(decoding: serialized("x", .latin1), as: UTF8.self)
#expect(text.hasPrefix(""))
}
@Test("UTF-16 output carries a byte-order mark and round-trips")
func test_utf16() throws {
let bytes = try serialized("hi\u{4E2D}", .utf16BigEndian)
#expect(Array(bytes.prefix(2)) == [0xFE, 0xFF]) // BOM
try #expect(rootText(PureXML.parse(bytes: bytes)) == "hi\u{4E2D}")
}
/// Encodes the content, asserts it was truly encoded (no character-reference
/// fallback), then re-parses it back to the original text.
private func roundTrip(_ content: String, _ encoding: PureXML.Parsing.InputEncoding) throws {
let bytes = try serialized("\(content)", encoding)
#expect(!String(decoding: bytes, as: UTF8.self).contains(""))
try #expect(rootText(PureXML.parse(bytes: bytes)) == content)
}
@Test("Shift-JIS and EUC-JP round-trip Japanese")
func test_japanese() throws {
try roundTrip("\u{65E5}\u{672C}\u{FF71}", .shiftJIS) // 日本 + half-width katakana ア
try roundTrip("\u{65E5}\u{672C}", .eucJP)
}
@Test("EUC-KR round-trips Hangul")
func test_korean() throws {
try roundTrip("\u{AC00}\u{D55C}", .eucKR) // 가한
}
@Test("GBK and Big5 round-trip Chinese")
func test_chinese() throws {
try roundTrip("\u{4E2D}\u{6587}", .gbk) // 中文
try roundTrip("\u{4E2D}\u{6587}", .big5)
}
@Test("GB18030 round-trips a two-byte BMP char and a four-byte astral char")
func test_gb18030() throws {
try roundTrip("\u{4E2D}", .gb18030) // 中, two-byte
try roundTrip("\u{20000}", .gb18030) // 𠀀, four-byte astral
// The astral char must use the four-byte form, not a character reference.
let bytes = try serialized("\u{20000}", .gb18030)
try #expect(rootText(PureXML.parse(bytes: bytes)) == "\u{20000}")
}
}