/ Sources / FoundationXML / XMLElement.swift
XMLElement.swift
  1  // This source file is part of the Swift.org open source project
  2  //
  3  // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
  4  // Licensed under Apache License v2.0 with Runtime Library Exception
  5  //
  6  // See http://swift.org/LICENSE.txt for license information
  7  // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
  8  //
  9  
 10  #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
 11  import SwiftFoundation
 12  #else
 13  import Foundation
 14  #endif
 15  @_implementationOnly import CoreFoundation
 16  @_implementationOnly import CFXMLInterface
 17  
 18  /*!
 19      @class XMLElement
 20      @abstract An XML element
 21      @discussion Note: Trying to add a document, namespace, attribute, or node with a parent throws an exception. To add a node with a parent first detach or create a copy of it.
 22  */
 23  open class XMLElement: XMLNode {
 24  
 25      /*!
 26          @method initWithName:
 27          @abstract Returns an element <tt>&lt;name>&lt;/name></tt>.
 28      */
 29      public convenience init(name: String) {
 30          setupXMLParsing()
 31          self.init(name: name, uri: nil)
 32      }
 33  
 34      /*!
 35          @method initWithName:URI:
 36          @abstract Returns an element whose full QName is specified.
 37      */
 38      public init(name: String, uri URI: String?) {
 39          setupXMLParsing()
 40          super.init(kind: .element, options: [])
 41          self.uri = URI
 42          self.name = name
 43      }
 44  
 45      /*!
 46          @method initWithName:stringValue:
 47          @abstract Returns an element with a single text node child <tt>&lt;name>string&lt;/name></tt>.
 48      */
 49      public convenience init(name: String, stringValue string: String?) {
 50          setupXMLParsing()
 51          self.init(name: name, uri: nil)
 52          if let string = string {
 53              let child = _CFXMLNewTextNode(string)
 54              _CFXMLNodeAddChild(_xmlNode, child)
 55          }
 56      }
 57  
 58      /*!
 59          @method initWithXMLString:error:
 60          @abstract Returns an element created from a string. Parse errors are collected in <tt>error</tt>.
 61      */
 62      public convenience init(xmlString string: String) throws {
 63          setupXMLParsing()
 64          
 65          // If we prepend the XML line to the string
 66          let docString = """
 67          <?xml version="1.0" encoding="utf-8" standalone="yes"?>\(string)
 68          """
 69          // we can use the document string parser to get the element
 70          let doc = try XMLDocument(xmlString: docString, options: [])
 71          // We know the doc has a root element and first child or else the above line would have thrown
 72          self.init(ptr:  _CFXMLCopyNode(_CFXMLNodeGetFirstChild(doc._xmlNode)!, true))
 73      }
 74  
 75      public convenience override init(kind: XMLNode.Kind, options: XMLNode.Options = []) {
 76          setupXMLParsing()
 77          self.init(name: "", uri: nil)
 78      }
 79  
 80      /*!
 81          @method elementsForName:
 82          @abstract Returns all of the child elements that match this name.
 83      */
 84      open func elements(forName name: String) -> [XMLElement] {
 85          return self.filter({ _CFXMLNodeGetType($0._xmlNode) == _kCFXMLTypeElement }).filter({ $0.name == name }).compactMap({ $0 as? XMLElement })
 86      }
 87  
 88      /*!
 89          @method elementsForLocalName:URI
 90          @abstract Returns all of the child elements that match this localname URI pair.
 91      */
 92      open func elements(forLocalName localName: String, uri URI: String?) -> [XMLElement] {
 93          return self.filter({ _CFXMLNodeGetType($0._xmlNode) == _kCFXMLTypeElement }).filter({ $0.localName == localName && $0.uri == uri }).compactMap({ $0 as? XMLElement })
 94      }
 95  
 96      /*!
 97          @method addAttribute:
 98          @abstract Adds an attribute. Attributes with duplicate names replace the old one.
 99      */
100      open func addAttribute(_ attribute: XMLNode) {
101          guard let cfname = _CFXMLNodeCopyName(attribute._xmlNode) else {
102              fatalError("Attributes must have a name!")
103          }
104          
105          let name = unsafeBitCast(cfname, to: NSString.self) as String
106  
107          removeAttribute(forName: name)
108          _CFXMLCompletePropURI(attribute._xmlNode, _xmlNode);        
109          addChild(attribute)
110      }
111  
112      /*!
113          @method removeAttributeForName:
114          @abstract Removes an attribute based on its name.
115      */
116      open func removeAttribute(forName name: String) {
117          if let prop = _CFXMLNodeHasProp(_xmlNode, name, nil) {
118              let propNode = XMLNode._objectNodeForNode(_CFXMLNodePtr(prop))
119              _childNodes.remove(propNode)
120              // We can't use `xmlRemoveProp` because someone else may still have a reference to this attribute
121              _CFXMLUnlinkNode(_CFXMLNodePtr(prop))
122          }
123      }
124  
125      /*!
126          @method setAttributes
127          @abstract Set the attributes. In the case of duplicate names, the first attribute with the name is used.
128      */
129      open var attributes: [XMLNode]? {
130          get {
131              var result: [XMLNode] = []
132              var nextAttribute = _CFXMLNodeProperties(_xmlNode)
133              while let attribute = nextAttribute {
134                  result.append(XMLNode._objectNodeForNode(attribute))
135                  nextAttribute = _CFXMLNodeGetNextSibling(attribute)
136              }
137              return !result.isEmpty ? result : nil // This appears to be how Darwin does it
138          }
139  
140          set {
141              removeAttributes()
142  
143              guard let attributes = newValue else {
144                  return
145              }
146  
147              for attribute in attributes {
148                  addAttribute(attribute)
149              }
150          }
151      }
152  
153      private func removeAttributes() {
154          var nextAttribute = _CFXMLNodeProperties(_xmlNode)
155          while let attribute = nextAttribute {
156              var shouldFreeNode = true
157              if let privateData = _CFXMLNodeGetPrivateData(attribute) {
158                  _childNodes.remove(unsafeBitCast(privateData, to: XMLNode.self))
159  
160                  shouldFreeNode = false
161              }
162  
163              let temp = _CFXMLNodeGetNextSibling(attribute)
164              _CFXMLUnlinkNode(attribute)
165              if shouldFreeNode {
166                  _CFXMLFreeNode(attribute)
167              }
168  
169              nextAttribute = temp
170          }
171      }
172  
173      /*!
174       @method setAttributesWithDictionary:
175       @abstract Set the attributes based on a name-value dictionary.
176       */
177      open func setAttributesWith(_ attributes: [String : String]) {
178          removeAttributes()
179          for (name, value) in attributes {
180              addAttribute(XMLNode.attribute(withName: name, stringValue: value) as! XMLNode)
181          }
182      }
183  
184      /*!
185          @method attributeForName:
186          @abstract Returns an attribute matching this name.
187      */
188      open func attribute(forName name: String) -> XMLNode? {
189          guard let attribute = _CFXMLNodeHasProp(_xmlNode, name, nil) else { return nil }
190          return XMLNode._objectNodeForNode(attribute)
191      }
192  
193      /*!
194          @method attributeForLocalName:URI:
195          @abstract Returns an attribute matching this localname URI pair.
196      */
197      open func attribute(forLocalName localName: String, uri URI: String?) -> XMLNode? {
198          guard let attribute = _CFXMLNodeHasProp(_xmlNode, localName, URI) else { return nil }
199          return XMLNode._objectNodeForNode(attribute)
200      }
201  
202      /*!
203          @method addNamespace:URI:
204          @abstract Adds a namespace. Namespaces with duplicate names are not added.
205      */
206      open func addNamespace(_ aNamespace: XMLNode) {
207          if ((namespaces ?? []).compactMap({ $0.name }).contains(aNamespace.name ?? "")) {
208              return
209          }
210          _CFXMLAddNamespace(_xmlNode, aNamespace._xmlNode)
211      }
212  
213      /*!
214          @method addNamespace:URI:
215          @abstract Removes a namespace with a particular name.
216      */
217      open func removeNamespace(forPrefix name: String) {
218          _CFXMLRemoveNamespace(_xmlNode, name)
219      }
220  
221      /*!
222          @method namespaces
223          @abstract Set the namespaces. In the case of duplicate names, the first namespace with the name is used.
224      */
225      open var namespaces: [XMLNode]? {
226          get {
227              var count: Int = 0
228              if let result = _CFXMLNamespaces(_xmlNode, &count) {
229                  defer {
230                      free(result)
231                  }
232                  let namespacePtrs = UnsafeBufferPointer<_CFXMLNodePtr>(start: result, count: count)
233                  return namespacePtrs.map { XMLNode._objectNodeForNode($0) }
234              }
235  
236              return nil
237          }
238  
239          set {
240              if var nodes = newValue?.map({ $0._xmlNode! }) {
241                  nodes.withUnsafeMutableBufferPointer({ (bufPtr) in
242                      _CFXMLSetNamespaces(_xmlNode, bufPtr.baseAddress, bufPtr.count)
243                  })
244              } else {
245                  _CFXMLSetNamespaces(_xmlNode, nil, 0);
246              }
247          }
248      }
249  
250      /*!
251          @method namespaceForPrefix:
252          @abstract Returns the namespace matching this prefix.
253      */
254      open func namespace(forPrefix name: String) -> XMLNode? {
255          return (namespaces ?? []).first { $0.name == name }
256      }
257  
258      /*!
259          @method resolveNamespaceForName:
260          @abstract Returns the namespace who matches the prefix of the name given. Looks in the entire namespace chain.
261      */
262      open func resolveNamespace(forName name: String) -> XMLNode? {
263          // Legitimate question: why not use XMLNode's methods?
264          // Because Darwin does the split manually here, and we want to match that rather than asking libxml2.
265          let prefix: String
266          if let colon = name.firstIndex(of: ":") {
267              prefix = String(name[name.startIndex ..< colon])
268          } else {
269              prefix = ""
270          }
271          
272          var current: XMLElement? = self
273          while let examined = current {
274              if let namespace = examined.namespace(forPrefix: prefix) {
275                  return namespace
276              }
277              
278              current = examined.parent as? XMLElement
279              guard current?.kind == .element else { break }
280          }
281          
282          if !prefix.isEmpty {
283              return XMLNode.predefinedNamespace(forPrefix: prefix)
284          }
285          
286          return nil
287      }
288  
289      /*!
290          @method resolvePrefixForNamespaceURI:
291          @abstract Returns the URI of this prefix. Looks in the entire namespace chain.
292      */
293      open func resolvePrefix(forNamespaceURI namespaceURI: String) -> String? {
294          var current: XMLElement? = self
295          while let examined = current {
296              if let namespace = (examined.namespaces ?? []).first(where: { $0.stringValue == namespaceURI }) {
297                  return namespace.name
298              }
299              
300              current = examined.parent as? XMLElement
301              guard current?.kind == .element else { break }
302          }
303          
304          if let namespace = XMLNode._defaultNamespacesByURI[namespaceURI] {
305              return namespace.name
306          }
307          
308          return nil
309      }
310  
311      /*!
312          @method insertChild:atIndex:
313          @abstract Inserts a child at a particular index.
314      */
315      open func insertChild(_ child: XMLNode, at index: Int) {
316          _insertChild(child, atIndex: index)
317      }
318  
319      /*!
320          @method insertChildren:atIndex:
321          @abstract Insert several children at a particular index.
322      */
323      open func insertChildren(_ children: [XMLNode], at index: Int) {
324          _insertChildren(children, atIndex: index)
325      }
326  
327      /*!
328          @method removeChildAtIndex:atIndex:
329          @abstract Removes a child at a particular index.
330      */
331      open func removeChild(at index: Int) {
332          _removeChildAtIndex(index)
333      }
334  
335      /*!
336          @method setChildren:
337          @abstract Removes all existing children and replaces them with the new children. Set children to nil to simply remove all children.
338      */
339      open func setChildren(_ children: [XMLNode]?) {
340          _setChildren(children)
341      }
342  
343      /*!
344          @method addChild:
345          @abstract Adds a child to the end of the existing children.
346      */
347      open func addChild(_ child: XMLNode) {
348          _addChild(child)
349      }
350  
351      /*!
352          @method replaceChildAtIndex:withNode:
353          @abstract Replaces a child at a particular index with another child.
354      */
355      open func replaceChild(at index: Int, with node: XMLNode) {
356          _replaceChildAtIndex(index, withNode: node)
357      }
358  
359      /*!
360          @method normalizeAdjacentTextNodesPreservingCDATA:
361          @abstract Adjacent text nodes are coalesced. If the node's value is the empty string, it is removed. This should be called with a value of NO before using XQuery or XPath.
362      */
363      open func normalizeAdjacentTextNodesPreservingCDATA(_ preserve: Bool) {
364          // Replicate Darwin behavior: no change occurs at all in this case.
365          guard childCount != 1 else { return }
366          
367          var text = ""
368          var index = 0
369          let count = childCount
370          var children: [XMLNode] = []
371          
372          while index < count {
373              let child = self.children![index]
374              let isText = child.kind == .text
375              let isCDataToPreserve = preserve ? (isText && child.isCData) : false
376                          
377              if isText && !isCDataToPreserve {
378                  if let stringValue = child.stringValue {
379                      text.append(contentsOf: stringValue)
380                  }
381              } else {
382                  if !text.isEmpty {
383                      let mergedText = XMLNode.text(withStringValue: text) as! XMLNode
384                      children.append(mergedText)
385                      text = ""
386                  }
387                  if child.kind == .element, let child = child as? XMLElement {
388                      child.normalizeAdjacentTextNodesPreservingCDATA(preserve)
389                  }
390                  children.append(child)
391              }
392              
393              index += 1
394          }
395          
396          if !text.isEmpty {
397              children.append(XMLNode.text(withStringValue: text) as! XMLNode)
398          }
399          
400          self.setChildren(children)
401      }
402  
403      internal override class func _objectNodeForNode(_ node: _CFXMLNodePtr) -> XMLElement {
404          precondition(_CFXMLNodeGetType(node) == _kCFXMLTypeElement)
405  
406          if let privateData = _CFXMLNodeGetPrivateData(node) {
407              return unsafeBitCast(privateData, to: XMLElement.self)
408          }
409  
410          return XMLElement(ptr: node)
411      }
412  
413      internal override init(ptr: _CFXMLNodePtr) {
414          super.init(ptr: ptr)
415      }
416  }
417  
418  extension XMLElement {
419      /*!
420          @method setAttributesAs:
421          @abstract Set the attributes base on a name-value dictionary.
422          @discussion This method is deprecated and does not function correctly. Use -setAttributesWith: instead.
423       */
424      @available(*, unavailable, renamed:"setAttributesWith")
425      public func setAttributesAs(_ attributes: [NSObject : AnyObject]) { NSUnsupported() }
426  }