Skip to content

Commit

Permalink
refactor: more breaking out of files
Browse files Browse the repository at this point in the history
  • Loading branch information
drmohundro committed May 3, 2022
1 parent 8c6b0ce commit 9ee0729
Show file tree
Hide file tree
Showing 9 changed files with 472 additions and 278 deletions.
90 changes: 70 additions & 20 deletions SWXMLHash.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

File renamed without changes.
43 changes: 43 additions & 0 deletions Source/Errors/ParsingError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// ParsingError.swift
// SWXMLHash
//
// Copyright (c) 2022 David Mohundro
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

public struct ParsingError: Error {
public let line: Int
public let column: Int
}

extension ParsingError: CustomStringConvertible {
public var description: String {
"There was a parsing error on line: \(line), column: \(column)"
}
}

extension ParsingError: LocalizedError {
public var errorDescription: String? {
"There was a parsing error on line: \(line), column: \(column)"
}
}
File renamed without changes.
107 changes: 107 additions & 0 deletions Source/FullXMLParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// FullXMLParser.swift
// SWXMLHash
//
// Copyright (c) 2014 David Mohundro
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

/// The implementation of XMLParserDelegate and where the parsing actually happens.
class FullXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
required init(_ options: XMLHashOptions) {
root = XMLElement(name: rootElementName, options: options)
self.options = options
super.init()
}

let root: XMLElement
var parentStack = Stack<XMLElement>()
let options: XMLHashOptions
var parsingError: ParsingError?

func parse(_ data: Data) -> XMLIndexer {
// clear any prior runs of parse... expected that this won't be necessary,
// but you never know
parentStack.removeAll()

parentStack.push(root)

let parser = XMLParser(data: data)
parser.shouldProcessNamespaces = options.shouldProcessNamespaces
parser.delegate = self
_ = parser.parse()

if options.detectParsingErrors, let err = parsingError {
return XMLIndexer.parsingError(err)
} else {
return XMLIndexer(root)
}
}

func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String]) {
let currentNode = parentStack
.top()
.addElement(elementName, withAttributes: attributeDict, caseInsensitive: self.options.caseInsensitive)

parentStack.push(currentNode)
}

func parser(_ parser: XMLParser, foundCharacters string: String) {
let current = parentStack.top()

current.addText(string)
}

func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
parentStack.drop()
}

func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
if let cdataText = String(data: CDATABlock, encoding: String.Encoding.utf8) {
let current = parentStack.top()

current.addText(cdataText)
}
}

func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
#if os(Linux) && !swift(>=4.1.50)
if let err = parseError as? NSError {
parsingError = ParsingError(
line: err.userInfo["NSXMLParserErrorLineNumber"] as? Int ?? 0,
column: err.userInfo["NSXMLParserErrorColumn"] as? Int ?? 0)
}
#else
let err = parseError as NSError
parsingError = ParsingError(
line: err.userInfo["NSXMLParserErrorLineNumber"] as? Int ?? 0,
column: err.userInfo["NSXMLParserErrorColumn"] as? Int ?? 0)
#endif
}
}
79 changes: 79 additions & 0 deletions Source/IndexOp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// IndexOp.swift
// SWXMLHash
//
// Copyright (c) 2022 David Mohundro
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

/// Represents an indexed operation against a lazily parsed `XMLIndexer`
public class IndexOp {
var index: Int
let key: String

init(_ key: String) {
self.key = key
self.index = -1
}

func toString() -> String {
if index >= 0 {
return key + " " + index.description
}

return key
}
}

/// Represents a collection of `IndexOp` instances. Provides a means of iterating them
/// to find a match in a lazily parsed `XMLIndexer` instance.
public class IndexOps {
var ops: [IndexOp] = []

let parser: LazyXMLParser

init(parser: LazyXMLParser) {
self.parser = parser
}

func findElements() -> XMLIndexer {
parser.startParsing(ops)
let indexer = XMLIndexer(parser.root)
var childIndex = indexer
for oper in ops {
childIndex = childIndex[oper.key]
if oper.index >= 0 {
childIndex = childIndex[oper.index]
}
}
ops.removeAll(keepingCapacity: false)
return childIndex
}

func stringify() -> String {
var ret = ""
for oper in ops {
ret += "[" + oper.toString() + "]"
}
return ret
}
}
123 changes: 123 additions & 0 deletions Source/LazyXMLParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// LazyXMLParser.swift
// SWXMLHash
//
// Copyright (c) 2022 David Mohundro
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

/// The implementation of XMLParserDelegate and where the lazy parsing actually happens.
class LazyXMLParser: NSObject, SimpleXmlParser, XMLParserDelegate {
required init(_ options: XMLHashOptions) {
root = XMLElement(name: rootElementName, options: options)
self.options = options
super.init()
}

var root: XMLElement
var parentStack = Stack<XMLElement>()
var elementStack = Stack<String>()

var data: Data?
var ops: [IndexOp] = []
let options: XMLHashOptions

func parse(_ data: Data) -> XMLIndexer {
self.data = data
return XMLIndexer(self)
}

func startParsing(_ ops: [IndexOp]) {
// reset state for a new lazy parsing run
root = XMLElement(name: rootElementName, options: root.options)
parentStack.removeAll()
parentStack.push(root)

self.ops = ops
let parser = XMLParser(data: data!)
parser.shouldProcessNamespaces = options.shouldProcessNamespaces
parser.delegate = self
_ = parser.parse()
}

func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String]) {
elementStack.push(elementName)

if !onMatch() {
return
}

let currentNode = parentStack
.top()
.addElement(elementName, withAttributes: attributeDict, caseInsensitive: self.options.caseInsensitive)
parentStack.push(currentNode)
}

func parser(_ parser: XMLParser, foundCharacters string: String) {
if !onMatch() {
return
}

let current = parentStack.top()

current.addText(string)
}

func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
if !onMatch() {
return
}

if let cdataText = String(data: CDATABlock, encoding: String.Encoding.utf8) {
let current = parentStack.top()

current.addText(cdataText)
}
}

func parser(_ parser: XMLParser,
didEndElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?) {
let match = onMatch()

elementStack.drop()

if match {
parentStack.drop()
}
}

func onMatch() -> Bool {
// we typically want to compare against the elementStack to see if it matches ops, *but*
// if we're on the first element, we'll instead compare the other direction.
if elementStack.items.count > ops.count {
return elementStack.items.starts(with: ops.map { $0.key })
} else {
return ops.map { $0.key }.starts(with: elementStack.items)
}
}
}
Loading

0 comments on commit 9ee0729

Please sign in to comment.