NetNewsWire/Modules/Parser/Sources/OPMLParser/OPMLParser.swift

120 lines
2.5 KiB
Swift
Raw Normal View History

//
// OPMLParser.swift
//
//
// Created by Brent Simmons on 8/18/24.
//
import Foundation
2024-08-27 07:39:46 +02:00
import SAX
public final class OPMLParser {
2024-08-27 07:39:46 +02:00
private let parserData: ParserData
private var data: Data {
parserData.data
}
2024-08-27 07:39:46 +02:00
private var opmlDocument: OPMLDocument?
private var itemStack = [OPMLItem]()
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
}
/// Returns nil if data cant be parsed (if its 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
}
init(_ parserData: ParserData) {
2024-08-27 07:39:46 +02:00
self.parserData = parserData
}
}
private extension OPMLParser {
2024-08-27 07:39:46 +02:00
func parse() {
guard canParseData() else {
2024-08-27 07:39:46 +02:00
return
}
2024-08-27 07:39:46 +02:00
opmlDocument = OPMLDocument(url: parserData.url)
push(opmlDocument!)
let saxParser = SAXParser(delegate: self, data: data)
saxParser.parse()
}
func canParseData() -> Bool {
data.containsASCIIString("<opml")
}
2024-08-27 05:03:58 +02:00
func push(_ item: OPMLItem) {
itemStack.append(item)
}
func popItem() {
guard itemStack.count > 0 else {
assertionFailure("itemStack.count must be > 0")
2024-08-27 05:03:58 +02:00
return
}
2024-08-27 07:39:46 +02:00
_ = itemStack.dropLast()
}
}
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-27 05:03:58 +02:00
currentItem?.add(item)
push(item)
}
2024-08-27 07:39:46 +02:00
public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) {
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
}
2024-08-27 08:02:22 +02:00
saxParser.endStoringCharacters()
2024-08-27 05:03:58 +02:00
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-27 07:39:46 +02:00
public func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int) {
2024-08-27 05:03:58 +02:00
// Nothing to do, but method is required.
}
}