forked from CoreOffice/XMLCoder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
XMLStackParser.swift
200 lines (171 loc) · 6.31 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Shawn Moore on 11/14/17.
//
import Foundation
#if canImport(FoundationXML)
import FoundationXML
#endif
class XMLStackParser: NSObject {
var root: XMLCoderElement?
private var stack: [XMLCoderElement] = []
private let trimValueWhitespaces: Bool
private let removeWhitespaceElements: Bool
init(trimValueWhitespaces: Bool = true, removeWhitespaceElements: Bool = false) {
self.trimValueWhitespaces = trimValueWhitespaces
self.removeWhitespaceElements = removeWhitespaceElements
super.init()
}
static func parse(
with data: Data,
errorContextLength length: UInt,
shouldProcessNamespaces: Bool,
trimValueWhitespaces: Bool,
removeWhitespaceElements: Bool
) throws -> Box {
let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces,
removeWhitespaceElements: removeWhitespaceElements)
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
}
let lowerBound = String.Index(utf16Offset: lowerBoundIndex, in: string)
let upperBound = String.Index(utf16Offset: upperBoundIndex, in: string)
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 attributes = attributeDict.map { key, value in
XMLCoderElement.Attribute(key: key, value: value)
}
let element = XMLCoderElement(key: elementName, attributes: attributes)
stack.append(element)
}
func parser(_: XMLParser,
didEndElement _: String,
namespaceURI _: String?,
qualifiedName _: String?)
{
guard let element = stack.popLast() else {
return
}
let updatedElement = removeWhitespaceElements ? elementWithFilteredElements(element: element) : element
withCurrentElement { currentElement in
currentElement.append(element: updatedElement, forKey: updatedElement.key)
}
if stack.isEmpty {
root = updatedElement
}
}
func elementWithFilteredElements(element: XMLCoderElement) -> XMLCoderElement {
var hasWhitespaceElements: Bool = false
var hasNonWhitespaceElements: Bool = false
var filteredElements: [XMLCoderElement] = []
for ele in element.elements {
if ele.isWhitespaceWithNoElements() {
hasWhitespaceElements = true
} else {
hasNonWhitespaceElements = true
filteredElements.append(ele)
}
}
let updatedElement: XMLCoderElement
if hasWhitespaceElements && hasNonWhitespaceElements {
updatedElement = XMLCoderElement(key: element.key, elements: filteredElements, attributes: element.attributes)
} else {
updatedElement = element
}
return updatedElement
}
func parser(_: XMLParser, foundCharacters string: String) {
let processedString = process(string: string)
guard processedString.count > 0, string.count != 0 else {
return
}
withCurrentElement { currentElement in
currentElement.append(string: processedString)
}
}
func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
guard let string = String(data: CDATABlock, encoding: .utf8) else {
return
}
withCurrentElement { currentElement in
currentElement.append(cdata: string)
}
}
}