diff --git a/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift b/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift index 740455fa..0615ca2f 100644 --- a/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift +++ b/Sources/XMLCoder/Auxiliaries/KeyedStorage.swift @@ -77,11 +77,12 @@ extension KeyedStorage where Key == String, Value == Box { let hasElements = !element.elements.isEmpty let hasAttributes = !element.attributes.isEmpty + let hasText = element.stringValue != nil if hasElements || hasAttributes { result.append(element.transformToBoxTree(), at: element.key) - } else if let value = element.value { - result.append(StringBox(value), at: element.key) + } else if hasText { + result.append(element.transformToBoxTree(), at: element.key) } else { result.append(SingleKeyedBox(key: element.key, element: NullBox()), at: element.key) } diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 584b6e88..805a0d0e 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -23,53 +23,107 @@ struct XMLCoderElement: Equatable { ] let key: String - private(set) var value: String? + private(set) var stringValue: String? private(set) var elements: [XMLCoderElement] = [] private(set) var attributes: [Attribute] = [] + private(set) var containsTextNodes: Bool = false + + var isStringNode: Bool { + return key == "" + } + + var isCDATANode: Bool { + return key == "#CDATA" + } + + var isTextNode: Bool { + return isStringNode || isCDATANode + } init( key: String, - value: String? = nil, elements: [XMLCoderElement] = [], attributes: [Attribute] = [] ) { self.key = key - self.value = value + stringValue = nil self.elements = elements self.attributes = attributes } - mutating func append(value string: String) { - guard value != nil else { - value = string - return - } - value?.append(string) + init( + key: String, + stringValue string: String, + attributes: [Attribute] = [] + ) { + self.key = key + elements = [XMLCoderElement(stringValue: string)] + self.attributes = attributes + containsTextNodes = true + } + + init( + key: String, + cdataValue string: String, + attributes: [Attribute] = [] + ) { + self.key = key + elements = [XMLCoderElement(cdataValue: string)] + self.attributes = attributes + containsTextNodes = true + } + + init(stringValue string: String) { + key = "" + stringValue = string + } + + init(cdataValue string: String) { + key = "#CDATA" + stringValue = string } mutating func append(element: XMLCoderElement, forKey key: String) { elements.append(element) + containsTextNodes = containsTextNodes || element.isTextNode } - func transformToBoxTree() -> KeyedBox { + mutating func append(string: String) { + if elements.last?.isTextNode == true { + let oldValue = elements[elements.count - 1].stringValue ?? "" + elements[elements.count - 1].stringValue = oldValue + string + } else { + elements.append(XMLCoderElement(stringValue: string)) + } + containsTextNodes = true + } + + mutating func append(cdata string: String) { + if elements.last?.isCDATANode == true { + let oldValue = elements[elements.count - 1].stringValue ?? "" + elements[elements.count - 1].stringValue = oldValue + string + } else { + elements.append(XMLCoderElement(cdataValue: string)) + } + containsTextNodes = true + } + + func transformToBoxTree() -> Box { + if isTextNode { + return StringBox(stringValue!) + } + let attributes = KeyedStorage(self.attributes.map { attribute in (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) }) let storage = KeyedStorage() - var elements = self.elements.reduce(storage) { $0.merge(element: $1) } - - // Handle attributed unkeyed value zap - // Value should be zap. Detect only when no other elements exist - if elements.isEmpty, let value = value { - elements.append(StringBox(value), at: "") - } + let elements = self.elements.reduce(storage) { $0.merge(element: $1) } return KeyedBox(elements: elements, attributes: attributes) } func toXMLString(with header: XMLHeader? = nil, withCDATA cdata: Bool, - formatting: XMLEncoder.OutputFormatting, - ignoreEscaping _: Bool = false) -> String { + formatting: XMLEncoder.OutputFormatting) -> String { if let header = header, let headerXML = header.toXML() { return headerXML + _toXMLString(withCDATA: cdata, formatting: formatting) } @@ -100,6 +154,14 @@ struct XMLCoderElement: Equatable { formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool ) -> String { + if let stringValue = element.stringValue { + if element.isCDATANode || cdata { + return "" + } else { + return stringValue.escape(XMLCoderElement.escapedCharacterSet) + } + } + var string = "" string += element._toXMLString( indented: level + 1, withCDATA: cdata, formatting: formatting @@ -149,7 +211,7 @@ struct XMLCoderElement: Equatable { at: level, cdata: cdata, formatting: formatting, - prettyPrinted: prettyPrinted) + prettyPrinted: prettyPrinted && !containsTextNodes) } } @@ -195,39 +257,28 @@ struct XMLCoderElement: Equatable { private func _toXMLString( indented level: Int = 0, withCDATA cdata: Bool, - formatting: XMLEncoder.OutputFormatting, - ignoreEscaping: Bool = false + formatting: XMLEncoder.OutputFormatting ) -> String { let prettyPrinted = formatting.contains(.prettyPrinted) let indentation = String( repeating: " ", count: (prettyPrinted ? level : 0) * 4 ) var string = indentation + if !key.isEmpty { string += "<\(key)" } formatXMLAttributes(formatting, &string) - if let value = value { + if !elements.isEmpty { + let prettyPrintElements = prettyPrinted && !containsTextNodes if !key.isEmpty { - string += ">" - } - if !ignoreEscaping { - string += (cdata == true ? "" : - "\(value.escape(XMLCoderElement.escapedCharacterSet))") - } else { - string += "\(value)" + string += prettyPrintElements ? ">\n" : ">" } + formatXMLElements(formatting, &string, level, cdata, prettyPrintElements) - if !key.isEmpty { - string += "" - } - } else if !elements.isEmpty { - string += prettyPrinted ? ">\n" : ">" - formatXMLElements(formatting, &string, level, cdata, prettyPrinted) - - string += indentation + if prettyPrintElements { string += indentation } if !key.isEmpty { string += "" } @@ -298,8 +349,11 @@ extension XMLCoderElement { } init(key: String, box: SimpleBox) { - self.init(key: key) - value = box.xmlString + if let value = box.xmlString { + self.init(key: key, stringValue: value) + } else { + self.init(key: key) + } } init(key: String, box: Box) { diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 8890b8c6..884ebf13 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -26,7 +26,7 @@ class XMLStackParser: NSObject { errorContextLength length: UInt, shouldProcessNamespaces: Bool, trimValueWhitespaces: Bool - ) throws -> KeyedBox { + ) throws -> Box { let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces) let node = try parser.parse( @@ -159,8 +159,13 @@ extension XMLStackParser: XMLParserDelegate { } func parser(_: XMLParser, foundCharacters string: String) { + let processedString = process(string: string) + guard processedString.count > 0, string.count != 0 else { + return + } + withCurrentElement { currentElement in - currentElement.append(value: process(string: string)) + currentElement.append(string: processedString) } } @@ -170,7 +175,7 @@ extension XMLStackParser: XMLParserDelegate { } withCurrentElement { currentElement in - currentElement.append(value: process(string: string)) + currentElement.append(cdata: string) } } } diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 07e1cb15..b6756dfa 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -212,9 +212,13 @@ extension XMLDecoderImplementation { else { throw error } return value case let singleKeyedBox as SingleKeyedBox: - guard let value = singleKeyedBox.element as? B - else { throw error } - return value + if let value = singleKeyedBox.element as? B { + return value + } else if let box = singleKeyedBox.element as? KeyedBox, let value = box.elements[""].first as? B { + return value + } else { + throw error + } case is NullBox: throw error case let keyedBox as KeyedBox: diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index 7f115097..7aea290d 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -162,7 +162,7 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { let elements = container.unboxed.elements[key.stringValue] - if let containsKeyed = elements as? [KeyedBox], let keyed = containsKeyed.first { + if let containsKeyed = elements as? [KeyedBox], containsKeyed.count == 1, let keyed = containsKeyed.first { return XMLUnkeyedDecodingContainer( referencing: decoder, wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init)) diff --git a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift index f6f1b2ff..d93e2124 100644 --- a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift +++ b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift @@ -13,7 +13,7 @@ class XMLElementTests: XCTestCase { let null = XMLCoderElement(key: "foo") XCTAssertEqual(null.key, "foo") - XCTAssertNil(null.value) + XCTAssertNil(null.stringValue) XCTAssertEqual(null.elements, []) XCTAssertEqual(null.attributes, []) } @@ -22,7 +22,7 @@ class XMLElementTests: XCTestCase { let keyed = XMLCoderElement(key: "foo", box: UnkeyedBox()) XCTAssertEqual(keyed.key, "foo") - XCTAssertNil(keyed.value) + XCTAssertNil(keyed.stringValue) XCTAssertEqual(keyed.elements, []) XCTAssertEqual(keyed.attributes, []) } @@ -34,17 +34,18 @@ class XMLElementTests: XCTestCase { )) XCTAssertEqual(keyed.key, "foo") - XCTAssertNil(keyed.value) + XCTAssertNil(keyed.stringValue) XCTAssertEqual(keyed.elements, []) XCTAssertEqual(keyed.attributes, [Attribute(key: "blee", value: "42")]) } func testInitSimple() { let keyed = XMLCoderElement(key: "foo", box: StringBox("bar")) + let element = XMLCoderElement(stringValue: "bar") XCTAssertEqual(keyed.key, "foo") - XCTAssertEqual(keyed.value, "bar") - XCTAssertEqual(keyed.elements, []) + XCTAssertNil(keyed.stringValue) + XCTAssertEqual(keyed.elements, [element]) XCTAssertEqual(keyed.attributes, []) } } diff --git a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift index 5a558f6f..4c0dc11f 100644 --- a/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift +++ b/Tests/XMLCoderTests/Auxiliary/XMLStackParserTests.swift @@ -29,15 +29,14 @@ class XMLStackParserTests: XCTestCase { let expected = XMLCoderElement( key: "container", - value: "", elements: [ XMLCoderElement( key: "value", - value: "42" + stringValue: "42" ), XMLCoderElement( key: "data", - value: "lorem ipsum" + cdataValue: "lorem ipsum" ), ] ) diff --git a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift index 0707a128..0a5b1063 100644 --- a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift +++ b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift @@ -12,24 +12,21 @@ class BoxTreeTests: XCTestCase { func testNestedValues() throws { let e1 = XMLCoderElement( key: "foo", - value: "456", - elements: [], + stringValue: "456", attributes: [Attribute(key: "id", value: "123")] ) let e2 = XMLCoderElement( key: "foo", - value: "123", - elements: [], + stringValue: "123", attributes: [Attribute(key: "id", value: "789")] ) let root = XMLCoderElement( key: "container", - value: nil, elements: [e1, e2], attributes: [] ) - let boxTree = root.transformToBoxTree() + let boxTree = root.transformToBoxTree() as! KeyedBox let foo = boxTree.elements["foo"] XCTAssertEqual(foo.count, 2) } diff --git a/Tests/XMLCoderTests/Minimal/MixedContentTests.swift b/Tests/XMLCoderTests/Minimal/MixedContentTests.swift new file mode 100644 index 00000000..de72ff96 --- /dev/null +++ b/Tests/XMLCoderTests/Minimal/MixedContentTests.swift @@ -0,0 +1,65 @@ +// +// MixedContentTests.swift +// XMLCoderTests +// +// Created by Christopher Williams on 11/21/19. +// + +import Foundation + +import XCTest +@testable import XMLCoder + +class MixedContentTests: XCTestCase { + enum TextItem: Codable, Equatable { + case bold(String) + case text(String) + + enum CodingKeys: String, XMLChoiceCodingKey { + case bold = "b" + case text = "" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .text(text): + try container.encode(text, forKey: .text) + case let .bold(text): + try container.encode(text, forKey: .bold) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let key = container.allKeys.first! + switch key { + case .bold: + let string = try container.decode(String.self, forKey: .bold) + self = .bold(string) + case .text: + let string = try container.decode(String.self, forKey: .text) + self = .text(string) + } + } + } + + func testMixed() throws { + let decoder = XMLDecoder() + let encoder = XMLEncoder() + + let xmlString = + """ + firstbold textsecond + """ + + let xmlData = xmlString.data(using: .utf8)! + + let decoded = try decoder.decode([TextItem].self, from: xmlData) + XCTAssertEqual(decoded, [.text("first"), .bold("bold text"), .text("second")]) + + let encoded = try encoder.encode(decoded, withRootKey: "container") + let encodedString = String(data: encoded, encoding: .utf8) + XCTAssertEqual(encodedString, xmlString) + } +}