| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- /**
- * 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 `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` 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<T>() 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
- }
- }
- extension String {
- /// String representation of self with XML special characters escaped.
- public 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
- }
- }
|