-
Notifications
You must be signed in to change notification settings - Fork 104
/
XMLStackParser.swift
165 lines (139 loc) · 4.9 KB
/
XMLStackParser.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//
// XMLStackParser.swift
// XMLCoder
//
// Created by Shawn Moore on 11/14/17.
// Copyright © 2017 Shawn Moore. All rights reserved.
//
import Foundation
class XMLStackParser: NSObject {
var root: XMLCoderElement?
private var stack: [XMLCoderElement] = []
private let trimValueWhitespaces: Bool
init(trimValueWhitespaces: Bool = true) {
self.trimValueWhitespaces = trimValueWhitespaces
super.init()
}
static func parse(
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
trimValueWhitespaces: Bool
) throws -> KeyedBox {
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces)
let node = try parser.parse(
with: data,
errorContextLength: length,
shouldProcessNamespaces: shouldProcessNamespaces
)
return node.transformToBoxTree()
}
func parse(
with data: Data,
errorContextLength: UInt,
shouldProcessNamespaces: Bool
) throws -> XMLCoderElement {
let xmlParser = XMLParser(data: data)
xmlParser.shouldProcessNamespaces = shouldProcessNamespaces
xmlParser.delegate = self
guard !xmlParser.parse(), root == nil else {
return root!
}
guard let error = xmlParser.parserError else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data could not be parsed into XML."
))
}
// `lineNumber` isn't 0-indexed, so 0 is an invalid value for context
guard errorContextLength > 0 && xmlParser.lineNumber > 0 else {
throw error
}
let string = String(data: data, encoding: .utf8) ?? ""
let lines = string.split(separator: "\n")
var errorPosition = 0
let offset = Int(errorContextLength / 2)
for i in 0..<xmlParser.lineNumber - 1 {
errorPosition += lines[i].count
}
errorPosition += xmlParser.columnNumber
var lowerBoundIndex = 0
if errorPosition - offset > 0 {
lowerBoundIndex = errorPosition - offset
}
var upperBoundIndex = string.count
if errorPosition + offset < string.count {
upperBoundIndex = errorPosition + offset
}
#if compiler(>=5.0)
let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string)
let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string)
#else
let lowerBound = String.Index(encodedOffset: lowerBoundIndex)
let upperBound = String.Index(encodedOffset: upperBoundIndex)
#endif
let context = string[lowerBound..<upperBound]
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: """
\(error.localizedDescription) \
at line \(xmlParser.lineNumber), column \(xmlParser.columnNumber):
`\(context)`
""",
underlyingError: error
))
}
func withCurrentElement(_ body: (inout XMLCoderElement) throws -> ()) rethrows {
guard !stack.isEmpty else {
return
}
try body(&stack[stack.count - 1])
}
/// Trim whitespaces for a given string if needed.
func process(string: String) -> String {
return trimValueWhitespaces
? string.trimmingCharacters(in: .whitespacesAndNewlines)
: string
}
}
extension XMLStackParser: XMLParserDelegate {
func parserDidStartDocument(_: XMLParser) {
root = nil
stack = []
}
func parser(_: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName: String?,
attributes attributeDict: [String: String] = [:]) {
let element = XMLCoderElement(key: elementName, attributes: attributeDict)
stack.append(element)
}
func parser(_: XMLParser,
didEndElement _: String,
namespaceURI _: String?,
qualifiedName _: String?) {
guard let element = stack.popLast() else {
return
}
withCurrentElement { currentElement in
currentElement.append(element: element, forKey: element.key)
}
if stack.isEmpty {
root = element
}
}
func parser(_: XMLParser, foundCharacters string: String) {
withCurrentElement { currentElement in
currentElement.append(value: process(string: string))
}
}
func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
guard let string = String(data: CDATABlock, encoding: .utf8) else {
return
}
withCurrentElement { currentElement in
currentElement.append(value: process(string: string))
}
}
}