286 lines
9.5 KiB
Swift
286 lines
9.5 KiB
Swift
//
|
|
// Element.swift
|
|
//
|
|
// Copyright (c) 2014-2016 Marko Tadić <tadija@me.com> http://tadija.net
|
|
//
|
|
// 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
|
|
|
|
/**
|
|
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 `AEXMLElement` object.
|
|
*/
|
|
internal class AEXMLElement {
|
|
|
|
// MARK: - Properties
|
|
|
|
/// Every `AEXMLElement` should have its parent element instead of `AEXMLDocument` which parent is `nil`.
|
|
open internal(set) weak var parent: AEXMLElement?
|
|
|
|
/// Child XML elements.
|
|
open internal(set) var children = [AEXMLElement]()
|
|
|
|
/// 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: AEXMLError?
|
|
|
|
/// String representation of `value` property (if `value` is `nil` this is empty String).
|
|
open var string: String { return value ?? String() }
|
|
|
|
/// Boolean representation of `value` property (if `value` is "true" or 1 this is `True`, otherwise `False`).
|
|
open var bool: Bool { return string.lowercased() == "true" || Int(string) == 1 ? true : false }
|
|
|
|
/// Integer representation of `value` property (this is **0** if `value` can't be represented as Integer).
|
|
open var int: Int { return Int(string) ?? 0 }
|
|
|
|
/// Double representation of `value` property (this is **0.00** if `value` can't be represented as Double).
|
|
open var double: Double { return Double(string) ?? 0.00 }
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
/**
|
|
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 `AEXMLElement` object.
|
|
*/
|
|
public init(name: String, value: String? = nil, attributes: [String : String] = [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) -> AEXMLElement {
|
|
guard let
|
|
first = children.first(where: { $0.name == key })
|
|
else {
|
|
let errorElement = AEXMLElement(name: key)
|
|
errorElement.error = AEXMLError.elementNotFound
|
|
return errorElement
|
|
}
|
|
return first
|
|
}
|
|
|
|
/// Returns all of the elements with equal name as `self` **(nil if not exists)**.
|
|
open var all: [AEXMLElement]? { return parent?.children.filter { $0.name == self.name } }
|
|
|
|
/// Returns the first element with equal name as `self` **(nil if not exists)**.
|
|
open var first: AEXMLElement? { return all?.first }
|
|
|
|
/// Returns the last element with equal name as `self` **(nil if not exists)**.
|
|
open var last: AEXMLElement? { return all?.last }
|
|
|
|
/// Returns number of all elements with equal name as `self`.
|
|
open var count: Int { return all?.count ?? 0 }
|
|
|
|
fileprivate func filter(withCondition condition: (AEXMLElement) -> Bool) -> [AEXMLElement]? {
|
|
guard let elements = all else { return nil }
|
|
|
|
var found = [AEXMLElement]()
|
|
for element in elements {
|
|
if condition(element) {
|
|
found.append(element)
|
|
}
|
|
}
|
|
|
|
return found.count > 0 ? found : nil
|
|
}
|
|
|
|
/**
|
|
Returns all elements with given value.
|
|
|
|
- parameter value: XML element value.
|
|
|
|
- returns: Optional Array of found XML elements.
|
|
*/
|
|
open func all(withValue value: String) -> [AEXMLElement]? {
|
|
let found = filter { (element) -> Bool in
|
|
return element.value == value
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
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]) -> [AEXMLElement]? {
|
|
let found = filter { (element) -> Bool in
|
|
var countAttributes = 0
|
|
for (key, value) in attributes {
|
|
if element.attributes[key] == value {
|
|
countAttributes += 1
|
|
}
|
|
}
|
|
return countAttributes == attributes.count
|
|
}
|
|
return found
|
|
}
|
|
|
|
// 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: AEXMLElement) -> AEXMLElement {
|
|
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] = [String : String]()) -> AEXMLElement
|
|
{
|
|
let child = AEXMLElement(name: name, value: value, attributes: attributes)
|
|
return addChild(child)
|
|
}
|
|
|
|
/// Removes `self` from `parent` XML element.
|
|
open func removeFromParent() {
|
|
parent?.removeChild(self)
|
|
}
|
|
|
|
fileprivate func removeChild(_ child: AEXMLElement) {
|
|
if let childIndex = children.firstIndex(where: { $0 === child }) {
|
|
children.remove(at: childIndex)
|
|
}
|
|
}
|
|
|
|
fileprivate var parentsCount: Int {
|
|
var count = 0
|
|
var element = self
|
|
|
|
while let parent = element.parent {
|
|
count += 1
|
|
element = parent
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
fileprivate func indent(withDepth depth: Int) -> String {
|
|
var count = depth
|
|
var indent = String()
|
|
|
|
while count > 0 {
|
|
indent += "\t"
|
|
count -= 1
|
|
}
|
|
|
|
return indent
|
|
}
|
|
|
|
/// 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 {
|
|
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 {
|
|
// insert string value and close element
|
|
xml += ">\(string.xmlEscaped)</\(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: "")
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
}
|