Create OPMLParserTests.
This commit is contained in:
parent
f63af89e31
commit
4349dd26ff
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParser"
|
||||||
|
BuildableName = "OPMLParser"
|
||||||
|
BlueprintName = "OPMLParser"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParser"
|
||||||
|
BuildableName = "OPMLParser"
|
||||||
|
BlueprintName = "OPMLParser"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParserTests"
|
||||||
|
BuildableName = "OPMLParserTests"
|
||||||
|
BlueprintName = "OPMLParserTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParser"
|
||||||
|
BuildableName = "OPMLParser"
|
||||||
|
BlueprintName = "OPMLParser"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "Parser"
|
||||||
|
BuildableName = "Parser"
|
||||||
|
BlueprintName = "Parser"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "SAX"
|
||||||
|
BuildableName = "SAX"
|
||||||
|
BlueprintName = "SAX"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParserTests"
|
||||||
|
BuildableName = "OPMLParserTests"
|
||||||
|
BlueprintName = "OPMLParserTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ParserTests"
|
||||||
|
BuildableName = "ParserTests"
|
||||||
|
BlueprintName = "ParserTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "OPMLParser"
|
||||||
|
BuildableName = "OPMLParser"
|
||||||
|
BlueprintName = "OPMLParser"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1530"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "SAX"
|
||||||
|
BuildableName = "SAX"
|
||||||
|
BlueprintName = "SAX"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "SAX"
|
||||||
|
BuildableName = "SAX"
|
||||||
|
BlueprintName = "SAX"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -15,13 +15,26 @@ let package = Package(
|
|||||||
.library(
|
.library(
|
||||||
name: "SAX",
|
name: "SAX",
|
||||||
type: .dynamic,
|
type: .dynamic,
|
||||||
targets: ["SAX"])
|
targets: ["SAX"]),
|
||||||
|
.library(
|
||||||
|
name: "OPMLParser",
|
||||||
|
type: .dynamic,
|
||||||
|
targets: ["OPMLParser"])
|
||||||
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
|
.target(
|
||||||
|
name: "OPMLParser",
|
||||||
|
dependencies: [
|
||||||
|
"SAX"
|
||||||
|
],
|
||||||
|
swiftSettings: [
|
||||||
|
.enableExperimentalFeature("StrictConcurrency")
|
||||||
|
]),
|
||||||
.target(
|
.target(
|
||||||
name: "Parser",
|
name: "Parser",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@ -41,6 +54,10 @@ let package = Package(
|
|||||||
dependencies: ["Parser"],
|
dependencies: ["Parser"],
|
||||||
exclude: ["Info.plist"],
|
exclude: ["Info.plist"],
|
||||||
resources: [.copy("Resources")]),
|
resources: [.copy("Resources")]),
|
||||||
|
.testTarget(
|
||||||
|
name: "OPMLParserTests",
|
||||||
|
dependencies: ["OPMLParser"],
|
||||||
|
resources: [.copy("Resources")]),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SAX
|
||||||
|
|
||||||
public struct OPMLFeedSpecifier: Sendable {
|
public struct OPMLFeedSpecifier: Sendable {
|
||||||
|
|
@ -27,8 +27,9 @@ public class OPMLItem {
|
|||||||
|
|
||||||
if let feedURL = attributes?.opml_xmlUrl {
|
if let feedURL = attributes?.opml_xmlUrl {
|
||||||
self.feedSpecifier = OPMLFeedSpecifier(title: self.titleFromAttributes, feedDescription: attributes?.opml_description, homePageURL: attributes?.opml_htmlUrl, feedURL: feedURL)
|
self.feedSpecifier = OPMLFeedSpecifier(title: self.titleFromAttributes, feedDescription: attributes?.opml_description, homePageURL: attributes?.opml_htmlUrl, feedURL: feedURL)
|
||||||
|
} else {
|
||||||
|
self.feedSpecifier = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(_ item: OPMLItem) {
|
func add(_ item: OPMLItem) {
|
@ -6,13 +6,16 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SAX
|
||||||
|
|
||||||
public final class OPMLParser {
|
public final class OPMLParser {
|
||||||
|
|
||||||
private let url: String
|
private let parserData: ParserData
|
||||||
private let data: Data
|
private var data: Data {
|
||||||
|
parserData.data
|
||||||
|
}
|
||||||
|
|
||||||
private let opmlDocument: OPMLDocument
|
private var opmlDocument: OPMLDocument?
|
||||||
|
|
||||||
private var itemStack = [OPMLItem]()
|
private var itemStack = [OPMLItem]()
|
||||||
private var currentItem: OPMLItem? {
|
private var currentItem: OPMLItem? {
|
||||||
@ -28,26 +31,26 @@ public final class OPMLParser {
|
|||||||
public static func document(with parserData: ParserData) -> OPMLDocument? {
|
public static func document(with parserData: ParserData) -> OPMLDocument? {
|
||||||
|
|
||||||
let opmlParser = OPMLParser(parserData)
|
let opmlParser = OPMLParser(parserData)
|
||||||
return opmlParser.parse()
|
opmlParser.parse()
|
||||||
|
return opmlParser.opmlDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ parserData: ParserData) {
|
init(_ parserData: ParserData) {
|
||||||
|
|
||||||
self.url = parserData.url
|
self.parserData = parserData
|
||||||
self.data = parserData.data
|
|
||||||
self.opmlDocument = OPMLDocument(url: parserData.url)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension OPMLParser {
|
private extension OPMLParser {
|
||||||
|
|
||||||
func parse() -> OPMLDocument? {
|
func parse() {
|
||||||
|
|
||||||
guard canParseData() else {
|
guard canParseData() else {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pushItem(opmlDocument)
|
opmlDocument = OPMLDocument(url: parserData.url)
|
||||||
|
push(opmlDocument!)
|
||||||
|
|
||||||
let saxParser = SAXParser(delegate: self, data: data)
|
let saxParser = SAXParser(delegate: self, data: data)
|
||||||
saxParser.parse()
|
saxParser.parse()
|
||||||
@ -70,20 +73,20 @@ private extension OPMLParser {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
itemStack.dropLast()
|
_ = itemStack.dropLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension OPMLParser: SAXParserDelegate {
|
extension OPMLParser: SAXParserDelegate {
|
||||||
|
|
||||||
func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer<XMLPointer?>?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer<XMLPointer?>?) {
|
public func saxParser(_ saxParser: SAXParser, xmlStartElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer<XMLPointer?>?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer<XMLPointer?>?) {
|
||||||
|
|
||||||
if SAXEqualStrings(localName, XMLKey.title) {
|
if SAXEqualTags(localName, XMLKey.title) {
|
||||||
saxParser.beginStoringCharacters()
|
saxParser.beginStoringCharacters()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !SAXEqualStrings(localName, XMLKey.outline) {
|
if !SAXEqualTags(localName, XMLKey.outline) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,21 +97,21 @@ extension OPMLParser: SAXParserDelegate {
|
|||||||
push(item)
|
push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) {
|
public func saxParser(_ saxParser: SAXParser, xmlEndElement localName: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?) {
|
||||||
|
|
||||||
if SAXEqualStrings(localname, XMLKey.title) {
|
if SAXEqualTags(localName, XMLKey.title) {
|
||||||
if let item = currentItem as? OPMLDocument {
|
if let item = currentItem as? OPMLDocument {
|
||||||
item.title = saxParser.currentStringWithTrimmedWhitespace
|
item.title = saxParser.currentStringWithTrimmedWhitespace
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if SAXEqualStrings(localName, XMLKey.outline) {
|
if SAXEqualTags(localName, XMLKey.outline) {
|
||||||
popItem()
|
popItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int) {
|
public func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int) {
|
||||||
|
|
||||||
// Nothing to do, but method is required.
|
// Nothing to do, but method is required.
|
||||||
}
|
}
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Data {
|
public extension Data {
|
||||||
|
|
||||||
/// Return true if the data contains a given String.
|
/// Return true if the data contains a given String.
|
||||||
///
|
///
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension Dictionary where Key == String, Value == String {
|
public extension Dictionary where Key == String, Value == String {
|
||||||
|
|
||||||
func object(forCaseInsensitiveKey key: String) -> String? {
|
func object(forCaseInsensitiveKey key: String) -> String? {
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension String {
|
public extension String {
|
||||||
|
|
||||||
var nilIfEmptyOrWhitespace: String? {
|
var nilIfEmptyOrWhitespace: String? {
|
||||||
return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
|
return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
|
||||||
|
@ -9,7 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
public struct HTMLTag: Sendable {
|
public struct HTMLTag: Sendable {
|
||||||
|
|
||||||
public enum TagType {
|
public enum TagType: Sendable {
|
||||||
case link
|
case link
|
||||||
case meta
|
case meta
|
||||||
}
|
}
|
@ -9,8 +9,8 @@ import Foundation
|
|||||||
|
|
||||||
public struct ParserData: Sendable {
|
public struct ParserData: Sendable {
|
||||||
|
|
||||||
let url: String
|
public let url: String
|
||||||
let data: Data
|
public let data: Data
|
||||||
|
|
||||||
public init(url: String, data: Data) {
|
public init(url: String, data: Data) {
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import libxml2
|
import libxml2
|
||||||
|
|
||||||
typealias XMLPointer = UnsafePointer<xmlChar>
|
public typealias XMLPointer = UnsafePointer<xmlChar>
|
||||||
|
|
||||||
protocol SAXParserDelegate {
|
public protocol SAXParserDelegate {
|
||||||
|
|
||||||
func saxParser(_: SAXParser, xmlStartElement: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer<XMLPointer?>?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer<XMLPointer?>?)
|
func saxParser(_: SAXParser, xmlStartElement: XMLPointer, prefix: XMLPointer?, uri: XMLPointer?, namespaceCount: Int, namespaces: UnsafePointer<XMLPointer?>?, attributeCount: Int, attributesDefaultedCount: Int, attributes: UnsafePointer<XMLPointer?>?)
|
||||||
|
|
||||||
@ -19,11 +19,11 @@ protocol SAXParserDelegate {
|
|||||||
func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int)
|
func saxParser(_: SAXParser, xmlCharactersFound: XMLPointer, count: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SAXParser {
|
public final class SAXParser {
|
||||||
|
|
||||||
fileprivate let delegate: SAXParserDelegate
|
fileprivate let delegate: SAXParserDelegate
|
||||||
|
|
||||||
var currentCharacters: Data? { // UTF-8 encoded
|
public var currentCharacters: Data? { // UTF-8 encoded
|
||||||
|
|
||||||
guard storingCharacters else {
|
guard storingCharacters else {
|
||||||
return nil
|
return nil
|
||||||
@ -33,7 +33,7 @@ final class SAXParser {
|
|||||||
|
|
||||||
// Conveniences to get string version of currentCharacters
|
// Conveniences to get string version of currentCharacters
|
||||||
|
|
||||||
var currentString: String? {
|
public var currentString: String? {
|
||||||
|
|
||||||
guard let d = currentCharacters, !d.isEmpty else {
|
guard let d = currentCharacters, !d.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
@ -41,7 +41,7 @@ final class SAXParser {
|
|||||||
return String(data: d, encoding: .utf8)
|
return String(data: d, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentStringWithTrimmedWhitespace: String? {
|
public var currentStringWithTrimmedWhitespace: String? {
|
||||||
|
|
||||||
guard let s = currentString else {
|
guard let s = currentString else {
|
||||||
return nil
|
return nil
|
||||||
@ -53,13 +53,13 @@ final class SAXParser {
|
|||||||
private var storingCharacters = false
|
private var storingCharacters = false
|
||||||
private var characters = Data()
|
private var characters = Data()
|
||||||
|
|
||||||
init(delegate: SAXParserDelegate, data: Data) {
|
public init(delegate: SAXParserDelegate, data: Data) {
|
||||||
|
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
self.data = data
|
self.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse() {
|
public func parse() {
|
||||||
|
|
||||||
guard !data.isEmpty else {
|
guard !data.isEmpty else {
|
||||||
return
|
return
|
||||||
@ -69,7 +69,7 @@ final class SAXParser {
|
|||||||
xmlCtxtUseOptions(context, Int32(XML_PARSE_RECOVER.rawValue | XML_PARSE_NOENT.rawValue))
|
xmlCtxtUseOptions(context, Int32(XML_PARSE_RECOVER.rawValue | XML_PARSE_NOENT.rawValue))
|
||||||
|
|
||||||
data.withUnsafeBytes { bufferPointer in
|
data.withUnsafeBytes { bufferPointer in
|
||||||
if let bytes = bufferPointer.bindMemory(to: xmlChar.self).baseAddress {
|
if let bytes = bufferPointer.bindMemory(to: CChar.self).baseAddress {
|
||||||
xmlParseChunk(context, bytes, Int32(data.count), 0)
|
xmlParseChunk(context, bytes, Int32(data.count), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@ final class SAXParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Delegate can call from xmlStartElement. Characters will be available in xmlEndElement as currentCharacters property. Storing characters is stopped after each xmlEndElement.
|
/// Delegate can call from xmlStartElement. Characters will be available in xmlEndElement as currentCharacters property. Storing characters is stopped after each xmlEndElement.
|
||||||
func beginStoringCharacters() {
|
public func beginStoringCharacters() {
|
||||||
|
|
||||||
storingCharacters = true
|
storingCharacters = true
|
||||||
characters.count = 0
|
characters.count = 0
|
||||||
@ -91,7 +91,7 @@ final class SAXParser {
|
|||||||
characters.count = 0
|
characters.count = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func attributesDictionary(_ attributes: UnsafePointer<XMLPointer?>?, attributeCount: Int) -> [String: String]? {
|
public func attributesDictionary(_ attributes: UnsafePointer<XMLPointer?>?, attributeCount: Int) -> [String: String]? {
|
||||||
|
|
||||||
guard attributeCount > 0, let attributes else {
|
guard attributeCount > 0, let attributes else {
|
||||||
return nil
|
return nil
|
||||||
@ -154,7 +154,7 @@ private extension SAXParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startElement(_ context: UnsafeMutableRawPointer?, name: XMLPointer?, prefix: XMLPointer?, URI: XMLPointer?, nb_namespaces: CInt, namespaces: UnsafePointer<XMLPointer?>?, nb_attributes: CInt, nb_defaulted: CInt, attributes: UnsafeMutablePointer<XMLPointer?>?) {
|
private func startElement(_ context: UnsafeMutableRawPointer?, name: XMLPointer?, prefix: XMLPointer?, URI: XMLPointer?, nb_namespaces: CInt, namespaces: UnsafeMutablePointer<XMLPointer?>?, nb_attributes: CInt, nb_defaulted: CInt, attributes: UnsafeMutablePointer<XMLPointer?>?) {
|
||||||
|
|
||||||
guard let context, let name else {
|
guard let context, let name else {
|
||||||
return
|
return
|
||||||
@ -194,8 +194,9 @@ nonisolated(unsafe) private var saxHandlerStruct: xmlSAXHandler = {
|
|||||||
var handler = xmlSAXHandler()
|
var handler = xmlSAXHandler()
|
||||||
|
|
||||||
handler.characters = charactersFound
|
handler.characters = charactersFound
|
||||||
handler.startElement = startElement
|
handler.startElementNs = startElement
|
||||||
handler.endElement = endElement
|
handler.endElementNs = endElement
|
||||||
|
handler.initialized = XML_SAX2_MAGIC
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
}()
|
}()
|
||||||
|
@ -8,11 +8,26 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import libxml2
|
import libxml2
|
||||||
|
|
||||||
func SAXEqualStrings(_ s1: XMLPointer, _ s2: XMLPointer, length: Int? = nil) -> Bool {
|
public func SAXEqualTags(_ localName: XMLPointer, _ tag: ContiguousArray<Int8>) -> Bool {
|
||||||
|
|
||||||
if let length {
|
return tag.withUnsafeBufferPointer { bufferPointer in
|
||||||
return xmlStrncmp(s1, s2, Int32(length)) == 0
|
|
||||||
|
let tagCount = tag.count
|
||||||
|
|
||||||
|
for i in 0..<tagCount {
|
||||||
|
|
||||||
|
let localNameCharacter = localName[i]
|
||||||
|
if localNameCharacter == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let tagCharacter = UInt8(tag[i])
|
||||||
|
if localNameCharacter != tagCharacter {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// localName might actually be longer — make sure it’s the same length as tag.
|
||||||
|
return localName[tagCount] == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return xmlStrEqual(s1, s2) != 0
|
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
import Parser
|
import SAX
|
||||||
import ParserObjC
|
@testable import OPMLParser
|
||||||
|
|
||||||
class OPMLTests: XCTestCase {
|
class OPMLTests: XCTestCase {
|
||||||
|
|
||||||
@ -18,21 +18,23 @@ class OPMLTests: XCTestCase {
|
|||||||
|
|
||||||
// 0.002 sec on my 2012 iMac.
|
// 0.002 sec on my 2012 iMac.
|
||||||
self.measure {
|
self.measure {
|
||||||
let _ = try! RSOPMLParser.parseOPML(with: self.subsData)
|
let _ = OPMLParser.document(with: self.subsData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testNotOPML() {
|
func testNotOPML() {
|
||||||
|
|
||||||
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
let d = parserData("DaringFireball", "rss", "http://daringfireball.net/")
|
||||||
XCTAssertThrowsError(try RSOPMLParser.parseOPML(with: d))
|
XCTAssertNil(OPMLParser.document(with: d))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSubsStructure() {
|
func testSubsStructure() {
|
||||||
let opmlDocument = try! RSOPMLParser.parseOPML(with: subsData)
|
let opmlDocument = OPMLParser.document(with: subsData)
|
||||||
XCTAssertEqual("Subs", opmlDocument.title)
|
XCTAssertNotNil(opmlDocument)
|
||||||
XCTAssertEqual("http://example.org/", opmlDocument.url)
|
|
||||||
recursivelyCheckOPMLStructure(opmlDocument)
|
XCTAssertEqual("Subs", opmlDocument!.title)
|
||||||
|
XCTAssertEqual("http://example.org/", opmlDocument!.url)
|
||||||
|
recursivelyCheckOPMLStructure(opmlDocument!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -42,23 +44,23 @@ class OPMLTests: XCTestCase {
|
|||||||
// which appears to be true with OPML generated by The Old Reader.
|
// which appears to be true with OPML generated by The Old Reader.
|
||||||
|
|
||||||
let d = parserData("SubsNoTitleAttributes", "opml", "http://example.org/")
|
let d = parserData("SubsNoTitleAttributes", "opml", "http://example.org/")
|
||||||
let opmlDocument = try! RSOPMLParser.parseOPML(with: d)
|
let opmlDocument = OPMLParser.document(with: d)
|
||||||
recursivelyCheckOPMLStructure(opmlDocument)
|
recursivelyCheckOPMLStructure(opmlDocument!)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension OPMLTests {
|
private extension OPMLTests {
|
||||||
|
|
||||||
func recursivelyCheckOPMLStructure(_ item: RSOPMLItem) {
|
func recursivelyCheckOPMLStructure(_ item: OPMLItem) {
|
||||||
let feedSpecifier = item.feedSpecifier
|
let feedSpecifier = item.feedSpecifier
|
||||||
if !(item is RSOPMLDocument) {
|
if !(item is OPMLDocument) {
|
||||||
XCTAssertNotNil((item.attributes! as NSDictionary).opml_text)
|
XCTAssertNotNil(item.attributes!.opml_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it has no children, it should have a feed specifier. The converse is also true.
|
// If it has no children, it should have a feed specifier. The converse is also true.
|
||||||
var isFolder = item.children != nil && item.children!.count > 0
|
var isFolder = item.items != nil && item.items!.count > 0
|
||||||
if !isFolder && (item.attributes! as NSDictionary).opml_title == "Skip" {
|
if !isFolder && item.attributes?.opml_title == "Skip" {
|
||||||
isFolder = true
|
isFolder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,10 +72,17 @@ private extension OPMLTests {
|
|||||||
XCTAssertNil(feedSpecifier)
|
XCTAssertNil(feedSpecifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.children != nil && item.children!.count > 0 {
|
if item.items != nil && item.items!.count > 0 {
|
||||||
for oneItem in item.children! {
|
for oneItem in item.items! {
|
||||||
recursivelyCheckOPMLStructure(oneItem)
|
recursivelyCheckOPMLStructure(oneItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parserData(_ filename: String, _ fileExtension: String, _ url: String) -> ParserData {
|
||||||
|
let filename = "Resources/\(filename)"
|
||||||
|
let path = Bundle.module.path(forResource: filename, ofType: fileExtension)!
|
||||||
|
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
|
||||||
|
return ParserData(url: url, data: data)
|
||||||
|
}
|
2278
Modules/Parser/Tests/OPMLParserTests/Resources/DaringFireball.rss
Executable file
2278
Modules/Parser/Tests/OPMLParserTests/Resources/DaringFireball.rss
Executable file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user