From 41df6411298b3f0504767d79700ff91b50bb2b6d Mon Sep 17 00:00:00 2001 From: David Mohundro Date: Tue, 3 May 2022 18:46:40 -0500 Subject: [PATCH] refactor: break out into even more files --- SWXMLHash.xcodeproj/project.pbxproj | 60 +++ Source/String+Extensions.swift | 39 ++ Source/TextElement.swift | 43 +++ Source/XMLAttribute.swift | 43 +++ Source/XMLContent.swift | 29 ++ Source/XMLElement.swift | 179 +++++++++ Source/XMLHash.swift | 551 ---------------------------- Source/XMLIndexer.swift | 364 ++++++++++++++++++ 8 files changed, 757 insertions(+), 551 deletions(-) create mode 100644 Source/String+Extensions.swift create mode 100644 Source/TextElement.swift create mode 100644 Source/XMLAttribute.swift create mode 100644 Source/XMLContent.swift create mode 100644 Source/XMLElement.swift create mode 100644 Source/XMLIndexer.swift diff --git a/SWXMLHash.xcodeproj/project.pbxproj b/SWXMLHash.xcodeproj/project.pbxproj index ab72eff..9cbf1ce 100644 --- a/SWXMLHash.xcodeproj/project.pbxproj +++ b/SWXMLHash.xcodeproj/project.pbxproj @@ -11,6 +11,30 @@ 54B83CC61C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B83CC41C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift */; }; 54B83CC71C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B83CC41C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift */; }; 54B83CC81C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B83CC41C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift */; }; + 5AF0E03C7AC0B144A711143C /* XMLElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */; }; + 5AF0E228F0D4FD1B85A66B5E /* XMLContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */; }; + 5AF0E2C961494B70E578CFF2 /* XMLContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */; }; + 5AF0E2DFC1636E975797A394 /* XMLAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */; }; + 5AF0E2F3CA5A347BBE64EDC5 /* TextElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E710E1B7BA748FBC986B /* TextElement.swift */; }; + 5AF0E3BD03782E18AA13F24A /* TextElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E710E1B7BA748FBC986B /* TextElement.swift */; }; + 5AF0E4FBCBF97B710D484E96 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */; }; + 5AF0E509C3E12F83B1E8D2B3 /* XMLContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */; }; + 5AF0E51E7FB4BC99D267F72F /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */; }; + 5AF0E5270F4914C039407BD0 /* XMLElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */; }; + 5AF0E586635337278611A179 /* XMLIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */; }; + 5AF0E6E95E6DE770F0383CF4 /* XMLIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */; }; + 5AF0E72422B26DCA16716C20 /* XMLAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */; }; + 5AF0E83E20CB0E145957F255 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */; }; + 5AF0E848394370F0E94BF9A0 /* XMLContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */; }; + 5AF0E9F306850236785A4512 /* XMLIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */; }; + 5AF0EA10D807D39E5A877DCA /* TextElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E710E1B7BA748FBC986B /* TextElement.swift */; }; + 5AF0EA56B730695C216E4571 /* XMLIndexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */; }; + 5AF0EAD8FF3226490385B398 /* XMLAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */; }; + 5AF0EBE38E3E25CE4A5276E7 /* XMLElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */; }; + 5AF0ED095AF755874AD7EBFF /* XMLAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */; }; + 5AF0ED9738E0194A03FE6D4C /* TextElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E710E1B7BA748FBC986B /* TextElement.swift */; }; + 5AF0EE0166578A68DEB25C84 /* XMLElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */; }; + 5AF0EEA3155F6F08B3F3DB71 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */; }; 6317F1A6282179E200F6C364 /* Float+XMLDeserialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6317F1A5282179E200F6C364 /* Float+XMLDeserialization.swift */; }; 6317F1A7282179E200F6C364 /* Float+XMLDeserialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6317F1A5282179E200F6C364 /* Float+XMLDeserialization.swift */; }; 6317F1A8282179E200F6C364 /* Float+XMLDeserialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6317F1A5282179E200F6C364 /* Float+XMLDeserialization.swift */; }; @@ -194,6 +218,12 @@ /* Begin PBXFileReference section */ 54B83CC41C849D9B00D588B5 /* XMLIndexer+XMLIndexerDeserializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XMLIndexer+XMLIndexerDeserializable.swift"; sourceTree = ""; }; + 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLElement.swift; sourceTree = ""; }; + 5AF0E710E1B7BA748FBC986B /* TextElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextElement.swift; sourceTree = ""; }; + 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLContent.swift; sourceTree = ""; }; + 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLIndexer.swift; sourceTree = ""; }; + 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLAttribute.swift; sourceTree = ""; }; 6317F1A5282179E200F6C364 /* Float+XMLDeserialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float+XMLDeserialization.swift"; sourceTree = ""; }; 63ED3C002821A723006A8A08 /* String+XMLDeserialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+XMLDeserialization.swift"; sourceTree = ""; }; 63ED3C012821A723006A8A08 /* XMLDeserialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLDeserialization.swift; sourceTree = ""; }; @@ -376,6 +406,12 @@ CD6083F4196CA106000B4F8D /* SWXMLHash.h */, CD60840B196CA11D000B4F8D /* XMLHash.swift */, 63ED3C282821AF4D006A8A08 /* XMLHashOptions.swift */, + 5AF0EADFD21AC848F6BECA1D /* XMLContent.swift */, + 5AF0E710E1B7BA748FBC986B /* TextElement.swift */, + 5AF0EF51D084FE1024D1CE0D /* XMLAttribute.swift */, + 5AF0E09CA8C0AAEABBD56BB4 /* XMLElement.swift */, + 5AF0ECA326E0FDA144CE093F /* String+Extensions.swift */, + 5AF0ECC2B709D6BF9A68F3D1 /* XMLIndexer.swift */, ); path = Source; sourceTree = ""; @@ -749,6 +785,12 @@ 63ED3C6A2821D247006A8A08 /* Stack.swift in Sources */, 63ED3C5E2821D247006A8A08 /* IndexOp.swift in Sources */, 63ED3C6F2821D311006A8A08 /* ParsingError.swift in Sources */, + 5AF0E848394370F0E94BF9A0 /* XMLContent.swift in Sources */, + 5AF0E2F3CA5A347BBE64EDC5 /* TextElement.swift in Sources */, + 5AF0E2DFC1636E975797A394 /* XMLAttribute.swift in Sources */, + 5AF0EBE38E3E25CE4A5276E7 /* XMLElement.swift in Sources */, + 5AF0E83E20CB0E145957F255 /* String+Extensions.swift in Sources */, + 5AF0E9F306850236785A4512 /* XMLIndexer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -796,6 +838,12 @@ 63ED3C6B2821D247006A8A08 /* Stack.swift in Sources */, 63ED3C5F2821D247006A8A08 /* IndexOp.swift in Sources */, 63ED3C702821D311006A8A08 /* ParsingError.swift in Sources */, + 5AF0E228F0D4FD1B85A66B5E /* XMLContent.swift in Sources */, + 5AF0E3BD03782E18AA13F24A /* TextElement.swift in Sources */, + 5AF0E72422B26DCA16716C20 /* XMLAttribute.swift in Sources */, + 5AF0EE0166578A68DEB25C84 /* XMLElement.swift in Sources */, + 5AF0EEA3155F6F08B3F3DB71 /* String+Extensions.swift in Sources */, + 5AF0EA56B730695C216E4571 /* XMLIndexer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -843,6 +891,12 @@ 63ED3C6C2821D247006A8A08 /* Stack.swift in Sources */, 63ED3C602821D247006A8A08 /* IndexOp.swift in Sources */, 63ED3C712821D311006A8A08 /* ParsingError.swift in Sources */, + 5AF0E2C961494B70E578CFF2 /* XMLContent.swift in Sources */, + 5AF0ED9738E0194A03FE6D4C /* TextElement.swift in Sources */, + 5AF0EAD8FF3226490385B398 /* XMLAttribute.swift in Sources */, + 5AF0E03C7AC0B144A711143C /* XMLElement.swift in Sources */, + 5AF0E51E7FB4BC99D267F72F /* String+Extensions.swift in Sources */, + 5AF0E6E95E6DE770F0383CF4 /* XMLIndexer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -890,6 +944,12 @@ 63ED3C6D2821D247006A8A08 /* Stack.swift in Sources */, 63ED3C612821D247006A8A08 /* IndexOp.swift in Sources */, 63ED3C722821D311006A8A08 /* ParsingError.swift in Sources */, + 5AF0E509C3E12F83B1E8D2B3 /* XMLContent.swift in Sources */, + 5AF0EA10D807D39E5A877DCA /* TextElement.swift in Sources */, + 5AF0ED095AF755874AD7EBFF /* XMLAttribute.swift in Sources */, + 5AF0E5270F4914C039407BD0 /* XMLElement.swift in Sources */, + 5AF0E4FBCBF97B710D484E96 /* String+Extensions.swift in Sources */, + 5AF0E586635337278611A179 /* XMLIndexer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/String+Extensions.swift b/Source/String+Extensions.swift new file mode 100644 index 0000000..48b845f --- /dev/null +++ b/Source/String+Extensions.swift @@ -0,0 +1,39 @@ +// +// String+Extensions.swift +// SWXMLHash +// +// Copyright (c) 2022 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +internal extension String { + func compare(_ str2: String?, _ insensitive: Bool) -> Bool { + guard let str2 = str2 else { + return false + } + let str1 = self + if insensitive { + return str1.caseInsensitiveCompare(str2) == .orderedSame + } + return str1 == str2 + } +} diff --git a/Source/TextElement.swift b/Source/TextElement.swift new file mode 100644 index 0000000..fcf4655 --- /dev/null +++ b/Source/TextElement.swift @@ -0,0 +1,43 @@ +// +// TextElement.swift +// SWXMLHash +// +// Copyright (c) 2022 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Models a text element +public class TextElement: XMLContent { + /// The underlying text value + public let text: String + + init(text: String) { + self.text = text + } +} + +extension TextElement: CustomStringConvertible { + /// The text value for a `TextElement` instance. + public var description: String { + text + } +} diff --git a/Source/XMLAttribute.swift b/Source/XMLAttribute.swift new file mode 100644 index 0000000..300be3d --- /dev/null +++ b/Source/XMLAttribute.swift @@ -0,0 +1,43 @@ +// +// XMLAttribute.swift +// SWXMLHash +// +// Copyright (c) 2022 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct XMLAttribute { + public let name: String + public let text: String + + init(name: String, text: String) { + self.name = name + self.text = text + } +} + +extension XMLAttribute: CustomStringConvertible { + /// The textual representation of an `XMLAttribute` instance. + public var description: String { + "\(name)=\"\(text)\"" + } +} diff --git a/Source/XMLContent.swift b/Source/XMLContent.swift new file mode 100644 index 0000000..b7be82e --- /dev/null +++ b/Source/XMLContent.swift @@ -0,0 +1,29 @@ +// +// XMLContent.swift +// SWXMLHash +// +// Copyright (c) 2014 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Models content for an XML doc, whether it is text or XML +public protocol XMLContent: CustomStringConvertible { } diff --git a/Source/XMLElement.swift b/Source/XMLElement.swift new file mode 100644 index 0000000..ca468f6 --- /dev/null +++ b/Source/XMLElement.swift @@ -0,0 +1,179 @@ +// +// XMLElement.swift +// SWXMLHash +// +// Copyright (c) 2022 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Models an XML element, including name, text and attributes +public class XMLElement: XMLContent { + /// The name of the element + public let name: String + + /// Whether the element is case insensitive or not + public var caseInsensitive: Bool { + options.caseInsensitive + } + + var userInfo: [CodingUserInfoKey: Any] { + options.userInfo + } + + /// All attributes + public var allAttributes = [String: XMLAttribute]() + + /// Find an attribute by name + public func attribute(by name: String) -> XMLAttribute? { + if caseInsensitive { + return allAttributes.first(where: { $0.key.compare(name, true) })?.value + } + return allAttributes[name] + } + + /// The inner text of the element, if it exists + public var text: String { + children.reduce("", { + if let element = $1 as? TextElement { + return $0 + element.text + } + + return $0 + }) + } + + /// The inner text of the element and its children + public var recursiveText: String { + children.reduce("", { + if let textElement = $1 as? TextElement { + return $0 + textElement.text + } else if let xmlElement = $1 as? XMLElement { + return $0 + xmlElement.recursiveText + } else { + return $0 + } + }) + } + + public var innerXML: String { + children.reduce("", { + $0.description + $1.description + }) + } + + /// All child elements (text or XML) + public var children = [XMLContent]() + + var count: Int = 0 + var index: Int + let options: XMLHashOptions + + var xmlChildren: [XMLElement] { + children.compactMap { $0 as? XMLElement } + } + + /** + Initialize an XMLElement instance + + - parameters: + - name: The name of the element to be initialized + - index: The index of the element to be initialized + - options: The XMLHash options + */ + init(name: String, index: Int = 0, options: XMLHashOptions) { + self.name = name + self.index = index + self.options = options + } + + /** + Adds a new XMLElement underneath this instance of XMLElement + + - parameters: + - name: The name of the new element to be added + - withAttributes: The attributes dictionary for the element being added + - returns: The XMLElement that has now been added + */ + + func addElement(_ name: String, withAttributes attributes: [String: String], caseInsensitive: Bool) -> XMLElement { + let element = XMLElement(name: name, index: count, options: options) + count += 1 + + children.append(element) + + for (key, value) in attributes { + element.allAttributes[key] = XMLAttribute(name: key, text: value) + } + + return element + } + + func addText(_ text: String) { + let elem = TextElement(text: text) + + children.append(elem) + } +} + +extension XMLElement: CustomStringConvertible { + /// The tag, attributes and content for a `XMLElement` instance (content) + public var description: String { + let attributesString = allAttributes.reduce("", { $0 + " " + $1.1.description }) + + if !children.isEmpty { + var xmlReturn = [String]() + xmlReturn.append("<\(name)\(attributesString)>") + for child in children { + xmlReturn.append(child.description) + } + xmlReturn.append("") + return xmlReturn.joined() + } + + return "<\(name)\(attributesString)>\(text)" + } +} + +/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +extension XMLElement { + /** + Find an attribute by name using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for self[String] + */ + public func attribute(by name: N) -> XMLAttribute? where N.RawValue == String { + attribute(by: name.rawValue) + } +} + +// Workaround for "'XMLElement' is ambiguous for type lookup in this context" error on macOS. +// +// On macOS, `XMLElement` is defined in Foundation. +// So, the code referencing `XMLElement` generates above error. +// Following code allow to using `SWXMLhash.XMLElement` in client codes. +extension XMLHash { + public typealias XMLElement = XMLHashXMLElement +} + +public typealias XMLHashXMLElement = XMLElement diff --git a/Source/XMLHash.swift b/Source/XMLHash.swift index 91dca0d..3b507c7 100644 --- a/Source/XMLHash.swift +++ b/Source/XMLHash.swift @@ -23,12 +23,6 @@ // THE SOFTWARE. // -// swiftlint exceptions: -// - Disabled file_length because there are a number of users that still pull the -// source down as is and it makes pulling the code into a project easier. - -// swiftlint:disable file_length - import Foundation #if canImport(FoundationXML) import FoundationXML @@ -215,548 +209,3 @@ extension XMLParserDelegate { } #endif - -/// Returned from XMLHash, allows easy element lookup into XML data. -public enum XMLIndexer { - case element(XMLElement) - case list([XMLElement]) - case stream(IndexOps) - case xmlError(IndexingError) - case parsingError(ParsingError) - -// swiftlint:disable identifier_name - // unavailable - @available(*, unavailable, renamed: "element(_:)") - public static func Element(_: XMLElement) -> XMLIndexer { - fatalError("unavailable") - } - @available(*, unavailable, renamed: "list(_:)") - public static func List(_: [XMLElement]) -> XMLIndexer { - fatalError("unavailable") - } - @available(*, unavailable, renamed: "stream(_:)") - public static func Stream(_: IndexOps) -> XMLIndexer { - fatalError("unavailable") - } - @available(*, unavailable, renamed: "xmlError(_:)") - public static func XMLError(_: IndexingError) -> XMLIndexer { - fatalError("unavailable") - } - @available(*, unavailable, renamed: "withAttribute(_:_:)") - public static func withAttr(_ attr: String, _ value: String) throws -> XMLIndexer { - fatalError("unavailable") - } -// swiftlint:enable identifier_name - - /// The underlying XMLElement at the currently indexed level of XML. - public var element: XMLElement? { - switch self { - case .element(let elem): - return elem - case .stream(let ops): - let list = ops.findElements() - return list.element - default: - return nil - } - } - - /// All elements at the currently indexed level - public var all: [XMLIndexer] { - allElements.map { XMLIndexer($0) } - } - - private var allElements: [XMLElement] { - switch self { - case .list(let list): - return list - case .element(let elem): - return [elem] - case .stream(let ops): - let list = ops.findElements() - return list.allElements - default: - return [] - } - } - - /// All child elements from the currently indexed level - public var children: [XMLIndexer] { - childElements.map { XMLIndexer($0) } - } - - private var childElements: [XMLElement] { - var list = [XMLElement]() - for elem in all.compactMap({ $0.element }) { - for elem in elem.xmlChildren { - list.append(elem) - } - } - return list - } - - @available(*, unavailable, renamed: "filterChildren(_:)") - public func filter(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { - filterChildren(included) - } - - public func filterChildren(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { - let children = handleFilteredResults(list: childElements, included: included) - if let current = self.element { - let filteredElem = XMLElement(name: current.name, index: current.index, options: current.options) - filteredElem.children = children.allElements - return .element(filteredElem) - } - return .xmlError(IndexingError.error) - } - - public func filterAll(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { - handleFilteredResults(list: allElements, included: included) - } - - private func handleFilteredResults(list: [XMLElement], - included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { - let results = zip(list.indices, list).filter { included($1, $0) }.map { $1 } - if results.count == 1 { - return .element(results.first!) - } - return .list(results) - } - - public var userInfo: [CodingUserInfoKey: Any] { - switch self { - case .element(let elem): - return elem.userInfo - default: - return [:] - } - } - - /** - Allows for element lookup by matching attribute values. - - - parameters: - - attr: should the name of the attribute to match on - - value: should be the value of the attribute to match on - - throws: an XMLIndexer.XMLError if an element with the specified attribute isn't found - - returns: instance of XMLIndexer - */ - public func withAttribute(_ attr: String, _ value: String) throws -> XMLIndexer { - switch self { - case .stream(let opStream): - let match = opStream.findElements() - return try match.withAttribute(attr, value) - case .list(let list): - if let elem = list.first(where: { - value.compare($0.attribute(by: attr)?.text, $0.caseInsensitive) - }) { - return .element(elem) - } - throw IndexingError.attributeValue(attr: attr, value: value) - case .element(let elem): - if value.compare(elem.attribute(by: attr)?.text, elem.caseInsensitive) { - return .element(elem) - } - throw IndexingError.attributeValue(attr: attr, value: value) - default: - throw IndexingError.attribute(attr: attr) - } - } - - /** - Initializes the XMLIndexer - - - parameter _: should be an instance of XMLElement, but supports other values for error handling - - throws: an Error if the object passed in isn't an XMLElement or LaxyXMLParser - */ - public init(_ rawObject: AnyObject) throws { - switch rawObject { - case let value as XMLElement: - self = .element(value) - case let value as LazyXMLParser: - self = .stream(IndexOps(parser: value)) - default: - throw IndexingError.initialize(instance: rawObject) - } - } - - /** - Initializes the XMLIndexer - - - parameter _: an instance of XMLElement - */ - public init(_ elem: XMLElement) { - self = .element(elem) - } - - init(_ stream: LazyXMLParser) { - self = .stream(IndexOps(parser: stream)) - } - - /** - Find an XML element at the current level by element name - - - parameter key: The element name to index by - - returns: instance of XMLIndexer to match the element (or elements) found by key - - throws: Throws an XMLIndexingError.Key if no element was found - */ - public func byKey(_ key: String) throws -> XMLIndexer { - switch self { - case .stream(let opStream): - let oper = IndexOp(key) - opStream.ops.append(oper) - return .stream(opStream) - case .element(let elem): - let match = elem.xmlChildren.filter({ - $0.name.compare(key, $0.caseInsensitive) - }) - if !match.isEmpty { - if match.count == 1 { - return .element(match[0]) - } else { - return .list(match) - } - } - throw IndexingError.key(key: key) - default: - throw IndexingError.key(key: key) - } - } - - /** - Find an XML element at the current level by element name - - - parameter key: The element name to index by - - returns: instance of XMLIndexer to match the element (or elements) found by - */ - public subscript(key: String) -> XMLIndexer { - do { - return try self.byKey(key) - } catch let error as IndexingError { - return .xmlError(error) - } catch { - return .xmlError(IndexingError.key(key: key)) - } - } - - /** - Find an XML element by index within a list of XML Elements at the current level - - - parameter index: The 0-based index to index by - - throws: XMLIndexer.XMLError if the index isn't found - - returns: instance of XMLIndexer to match the element (or elements) found by index - */ - public func byIndex(_ index: Int) throws -> XMLIndexer { - switch self { - case .stream(let opStream): - opStream.ops[opStream.ops.count - 1].index = index - return .stream(opStream) - case .list(let list): - if index < list.count { - return .element(list[index]) - } - return .xmlError(IndexingError.index(idx: index)) - case .element(let elem): - if index == 0 { - return .element(elem) - } - return .xmlError(IndexingError.index(idx: index)) - default: - return .xmlError(IndexingError.index(idx: index)) - } - } - - /** - Find an XML element by index - - - parameter index: The 0-based index to index by - - returns: instance of XMLIndexer to match the element (or elements) found by index - */ - public subscript(index: Int) -> XMLIndexer { - do { - return try byIndex(index) - } catch let error as IndexingError { - return .xmlError(error) - } catch { - return .xmlError(IndexingError.index(idx: index)) - } - } -} - -/// XMLIndexer extensions - -extension XMLIndexer: CustomStringConvertible { - /// The XML representation of the XMLIndexer at the current level - public var description: String { - switch self { - case .list(let list): - return list.reduce("", { $0 + $1.description }) - case .element(let elem): - if elem.name == rootElementName { - return elem.children.reduce("", { $0 + $1.description }) - } - - return elem.description - default: - return "" - } - } -} - -/// Models content for an XML doc, whether it is text or XML -public protocol XMLContent: CustomStringConvertible { } - -/// Models a text element -public class TextElement: XMLContent { - /// The underlying text value - public let text: String - - init(text: String) { - self.text = text - } -} - -public struct XMLAttribute { - public let name: String - public let text: String - - init(name: String, text: String) { - self.name = name - self.text = text - } -} - -/// Models an XML element, including name, text and attributes -public class XMLElement: XMLContent { - /// The name of the element - public let name: String - - /// Whether the element is case insensitive or not - public var caseInsensitive: Bool { - options.caseInsensitive - } - - var userInfo: [CodingUserInfoKey: Any] { - options.userInfo - } - - /// All attributes - public var allAttributes = [String: XMLAttribute]() - - /// Find an attribute by name - public func attribute(by name: String) -> XMLAttribute? { - if caseInsensitive { - return allAttributes.first(where: { $0.key.compare(name, true) })?.value - } - return allAttributes[name] - } - - /// The inner text of the element, if it exists - public var text: String { - children.reduce("", { - if let element = $1 as? TextElement { - return $0 + element.text - } - - return $0 - }) - } - - /// The inner text of the element and its children - public var recursiveText: String { - children.reduce("", { - if let textElement = $1 as? TextElement { - return $0 + textElement.text - } else if let xmlElement = $1 as? XMLElement { - return $0 + xmlElement.recursiveText - } else { - return $0 - } - }) - } - - public var innerXML: String { - children.reduce("", { - $0.description + $1.description - }) - } - - /// All child elements (text or XML) - public var children = [XMLContent]() - - var count: Int = 0 - var index: Int - let options: XMLHashOptions - - var xmlChildren: [XMLElement] { - children.compactMap { $0 as? XMLElement } - } - - /** - Initialize an XMLElement instance - - - parameters: - - name: The name of the element to be initialized - - index: The index of the element to be initialized - - options: The XMLHash options - */ - init(name: String, index: Int = 0, options: XMLHashOptions) { - self.name = name - self.index = index - self.options = options - } - - /** - Adds a new XMLElement underneath this instance of XMLElement - - - parameters: - - name: The name of the new element to be added - - withAttributes: The attributes dictionary for the element being added - - returns: The XMLElement that has now been added - */ - - func addElement(_ name: String, withAttributes attributes: [String: String], caseInsensitive: Bool) -> XMLElement { - let element = XMLElement(name: name, index: count, options: options) - count += 1 - - children.append(element) - - for (key, value) in attributes { - element.allAttributes[key] = XMLAttribute(name: key, text: value) - } - - return element - } - - func addText(_ text: String) { - let elem = TextElement(text: text) - - children.append(elem) - } -} - -extension TextElement: CustomStringConvertible { - /// The text value for a `TextElement` instance. - public var description: String { - text - } -} - -extension XMLAttribute: CustomStringConvertible { - /// The textual representation of an `XMLAttribute` instance. - public var description: String { - "\(name)=\"\(text)\"" - } -} - -extension XMLElement: CustomStringConvertible { - /// The tag, attributes and content for a `XMLElement` instance (content) - public var description: String { - let attributesString = allAttributes.reduce("", { $0 + " " + $1.1.description }) - - if !children.isEmpty { - var xmlReturn = [String]() - xmlReturn.append("<\(name)\(attributesString)>") - for child in children { - xmlReturn.append(child.description) - } - xmlReturn.append("") - return xmlReturn.joined() - } - - return "<\(name)\(attributesString)>\(text)" - } -} - -// Workaround for "'XMLElement' is ambiguous for type lookup in this context" error on macOS. -// -// On macOS, `XMLElement` is defined in Foundation. -// So, the code referencing `XMLElement` generates above error. -// Following code allow to using `SWXMLhash.XMLElement` in client codes. -extension XMLHash { - public typealias XMLElement = XMLHashXMLElement -} - -public typealias XMLHashXMLElement = XMLElement - -fileprivate extension String { - func compare(_ str2: String?, _ insensitive: Bool) -> Bool { - guard let str2 = str2 else { - return false - } - let str1 = self - if insensitive { - return str1.caseInsensitiveCompare(str2) == .orderedSame - } - return str1 == str2 - } -} - -// MARK: - XMLIndexer String RawRepresentables - -/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables - Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ -extension XMLIndexer { - /** - Allows for element lookup by matching attribute values - using a String backed RawRepresentables (E.g. `String` backed `enum` cases) - - - Note: - Convenience for withAttribute(String, String) - - - parameters: - - attr: should the name of the attribute to match on - - value: should be the value of the attribute to match on - - throws: an XMLIndexer.XMLError if an element with the specified attribute isn't found - - returns: instance of XMLIndexer - */ - public func withAttribute(_ attr: A, _ value: V) throws -> XMLIndexer - where A.RawValue == String, V.RawValue == String { - try withAttribute(attr.rawValue, value.rawValue) - } - - /** - Find an XML element at the current level by element name - using a String backed RawRepresentable (E.g. `String` backed `enum` cases) - - - Note: - Convenience for byKey(String) - - - parameter key: The element name to index by - - returns: instance of XMLIndexer to match the element (or elements) found by key - - throws: Throws an XMLIndexingError.Key if no element was found - */ - public func byKey(_ key: K) throws -> XMLIndexer where K.RawValue == String { - try byKey(key.rawValue) - } - - /** - Find an XML element at the current level by element name - using a String backed RawRepresentable (E.g. `String` backed `enum` cases) - - - Note: - Convenience for self[String] - - - parameter key: The element name to index by - - returns: instance of XMLIndexer to match the element (or elements) found by - */ - public subscript(key: K) -> XMLIndexer where K.RawValue == String { - self[key.rawValue] - } -} - -// MARK: - XMLElement String RawRepresentables - -/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables - Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ -extension XMLElement { - /** - Find an attribute by name using a String backed RawRepresentable (E.g. `String` backed `enum` cases) - - - Note: - Convenience for self[String] - */ - public func attribute(by name: N) -> XMLAttribute? where N.RawValue == String { - attribute(by: name.rawValue) - } -} diff --git a/Source/XMLIndexer.swift b/Source/XMLIndexer.swift new file mode 100644 index 0000000..e21529d --- /dev/null +++ b/Source/XMLIndexer.swift @@ -0,0 +1,364 @@ +// +// XMLIndexer.swift +// SWXMLHash +// +// Copyright (c) 2022 David Mohundro +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +/// Returned from XMLHash, allows easy element lookup into XML data. +public enum XMLIndexer { + case element(XMLElement) + case list([XMLElement]) + case stream(IndexOps) + case xmlError(IndexingError) + case parsingError(ParsingError) + +// swiftlint:disable identifier_name + // unavailable + @available(*, unavailable, renamed: "element(_:)") + public static func Element(_: XMLElement) -> XMLIndexer { + fatalError("unavailable") + } + @available(*, unavailable, renamed: "list(_:)") + public static func List(_: [XMLElement]) -> XMLIndexer { + fatalError("unavailable") + } + @available(*, unavailable, renamed: "stream(_:)") + public static func Stream(_: IndexOps) -> XMLIndexer { + fatalError("unavailable") + } + @available(*, unavailable, renamed: "xmlError(_:)") + public static func XMLError(_: IndexingError) -> XMLIndexer { + fatalError("unavailable") + } + @available(*, unavailable, renamed: "withAttribute(_:_:)") + public static func withAttr(_ attr: String, _ value: String) throws -> XMLIndexer { + fatalError("unavailable") + } +// swiftlint:enable identifier_name + + /// The underlying XMLElement at the currently indexed level of XML. + public var element: XMLElement? { + switch self { + case .element(let elem): + return elem + case .stream(let ops): + let list = ops.findElements() + return list.element + default: + return nil + } + } + + /// All elements at the currently indexed level + public var all: [XMLIndexer] { + allElements.map { XMLIndexer($0) } + } + + private var allElements: [XMLElement] { + switch self { + case .list(let list): + return list + case .element(let elem): + return [elem] + case .stream(let ops): + let list = ops.findElements() + return list.allElements + default: + return [] + } + } + + /// All child elements from the currently indexed level + public var children: [XMLIndexer] { + childElements.map { XMLIndexer($0) } + } + + private var childElements: [XMLElement] { + var list = [XMLElement]() + for elem in all.compactMap({ $0.element }) { + for elem in elem.xmlChildren { + list.append(elem) + } + } + return list + } + + @available(*, unavailable, renamed: "filterChildren(_:)") + public func filter(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { + filterChildren(included) + } + + public func filterChildren(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { + let children = handleFilteredResults(list: childElements, included: included) + if let current = self.element { + let filteredElem = XMLElement(name: current.name, index: current.index, options: current.options) + filteredElem.children = children.allElements + return .element(filteredElem) + } + return .xmlError(IndexingError.error) + } + + public func filterAll(_ included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { + handleFilteredResults(list: allElements, included: included) + } + + private func handleFilteredResults(list: [XMLElement], + included: (_ elem: XMLElement, _ index: Int) -> Bool) -> XMLIndexer { + let results = zip(list.indices, list).filter { included($1, $0) }.map { $1 } + if results.count == 1 { + return .element(results.first!) + } + return .list(results) + } + + public var userInfo: [CodingUserInfoKey: Any] { + switch self { + case .element(let elem): + return elem.userInfo + default: + return [:] + } + } + + /** + Allows for element lookup by matching attribute values. + + - parameters: + - attr: should the name of the attribute to match on + - value: should be the value of the attribute to match on + - throws: an XMLIndexer.XMLError if an element with the specified attribute isn't found + - returns: instance of XMLIndexer + */ + public func withAttribute(_ attr: String, _ value: String) throws -> XMLIndexer { + switch self { + case .stream(let opStream): + let match = opStream.findElements() + return try match.withAttribute(attr, value) + case .list(let list): + if let elem = list.first(where: { + value.compare($0.attribute(by: attr)?.text, $0.caseInsensitive) + }) { + return .element(elem) + } + throw IndexingError.attributeValue(attr: attr, value: value) + case .element(let elem): + if value.compare(elem.attribute(by: attr)?.text, elem.caseInsensitive) { + return .element(elem) + } + throw IndexingError.attributeValue(attr: attr, value: value) + default: + throw IndexingError.attribute(attr: attr) + } + } + + /** + Initializes the XMLIndexer + + - parameter _: should be an instance of XMLElement, but supports other values for error handling + - throws: an Error if the object passed in isn't an XMLElement or LaxyXMLParser + */ + public init(_ rawObject: AnyObject) throws { + switch rawObject { + case let value as XMLElement: + self = .element(value) + case let value as LazyXMLParser: + self = .stream(IndexOps(parser: value)) + default: + throw IndexingError.initialize(instance: rawObject) + } + } + + /** + Initializes the XMLIndexer + + - parameter _: an instance of XMLElement + */ + public init(_ elem: XMLElement) { + self = .element(elem) + } + + init(_ stream: LazyXMLParser) { + self = .stream(IndexOps(parser: stream)) + } + + /** + Find an XML element at the current level by element name + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by key + - throws: Throws an XMLIndexingError.Key if no element was found + */ + public func byKey(_ key: String) throws -> XMLIndexer { + switch self { + case .stream(let opStream): + let oper = IndexOp(key) + opStream.ops.append(oper) + return .stream(opStream) + case .element(let elem): + let match = elem.xmlChildren.filter({ + $0.name.compare(key, $0.caseInsensitive) + }) + if !match.isEmpty { + if match.count == 1 { + return .element(match[0]) + } else { + return .list(match) + } + } + throw IndexingError.key(key: key) + default: + throw IndexingError.key(key: key) + } + } + + /** + Find an XML element at the current level by element name + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by + */ + public subscript(key: String) -> XMLIndexer { + do { + return try self.byKey(key) + } catch let error as IndexingError { + return .xmlError(error) + } catch { + return .xmlError(IndexingError.key(key: key)) + } + } + + /** + Find an XML element by index within a list of XML Elements at the current level + + - parameter index: The 0-based index to index by + - throws: XMLIndexer.XMLError if the index isn't found + - returns: instance of XMLIndexer to match the element (or elements) found by index + */ + public func byIndex(_ index: Int) throws -> XMLIndexer { + switch self { + case .stream(let opStream): + opStream.ops[opStream.ops.count - 1].index = index + return .stream(opStream) + case .list(let list): + if index < list.count { + return .element(list[index]) + } + return .xmlError(IndexingError.index(idx: index)) + case .element(let elem): + if index == 0 { + return .element(elem) + } + return .xmlError(IndexingError.index(idx: index)) + default: + return .xmlError(IndexingError.index(idx: index)) + } + } + + /** + Find an XML element by index + + - parameter index: The 0-based index to index by + - returns: instance of XMLIndexer to match the element (or elements) found by index + */ + public subscript(index: Int) -> XMLIndexer { + do { + return try byIndex(index) + } catch let error as IndexingError { + return .xmlError(error) + } catch { + return .xmlError(IndexingError.index(idx: index)) + } + } +} + +/// XMLIndexer extensions + +extension XMLIndexer: CustomStringConvertible { + /// The XML representation of the XMLIndexer at the current level + public var description: String { + switch self { + case .list(let list): + return list.reduce("", { $0 + $1.description }) + case .element(let elem): + if elem.name == rootElementName { + return elem.children.reduce("", { $0 + $1.description }) + } + + return elem.description + default: + return "" + } + } +} + +/*: Provides XMLIndexer Serialization/Deserialization using String backed RawRepresentables + Added by [PeeJWeeJ](https://github.com/PeeJWeeJ) */ +extension XMLIndexer { + /** + Allows for element lookup by matching attribute values + using a String backed RawRepresentables (E.g. `String` backed `enum` cases) + + - Note: + Convenience for withAttribute(String, String) + + - parameters: + - attr: should the name of the attribute to match on + - value: should be the value of the attribute to match on + - throws: an XMLIndexer.XMLError if an element with the specified attribute isn't found + - returns: instance of XMLIndexer + */ + public func withAttribute(_ attr: A, _ value: V) throws -> XMLIndexer + where A.RawValue == String, V.RawValue == String { + try withAttribute(attr.rawValue, value.rawValue) + } + + /** + Find an XML element at the current level by element name + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for byKey(String) + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by key + - throws: Throws an XMLIndexingError.Key if no element was found + */ + public func byKey(_ key: K) throws -> XMLIndexer where K.RawValue == String { + try byKey(key.rawValue) + } + + /** + Find an XML element at the current level by element name + using a String backed RawRepresentable (E.g. `String` backed `enum` cases) + + - Note: + Convenience for self[String] + + - parameter key: The element name to index by + - returns: instance of XMLIndexer to match the element (or elements) found by + */ + public subscript(key: K) -> XMLIndexer where K.RawValue == String { + self[key.rawValue] + } +}