2024-08-19 07:07:17 +02:00
|
|
|
|
//
|
|
|
|
|
// OPMLParser.swift
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 8/18/24.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2024-08-27 07:39:46 +02:00
|
|
|
|
import SAX
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
|
|
|
|
public final class OPMLParser {
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
private let parserData: ParserData
|
|
|
|
|
private var data: Data {
|
|
|
|
|
parserData.data
|
|
|
|
|
}
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
private var opmlDocument: OPMLDocument?
|
2024-08-26 07:00:27 +02:00
|
|
|
|
|
2024-08-19 07:07:17 +02:00
|
|
|
|
private var itemStack = [OPMLItem]()
|
2024-08-26 07:00:27 +02:00
|
|
|
|
private var currentItem: OPMLItem? {
|
|
|
|
|
itemStack.last
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 05:03:58 +02:00
|
|
|
|
struct XMLKey {
|
|
|
|
|
static let title = "title".utf8CString
|
|
|
|
|
static let outline = "outline".utf8CString
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
/// Returns nil if data can’t be parsed (if it’s not OPML).
|
|
|
|
|
public static func document(with parserData: ParserData) -> OPMLDocument? {
|
|
|
|
|
|
|
|
|
|
let opmlParser = OPMLParser(parserData)
|
2024-08-27 07:39:46 +02:00
|
|
|
|
opmlParser.parse()
|
|
|
|
|
return opmlParser.opmlDocument
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
init(_ parserData: ParserData) {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
self.parserData = parserData
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
2024-08-26 07:00:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension OPMLParser {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
func parse() {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
|
|
|
|
guard canParseData() else {
|
2024-08-27 07:39:46 +02:00
|
|
|
|
return
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
opmlDocument = OPMLDocument(url: parserData.url)
|
|
|
|
|
push(opmlDocument!)
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
let saxParser = SAXParser(delegate: self, data: data)
|
|
|
|
|
saxParser.parse()
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
func canParseData() -> Bool {
|
|
|
|
|
|
|
|
|
|
data.containsASCIIString("<opml")
|
|
|
|
|
}
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 05:03:58 +02:00
|
|
|
|
func push(_ item: OPMLItem) {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
itemStack.append(item)
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
func popItem() {
|
|
|
|
|
|
|
|
|
|
guard itemStack.count > 0 else {
|
|
|
|
|
assertionFailure("itemStack.count must be > 0")
|
2024-08-27 05:03:58 +02:00
|
|
|
|
return
|
2024-08-26 07:00:27 +02:00
|
|
|
|
}
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
_ = itemStack.dropLast()
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
2024-08-26 07:00:27 +02:00
|
|
|
|
}
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-26 07:00:27 +02:00
|
|
|
|
extension OPMLParser: SAXParserDelegate {
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
public func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer<XMLPointer?>?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer<XMLPointer?>?) {
|
2024-08-27 05:03:58 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
if SAXEqualTags(localName, XMLKey.title) {
|
2024-08-27 05:03:58 +02:00
|
|
|
|
saxParser.beginStoringCharacters()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
if !SAXEqualTags(localName, XMLKey.outline) {
|
2024-08-27 05:03:58 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let attributesDictionary = saxParser.attributesDictionary(attributes, attributeCount: attributeCount)
|
|
|
|
|
let item = OPMLItem(attributes: attributesDictionary)
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 05:03:58 +02:00
|
|
|
|
currentItem?.add(item)
|
|
|
|
|
push(item)
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
if SAXEqualTags(localName, XMLKey.title) {
|
2024-08-27 05:03:58 +02:00
|
|
|
|
if let item = currentItem as? OPMLDocument {
|
|
|
|
|
item.title = saxParser.currentStringWithTrimmedWhitespace
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
if SAXEqualTags(localName, XMLKey.outline) {
|
2024-08-27 05:03:58 +02:00
|
|
|
|
popItem()
|
|
|
|
|
}
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-08-27 07:39:46 +02:00
|
|
|
|
public func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int) {
|
2024-08-19 07:07:17 +02:00
|
|
|
|
|
2024-08-27 05:03:58 +02:00
|
|
|
|
// Nothing to do, but method is required.
|
2024-08-19 07:07:17 +02:00
|
|
|
|
}
|
|
|
|
|
}
|