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><name></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><name>string</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 }