diff --git a/CHANGELOG.md b/CHANGELOG.md index 0117eaa..c04bb21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Single-byte legacy encodings (#97, partial): the byte decoder gains a table-based single-byte path and four encodings expressible exactly (so transcription-error-free): ISO-8859-15 (Latin-9, euro), ISO-8859-9 (Latin-5, Turkish), ISO-8859-5 (Latin/Cyrillic), and Windows-1254 (Turkish, composed from Windows-1252). They are selected by the XML declaration's `encoding` name, for both the whole-buffer and streaming decoders. The remaining legacy encodings are data drop-ins into the same path. +- Single-byte legacy encodings (#97, partial): the byte decoder gains a table-based single-byte path and five legacy encodings: ISO-8859-15 (Latin-9, euro), ISO-8859-9 (Latin-5, Turkish), ISO-8859-5 (Latin/Cyrillic), Windows-1254 (Turkish, composed from Windows-1252), and ISO-8859-2 (Latin-2, vendored verbatim from the authoritative Unicode mapping file). They are selected by the XML declaration's `encoding` name, for both the whole-buffer and streaming decoders. The remaining legacy encodings are data drop-ins into the same path. ### Fixed diff --git a/README.md b/README.md index 3d2e53a..bd7e9be 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ flowchart TB PureXML is a working, dependency-free XML library today: parse, emit, validate, query, and stream documents on macOS, Linux, Windows, and WASM. The test suite -currently runs **754 tests in 117 suites** (`swift test`). +currently runs **755 tests in 117 suites** (`swift test`). ### Shipped (libxml2-aligned surface) diff --git a/Sources/Parsing/ByteDecoder.swift b/Sources/Parsing/ByteDecoder.swift index b4ae670..ce695f3 100644 --- a/Sources/Parsing/ByteDecoder.swift +++ b/Sources/Parsing/ByteDecoder.swift @@ -32,6 +32,7 @@ extension PureXML.Parsing { case .latin1: { Unicode.Scalar($0) } case .windows1252: windows1252Scalar case .windows1254: SingleByte.windows1254 + case .latin2: SingleByte.iso8859_2 case .latinCyrillic: SingleByte.iso8859_5 case .latin5: SingleByte.iso8859_9 case .latin9: SingleByte.iso8859_15 @@ -100,6 +101,7 @@ extension PureXML.Parsing { "iso-8859-1": .latin1, "latin1": .latin1, "latin-1": .latin1, "l1": .latin1, "windows-1252": .windows1252, "cp1252": .windows1252, "cp-1252": .windows1252, "windows-1254": .windows1254, "cp1254": .windows1254, + "iso-8859-2": .latin2, "iso8859-2": .latin2, "latin2": .latin2, "latin-2": .latin2, "l2": .latin2, "iso-8859-5": .latinCyrillic, "iso8859-5": .latinCyrillic, "cyrillic": .latinCyrillic, "iso-8859-9": .latin5, "iso8859-9": .latin5, "latin5": .latin5, "latin-5": .latin5, "l5": .latin5, "iso-8859-15": .latin9, "iso8859-15": .latin9, "latin9": .latin9, "latin-9": .latin9, "l9": .latin9, diff --git a/Sources/Parsing/InputEncoding.swift b/Sources/Parsing/InputEncoding.swift index 055ddec..7516dd8 100644 --- a/Sources/Parsing/InputEncoding.swift +++ b/Sources/Parsing/InputEncoding.swift @@ -11,6 +11,7 @@ public extension PureXML.Parsing { case latin1 case windows1252 case windows1254 // Turkish + case latin2 // ISO-8859-2 case latinCyrillic // ISO-8859-5 case latin5 // ISO-8859-9 (Turkish) case latin9 // ISO-8859-15 diff --git a/Sources/Parsing/SingleByteEncodings.swift b/Sources/Parsing/SingleByteEncodings.swift index 153544d..6427252 100644 --- a/Sources/Parsing/SingleByteEncodings.swift +++ b/Sources/Parsing/SingleByteEncodings.swift @@ -64,7 +64,20 @@ extension PureXML.Parsing.ByteDecoder { } } - private static func scalar(_ value: UInt32) -> Unicode.Scalar { + /// ISO-8859-2 (Latin-2, Central European). Vendored from the authoritative + /// Unicode mapping file. + static func iso8859_2(_ byte: UInt8) -> Unicode.Scalar { + upperHalf(byte, latin2Upper) + } + + /// Maps a byte through a 96-entry upper-half table (`0xA0`-`0xFF`); bytes + /// below `0xA0` are ASCII or C1 controls (identity). Used by the encodings + /// whose upper half is vendored from the Unicode mapping files. + static func upperHalf(_ byte: UInt8, _ table: [UInt16]) -> Unicode.Scalar { + byte >= 0xA0 ? scalar(UInt32(table[Int(byte) - 0xA0])) : Unicode.Scalar(byte) + } + + static func scalar(_ value: UInt32) -> Unicode.Scalar { Unicode.Scalar(value) ?? "\u{FFFD}" } } diff --git a/Sources/Parsing/SingleByteTables.swift b/Sources/Parsing/SingleByteTables.swift new file mode 100644 index 0000000..f4b4d91 --- /dev/null +++ b/Sources/Parsing/SingleByteTables.swift @@ -0,0 +1,18 @@ +extension PureXML.Parsing.ByteDecoder.SingleByte { + /// The upper half (bytes 0xA0-0xFF) of ISO-8859-2 (Latin-2), vendored verbatim + /// from the Unicode mapping file `MAPPINGS/ISO8859/8859-2.TXT`. + static let latin2Upper: [UInt16] = [ + 0x00A0, 0x0104, 0x02D8, 0x0141, 0x00A4, 0x013D, 0x015A, 0x00A7, + 0x00A8, 0x0160, 0x015E, 0x0164, 0x0179, 0x00AD, 0x017D, 0x017B, + 0x00B0, 0x0105, 0x02DB, 0x0142, 0x00B4, 0x013E, 0x015B, 0x02C7, + 0x00B8, 0x0161, 0x015F, 0x0165, 0x017A, 0x02DD, 0x017E, 0x017C, + 0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7, + 0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E, + 0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7, + 0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF, + 0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7, + 0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F, + 0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7, + 0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9, + ] +} diff --git a/Tests/EncodingTests.swift b/Tests/EncodingTests.swift index 81303b6..ec13a7a 100644 --- a/Tests/EncodingTests.swift +++ b/Tests/EncodingTests.swift @@ -100,6 +100,14 @@ struct EncodingTests { try #expect(decoded("ISO-8859-9", [0xE9]) == "\u{E9}") // é, unchanged from Latin-1 } + @Test("Decodes ISO-8859-2 (Latin-2) from the vendored table") + func test_iso8859_2() throws { + try #expect(decoded("ISO-8859-2", [0xA1]) == "\u{0104}") // Ą + try #expect(decoded("ISO-8859-2", [0xE8]) == "\u{010D}") // č + try #expect(decoded("ISO-8859-2", [0xE9]) == "\u{E9}") // é + try #expect(decoded("ISO-8859-2", [0xFF]) == "\u{02D9}") // ˙ + } + @Test("Decodes ISO-8859-5: the Cyrillic block") func test_iso8859_5() throws { try #expect(decoded("ISO-8859-5", [0xB0]) == "\u{0410}") // А