diff --git a/README.md b/README.md
index cf41a19c..4a341946 100644
--- a/README.md
+++ b/README.md
@@ -212,6 +212,16 @@ Starting with [version 0.5](https://github.com/MaxDesiatov/XMLCoder/releases/tag
you can now set a property `trimValueWhitespaces` to `false` (the default value is `true`) on
`XMLDecoder` instance to preserve all whitespaces in decoded strings.
+
+### Remove whitespace elements
+
+When decoding pretty-printed XML while `trimValueWhitespaces` is set to `false`, it's possible
+for whitespace elements to be added as child elements on an instance of `XMLCoderElement`. These
+whitespace elements make it impossible to decode data structures that require custom `Decodable` logic.
+Starting with [version 0.13.0](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.13.0) you can
+set a property `removeWhitespaceElements` to `true` (the default value is `false`) on
+`XMLDecoder` to remove these whitespace elements.
+
### Choice element coding
Starting with [version 0.8](https://github.com/MaxDesiatov/XMLCoder/releases/tag/0.8.0),
diff --git a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift
index f7a71091..72e63c3d 100644
--- a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift
+++ b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift
@@ -44,3 +44,9 @@ extension StringProtocol {
self = lowercasingFirstLetter()
}
}
+
+extension String {
+ func isAllWhitespace() -> Bool {
+ return self.trimmingCharacters(in: .whitespacesAndNewlines) == ""
+ }
+}
diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
index db3a12f3..c3a96673 100644
--- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
+++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
@@ -392,3 +392,10 @@ extension XMLCoderElement {
}
}
}
+
+extension XMLCoderElement {
+ func isWhitespaceWithNoElements() -> Bool {
+ let stringValueIsWhitespaceOrNil = stringValue?.isAllWhitespace() ?? true
+ return self.key == "" && stringValueIsWhitespaceOrNil && self.elements.isEmpty
+ }
+}
diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
index 41833a71..ceaceba1 100644
--- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
+++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
@@ -15,9 +15,11 @@ class XMLStackParser: NSObject {
var root: XMLCoderElement?
private var stack: [XMLCoderElement] = []
private let trimValueWhitespaces: Bool
+ private let removeWhitespaceElements: Bool
- init(trimValueWhitespaces: Bool = true) {
+ init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
+ self.removeWhitespaceElements = removeWhitespaceElements
super.init()
}
@@ -25,9 +27,11 @@ class XMLStackParser: NSObject {
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
- trimValueWhitespaces: Bool
+ trimValueWhitespaces: Bool,
+ removeWhitespaceElements: Bool
) throws -> Box {
- let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces)
+ let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces,
+ removeWhitespaceElements: removeWhitespaceElements)
let node = try parser.parse(
with: data,
@@ -141,15 +145,36 @@ extension XMLStackParser: XMLParserDelegate {
return
}
+ let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element
+
withCurrentElement { currentElement in
- currentElement.append(element: element, forKey: element.key)
+ currentElement.append(element: updatedElement, forKey: updatedElement.key)
}
if stack.isEmpty {
- root = element
+ root = updatedElement
}
}
+ func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement {
+ var hasWhitespaceElements = false
+ var hasNonWhitespaceElements = false
+ var filteredElements: [XMLCoderElement] = []
+ for ele in element.elements {
+ if ele.isWhitespaceWithNoElements() {
+ hasWhitespaceElements = true
+ } else {
+ hasNonWhitespaceElements = true
+ filteredElements.append(ele)
+ }
+ }
+
+ if hasWhitespaceElements && hasNonWhitespaceElements {
+ return XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes)
+ }
+ return element
+ }
+
func parser(_: XMLParser, foundCharacters string: String) {
let processedString = process(string: string)
guard processedString.count > 0, string.count != 0 else {
diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift
index 91452184..7ec218cb 100644
--- a/Sources/XMLCoder/Decoder/XMLDecoder.swift
+++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift
@@ -310,6 +310,12 @@ open class XMLDecoder {
*/
open var trimValueWhitespaces: Bool
+ /** A boolean value that determines whether to remove pure whitespace elements
+ that have sibling elements that aren't pure whitespace. The default value
+ is `false`.
+ */
+ open var removeWhitespaceElements: Bool
+
/// Options set on the top-level encoder to pass down the decoding hierarchy.
struct Options {
let dateDecodingStrategy: DateDecodingStrategy
@@ -335,8 +341,9 @@ open class XMLDecoder {
// MARK: - Constructing a XML Decoder
/// Initializes `self` with default strategies.
- public init(trimValueWhitespaces: Bool = true) {
+ public init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
+ self.removeWhitespaceElements = removeWhitespaceElements
}
// MARK: - Decoding Values
@@ -356,7 +363,8 @@ open class XMLDecoder {
with: data,
errorContextLength: errorContextLength,
shouldProcessNamespaces: shouldProcessNamespaces,
- trimValueWhitespaces: trimValueWhitespaces
+ trimValueWhitespaces: trimValueWhitespaces,
+ removeWhitespaceElements: removeWhitespaceElements
)
let decoder = XMLDecoderImplementation(
diff --git a/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift b/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift
index efb593f7..19a3ef67 100644
--- a/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift
+++ b/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift
@@ -41,4 +41,39 @@ class StringExtensionsTests: XCTestCase {
}
XCTAssertEqual(expected, mutated)
}
+
+ func testIsAllWhitespace() {
+ let testString1 = ""
+ let testString2 = " "
+
+ let testString3 = "\n"
+ let testString4 = "\n "
+ let testString5 = " \n "
+ let testString6 = " \n"
+
+ let testString7 = "\r"
+ let testString8 = "\r "
+ let testString9 = " \r "
+ let testString10 = " \r"
+
+ let testString11 = "\r\n"
+ let testString12 = "\r\n "
+ let testString13 = " \r\n "
+ let testString14 = " \r\n"
+
+ XCTAssert(testString1.isAllWhitespace())
+ XCTAssert(testString2.isAllWhitespace())
+ XCTAssert(testString3.isAllWhitespace())
+ XCTAssert(testString4.isAllWhitespace())
+ XCTAssert(testString5.isAllWhitespace())
+ XCTAssert(testString6.isAllWhitespace())
+ XCTAssert(testString7.isAllWhitespace())
+ XCTAssert(testString8.isAllWhitespace())
+ XCTAssert(testString9.isAllWhitespace())
+ XCTAssert(testString10.isAllWhitespace())
+ XCTAssert(testString11.isAllWhitespace())
+ XCTAssert(testString12.isAllWhitespace())
+ XCTAssert(testString13.isAllWhitespace())
+ XCTAssert(testString14.isAllWhitespace())
+ }
}
diff --git a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
index 97d5f70a..fb828246 100644
--- a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
+++ b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift
@@ -49,4 +49,19 @@ class XMLElementTests: XCTestCase {
XCTAssertEqual(keyed.elements, [element])
XCTAssertEqual(keyed.attributes, [])
}
+
+ func testWhitespaceWithNoElements_keyed() {
+ let keyed = XMLCoderElement(key: "foo", isStringBoxCDATA: false, box: StringBox("bar"))
+ XCTAssertFalse(keyed.isWhitespaceWithNoElements())
+ }
+
+ func testWhitespaceWithNoElements_whitespace() {
+ let whitespaceElement1 = XMLCoderElement(stringValue: "\n ")
+ let whitespaceElement2 = XMLCoderElement(stringValue: "\n")
+ let whitespaceElement3 = XMLCoderElement(stringValue: " ")
+
+ XCTAssert(whitespaceElement1.isWhitespaceWithNoElements())
+ XCTAssert(whitespaceElement2.isWhitespaceWithNoElements())
+ XCTAssert(whitespaceElement3.isWhitespaceWithNoElements())
+ }
}
diff --git a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift
index deedec91..94588b1f 100644
--- a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift
+++ b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift
@@ -56,4 +56,106 @@ class XMLStackParserTests: XCTestCase {
shouldProcessNamespaces: false
))
}
+
+ func testNestedMembers_removeWhitespaceElements() throws {
+ let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: true)
+ let xmlData =
+ """
+
+
+
+ foo
+ bar
+
+
+ baz
+ qux
+
+
+
+ """.data(using: .utf8)!
+ let root = try parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
+
+ XCTAssertEqual(root.elements[0].key, "nestedStringList")
+
+ XCTAssertEqual(root.elements[0].elements[0].key, "member")
+ XCTAssertEqual(root.elements[0].elements[0].elements[0].key, "member")
+ XCTAssertEqual(root.elements[0].elements[0].elements[0].elements[0].stringValue, "foo")
+ XCTAssertEqual(root.elements[0].elements[0].elements[1].elements[0].stringValue, "bar")
+
+ XCTAssertEqual(root.elements[0].elements[1].key, "member")
+ XCTAssertEqual(root.elements[0].elements[1].elements[0].key, "member")
+ XCTAssertEqual(root.elements[0].elements[1].elements[0].elements[0].stringValue, "baz")
+ XCTAssertEqual(root.elements[0].elements[1].elements[1].elements[0].stringValue, "qux")
+ }
+
+ func testNestedMembers() throws {
+ let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: false)
+ let xmlData =
+ """
+
+
+
+ foo
+ bar
+
+
+ baz
+ qux
+
+
+
+ """.data(using: .utf8)!
+ let root = try parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
+
+ XCTAssertEqual(root.elements[0].key, "")
+ XCTAssertEqual(root.elements[0].stringValue, "\n ")
+
+ XCTAssertEqual(root.elements[1].key, "nestedStringList")
+ XCTAssertEqual(root.elements[1].elements[0].key, "")
+ XCTAssertEqual(root.elements[1].elements[0].stringValue, "\n ")
+ XCTAssertEqual(root.elements[1].elements[1].key, "member")
+ XCTAssertEqual(root.elements[1].elements[1].elements[0].stringValue, "\n ")
+
+ XCTAssertEqual(root.elements[1].elements[1].elements[1].key, "member")
+ XCTAssertEqual(root.elements[1].elements[1].elements[1].elements[0].stringValue, "foo")
+ XCTAssertEqual(root.elements[1].elements[1].elements[3].key, "member")
+ XCTAssertEqual(root.elements[1].elements[1].elements[3].elements[0].stringValue, "bar")
+
+ XCTAssertEqual(root.elements[1].elements[3].elements[1].key, "member")
+ XCTAssertEqual(root.elements[1].elements[3].elements[1].elements[0].stringValue, "baz")
+ XCTAssertEqual(root.elements[1].elements[3].elements[3].key, "member")
+ XCTAssertEqual(root.elements[1].elements[3].elements[3].elements[0].stringValue, "qux")
+ }
+
+ func testEscapableCharacters_removeWhitespaceElements() throws {
+ let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: true)
+ let xmlData =
+ """
+
+ escaped data: <
+
+ """.data(using: .utf8)!
+ let root = try parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
+
+ XCTAssertEqual(root.key, "SomeType")
+ XCTAssertEqual(root.elements[0].key, "strValue")
+ XCTAssertEqual(root.elements[0].elements[0].stringValue, "escaped data: <\r\n")
+ }
+
+ func testEscapableCharacters() throws {
+ let parser = XMLStackParser(trimValueWhitespaces: false, removeWhitespaceElements: false)
+ let xmlData =
+ """
+
+ escaped data: <
+
+ """.data(using: .utf8)!
+ let root = try parser.parse(with: xmlData, errorContextLength: 0, shouldProcessNamespaces: false)
+ XCTAssertEqual(root.key, "SomeType")
+ XCTAssertEqual(root.elements[0].key, "")
+ XCTAssertEqual(root.elements[0].stringValue, "\n ")
+ XCTAssertEqual(root.elements[1].elements[0].stringValue, "escaped data: <\r\n")
+ XCTAssertEqual(root.elements[2].stringValue, "\n")
+ }
}
diff --git a/Tests/XMLCoderTests/Minimal/NestedStringList.swift b/Tests/XMLCoderTests/Minimal/NestedStringList.swift
new file mode 100644
index 00000000..0dad92de
--- /dev/null
+++ b/Tests/XMLCoderTests/Minimal/NestedStringList.swift
@@ -0,0 +1,74 @@
+// Copyright (c) 2018-2020 XMLCoder contributors
+//
+// This software is released under the MIT License.
+// https://opensource.org/licenses/MIT
+//
+// Created by John Woo on 7/29/21.
+//
+
+import Foundation
+
+import XCTest
+@testable import XMLCoder
+
+class NestedStringList: XCTestCase {
+
+ struct TypeWithNestedStringList: Decodable {
+ let nestedStringList: [[String]]
+
+ enum CodingKeys: String, CodingKey {
+ case nestedStringList
+ }
+
+ enum NestedMemberKeys: String, CodingKey {
+ case member
+ }
+
+ public init (from decoder: Decoder) throws {
+ let containerValues = try decoder.container(keyedBy: CodingKeys.self)
+ let nestedStringListWrappedContainer = try containerValues.nestedContainer(keyedBy: NestedMemberKeys.self, forKey: .nestedStringList)
+ let nestedStringListContainer = try nestedStringListWrappedContainer.decodeIfPresent([[String]].self, forKey: .member)
+ var nestedStringListBuffer:[[String]] = []
+ if let nestedStringListContainer = nestedStringListContainer {
+ nestedStringListBuffer = [[String]]()
+ var listBuffer0: [String]? = nil
+ for listContainer0 in nestedStringListContainer {
+ listBuffer0 = [String]()
+ for stringContainer1 in listContainer0 {
+ listBuffer0?.append(stringContainer1)
+ }
+ if let listBuffer0 = listBuffer0 {
+ nestedStringListBuffer.append(listBuffer0)
+ }
+ }
+ }
+ nestedStringList = nestedStringListBuffer
+ }
+ }
+
+ func testRemoveWhitespaceElements() throws {
+ let decoder = XMLDecoder(trimValueWhitespaces: false, removeWhitespaceElements: true)
+ let xmlString =
+ """
+
+
+
+ foo: <
+ bar: <
+
+
+ baz: <
+ qux: <
+
+
+
+ """
+ let xmlData = xmlString.data(using: .utf8)!
+
+ let decoded = try decoder.decode(TypeWithNestedStringList.self, from: xmlData)
+ XCTAssertEqual(decoded.nestedStringList[0][0], "foo: <\r\n")
+ XCTAssertEqual(decoded.nestedStringList[0][1], "bar: <\r\n")
+ XCTAssertEqual(decoded.nestedStringList[1][0], "baz: <\r\n")
+ XCTAssertEqual(decoded.nestedStringList[1][1], "qux: <\r\n")
+ }
+}
diff --git a/Tests/XMLCoderTests/Minimal/StringTests.swift b/Tests/XMLCoderTests/Minimal/StringTests.swift
index ea5d6981..aca881f1 100644
--- a/Tests/XMLCoderTests/Minimal/StringTests.swift
+++ b/Tests/XMLCoderTests/Minimal/StringTests.swift
@@ -79,4 +79,18 @@ class StringTests: XCTestCase {
XCTAssertEqual(String(data: encoded, encoding: .utf8)!, xmlString)
}
}
+
+ func testRemoveWhitespaceElements() throws {
+ let decoder = XMLDecoder(trimValueWhitespaces: false)
+ let xmlString =
+ """
+
+ escaped data: <
+
+ """
+ let xmlData = xmlString.data(using: .utf8)!
+
+ let decoded = try decoder.decode(Container.self, from: xmlData)
+ XCTAssertEqual(decoded.value, "escaped data: <\r\n")
+ }
}