Swift [85] is a statically typed script programming language with a syntax inspired by functional languages such as Haskell and script like Python. In this section, we will show how to use it for both DOM and SAX parsing, which we used in XML processing in Java.
To show how to process an existing XML structure, we will use the compacting
process that we programmed in Java in Section 8.1. In Swift, DOM
parsing is achieved simply by calling the XMLDocument
constructor available as
Foundation
class. This
creates a DOM structure composed of instances of the
XMLNode
class.
Example 10.29. [DOMCompact.swift
] Text compaction of the cellar book
(Example 2.2) with Swift using the DOM model
Compare this program with Example 8.1.
1 import Foundationfunc processDOM(_ string:String)->String{
var res="" 5 func compact(_ nodeIn:XMLNode?,_ indent:String){
guard (nodeIn != nil) else {return} let node = nodeIn! var newIndent=indent 10 switch node.kind { case .element:
let name = node.name! let elem = node as! XMLElement res += name+"[" 15 newIndent += String(repeating:" ", count: name.count+1) var first = true if elem.attributes != nil {
for attr in elem.attributes! { if !first {res += "\n\(newIndent)"} 20 res += "@\(attr.name!)[\(attr.stringValue!)]" first = false } } if elem.children != nil {
25 for child in elem.children! { if !first {res += "\n\(newIndent)"} compact(child, newIndent)
first = false } 30 } res += "]" case .text:
res += node.stringValue!.trimmingCharacters(in: .whitespacesAndNewlines) 35 default: print("compact:unprocessed kind:\(node.kind)") } } 40 do { let doc = try XMLDocument(data: string.data(using: .utf16)!,
options: []) compact(doc.rootElement(),"")
return res+"\n" 45 } catch { print("Error in the input file:\(error)") return "" } } 50
func processDOM(path:String)->String { return processDOM(try! String(contentsOf: URL(fileURLWithPath: path))) }
|
Imports the |
|
Function that takes the XML input as a atring and returns the
compacted form as a string, here the |
|
A recursive function that processes an XML node in order to add to |
|
If the node is an element, it adds to the the name of the node followed by an opening bracket and adds the appropriate number of spaces to the current indentation. |
|
Deals with attributes which are contained in a list of nodes over which we
iterate. For each attribute, prints a |
|
Loops over the list of children possibly changing line if it not the first child. |
|
Recursively calls |
|
Processes a text node by adding it to |
|
Parses the file by creating a new XML document from the content of the input string. |
|
Calls the compacting process on the document node with an empty indentation string and ends the last line with a newline. |
|
Transforms the XML file identified by a path string to produce a string
containing the compacted form by calling the |
In order to process an XML structure with the SAX approach, we will use a
similar compacting process to the one we programmed in Java in Section 8.2. SAX parsing is achieved by creating an instance of
the xmlParserDelegate
class in which we define handler methods for some parsing events.
The parsing process will send call-backs to the handler of these
events, called a delegate in Swift, to create the compacted document
as a string.
SAX parsing in Swift is only a matter of defining the appropriate functions in the delegate.
Because nodes are not normalized, many successive text nodes can appear within an
element, so some care has to be taken to deal with this fact. Unfortunately, in Swift 5, internal
entities such as the ones used at the start of the CellarBook.xml file cannot be properly dealt with.
Example 10.30. [SAXCompact.swift
] Text compaction of the cellar book
(Example 2.2) with Swift using the SAX model
Compare this program with Example 8.3.
1 import Foundation// Caution: internal entities are not dealt properly with this API. func processSAX(string:String)->String { 5 class xmlParserDelegate:NSObject, XMLParserDelegate {
var res="" var closed=false var indent="" 10 var lastNodeWasText=false func parser(_ parser: XMLParser,
didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, 15 attributes attributeDict: [String : String] = [:]){ if closed { res += "\n\(indent)" closed = false } 20 indent += String(repeating: " ", count:elementName.count+1)
res += "\(elementName)[" var first = true for (name,val) in attributeDict {
if !first {res += "\n\(indent)"} 25 res += "@\(name)[\(val)]" first=false closed=true } lastNodeWasText=false 30 } func parser(_ parser: XMLParser,
didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?){ 35 res += "]" closed=true indent=String(indent.dropLast(elementName.count+1)) lastNodeWasText=false } 40 func parser(_ parser: XMLParser,
foundCharacters string: String){ let s=string.trimmingCharacters(in: .whitespacesAndNewlines) if s.count>0 { 45 if closed && !lastNodeWasText{ res += "\n\(indent)" closed = false } closed=true 50 res += "\(s)" } lastNodeWasText=true } 55 public func parserDidEndDocument(_ parser: XMLParser){
res += "\n" } func parser(_ parser: XMLParser,
60 parseErrorOccurred parseError: Error){ print("** parseErrorOccurred:\(parseError)") } func parser(_ parser: XMLParser, 65 validationErrorOccurred validationError: Error){ print("** validationErrorOccurred:\(validationError)") } } 70 let myDelegate=xmlParserDelegate()
let parser=XMLParser(data: string.data(using: .utf16)!)
parser.delegate = myDelegate
parser.parse()
return myDelegate.res 75 } func processSAX(path:String)->String{
return processSAX(string:try! String(contentsOf: URL(fileURLWithPath: path))) } 80
|
Imports the |
|
Defines the constructor of the class and initializes instance variables needed to output the element with the correct indentation and for keeping track of the parsing state. The last flag is used when many successive text nodes appears. |
|
When a new element is started, finishes the current indentation if needed. |
|
Updates the current indentation by adding the length of the element name. |
|
Prints the first attribute on the same line and the others on the subsequent lines properly indented. |
|
When an element finishes, closes the current bracket and updates the current indentation. |
|
For a non-empty text node, ends current line if needed and if the last node was not a text node. Writes the content of the node and indicates that the last node was a text node. |
|
When the document parsing finishes, add a terminating newline. |
|
Deals with a parsing error by printing an error message. |
|
Creates an instance of the SAX parser delegate. |
|
Creates an instance of the SAX parser. |
|
Sets the parser delegate to which the parsing will send events. |
|
Starts the parsing process and return the string containing the result of the compacting process. |
|
Transforms the XML file identified by a path string to produce a string
containing the compacted form by calling the |
In order to demonstrate the creation of new XML document, we will parse the
compact form produced in Section 10.5.1 or in Section 10.5.2 like we did in Chapter 9.
We first need a way to access appropriate tokens corresponding
to important signals in the input file. This is achieved by defining a
CompactTokenizer
class that
will return opening and closing square brackets, at-signs and the rest as a single
string. Newlines will also delimit tokens but will be ignored in the document
processing. The file is processed by a series of calls to nextToken
or
skip
. skip
provides a useful redundancy check for
tokens that are encountered in the input but are ignored for output.
In Swift, a new document is created using the constructor of the
XMLDocument
class.
Adding a new node is done with the
addChild
method that adds the new node as the last child of a
given node. The attributes of a node are added using the addAttribute
method.
Example 10.31. [DOMExpand.swift
] Swift compact form parsing to create an
XML document
A sample input for this program is Example 5.8 , which should yield Example 2.2. Compare this program with Example 9.2.
1 import Foundationclass CompactTokenizer {
var tokens:[String] 5 var iTok:Int init(string:String){
let pattern = "(\\[|\\]|@|[^\\[\\]@\\n]+)" let regex = try! NSRegularExpression(pattern: pattern, options: []) 10 let nsrange = NSRange(string.startIndex..<string.endIndex,in:string) iTok=0 tokens = regex.matches(in: string, options: [], range: nsrange).map {match in let matchedRange=Range(match.range(at:1),in:string)! 15 return string[matchedRange] .trimmingCharacters(in:.whitespacesAndNewlines) }.filter{!$0.isEmpty} } 20 func nextToken()->String {
let token = tokens[iTok] iTok+=1 return token } 25 func skip(_ string:String){
if nextToken() != string { print("\(string) expected at position \(iTok-1)") } 30 } func isAtEnd()->Bool {
return iTok>=tokens.count } 35 } func expand(string:String) -> XMLNode {
let ct=CompactTokenizer(string: string)
40 func expandElem(_ elem:XMLElement,_ inTok:String){
var tok = inTok while tok == "@" {
let attName=ct.nextToken() ct.skip("[") 45 elem.addAttribute(
XMLNode.attribute(withName: attName, stringValue: ct.nextToken()) as! XMLNode) ct.skip("]") tok=ct.nextToken() 50 } while tok != "]" && !ct.isAtEnd() {
let s=tok let nextTok=ct.nextToken() if nextTok == "[" {
55 let child = XMLNode.element(withName: s) as! XMLElement expandElem(child,ct.nextToken()) elem.addChild(child) tok = ct.nextToken() } else {
60 elem.addChild(XMLNode.text(withStringValue: s) as! XMLNode) tok = nextTok } } } 65 var rootName="" var token=ct.nextToken() while token != "[" { rootName = token 70 token = ct.nextToken() } let root = XMLElement(name: rootName)
let doc=XMLDocument(rootElement: root)
expandElem(root,ct.nextToken()) 75 return doc } func expand(path:String)->XMLNode {
return expand(string: try! String(contentsOf: URL(fileURLWithPath: path))) 80 }
|
Imports the |
|
Defines the |
|
Defines the |
|
Returns the next token. |
|
Checks that the current token is the same as the one given as parameter and retrieves the next one. If the current token does not match, then print an error message. |
|
Checks if there are still tokens left to process. |
|
Creates an XML document corresponding to the content of the input string. |
|
Creates an instance of the
|
|
Adds to an XML element the content starting with the current token
|
|
Because all attribute names start with an |
|
Adding an attribute is done by calling the |
|
All children are then processed in turn until a closing square
bracket is encountered. |
|
A child node is expanded by a recursive call whose result is added as the last child of the current element. |
|
A text node is added as the last child of the current element. |
|
The name of the root is the first token immediately followed by an opening square bracket. |
|
Create the document node by calling |
|
Calls the |