1
1
mirror of https://github.com/exyte/Macaw.git synced 2024-07-14 16:30:34 +03:00
Macaw/Dependencies/SWXMLHash/XMLHash.swift
2021-10-04 17:50:17 +07:00

1113 lines
34 KiB
Swift

//
// XMLHash.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.
//
// 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
#endif
let rootElementName = "SWXMLHash_Root_Element"
/// Parser options
public class XMLHashOptions {
internal init() {}
/// determines whether to parse the XML with lazy parsing or not
public var shouldProcessLazily = false
/// determines whether to parse XML namespaces or not (forwards to
/// `XMLParser.shouldProcessNamespaces`)
public var shouldProcessNamespaces = false
/// Matching element names, element values, attribute names, attribute values
/// will be case insensitive. This will not affect parsing (data does not change)
public var caseInsensitive = false
/// Encoding used for XML parsing. Default is set to UTF8
public var encoding = String.Encoding.utf8
/// Any contextual information set by the user for encoding
public var userInfo = [CodingUserInfoKey: Any]()
/// Detect XML parsing errors... defaults to false as this library will
/// attempt to handle HTML which isn't always XML-compatible
public var detectParsingErrors = false
}
/// Simple XML parser
public class XMLHash {
let options: XMLHashOptions
private init(_ options: XMLHashOptions = XMLHashOptions()) {
self.options = options
}
/**
Method to configure how parsing works.
- parameters:
- configAction: a block that passes in an `XMLHashOptions` object with
options to be set
- returns: an `XMLHash` instance
*/
class public func config(_ configAction: (XMLHashOptions) -> Void) -> XMLHash {
let opts = XMLHashOptions()
configAction(opts)
return XMLHash(opts)
}
/**
Begins parsing the passed in XML string.
- parameters:
- xml: an XML string. __Note__ that this is not a URL but a
string containing XML.
- returns: an `XMLIndexer` instance that can be iterated over
*/
public func parse(_ xml: String) -> XMLIndexer {
guard let data = xml.data(using: options.encoding) else {
return .xmlError(.encoding)
}
return parse(data)
}
/**
Begins parsing the passed in XML string.
- parameters:
- data: a `Data` instance containing XML
- returns: an `XMLIndexer` instance that can be iterated over
*/
public func parse(_ data: Data) -> XMLIndexer {
let parser: SimpleXmlParser = options.shouldProcessLazily
? LazyXMLParser(options)
: FullXMLParser(options)
return parser.parse(data)
}
/**
Method to parse XML passed in as a string.
- parameter xml: The XML to be parsed
- returns: An XMLIndexer instance that is used to look up elements in the XML
*/
class public func parse(_ xml: String) -> XMLIndexer {
XMLHash().parse(xml)
}
/**
Method to parse XML passed in as a Data instance.
- parameter data: The XML to be parsed
- returns: An XMLIndexer instance that is used to look up elements in the XML
*/
class public func parse(_ data: Data) -> XMLIndexer {
XMLHash().parse(data)
}
/**
Method to lazily parse XML passed in as a string.
- parameter xml: The XML to be parsed
- returns: An XMLIndexer instance that is used to look up elements in the XML
*/
class public func lazy(_ xml: String) -> XMLIndexer {
config { conf in conf.shouldProcessLazily = true }.parse(xml)
}
/**
Method to lazily parse XML passed in as a Data instance.
- parameter data: The XML to be parsed
- returns: An XMLIndexer instance that is used to look up elements in the XML
*/
class public func lazy(_ data: Data) -> XMLIndexer {
config { conf in conf.shouldProcessLazily = true }.parse(data)
}
}
struct Stack<T> {
var items = [T]()
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T {
items.removeLast()
}
mutating func drop() {
_ = pop()
}
mutating func removeAll() {
items.removeAll(keepingCapacity: false)
}
func top() -> T {
items[items.count - 1]
}
}
protocol SimpleXmlParser {
init(_ options: XMLHashOptions)
func parse(_ data: Data) -> XMLIndexer
}
#if os(Linux)
extension XMLParserDelegate {
func parserDidStartDocument(_ parser: XMLParser) { }
func parserDidEndDocument(_ parser: XMLParser) { }
func parser(_ parser: XMLParser,
foundNotationDeclarationWithName name: String,
publicID: String?,
systemID: String?) { }
func parser(_ parser: XMLParser,
foundUnparsedEntityDeclarationWithName name: String,
publicID: String?,
systemID: String?,
notationName: String?) { }
func parser(_ parser: XMLParser,
foundAttributeDeclarationWithName attributeName: String,
forElement elementName: String,
type: String?,
defaultValue: String?) { }
func parser(_ parser: XMLParser,
foundElementDeclarationWithName elementName: String,
model: String) { }
func parser(_ parser: XMLParser,
foundInternalEntityDeclarationWithName name: String,
value: String?) { }
func parser(_ parser: XMLParser,
foundExternalEntityDeclarationWithName name: String,
publicID: String?,
systemID: String?) { }
func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String]) { }
func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) { }
func parser(_ parser: XMLParser,
didStartMappingPrefix prefix: String,
toURI namespaceURI: String) { }
func parser(_ parser: XMLParser,
didEndMappingPrefix prefix: String) { }
func parser(_ parser: XMLParser,
foundCharacters string: String) { }
func parser(_ parser: XMLParser,
foundIgnorableWhitespace whitespaceString: String) { }
func parser(_ parser: XMLParser,
foundProcessingInstructionWithTarget target: String,
data: String?) { }
func parser(_ parser: XMLParser,
foundComment comment: String) { }
func parser(_ parser: XMLParser,
foundCDATA CDATABlock: Data) { }
func parser(_ parser: XMLParser,
resolveExternalEntityName name: String,
systemID: String?) -> Data? { nil }
func parser(_ parser: XMLParser,
parseErrorOccurred parseError: Error) { }
func parser(_ parser: XMLParser,
validationErrorOccurred validationError: Error) { }
}
#endif
/// The implementation of XMLParserDelegate and where the lazy parsing actually happens.
class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
required init(_ options: XMLHashOptions) {
root = XMLElement(name: rootElementName, options: options)
self.options = options
super.init()
}
var root: XMLElement
var parentStack = Stack<XMLElement>()
var elementStack = Stack<String>()
var data: Data?
var ops: [IndexOp] = []
let options: XMLHashOptions
func parse(_ data: Data) -> XMLIndexer {
self.data = data
return XMLIndexer(self)
}
func startParsing(_ ops: [IndexOp]) {
// reset state for a new lazy parsing run
root = XMLElement(name: rootElementName, options: root.options)
parentStack.removeAll()
parentStack.push(root)
self.ops = ops
let parser = XMLParser(data: data!)
parser.shouldProcessNamespaces = options.shouldProcessNamespaces
parser.delegate = self
_ = parser.parse()
}
func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String]) {
elementStack.push(elementName)
if !onMatch() {
return
}
let currentNode = parentStack
.top()
.addElement(elementName, withAttributes: attributeDict, caseInsensitive: self.options.caseInsensitive)
parentStack.push(currentNode)
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
if !onMatch() {
return
}
let current = parentStack.top()
current.addText(string)
}
func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
if !onMatch() {
return
}
if let cdataText = String(data: CDATABlock, encoding: String.Encoding.utf8) {
let current = parentStack.top()
current.addText(cdataText)
}
}
func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
let match = onMatch()
elementStack.drop()
if match {
parentStack.drop()
}
}
func onMatch() -> Bool {
// we typically want to compare against the elementStack to see if it matches ops, *but*
// if we're on the first element, we'll instead compare the other direction.
if elementStack.items.count > ops.count {
return elementStack.items.starts(with: ops.map { $0.key })
} else {
return ops.map { $0.key }.starts(with: elementStack.items)
}
}
}
/// The implementation of XMLParserDelegate and where the parsing actually happens.
class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
required init(_ options: XMLHashOptions) {
root = XMLElement(name: rootElementName, options: options)
self.options = options
super.init()
}
let root: XMLElement
var parentStack = Stack<XMLElement>()
let options: XMLHashOptions
var parsingError: ParsingError?
func parse(_ data: Data) -> XMLIndexer {
// clear any prior runs of parse... expected that this won't be necessary,
// but you never know
parentStack.removeAll()
parentStack.push(root)
let parser = XMLParser(data: data)
parser.shouldProcessNamespaces = options.shouldProcessNamespaces
parser.delegate = self
_ = parser.parse()
if options.detectParsingErrors, let err = parsingError {
return XMLIndexer.parsingError(err)
} else {
return XMLIndexer(root)
}
}
func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String]) {
let currentNode = parentStack
.top()
.addElement(elementName, withAttributes: attributeDict, caseInsensitive: self.options.caseInsensitive)
parentStack.push(currentNode)
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
let current = parentStack.top()
current.addText(string)
}
func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
parentStack.drop()
}
func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
if let cdataText = String(data: CDATABlock, encoding: String.Encoding.utf8) {
let current = parentStack.top()
current.addText(cdataText)
}
}
func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
#if os(Linux) && !swift(>=4.1.50)
if let err = parseError as? NSError {
parsingError = ParsingError(line: err.userInfo["NSXMLParserErrorLineNumber"] as? Int ?? 0,
column: err.userInfo["NSXMLParserErrorColumn"] as? Int ?? 0)
}
#else
let err = parseError as NSError
parsingError = ParsingError(line: err.userInfo["NSXMLParserErrorLineNumber"] as? Int ?? 0,
column: err.userInfo["NSXMLParserErrorColumn"] as? Int ?? 0)
#endif
}
}
/// Represents an indexed operation against a lazily parsed `XMLIndexer`
public class IndexOp {
var index: Int
let key: String
init(_ key: String) {
self.key = key
self.index = -1
}
func toString() -> String {
if index >= 0 {
return key + " " + index.description
}
return key
}
}
/// Represents a collection of `IndexOp` instances. Provides a means of iterating them
/// to find a match in a lazily parsed `XMLIndexer` instance.
public class IndexOps {
var ops: [IndexOp] = []
let parser: LazyXMLParser
init(parser: LazyXMLParser) {
self.parser = parser
}
func findElements() -> XMLIndexer {
parser.startParsing(ops)
let indexer = XMLIndexer(parser.root)
var childIndex = indexer
for oper in ops {
childIndex = childIndex[oper.key]
if oper.index >= 0 {
childIndex = childIndex[oper.index]
}
}
ops.removeAll(keepingCapacity: false)
return childIndex
}
func stringify() -> String {
var ret = ""
for oper in ops {
ret += "[" + oper.toString() + "]"
}
return ret
}
}
public struct ParsingError: Error {
public let line: Int
public let column: Int
}
/// Error type that is thrown when an indexing or parsing operation fails.
public enum IndexingError: Error {
case attribute(attr: String)
case attributeValue(attr: String, value: String)
case key(key: String)
case index(idx: Int)
case initialize(instance: AnyObject)
case encoding
case error
// swiftlint:disable identifier_name
// unavailable
@available(*, unavailable, renamed: "attribute(attr:)")
public static func Attribute(attr: String) -> IndexingError {
fatalError("unavailable")
}
@available(*, unavailable, renamed: "attributeValue(attr:value:)")
public static func AttributeValue(attr: String, value: String) -> IndexingError {
fatalError("unavailable")
}
@available(*, unavailable, renamed: "key(key:)")
public static func Key(key: String) -> IndexingError {
fatalError("unavailable")
}
@available(*, unavailable, renamed: "index(idx:)")
public static func Index(idx: Int) -> IndexingError {
fatalError("unavailable")
}
@available(*, unavailable, renamed: "initialize(instance:)")
public static func Init(instance: AnyObject) -> IndexingError {
fatalError("unavailable")
}
@available(*, unavailable, renamed: "error")
public static var Error: IndexingError {
fatalError("unavailable")
}
// swiftlint:enable identifier_name
}
/// 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 ""
}
}
}
extension IndexingError: CustomStringConvertible {
/// The description for the `IndexingError`.
public var description: String {
switch self {
case .attribute(let attr):
return "XML Attribute Error: Missing attribute [\"\(attr)\"]"
case let .attributeValue(attr, value):
return "XML Attribute Error: Missing attribute [\"\(attr)\"] with value [\"\(value)\"]"
case .key(let key):
return "XML Element Error: Incorrect key [\"\(key)\"]"
case .index(let index):
return "XML Element Error: Incorrect index [\"\(index)\"]"
case .initialize(let instance):
return "XML Indexer Error: initialization with Object [\"\(instance)\"]"
case .encoding:
return "String Encoding Error"
case .error:
return "Unknown Error"
}
}
}
/// 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 (<elem id="foo">content</elem>)
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("</\(name)>")
return xmlReturn.joined()
}
return "<\(name)\(attributesString)>\(text)</\(name)>"
}
}
// 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<A: RawRepresentable, V: RawRepresentable>(_ 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<K: RawRepresentable>(_ 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<K: RawRepresentable>(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<N: RawRepresentable>(by name: N) -> XMLAttribute? where N.RawValue == String {
attribute(by: name.rawValue)
}
}