From be2a5407354c52d50be22bf329637445530e7725 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Tue, 9 May 2023 16:04:22 -0400 Subject: [PATCH 01/12] wip --- Package.swift | 6 +- Sources/OPML/Exporter/Exporter.swift | 17 +++--- Sources/OPML/Models/OPMLEntry.swift | 1 - Tests/OPMLTests/Tests.swift | 88 ++++++++++++++-------------- 4 files changed, 55 insertions(+), 57 deletions(-) diff --git a/Package.swift b/Package.swift index 086f392..5c9bcff 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7 import PackageDescription let package = Package( @@ -15,7 +15,9 @@ let package = Package( .package(name: "Html", url: "https://github.com/pointfreeco/swift-html", from: "0.4.1") ], targets: [ - .target(name: "OPML", dependencies: ["Html"]), + .target(name: "OPML", dependencies: [ + .product(name: "Html", package: "swift-html"), + ]), .testTarget( name: "OPMLTests", dependencies: ["OPML"], diff --git a/Sources/OPML/Exporter/Exporter.swift b/Sources/OPML/Exporter/Exporter.swift index 116a53c..45e9b9d 100644 --- a/Sources/OPML/Exporter/Exporter.swift +++ b/Sources/OPML/Exporter/Exporter.swift @@ -2,17 +2,17 @@ import Foundation import Html public extension OPML { - var xml: String { render(document) } - func xml(indented: Bool) -> String { - if indented { - return debugRender(document, config: Config.pretty) - } - return xml - } + // TODO: Reintroduce indentation when bugs are fixed. Currently doesn't escape attribute values. +// func xml(indented: Bool) -> String { +// if indented { +// return debugRender(document, config: Config.pretty) +// } +// return xml +// } private static let dateFormatter = DateFormatter.iso8601 @@ -53,11 +53,9 @@ public extension OPML { } return .head(children) } - } extension OPMLEntry { - var htmlAttributes: [(String, String)] { var htmlAttributes: [(String, String)] = attributes?.compactMap { guard !$0.value.isEmpty else { return nil } @@ -70,5 +68,4 @@ extension OPMLEntry { var node: Node { .outline(attributes: htmlAttributes, children?.map { $0.node } ?? []) } - } diff --git a/Sources/OPML/Models/OPMLEntry.swift b/Sources/OPML/Models/OPMLEntry.swift index f267179..6612a4a 100644 --- a/Sources/OPML/Models/OPMLEntry.swift +++ b/Sources/OPML/Models/OPMLEntry.swift @@ -44,5 +44,4 @@ public struct OPMLEntry: Codable, Hashable { ] + (attributes ?? []) children = nil } - } diff --git a/Tests/OPMLTests/Tests.swift b/Tests/OPMLTests/Tests.swift index 67c96b3..aa1716c 100644 --- a/Tests/OPMLTests/Tests.swift +++ b/Tests/OPMLTests/Tests.swift @@ -1,44 +1,44 @@ -import XCTest -@testable import OPML - -class Tests: XCTestCase { - - func testOPMLNested() { - guard let file = Bundle.module.url(forResource: "rsparser", withExtension: "opml") else { - XCTFail("Missing opml file") - return - } - - do { - let parser = try OPMLParser(file: file) - let opml = try parser.parse() - XCTAssertEqual(opml.entries.flatMap { $0.children ?? [] }.count, 138) - - let programmingEntries = opml.entries.first(where: { $0.text == "Programming" })?.children - XCTAssertEqual(programmingEntries?.count ?? 0, 33) - } catch { - XCTFail(error.localizedDescription) - } - - } - - func testOPML2() { - guard let file = Bundle.module.url(forResource: "feedly", withExtension: "opml") else { - XCTFail("Missing opml file") - return - } - - do { - let parser = try OPMLParser(file: file) - let opml = try parser.parse() - XCTAssertEqual(opml.entries.count, 39) - } catch { - XCTFail(error.localizedDescription) - } - } - - func testMissingFile() { - XCTAssertThrowsError(try OPMLParser(file: URL(string: "file:///Resources/nonExistentFile.opml")!).parse()) - } - -} +//import XCTest +//@testable import OPML +// +//class Tests: XCTestCase { +// +// func testOPMLNested() { +// guard let file = Bundle.module.url(forResource: "rsparser", withExtension: "opml") else { +// XCTFail("Missing opml file") +// return +// } +// +// do { +// let parser = try OPMLParser(file: file) +// let opml = try parser.parse() +// XCTAssertEqual(opml.entries.flatMap { $0.children ?? [] }.count, 138) +// +// let programmingEntries = opml.entries.first(where: { $0.text == "Programming" })?.children +// XCTAssertEqual(programmingEntries?.count ?? 0, 33) +// } catch { +// XCTFail(error.localizedDescription) +// } +// +// } +// +// func testOPML2() { +// guard let file = Bundle.module.url(forResource: "feedly", withExtension: "opml") else { +// XCTFail("Missing opml file") +// return +// } +// +// do { +// let parser = try OPMLParser(file: file) +// let opml = try parser.parse() +// XCTAssertEqual(opml.entries.count, 39) +// } catch { +// XCTFail(error.localizedDescription) +// } +// } +// +// func testMissingFile() { +// XCTAssertThrowsError(try OPMLParser(file: URL(string: "file:///Resources/nonExistentFile.opml")!).parse()) +// } +// +//} From 20a323eb4309f665d0022fcf87795d00458d453f Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 18 Sep 2023 20:53:07 -0400 Subject: [PATCH 02/12] wip --- Package.swift | 8 +--- Sources/OPML/Transfer/OPML+Transferable.swift | 37 +++++++++++++++++++ Sources/OPML/Transfer/OPMLFile.swift | 29 +++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 Sources/OPML/Transfer/OPML+Transferable.swift create mode 100644 Sources/OPML/Transfer/OPMLFile.swift diff --git a/Package.swift b/Package.swift index 5c9bcff..d8e5df2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,13 +1,9 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 import PackageDescription let package = Package( name: "OPML", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - ], + platforms: [.iOS(.v15), .macOS(.v12)], products: [ .library(name: "OPML", targets: ["OPML"]) ], diff --git a/Sources/OPML/Transfer/OPML+Transferable.swift b/Sources/OPML/Transfer/OPML+Transferable.swift new file mode 100644 index 0000000..f891a6b --- /dev/null +++ b/Sources/OPML/Transfer/OPML+Transferable.swift @@ -0,0 +1,37 @@ +import SwiftUI +import UniformTypeIdentifiers + +@available(iOS 16.0, macOS 13.0, *) +extension OPML: Transferable { + public static var transferRepresentation: some TransferRepresentation { + FileRepresentation(contentType: .fileURL, + shouldAttemptToOpenInPlace: false // url is temporary + ) { opml in + let resultURL = FileManager.default.temporaryDirectory + .appending(component: opml.title ?? UUID().uuidString, directoryHint: .notDirectory) + .appendingPathExtension("opml") + if FileManager.default.fileExists(atPath: resultURL.path(percentEncoded: false)) { + try FileManager.default.removeItem(at: resultURL) + } + let data = opml.xml.data(using: .utf8) ?? Data() + try data.write(to: resultURL, options: [.atomic]) + let sentTransferredFile: SentTransferredFile = .init(resultURL, allowAccessingOriginalFile: true) + return sentTransferredFile + } importing: { opmlFile in + let data: Data = try .init( + contentsOf: opmlFile.file, + options: [.uncached] + ) + return (try? OPML(data)) ?? OPML(entries: []) + } + + DataRepresentation(contentType: .text) { opml in + opml.xml.data(using: .utf8) ?? Data() + } importing: { return (try? OPML($0)) ?? OPML(entries: []) } + .suggestedFileName("ChatOnMac-Packages.opml") + // DataRepresentation(contentType: UTType(exportedAs: "public.opml")) { opml in + // opml.xml(indented: true).data(using: .utf8) ?? Data() + // } importing: { return (try? OPML($0)) ?? OPML(entries: []) } +// .suggestedFileName("ManabiReaderUserFeeds.opml") + } +} diff --git a/Sources/OPML/Transfer/OPMLFile.swift b/Sources/OPML/Transfer/OPMLFile.swift new file mode 100644 index 0000000..6db3b41 --- /dev/null +++ b/Sources/OPML/Transfer/OPMLFile.swift @@ -0,0 +1,29 @@ +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +public struct OPMLFile: FileDocument { + // tell the system we support only plain text + public static var readableContentTypes = [UTType(exportedAs: "public.opml"), UTType.plainText] + + // by default our document is empty + public var text = "" + + // a simple initializer that creates new, empty documents + public init(initialText: String = "") { + text = initialText + } + + // this initializer loads data that has been saved previously + public init(configuration: ReadConfiguration) throws { + if let data = configuration.file.regularFileContents { + text = String(decoding: data, as: UTF8.self) + } + } + + // this will be called when the system wants to write our data to disk + public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + let data = Data(text.utf8) + return FileWrapper(regularFileWithContents: data) + } +} From f082fca86f5b58e40bd37d0c4b4309d3d27c872b Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 18 Sep 2023 21:04:54 -0400 Subject: [PATCH 03/12] wip --- Sources/OPML/Transfer/OPMLFile.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OPML/Transfer/OPMLFile.swift b/Sources/OPML/Transfer/OPMLFile.swift index 6db3b41..ac5a482 100644 --- a/Sources/OPML/Transfer/OPMLFile.swift +++ b/Sources/OPML/Transfer/OPMLFile.swift @@ -10,8 +10,8 @@ public struct OPMLFile: FileDocument { public var text = "" // a simple initializer that creates new, empty documents - public init(initialText: String = "") { - text = initialText + public init(opml: OPML) { + text = opml.xml } // this initializer loads data that has been saved previously From a974152e4b336e8b175799939bf1687de79c9430 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 18 Sep 2023 21:19:55 -0400 Subject: [PATCH 04/12] wip --- Sources/OPML/Transfer/OPMLFile.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/OPML/Transfer/OPMLFile.swift b/Sources/OPML/Transfer/OPMLFile.swift index ac5a482..d98721f 100644 --- a/Sources/OPML/Transfer/OPMLFile.swift +++ b/Sources/OPML/Transfer/OPMLFile.swift @@ -6,12 +6,15 @@ public struct OPMLFile: FileDocument { // tell the system we support only plain text public static var readableContentTypes = [UTType(exportedAs: "public.opml"), UTType.plainText] + public var title = "" + // by default our document is empty public var text = "" // a simple initializer that creates new, empty documents public init(opml: OPML) { text = opml.xml + title = opml.title ?? "" } // this initializer loads data that has been saved previously From 10427f505cb835259e67ef47f898196ebc57f318 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Tue, 19 Sep 2023 09:30:29 -0400 Subject: [PATCH 05/12] wip --- .../OPML/Conveniences/OPMLEntry+Values.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Sources/OPML/Conveniences/OPMLEntry+Values.swift diff --git a/Sources/OPML/Conveniences/OPMLEntry+Values.swift b/Sources/OPML/Conveniences/OPMLEntry+Values.swift new file mode 100644 index 0000000..b4618f6 --- /dev/null +++ b/Sources/OPML/Conveniences/OPMLEntry+Values.swift @@ -0,0 +1,17 @@ +import Foundation + +public extension OPMLEntry { + func attributeBoolValue(_ name: String) -> Bool? { + guard let value = attributes?.first(where: { $0.name == name })?.value else { return nil } + return value == "true" + } + + func attributeStringValue(_ name: String) -> String? { + return attributes?.first(where: { $0.name == name })?.value + } + + func attributeUUIDValue(_ name: String) -> UUID? { + guard let value = attributes?.first(where: { $0.name == name })?.value else { return nil } + return UUID(uuidString: value) + } +} From 4bcc9773b7c68b67def713c2a43ee87072911c46 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Thu, 26 Oct 2023 12:01:49 -0400 Subject: [PATCH 06/12] wip --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index d8e5df2..174622c 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: [ .target(name: "OPML", dependencies: [ .product(name: "Html", package: "swift-html"), - ]), + ])/*, .testTarget( name: "OPMLTests", dependencies: ["OPML"], From 02f5e0691e5030dd45704249117eb096c82fef4d Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Sat, 1 Jun 2024 13:14:35 -0400 Subject: [PATCH 07/12] wip --- Sources/OPML/Transfer/OPML+Transferable.swift | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Sources/OPML/Transfer/OPML+Transferable.swift b/Sources/OPML/Transfer/OPML+Transferable.swift index f891a6b..c77dbe3 100644 --- a/Sources/OPML/Transfer/OPML+Transferable.swift +++ b/Sources/OPML/Transfer/OPML+Transferable.swift @@ -1,37 +1,43 @@ import SwiftUI import UniformTypeIdentifiers +enum TransferError: Error { + case importFailed +} + @available(iOS 16.0, macOS 13.0, *) extension OPML: Transferable { public static var transferRepresentation: some TransferRepresentation { FileRepresentation(contentType: .fileURL, - shouldAttemptToOpenInPlace: false // url is temporary - ) { opml in + shouldAttemptToOpenInPlace: false) { opml in let resultURL = FileManager.default.temporaryDirectory - .appending(component: opml.title ?? UUID().uuidString, directoryHint: .notDirectory) + .appendingPathComponent(opml.title ?? UUID().uuidString) .appendingPathExtension("opml") - if FileManager.default.fileExists(atPath: resultURL.path(percentEncoded: false)) { + if FileManager.default.fileExists(atPath: resultURL.path) { try FileManager.default.removeItem(at: resultURL) } let data = opml.xml.data(using: .utf8) ?? Data() try data.write(to: resultURL, options: [.atomic]) - let sentTransferredFile: SentTransferredFile = .init(resultURL, allowAccessingOriginalFile: true) - return sentTransferredFile + return SentTransferredFile(resultURL, allowAccessingOriginalFile: true) } importing: { opmlFile in - let data: Data = try .init( - contentsOf: opmlFile.file, - options: [.uncached] - ) + let data = try Data(contentsOf: opmlFile.file, options: [.uncached]) + return (try? OPML(data)) ?? OPML(entries: []) + } + + DataRepresentation(contentType: .opml) { opml in + opml.xml.data(using: .utf8) ?? Data() + } importing: { data in return (try? OPML(data)) ?? OPML(entries: []) } - DataRepresentation(contentType: .text) { opml in + DataRepresentation(contentType: .utf8PlainText) { opml in opml.xml.data(using: .utf8) ?? Data() - } importing: { return (try? OPML($0)) ?? OPML(entries: []) } - .suggestedFileName("ChatOnMac-Packages.opml") - // DataRepresentation(contentType: UTType(exportedAs: "public.opml")) { opml in - // opml.xml(indented: true).data(using: .utf8) ?? Data() - // } importing: { return (try? OPML($0)) ?? OPML(entries: []) } -// .suggestedFileName("ManabiReaderUserFeeds.opml") + } importing: { data in + return (try? OPML(data)) ?? OPML(entries: []) + } } } + +extension UTType { + static let opml = UTType(exportedAs: "public.opml", conformingTo: .xml) +} From adbaf6a0cf61d87ab06fd8716b9470789c1ecea2 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 21 Jul 2025 16:54:57 -0400 Subject: [PATCH 08/12] add entry xml --- Sources/OPML/Exporter/Exporter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/OPML/Exporter/Exporter.swift b/Sources/OPML/Exporter/Exporter.swift index 45e9b9d..5aaa0aa 100644 --- a/Sources/OPML/Exporter/Exporter.swift +++ b/Sources/OPML/Exporter/Exporter.swift @@ -69,3 +69,9 @@ extension OPMLEntry { .outline(attributes: htmlAttributes, children?.map { $0.node } ?? []) } } + +public extension OPMLEntry { + var xml: String { + render(node) + } +} From 1aa8208653bd4713b0f3a04859651bb962b156da Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 21 Jul 2025 16:58:37 -0400 Subject: [PATCH 09/12] reenable tests --- Tests/OPMLTests/Tests.swift | 88 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/Tests/OPMLTests/Tests.swift b/Tests/OPMLTests/Tests.swift index aa1716c..67c96b3 100644 --- a/Tests/OPMLTests/Tests.swift +++ b/Tests/OPMLTests/Tests.swift @@ -1,44 +1,44 @@ -//import XCTest -//@testable import OPML -// -//class Tests: XCTestCase { -// -// func testOPMLNested() { -// guard let file = Bundle.module.url(forResource: "rsparser", withExtension: "opml") else { -// XCTFail("Missing opml file") -// return -// } -// -// do { -// let parser = try OPMLParser(file: file) -// let opml = try parser.parse() -// XCTAssertEqual(opml.entries.flatMap { $0.children ?? [] }.count, 138) -// -// let programmingEntries = opml.entries.first(where: { $0.text == "Programming" })?.children -// XCTAssertEqual(programmingEntries?.count ?? 0, 33) -// } catch { -// XCTFail(error.localizedDescription) -// } -// -// } -// -// func testOPML2() { -// guard let file = Bundle.module.url(forResource: "feedly", withExtension: "opml") else { -// XCTFail("Missing opml file") -// return -// } -// -// do { -// let parser = try OPMLParser(file: file) -// let opml = try parser.parse() -// XCTAssertEqual(opml.entries.count, 39) -// } catch { -// XCTFail(error.localizedDescription) -// } -// } -// -// func testMissingFile() { -// XCTAssertThrowsError(try OPMLParser(file: URL(string: "file:///Resources/nonExistentFile.opml")!).parse()) -// } -// -//} +import XCTest +@testable import OPML + +class Tests: XCTestCase { + + func testOPMLNested() { + guard let file = Bundle.module.url(forResource: "rsparser", withExtension: "opml") else { + XCTFail("Missing opml file") + return + } + + do { + let parser = try OPMLParser(file: file) + let opml = try parser.parse() + XCTAssertEqual(opml.entries.flatMap { $0.children ?? [] }.count, 138) + + let programmingEntries = opml.entries.first(where: { $0.text == "Programming" })?.children + XCTAssertEqual(programmingEntries?.count ?? 0, 33) + } catch { + XCTFail(error.localizedDescription) + } + + } + + func testOPML2() { + guard let file = Bundle.module.url(forResource: "feedly", withExtension: "opml") else { + XCTFail("Missing opml file") + return + } + + do { + let parser = try OPMLParser(file: file) + let opml = try parser.parse() + XCTAssertEqual(opml.entries.count, 39) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testMissingFile() { + XCTAssertThrowsError(try OPMLParser(file: URL(string: "file:///Resources/nonExistentFile.opml")!).parse()) + } + +} From 3cac769083340f16b7d7c12ca36357c55475e6ae Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 21 Jul 2025 17:30:50 -0400 Subject: [PATCH 10/12] reenable tests --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 174622c..d8e5df2 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: [ .target(name: "OPML", dependencies: [ .product(name: "Html", package: "swift-html"), - ])/*, + ]), .testTarget( name: "OPMLTests", dependencies: ["OPML"], From 69206b4a5ee7196375160896077799c7265859bd Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Mon, 21 Jul 2025 18:56:10 -0400 Subject: [PATCH 11/12] wip --- Package.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index d8e5df2..086f392 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,13 @@ -// swift-tools-version:5.8 +// swift-tools-version:5.5 import PackageDescription let package = Package( name: "OPML", - platforms: [.iOS(.v15), .macOS(.v12)], + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + ], products: [ .library(name: "OPML", targets: ["OPML"]) ], @@ -11,9 +15,7 @@ let package = Package( .package(name: "Html", url: "https://github.com/pointfreeco/swift-html", from: "0.4.1") ], targets: [ - .target(name: "OPML", dependencies: [ - .product(name: "Html", package: "swift-html"), - ]), + .target(name: "OPML", dependencies: ["Html"]), .testTarget( name: "OPMLTests", dependencies: ["OPML"], From 5e0e3a9b8a2197f8f48c5b034e763d8bd4809b56 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Tue, 22 Jul 2025 02:03:21 -0400 Subject: [PATCH 12/12] wip --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 086f392..f97a9bf 100644 --- a/Package.swift +++ b/Package.swift @@ -4,9 +4,9 @@ import PackageDescription let package = Package( name: "OPML", platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), + .iOS(.v14), + .macOS(.v11), + .tvOS(.v14), ], products: [ .library(name: "OPML", targets: ["OPML"])