// // OPMLParser.swift // // // Created by Brent Simmons on 8/18/24. // import Foundation import SAX public final class OPMLParser { private let parserData: ParserData private var data: Data { parserData.data } private var opmlDocument: OPMLDocument? private var itemStack = [OPMLItem]() private var currentItem: OPMLItem? { itemStack.last } struct XMLKey { static let title = "title".utf8CString static let outline = "outline".utf8CString } /// 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) opmlParser.parse() return opmlParser.opmlDocument } init(_ parserData: ParserData) { self.parserData = parserData } } private extension OPMLParser { func parse() { guard canParseData() else { return } opmlDocument = OPMLDocument(url: parserData.url) push(opmlDocument!) let saxParser = SAXParser(delegate: self, data: data) saxParser.parse() } func canParseData() -> Bool { data.containsASCIIString(" 0 else { assertionFailure("itemStack.count must be > 0") return } _ = itemStack.dropLast() } } extension OPMLParser: SAXParserDelegate { public func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer?) { if SAXEqualTags(localName, XMLKey.title) { saxParser.beginStoringCharacters() return } if !SAXEqualTags(localName, XMLKey.outline) { return } let attributesDictionary = saxParser.attributesDictionary(attributes, attributeCount: attributeCount) let item = OPMLItem(attributes: attributesDictionary) currentItem?.add(item) push(item) } public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) { if SAXEqualTags(localName, XMLKey.title) { if let item = currentItem as? OPMLDocument { item.title = saxParser.currentStringWithTrimmedWhitespace } return } if SAXEqualTags(localName, XMLKey.outline) { popItem() } } public func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int) { // Nothing to do, but method is required. } }