/**
* https://github.com/tadija/AEXML
* Copyright (c) Marko Tadić 2014-2019
* Licensed under the MIT license. See LICENSE file.
*/
import Foundation
#if canImport(FoundationXML)
import FoundationXML
#endif
/**
This is base class for holding XML structure.
You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
return `` element from `` XML as an `Element` object.
*/
@dynamicMemberLookup
open class Element {
// MARK: - Properties
/// Every `Element` should have its parent element instead of `XMLDocument` which parent is `nil`.
open internal(set) weak var parent: Element?
/// Child XML elements.
open internal(set) var children = [Element]()
/// XML Element name.
open var name: String
/// XML Element value.
open var value: String?
/// XML Element attributes.
open var attributes: [String : String]
/// Error value (`nil` if there is no error).
open var error: XMLError?
// MARK: - Init
/**
Designated initializer - all parameters are optional.
- parameter name: XML element name.
- parameter value: XML element value (defaults to `nil`).
- parameter attributes: XML element attributes (defaults to empty dictionary).
- returns: An initialized `Element` object.
*/
public init(name: String, value: String? = nil, attributes: [String : String] = [:]) {
self.name = name
self.value = value
self.attributes = attributes
}
// MARK: - XML Read
/// The first element with given name **(Empty element with error if not exists)**.
open subscript(key: String) -> Element {
if let first = children.first(where: { $0.name == key }) {
return first
}
let element = Element(name: key)
element.error = XMLError.elementNotFound
return element
}
public subscript(dynamicMember member: String) -> Element {
self[member]
}
open func get() throws -> T where T: LosslessStringConvertible {
guard let string = value, let value = T(string) else { throw XMLError.valueConversionFailed }
return value
}
open func date(formatter: DateFormatter) -> Date? {
guard let value = value else { return nil }
return formatter.date(from: value)
}
open func date(formatter: ISO8601DateFormatter = ISO8601DateFormatter()) -> Date? {
guard let value = value else { return nil }
return formatter.date(from: value)
}
/// Returns all of the elements with equal name as `self` **(nil if not exists)**.
open var all: [Element]? { return parent?.children.filter { $0.name == self.name } }
/// Returns the first element with equal name as `self` **(nil if not exists)**.
open var first: Element? { return all?.first }
/// Returns the last element with equal name as `self` **(nil if not exists)**.
open var last: Element? { return all?.last }
/// Returns number of all elements with equal name as `self`.
open var count: Int { return all?.count ?? 0 }
/**
Returns all elements with given value.
- parameter value: XML element value.
- returns: Optional Array of found XML elements.
*/
open func all(withValue value: String) -> [Element]? {
return all?.compactMap { $0.value == value ? $0 : nil }
}
/**
Returns all elements containing given attributes.
- parameter attributes: Array of attribute names.
- returns: Optional Array of found XML elements.
*/
open func all(containingAttributeKeys keys: [String]) -> [Element]? {
return all?.compactMap { element in
keys.reduce(true) { (result, key) in
result && Array(element.attributes.keys).contains(key)
} ? element : nil
}
}
/**
Returns all elements with given attributes.
- parameter attributes: Dictionary of Keys and Values of attributes.
- returns: Optional Array of found XML elements.
*/
open func all(withAttributes attributes: [String : String]) -> [Element]? {
let keys = Array(attributes.keys)
return all(containingAttributeKeys: keys)?.compactMap { element in
attributes.reduce(true) { (result, attribute) in
result && element.attributes[attribute.key] == attribute.value
} ? element : nil
}
}
/**
Returns all descendant elements which satisfy the given predicate.
Searching is done vertically; children are tested before siblings. Elements appear in the list
in the order in which they are found.
- parameter predicate: Function which returns `true` for a desired element and `false` otherwise.
- returns: Array of found XML elements.
*/
open func allDescendants(where predicate: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for child in children {
if predicate(child) {
result.append(child)
}
result.append(contentsOf: child.allDescendants(where: predicate))
}
return result
}
/**
Returns the first descendant element which satisfies the given predicate, or nil if no such element is found.
Searching is done vertically; children are tested before siblings.
- parameter predicate: Function which returns `true` for the desired element and `false` otherwise.
- returns: Optional Element.
*/
open func firstDescendant(where predicate: (Element) -> Bool) -> Element? {
for child in children {
if predicate(child) {
return child
} else if let descendant = child.firstDescendant(where: predicate) {
return descendant
}
}
return nil
}
/**
Indicates whether the element has a descendant satisfying the given predicate.
- parameter predicate: Function which returns `true` for the desired element and `false` otherwise.
- returns: Bool.
*/
open func hasDescendant(where predicate: (Element) -> Bool) -> Bool {
return firstDescendant(where: predicate) != nil
}
// MARK: - XML Write
/**
Adds child XML element to `self`.
- parameter child: Child XML element to add.
- returns: Child XML element with `self` as `parent`.
*/
@discardableResult
open func addChild(_ child: Element) -> Element {
child.parent = self
children.append(child)
return child
}
/**
Adds child XML element to `self`.
- parameter name: Child XML element name.
- parameter value: Child XML element value (defaults to `nil`).
- parameter attributes: Child XML element attributes (defaults to empty dictionary).
- returns: Child XML element with `self` as `parent`.
*/
@discardableResult
open func addChild(name: String,
value: String? = nil,
attributes: [String : String] = [:]) -> Element {
let child = Element(name: name, value: value, attributes: attributes)
return addChild(child)
}
/**
Adds an array of XML elements to `self`.
- parameter children: Child XML element array to add.
- returns: Child XML elements with `self` as `parent`.
*/
@discardableResult
open func addChildren(_ children: [Element]) -> [Element] {
children.forEach{ addChild($0) }
return children
}
/// Removes `self` from `parent` XML element.
open func removeFromParent() {
if let index = parent?.children.firstIndex(where: { $0 === self }) {
parent?.children.remove(at: index)
}
}
/// Complete hierarchy of `self` and `children` in **XML** escaped and formatted String
open var xml: String {
var xml = String()
// open element
xml += indent(withDepth: parentsCount - 1)
xml += "<\(name)"
if attributes.count > 0 {
// insert attributes
for (key, value) in attributes.sorted(by: { $0.key < $1.key }) {
xml += " \(key)=\"\(value.xmlEscaped)\""
}
}
if value == nil && children.count == 0 {
// close element
xml += " />"
} else {
if children.count > 0 {
// add children
xml += ">\n"
for child in children {
xml += "\(child.xml)\n"
}
// add indentation
xml += indent(withDepth: parentsCount - 1)
xml += "\(name)>"
} else if let string = value {
// insert string value and close element
xml += ">\(string.xmlEscaped)\(name)>"
} else {
// insert empty value and close element
xml += ">\(name)>"
}
}
return xml
}
/// Same as `xmlString` but without `\n` and `\t` characters
open var xmlCompact: String {
let chars = CharacterSet(charactersIn: "\n\t")
return xml.components(separatedBy: chars).joined(separator: "")
}
/// Same as `xmlString` but with 4 spaces instead '\t' characters
open var xmlSpaces: String {
let chars = CharacterSet(charactersIn: "\t")
return xml.components(separatedBy: chars).joined(separator: " ")
}
// MARK: - Helpers
private var parentsCount: Int {
var count = 0
var element = self
while let parent = element.parent {
count += 1
element = parent
}
return count
}
private func indent(withDepth depth: Int) -> String {
var count = depth
var indent = String()
while count > 0 {
indent += "\t"
count -= 1
}
return indent
}
}
public extension String {
/// String representation of self with XML special characters escaped.
var xmlEscaped: String {
// we need to make sure "&" is escaped first. Not doing this may break escaping the other characters
var escaped = replacingOccurrences(of: "&", with: "&", options: .literal)
// replace the other four special characters
let escapeChars = ["<" : "<", ">" : ">", "'" : "'", "\"" : """]
for (char, echar) in escapeChars {
escaped = escaped.replacingOccurrences(of: char, with: echar, options: .literal)
}
return escaped
}
}