/ Sources / FoundationXML / XMLDocument.swift
XMLDocument.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  // Input options
 19  //  NSXMLNodeOptionsNone
 20  //  NSXMLNodePreserveAll
 21  //  NSXMLNodePreserveNamespaceOrder
 22  //  NSXMLNodePreserveAttributeOrder
 23  //  NSXMLNodePreserveEntities
 24  //  NSXMLNodePreservePrefixes
 25  //  NSXMLNodePreserveCDATA
 26  //  NSXMLNodePreserveEmptyElements
 27  //  NSXMLNodePreserveQuotes
 28  //  NSXMLNodePreserveWhitespace
 29  //  NSXMLNodeLoadExternalEntities
 30  //  NSXMLNodeLoadExternalEntitiesSameOriginOnly
 31  
 32  //  NSXMLDocumentTidyHTML
 33  //  NSXMLDocumentTidyXML
 34  
 35  //  NSXMLDocumentValidate
 36  
 37  // Output options
 38  //  NSXMLNodePrettyPrint
 39  //  NSXMLDocumentIncludeContentTypeDeclaration
 40  
 41  extension XMLDocument {
 42  
 43      /*!
 44          @typedef XMLDocument.ContentKind
 45          @abstract Define what type of document this is.
 46          @constant XMLDocument.ContentKind.xml The default document type
 47          @constant XMLDocument.ContentKind.xhtml Set if XMLNode.Options.documentTidyHTML is set and HTML is detected
 48          @constant XMLDocument.ContentKind.html Outputs empty tags without a close tag, eg <br>
 49          @constant XMLDocument.ContentKind.text Output the string value of the document
 50      */
 51      public enum ContentKind : UInt {
 52  
 53          case xml
 54          case xhtml
 55          case html
 56          case text
 57      }
 58  }
 59  
 60  /*!
 61      @class XMLDocument
 62      @abstract An XML Document
 63  	@discussion Note: if the application of a method would result in more than one element in the children array, an exception is thrown. Trying to add a document, namespace, attribute, or node with a parent also throws an exception. To add a node with a parent first detach or create a copy of it.
 64  */
 65  open class XMLDocument : XMLNode {
 66      private var _xmlDoc: _CFXMLDocPtr {
 67          return _CFXMLDocPtr(_xmlNode)
 68      }
 69      
 70      public convenience init() {
 71          self.init(rootElement: nil)
 72      }
 73      
 74      /*!
 75          @method initWithXMLString:options:error:
 76          @abstract Returns a document created from either XML or HTML, if the HTMLTidy option is set. Parse errors are returned in <tt>error</tt>.
 77      */
 78      public convenience init(xmlString string: String, options mask: XMLNode.Options = []) throws {
 79          setupXMLParsing()
 80          guard let data = string.data(using: .utf8) else {
 81              // TODO: Throw an error
 82              fatalError("String: '\(string)' could not be converted to NSData using UTF-8 encoding")
 83          }
 84  
 85          try self.init(data: data, options: mask)
 86      }
 87  
 88      /*!
 89          @method initWithContentsOfURL:options:error:
 90          @abstract Returns a document created from the contents of an XML or HTML URL. Connection problems such as 404, parse errors are returned in <tt>error</tt>.
 91      */
 92      public convenience init(contentsOf url: URL, options mask: XMLNode.Options = []) throws {
 93          setupXMLParsing()
 94          let data = try Data(contentsOf: url, options: .mappedIfSafe)
 95  
 96          try self.init(data: data, options: mask)
 97      }
 98  
 99      /*!
100          @method initWithData:options:error:
101          @abstract Returns a document created from data. Parse errors are returned in <tt>error</tt>.
102      */
103      public init(data: Data, options mask: XMLNode.Options = []) throws {
104          setupXMLParsing()
105          let docPtr = _CFXMLDocPtrFromDataWithOptions(unsafeBitCast(data as NSData, to: CFData.self), UInt32(mask.rawValue))
106          super.init(ptr: _CFXMLNodePtr(docPtr))
107  
108          if mask.contains(.documentValidate) {
109              try validate()
110          }
111      }
112  
113      /*!
114          @method initWithRootElement:
115          @abstract Returns a document with a single child, the root element.
116      */
117      public init(rootElement element: XMLElement?) {
118          setupXMLParsing()
119          precondition(element?.parent == nil)
120  
121          super.init(kind: .document, options: [])
122          if let element = element {
123              _CFXMLDocSetRootElement(_xmlDoc, element._xmlNode)
124              _childNodes.insert(element)
125          }
126      }
127  
128      /*!
129          @method characterEncoding
130          @abstract Sets the character encoding to an IANA type.
131      */
132      open var characterEncoding: String? {
133          get {
134              let returned = _CFXMLDocCopyCharacterEncoding(_xmlDoc)
135              return returned == nil ? nil : unsafeBitCast(returned!, to: NSString.self) as String
136          }
137          set {
138              if let value = newValue {
139                  _CFXMLDocSetCharacterEncoding(_xmlDoc, value)
140              } else {
141                  _CFXMLDocSetCharacterEncoding(_xmlDoc, nil)
142              }
143          }
144      }
145  
146      /*!
147          @method version
148          @abstract Sets the XML version. Should be 1.0 or 1.1.
149      */
150      open var version: String? {
151          get {
152              let returned = _CFXMLDocCopyVersion(_xmlDoc)
153              return returned == nil ? nil : unsafeBitCast(returned!, to: NSString.self) as String
154          }
155          set {
156              if let value = newValue {
157                  precondition(value == "1.0" || value == "1.1")
158                  _CFXMLDocSetVersion(_xmlDoc, value)
159              } else {
160                  _CFXMLDocSetVersion(_xmlDoc, nil)
161              }
162          }
163      }
164  
165      /*!
166          @method standalone
167          @abstract Set whether this document depends on an external DTD. If this option is set the standalone declaration will appear on output.
168      */
169      open var isStandalone: Bool {
170          get {
171              return _CFXMLDocStandalone(_xmlDoc)
172          }
173          set {
174              _CFXMLDocSetStandalone(_xmlDoc, newValue)
175          }
176      }//primitive
177  
178      /*!
179          @method documentContentKind
180          @abstract The kind of document.
181      */
182      open var documentContentKind: XMLDocument.ContentKind  {
183          get {
184              let properties = _CFXMLDocProperties(_xmlDoc)
185  
186              if properties & Int32(_kCFXMLDocTypeHTML) != 0 {
187                  return .html
188              }
189  
190              return .xml
191          }
192  
193          set {
194              var properties = _CFXMLDocProperties(_xmlDoc)
195              switch newValue {
196              case .html:
197                  properties |= Int32(_kCFXMLDocTypeHTML)
198  
199              default:
200                  properties &= ~Int32(_kCFXMLDocTypeHTML)
201              }
202  
203              _CFXMLDocSetProperties(_xmlDoc, properties)
204          }
205      }//primitive
206  
207      /*!
208          @method MIMEType
209          @abstract Set the MIME type, eg text/xml.
210      */
211      open var mimeType: String?
212  
213      /*!
214          @method DTD
215          @abstract Set the associated DTD. This DTD will be output with the document.
216      */
217      /*@NSCopying*/ open var dtd: XMLDTD? {
218          get {
219              return XMLDTD._objectNodeForNode(_CFXMLDocDTD(_xmlDoc)!)
220          }
221          set {
222              if let currDTD = _CFXMLDocDTD(_xmlDoc) {
223                  if _CFXMLNodeGetPrivateData(currDTD) != nil {
224                      let DTD = XMLDTD._objectNodeForNode(currDTD)
225                      _CFXMLUnlinkNode(currDTD)
226                      _childNodes.remove(DTD)
227                  } else {
228                      _CFXMLFreeDTD(currDTD)
229                  }
230              }
231  
232              if let value = newValue {
233                  guard let dtd = value.copy() as? XMLDTD else {
234                      fatalError("Failed to copy DTD")
235                  }
236                  _CFXMLDocSetDTD(_xmlDoc, dtd._xmlDTD)
237                  _childNodes.insert(dtd)
238              } else {
239                  _CFXMLDocSetDTD(_xmlDoc, nil)
240              }
241          }
242      }//primitive
243  
244      /*!
245          @method setRootElement:
246          @abstract Set the root element. Removes all other children including comments and processing-instructions.
247      */
248      open func setRootElement(_ root: XMLElement) {
249          precondition(root.parent == nil)
250  
251          for child in _childNodes {
252              child.detach()
253          }
254  
255          _CFXMLDocSetRootElement(_xmlDoc, root._xmlNode)
256          _childNodes.insert(root)
257      }
258  
259      /*!
260          @method rootElement
261          @abstract The root element.
262      */
263      open func rootElement() -> XMLElement? {
264          guard let rootPtr = _CFXMLDocRootElement(_xmlDoc) else {
265              return nil
266          }
267  
268          return XMLNode._objectNodeForNode(rootPtr) as? XMLElement
269      }
270      
271      open override var childCount: Int {
272          return _CFXMLNodeGetElementChildCount(_xmlNode)
273      }
274  
275      /*!
276          @method insertChild:atIndex:
277          @abstract Inserts a child at a particular index.
278      */
279      open func insertChild(_ child: XMLNode, at index: Int) {
280          _insertChild(child, atIndex: index)
281      }
282  
283      /*!
284          @method insertChildren:atIndex:
285          @abstract Insert several children at a particular index.
286      */
287      open func insertChildren(_ children: [XMLNode], at index: Int) {
288          _insertChildren(children, atIndex: index)
289      }
290  
291      /*!
292          @method removeChildAtIndex:atIndex:
293          @abstract Removes a child at a particular index.
294      */
295      open func removeChild(at index: Int) {
296          _removeChildAtIndex(index)
297      }
298  
299      /*!
300          @method setChildren:
301          @abstract Removes all existing children and replaces them with the new children. Set children to nil to simply remove all children.
302      */
303      open func setChildren(_ children: [XMLNode]?) {
304          _setChildren(children)
305      }
306  
307      /*!
308          @method addChild:
309          @abstract Adds a child to the end of the existing children.
310      */
311      open func addChild(_ child: XMLNode) {
312          _addChild(child)
313      }
314  
315      /*!
316          @method replaceChildAtIndex:withNode:
317          @abstract Replaces a child at a particular index with another child.
318      */
319      open func replaceChild(at index: Int, with node: XMLNode) {
320          _replaceChildAtIndex(index, withNode: node)
321      }
322  
323      /*!
324          @method XMLData
325          @abstract Invokes XMLDataWithOptions with XMLNode.Options.none.
326      */
327      /*@NSCopying*/ open var xmlData: Data { return xmlData() }
328  
329      /*!
330          @method XMLDataWithOptions:
331          @abstract The representation of this node as it would appear in an XML document, encoded based on characterEncoding.
332      */
333      open func xmlData(options: XMLNode.Options = []) -> Data {
334          let string = xmlString(options: options)
335          // TODO: support encodings other than UTF-8
336  
337          return string.data(using: .utf8) ?? Data()
338      }
339  
340      /*!
341          @method objectByApplyingXSLT:arguments:error:
342          @abstract Applies XSLT with arguments (NSString key/value pairs) to this document, returning a new document.
343      */
344      @available(*, unavailable, message: "XSLT application is not currently supported in swift-corelibs-foundation")
345      open func object(byApplyingXSLT xslt: Data, arguments: [String : String]?) throws -> Any {
346          NSUnimplemented()
347      }
348  
349      /*!
350          @method objectByApplyingXSLTString:arguments:error:
351          @abstract Applies XSLT as expressed by a string with arguments (NSString key/value pairs) to this document, returning a new document.
352      */
353      @available(*, unavailable, message: "XSLT application is not currently supported in swift-corelibs-foundation")
354      open func object(byApplyingXSLTString xslt: String, arguments: [String : String]?) throws -> Any {
355          NSUnimplemented()
356      }
357  
358      /*!
359          @method objectByApplyingXSLTAtURL:arguments:error:
360          @abstract Applies the XSLT at a URL with arguments (NSString key/value pairs) to this document, returning a new document. Error may contain a connection error from the URL.
361      */
362      @available(*, unavailable, message: "XSLT application is not currently supported in swift-corelibs-foundation")
363      open func objectByApplyingXSLT(at xsltURL: URL, arguments argument: [String : String]?) throws -> Any {
364          NSUnimplemented()
365      }
366  
367      open func validate() throws {
368          var unmanagedError: Unmanaged<CFError>? = nil
369          let result = _CFXMLDocValidate(_xmlDoc, &unmanagedError)
370          if !result,
371              let unmanagedError = unmanagedError {
372              let error = unmanagedError.takeRetainedValue()
373              throw _CFErrorSPIForFoundationXMLUseOnly(unsafelyAssumingIsCFError: error)._nsObject
374          }
375      }
376  
377      internal override class func _objectNodeForNode(_ node: _CFXMLNodePtr) -> XMLDocument {
378          precondition(_CFXMLNodeGetType(node) == _kCFXMLTypeDocument)
379  
380          if let privateData = _CFXMLNodeGetPrivateData(node) {
381              return unsafeBitCast(privateData, to: XMLDocument.self)
382          }
383  
384          return XMLDocument(ptr: node)
385      }
386  
387      internal override init(ptr: _CFXMLNodePtr) {
388          super.init(ptr: ptr)
389      }
390  }