Element.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /**
  2. * https://github.com/tadija/AEXML
  3. * Copyright (c) Marko Tadić 2014-2019
  4. * Licensed under the MIT license. See LICENSE file.
  5. */
  6. import Foundation
  7. #if canImport(FoundationXML)
  8. import FoundationXML
  9. #endif
  10. /**
  11. This is base class for holding XML structure.
  12. You can access its structure by using subscript like this: `element["foo"]["bar"]` which would
  13. return `<bar></bar>` element from `<element><foo><bar></bar></foo></element>` XML as an `Element` object.
  14. */
  15. @dynamicMemberLookup
  16. open class Element {
  17. // MARK: - Properties
  18. /// Every `Element` should have its parent element instead of `XMLDocument` which parent is `nil`.
  19. open internal(set) weak var parent: Element?
  20. /// Child XML elements.
  21. open internal(set) var children = [Element]()
  22. /// XML Element name.
  23. open var name: String
  24. /// XML Element value.
  25. open var value: String?
  26. /// XML Element attributes.
  27. open var attributes: [String: String]
  28. /// Error value (`nil` if there is no error).
  29. open var error: XMLError?
  30. // MARK: - Init
  31. /**
  32. Designated initializer - all parameters are optional.
  33. - parameter name: XML element name.
  34. - parameter value: XML element value (defaults to `nil`).
  35. - parameter attributes: XML element attributes (defaults to empty dictionary).
  36. - returns: An initialized `Element` object.
  37. */
  38. public init(name: String, value: String? = nil, attributes: [String: String] = [:]) {
  39. self.name = name
  40. self.value = value
  41. self.attributes = attributes
  42. }
  43. // MARK: - XML Read
  44. /// The first element with given name **(Empty element with error if not exists)**.
  45. open subscript(key: String) -> Element {
  46. if let first = children.first(where: { $0.name == key }) {
  47. return first
  48. }
  49. let element = Element(name: key)
  50. element.error = XMLError.elementNotFound
  51. return element
  52. }
  53. public subscript(dynamicMember member: String) -> Element {
  54. self[member]
  55. }
  56. public subscript<T>(dynamicMember member: String) -> T? where T: LosslessStringConvertible {
  57. guard let string = self[member].value, let value = T(string) else { return nil }
  58. return value
  59. }
  60. open func get<T>() throws -> T where T: LosslessStringConvertible {
  61. guard let string = value, let value = T(string) else { throw XMLError.valueConversionFailed }
  62. return value
  63. }
  64. open func date(formatter: DateFormatter) -> Date? {
  65. guard let value = value else { return nil }
  66. return formatter.date(from: value)
  67. }
  68. open func date(formatter: ISO8601DateFormatter = .init()) -> Date? {
  69. guard let value = value else { return nil }
  70. return formatter.date(from: value)
  71. }
  72. /// Returns all of the elements with equal name as `self` **(nil if not exists)**.
  73. open var all: [Element]? { parent?.children.filter { $0.name == self.name } }
  74. /// Returns the first element with equal name as `self` **(nil if not exists)**.
  75. open var first: Element? { all?.first }
  76. /// Returns the last element with equal name as `self` **(nil if not exists)**.
  77. open var last: Element? { all?.last }
  78. /// Returns number of all elements with equal name as `self`.
  79. open var count: Int { all?.count ?? 0 }
  80. /**
  81. Returns all elements with given value.
  82. - parameter value: XML element value.
  83. - returns: Optional Array of found XML elements.
  84. */
  85. open func all(withValue value: String) -> [Element]? {
  86. all?.compactMap { $0.value == value ? $0 : nil }
  87. }
  88. /**
  89. Returns all elements containing given attributes.
  90. - parameter attributes: Array of attribute names.
  91. - returns: Optional Array of found XML elements.
  92. */
  93. open func all(containingAttributeKeys keys: [String]) -> [Element]? {
  94. all?.compactMap { element in
  95. keys.reduce(true) { result, key in
  96. result && Array(element.attributes.keys).contains(key)
  97. } ? element : nil
  98. }
  99. }
  100. /**
  101. Returns all elements with given attributes.
  102. - parameter attributes: Dictionary of Keys and Values of attributes.
  103. - returns: Optional Array of found XML elements.
  104. */
  105. open func all(withAttributes attributes: [String: String]) -> [Element]? {
  106. let keys = Array(attributes.keys)
  107. return all(containingAttributeKeys: keys)?.compactMap { element in
  108. attributes.reduce(true) { result, attribute in
  109. result && element.attributes[attribute.key] == attribute.value
  110. } ? element : nil
  111. }
  112. }
  113. /**
  114. Returns all descendant elements which satisfy the given predicate.
  115. Searching is done vertically; children are tested before siblings. Elements appear in the list
  116. in the order in which they are found.
  117. - parameter predicate: Function which returns `true` for a desired element and `false` otherwise.
  118. - returns: Array of found XML elements.
  119. */
  120. open func allDescendants(where predicate: (Element) -> Bool) -> [Element] {
  121. var result: [Element] = []
  122. for child in children {
  123. if predicate(child) {
  124. result.append(child)
  125. }
  126. result.append(contentsOf: child.allDescendants(where: predicate))
  127. }
  128. return result
  129. }
  130. /**
  131. Returns the first descendant element which satisfies the given predicate, or nil if no such element is found.
  132. Searching is done vertically; children are tested before siblings.
  133. - parameter predicate: Function which returns `true` for the desired element and `false` otherwise.
  134. - returns: Optional Element.
  135. */
  136. open func firstDescendant(where predicate: (Element) throws -> Bool) rethrows -> Element? {
  137. for child in children {
  138. if try predicate(child) {
  139. return child
  140. } else if let descendant = try child.firstDescendant(where: predicate) {
  141. return descendant
  142. }
  143. }
  144. return nil
  145. }
  146. /**
  147. Indicates whether the element has a descendant satisfying the given predicate.
  148. - parameter predicate: Function which returns `true` for the desired element and `false` otherwise.
  149. - returns: Bool.
  150. */
  151. open func hasDescendant(where predicate: (Element) throws -> Bool) rethrows -> Bool {
  152. try firstDescendant(where: predicate) != nil
  153. }
  154. // MARK: - XML Write
  155. /**
  156. Adds child XML element to `self`.
  157. - parameter child: Child XML element to add.
  158. - returns: Child XML element with `self` as `parent`.
  159. */
  160. @discardableResult
  161. open func addChild(_ child: Element) -> Element {
  162. child.parent = self
  163. children.append(child)
  164. return child
  165. }
  166. /**
  167. Adds child XML element to `self`.
  168. - parameter name: Child XML element name.
  169. - parameter value: Child XML element value (defaults to `nil`).
  170. - parameter attributes: Child XML element attributes (defaults to empty dictionary).
  171. - returns: Child XML element with `self` as `parent`.
  172. */
  173. @discardableResult
  174. open func addChild(name: String,
  175. value: String? = nil,
  176. attributes: [String: String] = [:]) -> Element {
  177. let child = Element(name: name, value: value, attributes: attributes)
  178. return addChild(child)
  179. }
  180. /**
  181. Adds an array of XML elements to `self`.
  182. - parameter children: Child XML element array to add.
  183. - returns: Child XML elements with `self` as `parent`.
  184. */
  185. @discardableResult
  186. open func addChildren(_ children: [Element]) -> [Element] {
  187. children.forEach { addChild($0) }
  188. return children
  189. }
  190. /// Removes `self` from `parent` XML element.
  191. open func removeFromParent() {
  192. if let index = parent?.children.firstIndex(where: { $0 === self }) {
  193. parent?.children.remove(at: index)
  194. }
  195. }
  196. /// Complete hierarchy of `self` and `children` in **XML** escaped and formatted String
  197. open var xml: String {
  198. var xml = String()
  199. // open element
  200. xml += indent(withDepth: parentsCount - 1)
  201. xml += "<\(name)"
  202. if attributes.count > 0 {
  203. // insert attributes
  204. for (key, value) in attributes.sorted(by: { $0.key < $1.key }) {
  205. xml += " \(key)=\"\(value.xmlEscaped)\""
  206. }
  207. }
  208. if value == nil, children.count == 0 {
  209. // close element
  210. xml += " />"
  211. } else {
  212. if children.count > 0 {
  213. // add children
  214. xml += ">\n"
  215. for child in children {
  216. xml += "\(child.xml)\n"
  217. }
  218. // add indentation
  219. xml += indent(withDepth: parentsCount - 1)
  220. xml += "</\(name)>"
  221. } else if let string = value {
  222. // insert string value and close element
  223. xml += ">\(string.xmlEscaped)</\(name)>"
  224. } else {
  225. // insert empty value and close element
  226. xml += "></\(name)>"
  227. }
  228. }
  229. return xml
  230. }
  231. /// Same as `xmlString` but without `\n` and `\t` characters
  232. open var xmlCompact: String {
  233. let chars = CharacterSet(charactersIn: "\n\t")
  234. return xml.components(separatedBy: chars).joined(separator: "")
  235. }
  236. /// Same as `xmlString` but with 4 spaces instead '\t' characters
  237. open var xmlSpaces: String {
  238. let chars = CharacterSet(charactersIn: "\t")
  239. return xml.components(separatedBy: chars).joined(separator: " ")
  240. }
  241. // MARK: - Helpers
  242. private var parentsCount: Int {
  243. var count = 0
  244. var element = self
  245. while let parent = element.parent {
  246. count += 1
  247. element = parent
  248. }
  249. return count
  250. }
  251. private func indent(withDepth depth: Int) -> String {
  252. var count = depth
  253. var indent = String()
  254. while count > 0 {
  255. indent += "\t"
  256. count -= 1
  257. }
  258. return indent
  259. }
  260. }
  261. extension String {
  262. /// String representation of self with XML special characters escaped.
  263. public var xmlEscaped: String {
  264. // we need to make sure "&" is escaped first. Not doing this may break escaping the other characters
  265. var escaped = replacingOccurrences(of: "&", with: "&amp;", options: .literal)
  266. // replace the other four special characters
  267. let escapeChars = ["<": "&lt;", ">": "&gt;", "'": "&apos;", "\"": "&quot;"]
  268. for (char, echar) in escapeChars {
  269. escaped = escaped.replacingOccurrences(of: char, with: echar, options: .literal)
  270. }
  271. return escaped
  272. }
  273. }