diff --git a/.gitignore b/.gitignore index b3ed1ff..9bff9d8 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,7 @@ DerivedData/ !default.perspectivev3 + +SwiftyXMLParser.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist + +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/SwiftyXMLParser/Accessor.swift b/SwiftyXMLParser/Accessor.swift index a68a686..3adafc3 100755 --- a/SwiftyXMLParser/Accessor.swift +++ b/SwiftyXMLParser/Accessor.swift @@ -128,23 +128,26 @@ extension XML { let accessor: Accessor switch self { case .singleElement(let element): - let filterdElements = element.childElements.filter { $0.name == key } - if filterdElements.isEmpty { + let childElements = element.childElements.filter { + if $0.ignoreNamespaces { + return key == $0.name.components(separatedBy: ":").last ?? $0.name + } else { + return key == $0.name + } + } + if childElements.isEmpty { let error = accessError("\(key) not found.") - accessor = Accessor(error) - } else if filterdElements.count == 1 { - accessor = Accessor(filterdElements[0]) + accessor = Accessor(error) + } else if childElements.count == 1 { + accessor = Accessor(childElements[0]) } else { - accessor = Accessor(filterdElements) + accessor = Accessor(childElements) } case .failure(let error): - accessor = Accessor(error) - case .sequence(_): - fallthrough + accessor = Accessor(error) default: let error = accessError("cannot access \(key), because of multiple elements") - accessor = Accessor(error) - break + accessor = Accessor(error) } return accessor } @@ -235,22 +238,27 @@ extension XML { } return name } - + + /// get and set text on single element public var text: String? { - let text: String? - switch self { - case .singleElement(let element): - text = element.text - case .failure(_), .sequence(_): - fallthrough - default: - text = nil - break + get { + switch self { + case .singleElement(let element): + return element.text + default: + return nil + } + } + set { + switch self { + case .singleElement(let element): + element.text = newValue + default: + break + } } - return text } - /// syntax sugar to access Bool Text public var bool: Bool? { return text.flatMap { $0 == "true" } @@ -272,19 +280,24 @@ extension XML { return text.flatMap({Double($0)}) } - /// access to XML Attributes + /// get and set XML attributes on single element public var attributes: [String: String] { - let attributes: [String: String] - switch self { - case .singleElement(let element): - attributes = element.attributes - case .failure(_), .sequence(_): - fallthrough - default: - attributes = [String: String]() - break + get { + switch self { + case .singleElement(let element): + return element.attributes + default: + return [String: String]() + } + } + set { + switch self { + case .singleElement(let element): + element.attributes = newValue + default: + break + } } - return attributes } /// access to child Elements @@ -396,6 +409,15 @@ extension XML { } } + public func append(_ newElement: Element) { + switch self { + case .singleElement(let element): + element.childElements.append(newElement) + default: + break + } + } + // MARK: - SequenceType public func makeIterator() -> AnyIterator { @@ -451,7 +473,7 @@ extension XML { } extension XML { - /// Conveter to make xml document from Accessor. + /// Converter to make xml document from Accessor. public class Converter { let accessor: XML.Accessor @@ -460,7 +482,9 @@ extension XML { } /** - If Accessor object has correct XML path, return the XML element, otherwith return error + Convert accessor back to XML document string. + + - Parameter withDeclaration:Prefix with standard XML declaration (default true) example: @@ -473,12 +497,12 @@ extension XML { ``` */ - public func makeDocument() throws -> String { + public func makeDocument(withDeclaration: Bool = true) throws -> String { if case .failure(let err) = accessor { throw err } - var doc: String = "" + var doc = withDeclaration ? "" : "" for hit in accessor { switch hit { case .singleElement(let element): @@ -496,7 +520,7 @@ extension XML { private func traverse(_ element: Element) -> String { let name = element.name let text = element.text ?? "" - let attrs = element.attributes.map { (k, v) in "\(k)=\"\(v)\"" }.joined(separator: " ") + let attrs = element.attributes.map { (k, v) in "\(k)=\"\(v)\"" }.joined(separator: " ") let childDocs = element.childElements.reduce("", { (result, element) in result + traverse(element) @@ -505,7 +529,7 @@ extension XML { if name == "XML.Parser.AbstructedDocumentRoot" { return childDocs } else { - return "<\(name) \(attrs)>\(text)\(childDocs)" + return "<\(name)\(attrs.isEmpty ? "" : " ")\(attrs)>\(text)\(childDocs)" } } } diff --git a/SwiftyXMLParser/Element.swift b/SwiftyXMLParser/Element.swift index 4bd3bfa..7182b3d 100755 --- a/SwiftyXMLParser/Element.swift +++ b/SwiftyXMLParser/Element.swift @@ -28,17 +28,34 @@ extension XML { open class Element { open var name: String open var text: String? - open var attributes = [String: String]() - open var childElements = [Element]() - open var lineNumberStart = -1 - open var lineNumberEnd = -1 + open var attributes: [String: String] + open var childElements: [Element] + open var lineNumberStart: Int + open var lineNumberEnd: Int open var CDATA: Data? + open var ignoreNamespaces: Bool // for println open weak var parentElement: Element? - - public init(name: String) { + + public init( + name: String, + text: String? = nil, + attributes: [String: String] = [:], + childElements: [Element] = [], + lineNumberStart: Int = -1, + lineNumberEnd: Int = -1, + CDATA: Data? = nil, + ignoreNamespaces: Bool = false + ) { self.name = name + self.text = text + self.attributes = attributes + self.childElements = childElements + self.lineNumberStart = lineNumberStart + self.lineNumberEnd = lineNumberEnd + self.CDATA = CDATA + self.ignoreNamespaces = ignoreNamespaces } } } diff --git a/SwiftyXMLParser/Parser.swift b/SwiftyXMLParser/Parser.swift index 09930c2..f23f883 100755 --- a/SwiftyXMLParser/Parser.swift +++ b/SwiftyXMLParser/Parser.swift @@ -33,7 +33,7 @@ extension XML { /// So the result of parsing is missing. /// See https://developer.apple.com/documentation/foundation/xmlparser/errorcode private(set) var error: XMLError? - + func parse(_ data: Data) -> Accessor { stack = [Element]() stack.append(documentRoot) @@ -46,22 +46,21 @@ extension XML { return Accessor(documentRoot) } } - - override init() { - trimmingManner = nil - } - - init(trimming manner: CharacterSet) { - trimmingManner = manner + + init(trimming manner: CharacterSet? = nil, ignoreNamespaces: Bool = false) { + self.trimmingManner = manner + self.ignoreNamespaces = ignoreNamespaces + self.documentRoot = Element(name: "XML.Parser.AbstructedDocumentRoot", ignoreNamespaces: ignoreNamespaces) } // MARK:- private - fileprivate var documentRoot = Element(name: "XML.Parser.AbstructedDocumentRoot") + fileprivate var documentRoot: Element fileprivate var stack = [Element]() fileprivate let trimmingManner: CharacterSet? + fileprivate let ignoreNamespaces: Bool func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { - let node = Element(name: elementName) + let node = Element(name: elementName, ignoreNamespaces: ignoreNamespaces) node.lineNumberStart = parser.lineNumber if !attributeDict.isEmpty { node.attributes = attributeDict diff --git a/SwiftyXMLParser/XML.swift b/SwiftyXMLParser/XML.swift index 884c304..bc201dc 100755 --- a/SwiftyXMLParser/XML.swift +++ b/SwiftyXMLParser/XML.swift @@ -86,57 +86,43 @@ public func ?<< (lhs: inout [T], rhs: T?) { ``` */ open class XML { + /** - Interface to parse NSData - - - parameter data:NSData XML document - - returns:Accessor object to access XML document - */ - open class func parse(_ data: Data) -> Accessor { - return Parser().parse(data) - } - - /** - Interface to parse String - - - Parameter str:String XML document - - Returns:Accessor object to access XML document - */ - open class func parse(_ str: String) throws -> Accessor { - guard let data = str.data(using: String.Encoding.utf8) else { - throw XMLError.failToEncodeString - } - - return Parser().parse(data) - } - - /** - Interface to parse NSData + Interface to parse Data - - parameter data:NSData XML document - - parameter manner:NSCharacterSet If you wannna trim Text, assign this arg + - parameter data:Data XML document + - parameter manner:CharacterSet If you want to trim text (default off) + - parameter ignoreNamespaces:Bool If set to true all accessors will ignore the first part of an element name up to a semicolon (default false) - returns:Accessor object to access XML document */ - open class func parse(_ data: Data, trimming manner: CharacterSet) -> Accessor { - return Parser(trimming: manner).parse(data) + open class func parse(_ data: Data, trimming manner: CharacterSet? = nil, ignoreNamespaces: Bool = false) -> Accessor { + return Parser(trimming: manner, ignoreNamespaces: ignoreNamespaces).parse(data) } /** Interface to parse String - - Parameter str:String XML document - - parameter manner:NSCharacterSet If you wannna trim Text, assign this arg - - Returns:Accessor object to access XML document + - parameter str:String XML document + - parameter manner:CharacterSet If you want to trim text (default off) + - parameter ignoreNamespaces:Bool If set to true all accessors will ignore the first part of an element name up to a semicolon (default false) + - returns:Accessor object to access XML document */ - open class func parse(_ str: String, trimming manner: CharacterSet) throws -> Accessor { + open class func parse(_ str: String, trimming manner: CharacterSet? = nil, ignoreNamespaces: Bool = false) throws -> Accessor { guard let data = str.data(using: String.Encoding.utf8) else { throw XMLError.failToEncodeString } - return Parser(trimming: manner).parse(data) + return Parser(trimming: manner, ignoreNamespaces: ignoreNamespaces).parse(data) } - - open class func document(_ accessor: Accessor) throws -> String { - return try Converter(accessor).makeDocument() + + /** + Convert accessor back to XML document string. + + - parameter accessor:XML accessor + - parameter withDeclaration:Prefix with standard XML declaration (default true) + - returns:XML document string + */ + open class func document(_ accessor: Accessor, withDeclaration: Bool = true) throws -> String { + return try Converter(accessor).makeDocument(withDeclaration: withDeclaration) } } diff --git a/SwiftyXMLParserTests/AccessorTests.swift b/SwiftyXMLParserTests/AccessorTests.swift index 53a8479..8604683 100755 --- a/SwiftyXMLParserTests/AccessorTests.swift +++ b/SwiftyXMLParserTests/AccessorTests.swift @@ -221,6 +221,56 @@ class AccessorTests: XCTestCase { XCTAssert(true, "fail to access name with Failure Accessor") } } + + func testSetText() throws { + var accessor = XML.Accessor(singleElement()) + accessor.text = "text2" + XCTAssertEqual(accessor.text, "text2", "set text on first single element") + + var element = accessor["ChildElement"].first + element.text = "childText1" + XCTAssertEqual(element.text, "childText1", "set text for first child element") + + element = accessor["ChildElement"].last + element.text = "childText2" + XCTAssertEqual(element.text, "childText2", "set text for last child element") + + XCTAssertEqual( + try XML.document(accessor), + "text2childText1childText2", + "end document has newly added texts" + ) + + var sequenceAccessor = XML.Accessor(sequence()) + sequenceAccessor.text = "text" + XCTAssertEqual(sequenceAccessor.text, nil, "cannot set text on sequence") + + var accessorElement = sequenceAccessor.first + accessorElement.text = "newText" + XCTAssertEqual(accessorElement.text, "newText", "set text for first element in sequence") + + accessorElement = sequenceAccessor.last + accessorElement.text = "newText2" + XCTAssertEqual(accessorElement.text, "newText2", "set text for last element in sequence") + + accessorElement = sequenceAccessor.first["ChildElement1"] + accessorElement.text = "childText" + XCTAssertEqual(accessorElement.text, nil, "cannot set text for sequence") + + accessorElement = sequenceAccessor.first["ChildElement1"].first + accessorElement.text = "childText1" + XCTAssertEqual(accessorElement.text, "childText1", "set text for first element of first child") + + accessorElement = sequenceAccessor.first["ChildElement1"].last + accessorElement.text = "childText2" + XCTAssertEqual(accessorElement.text, "childText2", "set text for last element of first child") + + XCTAssertEqual( + try XML.document(sequenceAccessor), + "newTextchildText1childText2newText2", + "end document has newly added texts" + ) + } func testAttributes() { let accessor = XML.Accessor(singleElement()) @@ -244,6 +294,41 @@ class AccessorTests: XCTestCase { XCTAssert(true, "fail to access name with Failure Accessor") } } + + func testSetAttributes() throws { + var accessor = XML.Accessor(singleElement()) + accessor.attributes = ["key": "newValue"] + XCTAssertEqual(accessor.attributes, ["key": "newValue"], "edit attribute on first single element") + + var element = accessor["ChildElement"].first + element.attributes = ["key": "childAttribute1"] + XCTAssertEqual(element.attributes, ["key": "childAttribute1"], "set attribute for first child element") + + element = accessor["ChildElement"].last + element.attributes = ["key": "childAttribute2"] + XCTAssertEqual(element.attributes, ["key": "childAttribute2"], "set attribute for last child element") + + XCTAssertEqual( + try XML.document(accessor), + "text", + "end document has updated attributes" + ) + + let accessor2 = XML.Accessor(singleElementWithChildrenAttributes()) + var element2 = accessor2["ChildElement1"] + element2.attributes["key1"] = "newValue1" + XCTAssertEqual(element2.attributes, ["key1": "newValue1"], "edit attribute for child element") + + element2 = accessor2["ChildElement2"] + element2.attributes["key2"] = "newValue2" + XCTAssertEqual(element2.attributes, ["key2": "newValue2"], "edit attribute for child element") + + XCTAssertEqual( + try XML.document(accessor2), + "", + "end document has updated attributes" + ) + } func testAll() { let accessor = XML.Accessor(singleElement()) @@ -343,7 +428,33 @@ class AccessorTests: XCTestCase { let failureTexts = failureAccessor.compactMap { $0.text } XCTAssertEqual(failureTexts, [], "has no text") } - + + func testAppend() throws { + let accessor = XML.Accessor(singleElement()) + + XCTAssertEqual(accessor["RootElement"].text, nil) + + accessor.append(singleElement()) + XCTAssertEqual(accessor["RootElement"].text, "text") + + XCTAssertEqual( + try XML.document(accessor), + "texttext", + "end document has added element" + ) + + let accessor2 = XML.Accessor(singleElement()) + + accessor2["ChildElement"].first.append(singleElementWithChildrenAttributes()) + XCTAssertEqual(accessor2["ChildElement"].first["RootElement", "ChildElement1"].attributes, ["key1": "value1"]) + XCTAssertEqual(accessor2["ChildElement"].first["RootElement", "ChildElement2"].attributes, ["key2": "value2"]) + + XCTAssertEqual( + try XML.document(accessor2), + "text", + "end document has added element" + ) + } func testIterator() { let accessor = XML.Accessor(singleElement()) @@ -369,32 +480,30 @@ class AccessorTests: XCTestCase { } fileprivate func singleElement() -> XML.Element { - let element = XML.Element(name: "RootElement") - element.text = "text" - element.attributes = ["key": "value"] - element.childElements = [ + return XML.Element(name: "RootElement", text: "text", attributes: ["key": "value"], childElements: [ XML.Element(name: "ChildElement"), XML.Element(name: "ChildElement") - ] - return element + ]) + } + + fileprivate func singleElementWithChildrenAttributes() -> XML.Element { + return XML.Element(name: "RootElement", childElements: [ + XML.Element(name: "ChildElement1", attributes: ["key1": "value1"]), + XML.Element(name: "ChildElement2", attributes: ["key2": "value2"]) + ]) } fileprivate func sequence() -> [XML.Element] { - let elem1 = XML.Element(name: "Element") - elem1.text = "text" - elem1.attributes = ["key": "value"] - elem1.childElements = [ - XML.Element(name: "ChildElement1"), - XML.Element(name: "ChildElement1") - ] - let elem2 = XML.Element(name: "Element") - elem2.text = "text2" - elem2.childElements = [ - XML.Element(name: "ChildElement2"), - XML.Element(name: "ChildElement2") + return [ + XML.Element(name: "Element", text: "text", attributes: ["key": "value"], childElements: [ + XML.Element(name: "ChildElement1"), + XML.Element(name: "ChildElement1") + ]), + XML.Element(name: "Element", text: "text2", childElements: [ + XML.Element(name: "ChildElement2"), + XML.Element(name: "ChildElement2") + ]) ] - let elements = [ elem1, elem2 ] - return elements } fileprivate func failure() -> XMLError { diff --git a/SwiftyXMLParserTests/ConverterTests.swift b/SwiftyXMLParserTests/ConverterTests.swift index 66bc978..d5674a0 100644 --- a/SwiftyXMLParserTests/ConverterTests.swift +++ b/SwiftyXMLParserTests/ConverterTests.swift @@ -116,16 +116,52 @@ class ConverterTests: XCTestCase { XCTAssertEqual(result, extpected) } } -} -extension XML.Element { - convenience init(name: String, - text: String? = nil, - attributes: [String: String] = [String: String](), - childElements: [XML.Element] = [XML.Element]()) { - self.init(name: name) - self.text = text - self.attributes = attributes - self.childElements = childElements + func testMakeDocumentWithoutAttributes() throws { + let element = XML.Element(name: "name") + let converter = XML.Converter(XML.Accessor(element)) + + XCTAssertEqual( + try converter.makeDocument(), + "", + "convert xml document without extra spaces when no attributes are provided" + ) + + let element2 = XML.Element(name: "name", + text: "text", + attributes: ["key": "value"], + childElements: [ + XML.Element(name: "name1"), + XML.Element(name: "name2", text: "text2") + ]) + let converter2 = XML.Converter(XML.Accessor(element2)) + + XCTAssertEqual( + try converter2.makeDocument(), + "texttext2", + "convert xml document with child elements without extra spaces when no attributes are provided" + ) + } + + func testMakeWithoutDeclaration() throws { + let element = XML.Element(name: "name") + let converter = XML.Converter(XML.Accessor(element)) + + XCTAssertEqual( + try converter.makeDocument(withDeclaration: false), + "", + "convert xml document without xml declaration header" + ) + + let element2 = XML.Element(name: "name", + text: "text", + attributes: ["key": "value"]) + let converter2 = XML.Converter(XML.Accessor(element2)) + + XCTAssertEqual( + try converter2.makeDocument(withDeclaration: false), + "text", + "convert xml document without xml declaration header" + ) } } diff --git a/SwiftyXMLParserTests/ParserTests.swift b/SwiftyXMLParserTests/ParserTests.swift index 4b7db0c..cc08b8e 100755 --- a/SwiftyXMLParserTests/ParserTests.swift +++ b/SwiftyXMLParserTests/ParserTests.swift @@ -145,6 +145,24 @@ class ParserTests: XCTestCase { XCTAssert(false, "fail") } } + + func testIgnoreNamespaces() { + let data = """ + + + childText1 + childText2 + + """.trimmingCharacters(in: .whitespacesAndNewlines).data(using: .utf8)! + + let xml = XML.Parser().parse(data) + XCTAssertEqual(xml["env:RootElement", "ns1:ChildElement"].text, "childText1", "can access element when including namespace") + XCTAssertEqual(xml["RootElement", "ChildElement"].first.text, nil, "cannot find elements with namespaces") + + let xmlIgnoreNamespaces = XML.Parser(ignoreNamespaces: true).parse(data) + XCTAssertEqual(xmlIgnoreNamespaces["RootElement", "ChildElement"].first.text, "childText1", "can find element when ignoring namespaces") + XCTAssertEqual(xmlIgnoreNamespaces["RootElement", "ChildElement"].last.text, "childText2", "can find element when ignoring namespaces") + } func testParseErrorToInvalidCharacter() { let str = "@ß123\u{1c}" diff --git a/SwiftyXMLParserTests/XMLTests.swift b/SwiftyXMLParserTests/XMLTests.swift index 4259f05..7a90bd5 100755 --- a/SwiftyXMLParserTests/XMLTests.swift +++ b/SwiftyXMLParserTests/XMLTests.swift @@ -57,7 +57,20 @@ class XMLTests: XCTestCase { XCTFail("fail to generate data") } } - + + func testParseWithArguments() { + if let data = try? Data(contentsOf: URL(fileURLWithPath: getPath("XMLDocument.xml"))) { + let xml = XML.parse(data, trimming: .whitespacesAndNewlines, ignoreNamespaces: true) + if let _ = xml["ResultSet"].error { + XCTFail("fail to parse") + + } else { + XCTAssert(true, "sucess to Parse") + } + } else { + XCTFail("fail to generate data") + } + } func testSuccessParseFromString() { if let string = try? String(contentsOfFile: getPath("XMLDocument.xml"), encoding: String.Encoding.utf8), @@ -72,7 +85,6 @@ class XMLTests: XCTestCase { } } - func testSuccessParseFromDoublebyteSpace() { guard let xml = try? XML.parse(" ") else { XCTFail("Fail Parse")