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"]) 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) + } +} diff --git a/Sources/OPML/Exporter/Exporter.swift b/Sources/OPML/Exporter/Exporter.swift index 116a53c..5aaa0aa 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,10 @@ extension OPMLEntry { var node: Node { .outline(attributes: htmlAttributes, children?.map { $0.node } ?? []) } +} +public extension OPMLEntry { + var xml: String { + render(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/Sources/OPML/Transfer/OPML+Transferable.swift b/Sources/OPML/Transfer/OPML+Transferable.swift new file mode 100644 index 0000000..c77dbe3 --- /dev/null +++ b/Sources/OPML/Transfer/OPML+Transferable.swift @@ -0,0 +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) { opml in + let resultURL = FileManager.default.temporaryDirectory + .appendingPathComponent(opml.title ?? UUID().uuidString) + .appendingPathExtension("opml") + 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]) + return SentTransferredFile(resultURL, allowAccessingOriginalFile: true) + } importing: { opmlFile in + 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: .utf8PlainText) { opml in + opml.xml.data(using: .utf8) ?? Data() + } importing: { data in + return (try? OPML(data)) ?? OPML(entries: []) + } + } +} + +extension UTType { + static let opml = UTType(exportedAs: "public.opml", conformingTo: .xml) +} diff --git a/Sources/OPML/Transfer/OPMLFile.swift b/Sources/OPML/Transfer/OPMLFile.swift new file mode 100644 index 0000000..d98721f --- /dev/null +++ b/Sources/OPML/Transfer/OPMLFile.swift @@ -0,0 +1,32 @@ +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] + + 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 + 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) + } +}