├── .gitignore ├── Tests ├── LinuxMain.swift └── BMLTests │ └── BMLTests.swift ├── .travis.yml ├── Package.swift ├── Sources ├── 🔨.swift ├── Scanner.swift ├── BML.swift └── Parser.swift ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | 4 | @testable import BMLTests 5 | 6 | XCTMain([ 7 | testCase(BMLTests.allTests), 8 | ]) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | language: generic 4 | sudo: required 5 | dist: trusty 6 | script: 7 | - eval "$(curl -sL https://swift.vapor.sh/ci)" 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "BML", 5 | dependencies: [ 6 | .Package(url: "https://github.com/vapor/core.git", majorVersion: 2), 7 | .Package(url: "https://github.com/vapor/node.git", majorVersion: 2) 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /Sources/🔨.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | 3 | extension Sequence where Iterator.Element == Byte { 4 | public var fasterString: String { 5 | let count = self.array.count 6 | 7 | let ptr = UnsafeMutablePointer.allocate(capacity: count + 1) 8 | ptr.initialize(to: 0, count: count + 1) 9 | defer { 10 | ptr.deinitialize() 11 | ptr.deallocate(capacity: count + 1) 12 | } 13 | 14 | ptr.withMemoryRebound(to: UInt8.self, capacity: count) { reboundPtr in 15 | self.array.withUnsafeBufferPointer { valuePtr in 16 | reboundPtr.assign(from: valuePtr.baseAddress!, count: count) 17 | } 18 | } 19 | 20 | // consider a better way of handling an invalid string 21 | return String(validatingUTF8: ptr) ?? "" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brett XML 2 | [![Language](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org) ![Build Status](https://travis-ci.org/BrettRToomey/brett-xml.svg?branch=master) 3 | [![codecov](https://codecov.io/gh/BrettRToomey/brett-xml/branch/master/graph/badge.svg)](https://codecov.io/gh/BrettRToomey/brett-xml) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/BrettRToomey/brett-xml/master/LICENSE) 5 | 6 | A pure Swift XML parser that's compatible with [Vapor's node](http://vapor.codes) data structure. 7 | 8 | ## Integration 9 | Update your `Package.swift` file. 10 | ```swift 11 | .Package(url: "https://github.com/BrettRToomey/brett-xml.git", majorVersion: 1) 12 | ``` 13 | 14 | ## Getting started 🚀 15 | BML is easy to use, just pass it a `String` or an array of `Byte`s. 16 | ```swift 17 | import BML 18 | let node = try XMLParser.parse("") 19 | print(node["book"]?["id"]?.value) // prints Optional("5") 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Brett R. Toomey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Scanner.swift: -------------------------------------------------------------------------------- 1 | 2 | struct Scanner { 3 | var pointer: UnsafePointer 4 | let endAddress: UnsafePointer 5 | var elements: UnsafeBufferPointer 6 | // assuming you don't mutate no copy _should_ occur 7 | let elementsCopy: [Element] 8 | } 9 | 10 | extension Scanner { 11 | init(_ data: [Element]) { 12 | self.elementsCopy = data 13 | self.elements = elementsCopy.withUnsafeBufferPointer { $0 } 14 | self.pointer = elements.baseAddress! 15 | self.endAddress = elements.endAddress 16 | } 17 | } 18 | 19 | extension Scanner { 20 | func peek(aheadBy n: Int = 0) -> Element? { 21 | guard pointer.advanced(by: n) < endAddress else { return nil } 22 | return pointer.advanced(by: n).pointee 23 | } 24 | 25 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 26 | @discardableResult 27 | mutating func pop() -> Element { 28 | assert(pointer != endAddress) 29 | defer { pointer = pointer.advanced(by: 1) } 30 | return pointer.pointee 31 | } 32 | 33 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 34 | @discardableResult 35 | mutating func attemptPop() throws -> Element { 36 | guard pointer < endAddress else { throw ScannerError.Reason.endOfStream } 37 | defer { pointer = pointer.advanced(by: 1) } 38 | return pointer.pointee 39 | } 40 | 41 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 42 | mutating func pop(_ n: Int) { 43 | assert(pointer.advanced(by: n) <= endAddress) 44 | pointer = pointer.advanced(by: n) 45 | } 46 | } 47 | 48 | extension Scanner { 49 | var isEmpty: Bool { 50 | return pointer == endAddress 51 | } 52 | } 53 | 54 | struct ScannerError: Swift.Error { 55 | let position: UInt 56 | let reason: Reason 57 | 58 | enum Reason: Swift.Error { 59 | case endOfStream 60 | } 61 | } 62 | 63 | extension UnsafeBufferPointer { 64 | fileprivate var endAddress: UnsafePointer { 65 | return baseAddress!.advanced(by: endIndex) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/BML.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | import Node 3 | 4 | public typealias XML = BML 5 | 6 | public final class BML { 7 | var _name: Bytes 8 | var _value: Bytes? 9 | 10 | var nameCache: String? 11 | var valueCache: String? 12 | 13 | public var name: String { 14 | if let nameCache = nameCache { 15 | return nameCache 16 | } else { 17 | let result = _name.fasterString 18 | nameCache = result 19 | return result 20 | } 21 | } 22 | 23 | public var value: String? { 24 | guard _value != nil else { 25 | return nil 26 | } 27 | 28 | if let valueCache = valueCache { 29 | return valueCache 30 | } else { 31 | let result = _value!.fasterString 32 | valueCache = result 33 | return result 34 | } 35 | } 36 | 37 | public var attributes: [BML] 38 | public var children: [BML] 39 | public weak var parent: BML? 40 | 41 | init( 42 | name: Bytes, 43 | value: Bytes? = nil, 44 | attributes: [BML] = [], 45 | children: [BML] = [], 46 | parent: BML? = nil 47 | ) { 48 | _name = name 49 | _value = value 50 | self.attributes = attributes 51 | self.children = children 52 | self.parent = parent 53 | } 54 | 55 | convenience init( 56 | name: String, 57 | value: String? = nil, 58 | attributes: [BML] = [], 59 | children: [BML] = [], 60 | parent: BML? = nil 61 | ) { 62 | self.init( 63 | name: name.bytes, 64 | value: value?.bytes, 65 | attributes: attributes, 66 | children: children, 67 | parent: parent 68 | ) 69 | } 70 | } 71 | 72 | extension BML { 73 | func addChild(key: Bytes, value: Bytes) { 74 | let sighting = BML(name: key, value: value, parent: self) 75 | add(child: sighting) 76 | } 77 | 78 | func addAttribute(key: Bytes, value: Bytes) { 79 | let sighting = BML(name: key, value: value, parent: self) 80 | add(attribute: sighting) 81 | } 82 | 83 | func add(child: BML) { 84 | child.parent = self 85 | children.append(child) 86 | } 87 | 88 | func add(attribute: BML) { 89 | attributes.append(attribute) 90 | } 91 | } 92 | 93 | extension BML { 94 | public func children(named name: String) -> [BML] { 95 | let name = name.bytes 96 | return children.filter { 97 | $0._name == name 98 | } 99 | } 100 | 101 | public func firstChild(named name: String) -> BML? { 102 | let name = name.bytes 103 | 104 | for child in children { 105 | if child._name == name { 106 | return child 107 | } 108 | } 109 | 110 | return nil 111 | } 112 | 113 | public func attributes(named name: String) -> [BML] { 114 | let name = name.bytes 115 | return children.filter { 116 | $0._name == name 117 | } 118 | } 119 | 120 | public func firstAttribute(named name: String) -> BML? { 121 | let name = name.bytes 122 | 123 | for attribute in attributes { 124 | if attribute._name == name { 125 | return attribute 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | } 132 | 133 | extension BML { 134 | public subscript(_ name: String) -> BML? { 135 | return firstChild(named: name) 136 | } 137 | } 138 | 139 | extension BML { 140 | /*func makeNode() -> Node { 141 | // String 142 | if let text = text?.fasterString, children.count == 0 { 143 | return .string(text) 144 | } 145 | 146 | // Array 147 | if children.count == 1, text == nil, children[0].value.count > 1 { 148 | return .array( 149 | children[0].value.map { 150 | $0.makeNode() 151 | } 152 | ) 153 | } 154 | 155 | // Object 156 | var object = Node.object([:]) 157 | 158 | if let text = text?.fasterString { 159 | object["text"] = .string(text) 160 | } 161 | 162 | children.forEach { 163 | let subObject: Node 164 | if $0.value.count == 1 { 165 | subObject = $0.value[0].makeNode() 166 | } else { 167 | subObject = .array( 168 | $0.value.map { 169 | $0.makeNode() 170 | } 171 | ) 172 | } 173 | object[$0.key.fasterString] = subObject 174 | } 175 | 176 | return object 177 | }*/ 178 | } 179 | -------------------------------------------------------------------------------- /Sources/Parser.swift: -------------------------------------------------------------------------------- 1 | import Core 2 | import Node 3 | 4 | /// \n, \t, ' ' or \r 5 | let whitespace: Set = [.newLine, .horizontalTab, .space, .carriageReturn] 6 | 7 | /// =, \n, \t, ' ' or \r 8 | let attributeTerminators = whitespace.union([.equals]) 9 | 10 | /// ", \n, \t, ' ' or \r 11 | let attributeValueTerminators = whitespace.union([.quote]) 12 | 13 | /// >, /, \n, \t, ' ' or \r 14 | let tagNameTerminators = whitespace.union([.greaterThan, .forwardSlash]) 15 | 16 | /// 20 | let cDataFooter: Bytes = [.rightSquareBracket, .rightSquareBracket, .greaterThan] 21 | 22 | /// !-- 23 | let commentHeader: Bytes = [.exclamationPoint, .hyphen, .hyphen] 24 | 25 | /// --> 26 | let commentFooter: Bytes = [.hyphen, .hyphen, .greaterThan] 27 | 28 | /// 32 | let selfClosingTagFooter: Bytes = [.forwardSlash, .greaterThan] 33 | 34 | extension Byte { 35 | /// < 36 | public static let lessThan: Byte = 0x3C 37 | 38 | /// > 39 | public static let greaterThan: Byte = 0x3E 40 | 41 | /// ! 42 | public static let exclamationPoint: Byte = 0x21 43 | 44 | /// T 45 | public static let T: Byte = 0x54 46 | } 47 | 48 | extension Byte { 49 | var hex: String { 50 | return "0x" + String(self, radix: 16).uppercased() 51 | } 52 | } 53 | 54 | public struct XMLParser { 55 | public enum Error: Swift.Error { 56 | case malformedXML(String) 57 | case unexpectedEndOfFile 58 | } 59 | 60 | var scanner: Scanner 61 | 62 | init(scanner: Scanner) { 63 | self.scanner = scanner 64 | } 65 | } 66 | 67 | extension XMLParser { 68 | public static func parse(_ xml: String) throws -> BML { 69 | return try parse(xml.bytes) 70 | } 71 | 72 | public static func parse(_ bytes: Bytes) throws -> BML { 73 | var parser = XMLParser(scanner: Scanner(bytes)) 74 | 75 | parser.trimByteOrderMark() 76 | 77 | guard let root = try parser.extractTag() else { 78 | throw Error.malformedXML("Expected root element.") 79 | } 80 | 81 | return root 82 | } 83 | 84 | mutating func trimByteOrderMark() { 85 | if 86 | scanner.peek() == 0xEF, 87 | scanner.peek(aheadBy: 1) == 0xBB, 88 | scanner.peek(aheadBy: 2) == 0xBF 89 | { 90 | scanner.pop(3) 91 | } 92 | } 93 | } 94 | 95 | extension XMLParser { 96 | //TODO(Brett): merge this functionality with extractObject 97 | mutating func extractTag() throws -> BML? { 98 | skipWhitespace() 99 | 100 | guard scanner.peek() != nil else { 101 | return nil 102 | } 103 | 104 | guard scanner.pop() == .lessThan else { 105 | throw Error.malformedXML("Expected tag") 106 | } 107 | 108 | guard let byte = scanner.peek() else { 109 | throw Error.unexpectedEndOfFile 110 | } 111 | 112 | switch byte { 113 | // Header 114 | case Byte.questionMark: 115 | extractHeader() 116 | return try extractTag() 117 | 118 | // Comment 119 | case Byte.exclamationPoint: 120 | try eatComment() 121 | return try extractTag() 122 | 123 | // Object 124 | default: 125 | return try extractObject() 126 | } 127 | } 128 | } 129 | 130 | extension XMLParser { 131 | mutating func extractHeader() { 132 | assert(scanner.peek() == .questionMark) 133 | scanner.pop() 134 | 135 | skip(until: .questionMark) 136 | if scanner.peek(aheadBy: 1) == .greaterThan { 137 | scanner.pop(2) 138 | } else { 139 | scanner.pop() 140 | } 141 | } 142 | 143 | mutating func extractObject() throws -> BML { 144 | // TODO(Brett): 145 | // I shouldn't have to do this, figure out which state is throwing 146 | // the pointer into a garbage state 147 | if scanner.peek() == .lessThan { 148 | scanner.pop() 149 | } 150 | 151 | let name = consume(until: tagNameTerminators) 152 | 153 | let sighting = BML(name: name) 154 | 155 | sighting.attributes = try extractAttributes() 156 | 157 | skipWhitespace() 158 | 159 | guard let token = scanner.peek() else { 160 | throw Error.unexpectedEndOfFile 161 | } 162 | 163 | // self-closing tag early-escape 164 | guard token != .forwardSlash else { 165 | try expect(selfClosingTagFooter) 166 | return sighting 167 | } 168 | 169 | guard token == .greaterThan else { 170 | throw Error.malformedXML("Expected `>` for tag: \(name.makeString())") 171 | } 172 | 173 | // > 174 | scanner.pop() 175 | 176 | outerLoop: while scanner.peek() != nil { 177 | skipWhitespace() 178 | 179 | guard let byte = scanner.peek() else { 180 | continue 181 | } 182 | 183 | switch byte { 184 | case Byte.lessThan: 185 | // closing tag 186 | 187 | switch scanner.peek(aheadBy: 1) { 188 | case Byte.forwardSlash?: 189 | if try isClosingTag(name) { 190 | break outerLoop 191 | } else { 192 | throw Error.malformedXML("Expected closing tag ") 193 | } 194 | 195 | case Byte.exclamationPoint?: 196 | switch scanner.peek(aheadBy: 2) { 197 | case Byte.hyphen?: 198 | try eatComment() 199 | 200 | case Byte.leftSquareBracket?: 201 | sighting._value = try extractCData() 202 | 203 | default: 204 | throw Error.malformedXML("Expected comment or CDATA near ! in tag \(name.fasterString)") 205 | } 206 | 207 | default: 208 | let subObject = try extractObject() 209 | sighting.add(child: subObject) 210 | } 211 | 212 | default: 213 | sighting._value = extractTagText() 214 | } 215 | } 216 | 217 | return sighting 218 | } 219 | 220 | mutating func extractTagText() -> Bytes { 221 | skipWhitespace() 222 | return consume(until: .lessThan) 223 | } 224 | 225 | mutating func extractCData() throws -> Bytes { 226 | try expect(cDataHeader) 227 | 228 | var cdata: [Byte] = [] 229 | 230 | while let byte = scanner.peek() { 231 | if byte == .rightSquareBracket { 232 | if find(cDataFooter) { 233 | // consume footer 234 | try expect(cDataFooter) 235 | break 236 | } 237 | } 238 | 239 | scanner.pop() 240 | cdata.append(byte) 241 | } 242 | 243 | return cdata 244 | } 245 | 246 | mutating func extractAttributes() throws -> [BML] { 247 | var attributes: [BML] = [] 248 | 249 | while let byte = scanner.peek(), byte != .greaterThan, byte != .forwardSlash { 250 | skipWhitespace() 251 | 252 | guard scanner.peek() != .greaterThan, scanner.peek() != .forwardSlash else { 253 | continue 254 | } 255 | 256 | let name = consume(until: attributeTerminators) 257 | let value = try extractAttributeValue() 258 | attributes.append(BML(name: name, value: value)) 259 | } 260 | 261 | return attributes 262 | } 263 | 264 | mutating func extractAttributeValue() throws -> Bytes { 265 | skip(until: .quote) 266 | 267 | try expect([.quote]) 268 | skipWhitespace() 269 | 270 | let value = consume(until: .quote) 271 | 272 | try expect([.quote]) 273 | 274 | return value 275 | } 276 | } 277 | 278 | extension XMLParser { 279 | mutating func isClosingTag(_ expectedTagName: Bytes) throws -> Bool { 280 | try expect(closingTagHeader) 281 | 282 | let tagName = consume(until: tagNameTerminators) 283 | 284 | skipWhitespace() 285 | try expect([.greaterThan]) 286 | 287 | return tagName.elementsEqual(expectedTagName) 288 | } 289 | 290 | mutating func eatComment() throws { 291 | try expect(commentHeader) 292 | 293 | while scanner.peek() != nil { 294 | skip(until: .hyphen) 295 | 296 | guard !find(commentFooter) else { 297 | // consume 298 | try expect(commentFooter) 299 | break 300 | } 301 | } 302 | } 303 | } 304 | 305 | extension XMLParser { 306 | mutating func find(_ bytes: Bytes, startingFrom start: Int = 0) -> Bool { 307 | var peeked = start 308 | 309 | for expect in bytes { 310 | guard scanner.peek(aheadBy: peeked) == expect else { 311 | return false 312 | } 313 | 314 | peeked += 1 315 | } 316 | 317 | return true 318 | } 319 | 320 | mutating func expect(_ bytes: Bytes) throws { 321 | for expected in bytes { 322 | guard scanner.peek() == expected else { 323 | throw Error.malformedXML("Expected \(bytes.fasterString)") 324 | } 325 | 326 | scanner.pop() 327 | } 328 | } 329 | } 330 | 331 | extension XMLParser { 332 | mutating func skip(until terminator: Byte) { 333 | while let byte = scanner.peek(), byte != terminator { 334 | scanner.pop() 335 | } 336 | } 337 | 338 | mutating func skip(until terminators: Set) { 339 | while let byte = scanner.peek(), !terminators.contains(byte) { 340 | scanner.pop() 341 | } 342 | } 343 | 344 | mutating func skip(while allowed: Set) { 345 | while let byte = scanner.peek(), allowed.contains(byte) { 346 | scanner.pop() 347 | } 348 | } 349 | mutating func skip(while closure: (Byte) -> Bool) { 350 | while let byte = scanner.peek(), closure(byte) { 351 | scanner.pop() 352 | } 353 | } 354 | 355 | mutating func skipWhitespace() { 356 | skip(while: whitespace) 357 | } 358 | 359 | mutating func consume(while closure: (Byte) -> Bool) -> Bytes { 360 | var consumed: [Byte] = [] 361 | 362 | while let byte = scanner.peek(), closure(byte) { 363 | consumed.append(byte) 364 | scanner.pop() 365 | } 366 | 367 | return consumed 368 | } 369 | 370 | mutating func consume(until terminator: Byte) -> Bytes { 371 | var consumed: [Byte] = [] 372 | 373 | while let byte = scanner.peek(), byte != terminator { 374 | consumed.append(byte) 375 | scanner.pop() 376 | } 377 | 378 | return consumed 379 | } 380 | 381 | mutating func consume(until terminators: Set) -> Bytes { 382 | var consumed: [Byte] = [] 383 | 384 | while let byte = scanner.peek(), !terminators.contains(byte) { 385 | consumed.append(byte) 386 | scanner.pop() 387 | } 388 | 389 | return consumed 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /Tests/BMLTests/BMLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import Node 4 | import Foundation 5 | 6 | @testable import BML 7 | 8 | class BMLTests: XCTestCase { 9 | static var allTests = [ 10 | ("testStringBasic", testStringBasic), 11 | ("testArrayBasic", testArrayBasic), 12 | ("testObjectBasic", testObjectBasic), 13 | ("testObjectUTF8", testObjectUTF8), 14 | ("testUTF8ByteOrderMark", testUTF8ByteOrderMark), 15 | ("testObjectEmbedded", testObjectEmbedded), 16 | ("testSelfClosing", testSelfClosing), 17 | ("testSelfClosingWithAttributes", testSelfClosingWithAttributes), 18 | ("testSelfClosingEmbedded", testSelfClosingEmbedded), 19 | ("testCDATA", testCDATA), 20 | ("testHugeXML", testHugeXML), 21 | ] 22 | 23 | func testStringBasic() { 24 | do { 25 | let result = try XMLParser.parse("Brett Toomey") 26 | 27 | result.expect(BML( 28 | name: "author", 29 | value: "Brett Toomey" 30 | )) 31 | } catch { 32 | XCTFail("Parser failed: \(error)") 33 | } 34 | } 35 | 36 | func testArrayBasic() { 37 | do { 38 | let result = try XMLParser.parse( 39 | "\n" + 40 | " A friend\n" + 41 | " Another friend\n" + 42 | " Third friend\n" + 43 | "" 44 | ) 45 | 46 | result.expect(BML( 47 | name: "friends", 48 | children: [ 49 | BML(name: "person", value: "A friend"), 50 | BML(name: "person", value: "Another friend"), 51 | BML(name: "person", value: "Third friend"), 52 | ] 53 | )) 54 | } catch { 55 | XCTFail("Parser failed: \(error)") 56 | } 57 | } 58 | 59 | func testObjectBasic() { 60 | do { 61 | let result = try XMLParser.parse( 62 | "" 63 | ) 64 | 65 | result.expect(BML( 66 | name: "book", 67 | attributes: [ 68 | BML(name: "id", value: "5") 69 | ] 70 | )) 71 | } catch { 72 | XCTFail("Parser failed: \(error)") 73 | } 74 | } 75 | 76 | func testObjectUTF8() { 77 | do { 78 | let result = try XMLParser.parse( 79 | "\n" + 80 | "\n" + 81 | "<俄语 լեզու=\"ռուսերեն\" another=\"value\">данные" 82 | ) 83 | 84 | result.expect(BML( 85 | name: "俄语", 86 | value: "данные", 87 | attributes: [ 88 | BML(name: "լեզու", value: "ռուսերեն"), 89 | BML(name: "another", value: "value") 90 | ] 91 | )) 92 | } catch { 93 | XCTFail("Parser failed: \(error)") 94 | } 95 | } 96 | 97 | func testUTF8ByteOrderMark() { 98 | do { 99 | let result = try XMLParser.parse("\u{FEFF}") 100 | result.expect(BML(name: "Book")) 101 | } catch { 102 | XCTFail("Parser failed: \(error)") 103 | } 104 | } 105 | 106 | func testObjectEmbedded() { 107 | do { 108 | let result = try XMLParser.parse( 109 | "\n" + 110 | " Die To Live Another Day\n" + 111 | " \n" + 112 | " James Blonde\n" + 113 | " 007\n" + 114 | " \n" + 115 | "" 116 | ) 117 | 118 | result.expect(BML( 119 | name: "Movie", 120 | attributes: [BML(name: "id", value: "1337")], 121 | children: [ 122 | BML(name: "Title", value: "Die To Live Another Day"), 123 | BML( 124 | name: "Director", 125 | children: [ 126 | BML(name: "Name", value: "James Blonde"), 127 | BML(name: "Age", value: "007") 128 | ] 129 | ) 130 | ] 131 | )) 132 | } catch { 133 | XCTFail("Parser failed: \(error)") 134 | } 135 | } 136 | 137 | func testSelfClosing() { 138 | do { 139 | let result = try XMLParser.parse("") 140 | result.expect(BML(name: "node")) 141 | } catch { 142 | XCTFail("Parser failed: \(error)") 143 | } 144 | } 145 | 146 | func testSelfClosingWithAttributes() { 147 | do { 148 | let result = try XMLParser.parse("") 149 | result.expect( 150 | BML( 151 | name: "node", 152 | attributes: [BML(name: "foo", value: "bar")] 153 | ) 154 | ) 155 | } catch { 156 | XCTFail("Parser failed: \(error)") 157 | } 158 | } 159 | 160 | func testSelfClosingEmbedded() { 161 | do { 162 | let result = try XMLParser.parse( 163 | "" + 164 | " " + 165 | "" 166 | ) 167 | 168 | result.expect(BML( 169 | name: "author", 170 | attributes: [ 171 | BML(name: "firstName", value: "Brett"), 172 | BML(name: "lastName", value: "Toomey") 173 | ], 174 | children: [ 175 | BML( 176 | name: "img", 177 | attributes: [BML(name: "url", value: "myimg.jpg")] 178 | ) 179 | ] 180 | )) 181 | } catch { 182 | XCTFail("Parser failed: \(error)") 183 | } 184 | } 185 | 186 | func testSelfClosingWhitespaceAfterAttributes() { 187 | do { 188 | let result = try XMLParser.parse("") 189 | result.expect( 190 | BML( 191 | name: "Book", 192 | attributes: [BML(name: "id", value: "1")] 193 | ) 194 | ) 195 | } catch { 196 | XCTFail("Parser failed: \(error)") 197 | } 198 | } 199 | 200 | func testCDATA() { 201 | do { 202 | let result = try XMLParser.parse("") 203 | result.expect(BML( 204 | name: "summary", 205 | value: "My text" 206 | )) 207 | } catch { 208 | XCTFail("Parser failed: \(error)") 209 | } 210 | } 211 | 212 | func testHugeXML() { 213 | let literal = 214 | "\n" + 215 | "\n" + 216 | " \n" + 217 | " Gambardella, Matthew\n" + 218 | " XML Developer's Guide\n" + 219 | " Computer\n" + 220 | " 44.95\n" + 221 | " 2000-10-01\n" + 222 | " An in-depth look at creating applications \n" + 223 | " with XML.\n" + 224 | " \n" + 225 | " \n" + 226 | " Ralls, Kim\n" + 227 | " Midnight Rain\n" + 228 | " Fantasy\n" + 229 | " 5.95\n" + 230 | " 2000-12-16\n" + 231 | " A former architect battles corporate zombies, \n" + 232 | " an evil sorceress, and her own childhood to become queen \n" + 233 | " of the world.\n" + 234 | " \n" + 235 | " \n" + 236 | " Corets, Eva\n" + 237 | " Maeve Ascendant\n" + 238 | " Fantasy\n" + 239 | " 5.95\n" + 240 | " 2000-11-17\n" + 241 | " After the collapse of a nanotechnology \n" + 242 | " society in England, the young survivors lay the \n" + 243 | " foundation for a new society.\n" + 244 | " \n" + 245 | " \n" + 246 | " Corets, Eva\n" + 247 | " Oberon's Legacy\n" + 248 | " Fantasy\n" + 249 | " 5.95\n" + 250 | " 2001-03-10\n" + 251 | " In post-apocalypse England, the mysterious \n" + 252 | " agent known only as Oberon helps to create a new life \n" + 253 | " for the inhabitants of London. Sequel to Maeve \n" + 254 | " Ascendant.\n" + 255 | " \n" + 256 | " \n" + 257 | " Corets, Eva\n" + 258 | " The Sundered Grail\n" + 259 | " Fantasy\n" + 260 | " 5.95\n" + 261 | " 2001-09-10\n" + 262 | " The two daughters of Maeve, half-sisters, \n" + 263 | " battle one another for control of England. Sequel to \n" + 264 | " Oberon's Legacy.\n" + 265 | " \n" + 266 | " \n" + 267 | " Randall, Cynthia\n" + 268 | " Lover Birds\n" + 269 | " Romance\n" + 270 | " 4.95\n" + 271 | " 2000-09-02\n" + 272 | " When Carla meets Paul at an ornithology \n" + 273 | " conference, tempers fly as feathers get ruffled.\n" + 274 | " \n" + 275 | " \n" + 276 | " Thurman, Paula\n" + 277 | " Splish Splash\n" + 278 | " Romance\n" + 279 | " 4.95\n" + 280 | " 2000-11-02\n" + 281 | " A deep sea diver finds true love twenty \n" + 282 | " thousand leagues beneath the sea.\n" + 283 | " \n" + 284 | " \n" + 285 | " Knorr, Stefan\n" + 286 | " Creepy Crawlies\n" + 287 | " Horror\n" + 288 | " 4.95\n" + 289 | " 2000-12-06\n" + 290 | " An anthology of horror stories about roaches,\n" + 291 | " centipedes, scorpions and other insects.\n" + 292 | " \n" + 293 | " \n" + 294 | " Kress, Peter\n" + 295 | " Paradox Lost\n" + 296 | " Science Fiction\n" + 297 | " 6.95\n" + 298 | " 2000-11-02\n" + 299 | " After an inadvertant trip through a Heisenberg\n" + 300 | " Uncertainty Device, James Salway discovers the problems \n" + 301 | " of being quantum.\n" + 302 | " \n" + 303 | " \n" + 304 | " O'Brien, Tim\n" + 305 | " Microsoft .NET: The Programming Bible\n" + 306 | " Computer\n" + 307 | " 36.95\n" + 308 | " 2000-12-09\n" + 309 | " Microsoft's .NET initiative is explored in \n" + 310 | " detail in this deep programmer's reference.\n" + 311 | " \n" + 312 | " \n" + 313 | " O'Brien, Tim\n" + 314 | " MSXML3: A Comprehensive Guide\n" + 315 | " Computer\n" + 316 | " 36.95\n" + 317 | " 2000-12-01\n" + 318 | " The Microsoft MSXML3 parser is covered in \n" + 319 | " detail, with attention to XML DOM interfaces, XSLT processing, \n" + 320 | " SAX and more.\n" + 321 | " \n" + 322 | " \n" + 323 | " Galos, Mike\n" + 324 | " Visual Studio 7: A Comprehensive Guide\n" + 325 | " Computer\n" + 326 | " 49.95\n" + 327 | " 2001-04-16\n" + 328 | " Microsoft Visual Studio 7 is explored in depth,\n" + 329 | " looking at how Visual Basic, Visual C++, C#, and ASP+ are \n" + 330 | " integrated into a comprehensive development \n" + 331 | " environment.\n" + 332 | " \n" + 333 | "" 334 | measure { 335 | _ = try! XMLParser.parse(literal) 336 | } 337 | } 338 | } 339 | 340 | extension BML { 341 | func expect(_ expected: BML, file: StaticString = #file, line: UInt = #line) { 342 | XCTAssertEqual(name, expected.name, file: file, line: line) 343 | XCTAssertEqual(value, expected.value, file: file, line: line) 344 | 345 | if attributes.count == expected.attributes.count { 346 | for (i, expectedAttribute) in expected.attributes.enumerated() { 347 | attributes[i].expect(expectedAttribute, file: file, line: line) 348 | } 349 | } else { 350 | XCTFail( 351 | "Attribute count for \(name) and \(expected.name) don't match (\(attributes.count) != \(expected.attributes.count))", 352 | file: file, line: line 353 | ) 354 | } 355 | 356 | if children.count == expected.children.count { 357 | for (i, expectedChild) in expected.children.enumerated() { 358 | children[i].expect(expectedChild, file: file, line: line) 359 | } 360 | } else { 361 | XCTFail( 362 | "Children count for \(name) and \(expected.name) don't match (\(children.count) != \(expected.children.count))", 363 | file: file, line: line 364 | ) 365 | } 366 | 367 | } 368 | } 369 | --------------------------------------------------------------------------------