Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up XMLElement, ArrayBox & DictionaryBox #28

Merged
merged 5 commits into from
Dec 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 91 additions & 77 deletions Sources/XMLCoder/Auxiliaries/XMLElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,111 +7,125 @@

import Foundation

class _XMLElement {
struct _XMLElement {
static let attributesKey = "___ATTRIBUTES"
static let escapedCharacterSet = [("&", "&amp"), ("<", "&lt;"), (">", "&gt;"), ("'", "&apos;"), ("\"", "&quot;")]

var key: String
var value: String?
var attributes: [String: String] = [:]
var children: [String: [_XMLElement]] = [:]
var elements: [String: [_XMLElement]] = [:]

init(key: String, value: String? = nil, attributes: [String: String] = [:], children: [String: [_XMLElement]] = [:]) {
init(key: String, value: String? = nil, attributes: [String: String] = [:], elements: [String: [_XMLElement]] = [:]) {
self.key = key
self.value = value
self.attributes = attributes
self.children = children
self.elements = elements
}

static func createRootElement(rootKey: String, object: UnkeyedBox) -> _XMLElement? {
let element = _XMLElement(key: rootKey)
init(key: String, box: UnkeyedBox) {
self.init(key: key)

_XMLElement.createElement(parentElement: element, key: rootKey, object: object)

return element
}

static func createRootElement(rootKey: String, object: KeyedBox) -> _XMLElement? {
let element = _XMLElement(key: rootKey)

_XMLElement.modifyElement(element: element, parentElement: nil, key: nil, object: object)

return element
self.elements[key] = box.map { box in
_XMLElement(key: key, box: box)
}
}

fileprivate static func modifyElement(element: _XMLElement, parentElement: _XMLElement?, key: String?, object: KeyedBox) {
let attributesBox = object[_XMLElement.attributesKey] as? KeyedBox
let uniqueAttributes: [(String, String)]? = attributesBox?.unbox().compactMap { key, box in
return box.xmlString().map { (key, $0) }
}
element.attributes = uniqueAttributes.map { Dictionary(uniqueKeysWithValues: $0) } ?? [:]

let objects = object.filter { key, _value in key != _XMLElement.attributesKey }
init(key: String, box: KeyedBox) {
self.init(key: key)

for (key, box) in objects {
_XMLElement.createElement(parentElement: element, key: key, object: box)
self.attributes = Dictionary(uniqueKeysWithValues: box.attributes.compactMap { key, box in
guard let value = box.xmlString() else {
return nil
}
return (key, value)
})

let elementsByKey: [(String, [_XMLElement])] = box.elements.map { key, box in
switch box {
case let unkeyedBox as UnkeyedBox:
// This basically injects the unkeyed children directly into self:
let elements = unkeyedBox.map { _XMLElement(key: key, box: $0) }
return (key, elements)
case let keyedBox as KeyedBox:
let elements = [_XMLElement(key: key, box: keyedBox)]
return (key, elements)
case let simpleBox as SimpleBox:
let elements = [_XMLElement(key: key, box: simpleBox)]
return (key, elements)
case let box:
preconditionFailure("Unclassified box: \(type(of: box))")
}
}

if let parentElement = parentElement, let key = key {
parentElement.children[key] = (parentElement.children[key] ?? []) + [element]
self.elements = Dictionary(elementsByKey) { existingElements, newElements in
existingElements + newElements
}
}

fileprivate static func createElement(parentElement: _XMLElement, key: String, object: Box) {
switch object {
case let box as UnkeyedBox:
for box in box.unbox() {
_XMLElement.createElement(parentElement: parentElement, key: key, object: box)
}
case let box as KeyedBox:
modifyElement(element: _XMLElement(key: key), parentElement: parentElement, key: key, object: box)
case _:
let element = _XMLElement(key: key, value: object.xmlString())
parentElement.children[key, default: []].append(element)
init(key: String, box: SimpleBox) {
self.init(key: key)
self.value = box.xmlString()
}

init(key: String, box: Box) {
switch box {
case let unkeyedBox as UnkeyedBox:
self.init(key: key, box: unkeyedBox)
case let keyedBox as KeyedBox:
self.init(key: key, box: keyedBox)
case let simpleBox as SimpleBox:
self.init(key: key, box: simpleBox)
case let box:
preconditionFailure("Unclassified box: \(type(of: box))")
}

}

func append(value string: String) {
mutating func append(value string: String) {
var value = self.value ?? ""
value += string.trimmingCharacters(in: .whitespacesAndNewlines)
self.value = value
}

func flatten() -> [String: Box] {
var node: [String: Box] = attributes.mapValues { StringBox($0) }
mutating func append(element: _XMLElement, forKey key: String) {
self.elements[key, default: []].append(element)
}

func flatten() -> KeyedBox {
let attributes = self.attributes.mapValues { StringBox($0) }

for childElement in children {
for child in childElement.value {
var elements: [String: Box] = [:]
for (key, value) in self.elements {
for child in value {
if let content = child.value {
if let oldContent = node[childElement.key] as? UnkeyedBox {
oldContent.append(StringBox(content))
// FIXME: Box is a reference type, so this shouldn't be necessary:
node[childElement.key] = oldContent
} else if let oldContent = node[childElement.key] {
node[childElement.key] = UnkeyedBox([oldContent, StringBox(content)])
} else {
node[childElement.key] = StringBox(content)
switch elements[key] {
case let unkeyedBox as UnkeyedBox:
var boxes = unkeyedBox.unbox()
boxes.append(StringBox(content))
elements[key] = UnkeyedBox(boxes)
case let keyedBox as StringBox:
elements[key] = UnkeyedBox([keyedBox, StringBox(content)])
case _:
elements[key] = StringBox(content)
}
} else if !child.children.isEmpty || !child.attributes.isEmpty {
let newValue = child.flatten()

if let existingValue = node[childElement.key] {
if let unkeyed = existingValue as? UnkeyedBox {
unkeyed.append(KeyedBox(newValue))
// FIXME: Box is a reference type, so this shouldn't be necessary:
node[childElement.key] = unkeyed
} else if !child.elements.isEmpty || !child.attributes.isEmpty {
let content = child.flatten()
if let existingValue = elements[key] {
if let unkeyedBox = existingValue as? UnkeyedBox {
var boxes = unkeyedBox.unbox()
boxes.append(content)
elements[key] = UnkeyedBox(boxes)
} else {
node[childElement.key] = UnkeyedBox([existingValue, KeyedBox(newValue)])
elements[key] = UnkeyedBox([existingValue, content])
}
} else {
node[childElement.key] = KeyedBox(newValue)
elements[key] = content
}
}
}
}
return node

return KeyedBox(elements: elements, attributes: attributes)
}

func toXMLString(with header: XMLHeader? = nil, withCDATA cdata: Bool, formatting: XMLEncoder.OutputFormatting, ignoreEscaping _: Bool = false) -> String {
Expand All @@ -122,20 +136,20 @@ class _XMLElement {
}

fileprivate func formatUnsortedXMLElements(_ string: inout String, _ level: Int, _ cdata: Bool, _ formatting: XMLEncoder.OutputFormatting, _ prettyPrinted: Bool) {
formatXMLElements(from: children.map { (key: $0, value: $1) }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
formatXMLElements(from: elements.map { (key: $0, value: $1) }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
}

fileprivate func elementString(for childElement: (key: String, value: [_XMLElement]), at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) -> String {
fileprivate func elementString(for element: (key: String, value: [_XMLElement]), at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) -> String {
var string = ""
for child in childElement.value {
for child in element.value {
string += child._toXMLString(indented: level + 1, withCDATA: cdata, formatting: formatting)
string += prettyPrinted ? "\n" : ""
}
return string
}

fileprivate func formatSortedXMLElements(_ string: inout String, _ level: Int, _ cdata: Bool, _ formatting: XMLEncoder.OutputFormatting, _ prettyPrinted: Bool) {
formatXMLElements(from: children.sorted { $0.key < $1.key }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
formatXMLElements(from: elements.sorted { $0.key < $1.key }, into: &string, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
}

fileprivate func attributeString(key: String, value: String) -> String {
Expand All @@ -148,9 +162,9 @@ class _XMLElement {
}
}

fileprivate func formatXMLElements(from children: [(key: String, value: [_XMLElement])], into string: inout String, at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) {
for childElement in children {
string += elementString(for: childElement, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
fileprivate func formatXMLElements(from elements: [(key: String, value: [_XMLElement])], into string: inout String, at level: Int, cdata: Bool, formatting: XMLEncoder.OutputFormatting, prettyPrinted: Bool) {
for element in elements {
string += elementString(for: element, at: level, cdata: cdata, formatting: formatting, prettyPrinted: prettyPrinted)
}
}

Expand Down Expand Up @@ -202,7 +216,7 @@ class _XMLElement {
string += "\(value)"
}
string += "</\(key)>"
} else if !children.isEmpty {
} else if !elements.isEmpty {
string += prettyPrinted ? ">\n" : ">"
formatXMLElements(formatting, &string, level, cdata, prettyPrinted)

Expand All @@ -222,9 +236,9 @@ extension _XMLElement: Equatable {
lhs.key == rhs.key,
lhs.value == rhs.value,
lhs.attributes == rhs.attributes,
lhs.children == rhs.children
else {
return false
lhs.elements == rhs.elements
else {
return false
}
return true
}
Expand Down
54 changes: 27 additions & 27 deletions Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

import Foundation

struct XMLElementContext {

}

class _XMLStackParser: NSObject {
var root: _XMLElement? = nil
var stack: [_XMLElement] = []
var currentNode: _XMLElement? = nil
private var stack: [_XMLElement] = []

var currentElementName: String? = nil
var currentElementData = ""

static func parse(with data: Data) throws -> [String: Box] {
static func parse(with data: Data) throws -> KeyedBox {
let parser = _XMLStackParser()

guard let node = try parser.parse(with: data) else {
Expand All @@ -42,6 +42,13 @@ class _XMLStackParser: NSObject {

return root
}

func withCurrentElement(_ body: (inout _XMLElement) throws -> ()) rethrows {
guard !stack.isEmpty else {
return
}
try body(&stack[stack.count - 1])
}
}

extension _XMLStackParser: XMLParserDelegate {
Expand All @@ -51,48 +58,41 @@ extension _XMLStackParser: XMLParserDelegate {
}

func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) {
let node = _XMLElement(key: elementName, attributes: attributeDict)

stack.append(node)

currentNode?.children[elementName, default: []].append(node)

currentNode = node
let element = _XMLElement(key: elementName, attributes: attributeDict)
stack.append(element)
}

func parser(_: XMLParser, didEndElement _: String, namespaceURI _: String?, qualifiedName _: String?) {
guard let poppedNode = stack.popLast() else {
guard var element = stack.popLast() else {
return
}

if let value = poppedNode.value {
poppedNode.value = value.isEmpty ? nil : value
if let value = element.value {
element.value = value.isEmpty ? nil : value
}

withCurrentElement { currentElement in
currentElement.append(element: element, forKey: element.key)
}

if stack.isEmpty {
root = poppedNode
root = element
}

currentNode = stack.last
}

func parser(_: XMLParser, foundCharacters string: String) {
guard let currentNode = currentNode else {
return
withCurrentElement { currentElement in
currentElement.append(value: string)
}

currentNode.append(value: string)
}

func parser(_: XMLParser, foundCDATA CDATABlock: Data) {
guard let string = String(data: CDATABlock, encoding: .utf8) else {
return
}
guard let currentNode = currentNode else {
return
withCurrentElement { currentElement in
currentElement.append(value: string)
}

currentNode.append(value: string)
}

func parser(_: XMLParser, parseErrorOccurred parseError: Error) {
Expand Down
Loading