From c575a950ddd203aefc71a1bc5da9cd21c94686e4 Mon Sep 17 00:00:00 2001 From: Daniel Saidi Date: Thu, 18 Jan 2024 10:16:03 +0100 Subject: [PATCH] Move data format menu into format namespace --- RELEASE_NOTES.md | 1 + .../Component/RichTextViewComponent.swift | 4 +- .../Data/NSAttributedString+Init.swift | 11 +- .../RichTextKit/Data/RichTextDataError.swift | 9 +- .../Data/RichTextDataFormat+Menu.swift | 81 +++++++++++++ .../RichTextKit/Data/RichTextDataFormat.swift | 107 +++++++++--------- .../Data/RichTextDataFormatMenu.swift | 76 ------------- .../RichTextKit/Data/RichTextDataReader.swift | 24 ++-- .../RichTextKit/Data/UTType+RichText.swift | 2 +- .../Export/RichTextExportMenu.swift | 18 +-- .../Export/RichTextExportService.swift | 6 +- .../StandardRichTextExportUrlResolver.swift | 6 +- Sources/RichTextKit/RichTextEditor.swift | 4 +- .../RichTextKit.docc/RichTextKit.md | 1 - .../Sharing/RichTextShareMenu.swift | 4 +- .../_Deprecated/RichTextData+Deprecated.swift | 4 + 16 files changed, 187 insertions(+), 171 deletions(-) create mode 100644 Sources/RichTextKit/Data/RichTextDataFormat+Menu.swift delete mode 100644 Sources/RichTextKit/Data/RichTextDataFormatMenu.swift create mode 100644 Sources/RichTextKit/_Deprecated/RichTextData+Deprecated.swift diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4dce8a239..4568c9d63 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -26,6 +26,7 @@ This release starts moving types and views that relate to other types into the t * `RichTextColorPicker` has been renamed to `RichTextColor.Picker`. * `RichTextCommand` views are now nested within the new `RichTextCommand` type. * `RichTextComponent` deprecates the font name and size functions. +* `RichTextDataFormatMenu` has been renamed to `RichTextDataFormat.Menu`. diff --git a/Sources/RichTextKit/Component/RichTextViewComponent.swift b/Sources/RichTextKit/Component/RichTextViewComponent.swift index fd4d64d4e..8f3192b96 100644 --- a/Sources/RichTextKit/Component/RichTextViewComponent.swift +++ b/Sources/RichTextKit/Component/RichTextViewComponent.swift @@ -56,7 +56,7 @@ public protocol RichTextViewComponent: AnyObject, // MARK: - Setup - /// Setup the view with a text and ``RichTextDataFormat``. + /// Setup the view with a text and data format. func setup( with text: NSAttributedString, format: RichTextDataFormat @@ -109,7 +109,7 @@ public extension RichTextViewComponent { setSelectedRange(safeRange) } - /// Setup the view with data and a ``RichTextDataFormat``. + /// Setup the view with data and a data format. func setup( with data: Data, format: RichTextDataFormat diff --git a/Sources/RichTextKit/Data/NSAttributedString+Init.swift b/Sources/RichTextKit/Data/NSAttributedString+Init.swift index cd9b48246..2b1be14b9 100644 --- a/Sources/RichTextKit/Data/NSAttributedString+Init.swift +++ b/Sources/RichTextKit/Data/NSAttributedString+Init.swift @@ -3,7 +3,7 @@ // RichTextKit // // Created by Daniel Saidi on 2022-06-03. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. // import Foundation @@ -74,7 +74,8 @@ private extension NSAttributedString { try self.init( data: data, options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes) + documentAttributes: &attributes + ) } /** @@ -85,7 +86,8 @@ private extension NSAttributedString { try self.init( data: data, options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes) + documentAttributes: &attributes + ) } #if macOS @@ -97,7 +99,8 @@ private extension NSAttributedString { try self.init( data: data, options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes) + documentAttributes: &attributes + ) } #endif } diff --git a/Sources/RichTextKit/Data/RichTextDataError.swift b/Sources/RichTextKit/Data/RichTextDataError.swift index e4d23f9e3..53671b4b5 100644 --- a/Sources/RichTextKit/Data/RichTextDataError.swift +++ b/Sources/RichTextKit/Data/RichTextDataError.swift @@ -3,17 +3,16 @@ // RichTextKit // // Created by Daniel Saidi on 2022-06-04. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. // import Foundation /** - This enum represents errors that can be thrown when getting - rich text data for a certain format. + This enum represents rich text data-related errors. */ -public enum RichTextDataError: Error { - +enum RichTextDataError: Error { + case invalidArchivedData(in: Data) case invalidPlainTextData(in: Data) case invalidData(in: String) diff --git a/Sources/RichTextKit/Data/RichTextDataFormat+Menu.swift b/Sources/RichTextKit/Data/RichTextDataFormat+Menu.swift new file mode 100644 index 000000000..00aa1106e --- /dev/null +++ b/Sources/RichTextKit/Data/RichTextDataFormat+Menu.swift @@ -0,0 +1,81 @@ +// +// RichTextDataFormat+Menu.swift +// RichTextKit +// +// Created by Daniel Saidi on 2022-12-19. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. +// + +#if iOS || macOS || os(visionOS) +import SwiftUI + +public extension RichTextDataFormat { + + /** + This menu can be used to trigger custom actions for any + list of ``RichTextDataFormat`` values. + + The menu uses customizable actions, which means that it + can be used in toolbars, menu bar commands etc. It also + has an optional `pdf` action, which for instance can be + used when exporting or sharing rich text. + */ + struct Menu: View { + + public init( + title: String, + icon: Image, + formats: [Format] = Format.libraryFormats, + formatAction: @escaping (Format) -> Void, + pdfAction: (() -> Void)? = nil + ) { + self.title = title + self.icon = icon + self.formats = formats + self.formatAction = formatAction + self.pdfAction = pdfAction + } + + public typealias Format = RichTextDataFormat + + private let title: String + private let icon: Image + private let formats: [Format] + private let formatAction: (Format) -> Void + private let pdfAction: (() -> Void)? + + public var body: some View { + SwiftUI.Menu { + ForEach(formats) { format in + Button { + formatAction(format) + } label: { + Label(format.fileFormatText, icon) + } + } + if let action = pdfAction { + Button(action: action) { + Label(RTKL10n.fileFormatPdf.text, icon) + } + } + } label: { + Label(title, icon) + } + } + } +} + +struct RichTextData_FormatMenu_Previews: PreviewProvider { + + static var previews: some View { + VStack { + RichTextDataFormat.Menu( + title: "Export...", + icon: .richTextActionExport, + formatAction: { _ in }, + pdfAction: {} + ) + } + } +} +#endif diff --git a/Sources/RichTextKit/Data/RichTextDataFormat.swift b/Sources/RichTextKit/Data/RichTextDataFormat.swift index 832ef2d70..393c4bfeb 100644 --- a/Sources/RichTextKit/Data/RichTextDataFormat.swift +++ b/Sources/RichTextKit/Data/RichTextDataFormat.swift @@ -1,140 +1,139 @@ // -// RichTextDataFormat.swift +// RichTextData+Format.swift // RichTextKit // // Created by Daniel Saidi on 2022-06-02. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. // import Foundation import UniformTypeIdentifiers - + /** This enum specifies rich text data formats. - - Different formats handle rich text in different ways. - - For instance, ``rtf`` supports rich text attributes, styles, - formatting etc. while ``plainText`` only handles plain text. - ``archivedData`` lets you archive text and attachments into - a binary archive, which is convenient if you stick to Apple - platforms, but restricts how the data can be used elsewhere. - - The ``archivedData`` format uses `rtk` a file extension, as - well as a `UTType.archivedData` uniform type. You can use a - ``RichTextDataFormat/vendorArchivedData(id:fileExtension:fileFormatText:uniformType:)`` - value to specify custom formats. - + + Different formats have different capabilities. For instance, + ``rtf`` supports rich text, styles, etc., while ``plainText`` + only handles text. ``archivedData`` can archive text, image + data and attachments in binary archives. This is convenient + when only targeting Apple platforms, but restricts how data + can be used elsewhere. + + ``archivedData`` uses an `rtk` file extension, as well as a + `UTType.archivedData` uniform type. You can create a custom ``vendorArchivedData(id:fileExtension:fileFormatText:uniformType:)`` + value to specify a custom data format. + Remember to configure your app for handling the UTTypes you want to support, as well as the file extensions you want to - open with the app. Have a look at the demo app for examples. + open with the app. Take a look at the demo app for examples. */ public enum RichTextDataFormat: Equatable, Identifiable { - + /// Archived data that's persisted with a keyed archiver. case archivedData - + /// Plain data is persisted as plain text. case plainText - + /// RTF data is persisted as formatted text. case rtf - + /// A vendor-specific archived data format. case vendorArchivedData( id: String, fileExtension: String, fileFormatText: String, - uniformType: UTType) + uniformType: UTType + ) } public extension Collection where Element == RichTextDataFormat { /// Get all library supported data formats. - static var libraryFormats: [RichTextDataFormat] { - RichTextDataFormat.libraryFormats + static var libraryFormats: [Element] { + Element.libraryFormats } } public extension RichTextDataFormat { /// Get all library supported data formats. - static var libraryFormats: [RichTextDataFormat] { + static var libraryFormats: [Self] { [.archivedData, .plainText, .rtf] } /// The format's unique identifier. var id: String { switch self { - case .archivedData: return "archivedData" - case .plainText: return "plainText" - case .rtf: return "rtf" - case .vendorArchivedData(let id, _, _, _): return id + case .archivedData: "archivedData" + case .plainText: "plainText" + case .rtf: "rtf" + case .vendorArchivedData(let id, _, _, _): id } } /// The formats that a format can be converted to. - var convertibleFormats: [RichTextDataFormat] { + var convertibleFormats: [Self] { switch self { - case .vendorArchivedData: return Self.libraryFormats.removing(.archivedData) - default: return Self.libraryFormats.removing(self) + case .vendorArchivedData: Self.libraryFormats.removing(.archivedData) + default: Self.libraryFormats.removing(self) } } /// The format's file format display text. var fileFormatText: String { switch self { - case .archivedData: return RTKL10n.fileFormatRtk.text - case .plainText: return RTKL10n.fileFormatTxt.text - case .rtf: return RTKL10n.fileFormatRtf.text - case .vendorArchivedData(_, _, let text, _): return text + case .archivedData: RTKL10n.fileFormatRtk.text + case .plainText: RTKL10n.fileFormatTxt.text + case .rtf: RTKL10n.fileFormatRtf.text + case .vendorArchivedData(_, _, let text, _): text } } /// Whether or not the format is an archived data type. var isArchivedDataFormat: Bool { switch self { - case .archivedData: return true - case .plainText: return false - case .rtf: return false - case .vendorArchivedData: return true + case .archivedData: true + case .plainText: false + case .rtf: false + case .vendorArchivedData: true } } /// The format's standard file extension. var standardFileExtension: String { switch self { - case .archivedData: return "rtk" - case .plainText: return "txt" - case .rtf: return "rtf" - case .vendorArchivedData(_, let ext, _, _): return ext + case .archivedData: "rtk" + case .plainText: "txt" + case .rtf: "rtf" + case .vendorArchivedData(_, let ext, _, _): ext } } /// Whether or not the format supports images. var supportsImages: Bool { switch self { - case .archivedData: return true - case .plainText: return false - case .rtf: return false - case .vendorArchivedData: return true + case .archivedData: true + case .plainText: false + case .rtf: false + case .vendorArchivedData: true } } /// The format's uniform type. var uniformType: UTType { switch self { - case .archivedData: return .archivedData - case .plainText: return .plainText - case .rtf: return .rtf - case .vendorArchivedData(_, _, _, let type): return type + case .archivedData: .archivedData + case .plainText: .plainText + case .rtf: .rtf + case .vendorArchivedData(_, _, _, let type): type } } } private extension Collection where Element == RichTextDataFormat { - func removing(_ format: RichTextDataFormat) -> [RichTextDataFormat] { + func removing(_ format: Element) -> [Element] { filter { $0 != format } } } diff --git a/Sources/RichTextKit/Data/RichTextDataFormatMenu.swift b/Sources/RichTextKit/Data/RichTextDataFormatMenu.swift deleted file mode 100644 index 5cb70edf8..000000000 --- a/Sources/RichTextKit/Data/RichTextDataFormatMenu.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// RichTextDataFormatMenu.swift -// RichTextKit -// -// Created by Daniel Saidi on 2022-12-19. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. -// - -#if iOS || macOS || os(visionOS) -import SwiftUI - -/** - This menu can be used to trigger various actions for a list - of ``RichTextDataFormat`` values. - - The menu uses customizable actions, which means that it can - be used in toolbars, macOS commands etc. It has an optional - `pdf` action, which for instance can be used when exporting - or sharing rich text. - */ -public struct RichTextDataFormatMenu: View { - - public init( - title: String, - icon: Image, - formats: [RichTextDataFormat] = RichTextDataFormat.libraryFormats, - formatAction: @escaping (RichTextDataFormat) -> Void, - pdfAction: (() -> Void)? = nil - ) { - self.title = title - self.icon = icon - self.formats = formats - self.formatAction = formatAction - self.pdfAction = pdfAction - } - - private let title: String - private let icon: Image - private let formats: [RichTextDataFormat] - private let formatAction: (RichTextDataFormat) -> Void - private let pdfAction: (() -> Void)? - - public var body: some View { - Menu { - ForEach(formats) { format in - Button { - formatAction(format) - } label: { - Label(format.fileFormatText, icon) - } - } - if let action = pdfAction { - Button(action: action) { - Label(RTKL10n.fileFormatPdf.text, icon) - } - } - } label: { - Label(title, icon) - } - } -} - -struct RichTextDataFormatMenu_Previews: PreviewProvider { - - static var previews: some View { - VStack { - RichTextDataFormatMenu( - title: "Export...", - icon: .richTextActionExport, - formatAction: { _ in }, - pdfAction: {} - ) - } - } -} -#endif diff --git a/Sources/RichTextKit/Data/RichTextDataReader.swift b/Sources/RichTextKit/Data/RichTextDataReader.swift index a24f73e68..bee457fe6 100644 --- a/Sources/RichTextKit/Data/RichTextDataReader.swift +++ b/Sources/RichTextKit/Data/RichTextDataReader.swift @@ -3,7 +3,7 @@ // RichTextKit // // Created by Daniel Saidi on 2022-06-03. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. // import Foundation @@ -27,12 +27,14 @@ public extension RichTextDataReader { - Parameters: - format: The data format to use. */ - func richTextData(for format: RichTextDataFormat) throws -> Data { + func richTextData( + for format: RichTextDataFormat + ) throws -> Data { switch format { - case .archivedData: return try richTextArchivedData() - case .plainText: return try richTextPlainTextData() - case .rtf: return try richTextRtfData() - case .vendorArchivedData: return try richTextArchivedData() + case .archivedData: try richTextArchivedData() + case .plainText: try richTextPlainTextData() + case .rtf: try richTextRtfData() + case .vendorArchivedData: try richTextArchivedData() } } } @@ -51,7 +53,7 @@ private extension RichTextDataReader { [.documentType: documentType] } - /// Generate ``RichTextDataFormat/archivedData`` data. + /// Generate archived formatted data. func richTextArchivedData() throws -> Data { try NSKeyedArchiver.archivedData( withRootObject: richText, @@ -59,7 +61,7 @@ private extension RichTextDataReader { ) } - /// Generate ``RichTextDataFormat/plainText`` data. + /// Generate plain text formatted data. func richTextPlainTextData() throws -> Data { let string = richText.string guard let data = string.data(using: .utf8) else { @@ -69,7 +71,7 @@ private extension RichTextDataReader { return data } - /// Generate ``RichTextDataFormat/rtf`` data. + /// Generate RTF formatted data. func richTextRtfData() throws -> Data { try richText.data( from: textRange, @@ -77,7 +79,7 @@ private extension RichTextDataReader { ) } - /// Generate ``RichTextDataFormat/rtfd`` data. + /// Generate RTFD formatted data. func richTextRtfdData() throws -> Data { try richText.data( from: textRange, @@ -86,7 +88,7 @@ private extension RichTextDataReader { } #if macOS - /// Generate ``RichTextDataFormat/word`` formatted data. + /// Generate Word formatted data. func richTextWordData() throws -> Data { try richText.data( from: textRange, diff --git a/Sources/RichTextKit/Data/UTType+RichText.swift b/Sources/RichTextKit/Data/UTType+RichText.swift index 45ccaf0c4..358bf2a24 100644 --- a/Sources/RichTextKit/Data/UTType+RichText.swift +++ b/Sources/RichTextKit/Data/UTType+RichText.swift @@ -3,7 +3,7 @@ // RichTextKit // // Created by Daniel Saidi on 2022-06-02. -// Copyright © 2022-2023 Daniel Saidi. All rights reserved. +// Copyright © 2022-2024 Daniel Saidi. All rights reserved. // import UniformTypeIdentifiers diff --git a/Sources/RichTextKit/Export/RichTextExportMenu.swift b/Sources/RichTextKit/Export/RichTextExportMenu.swift index a28b9769b..c3f5980e8 100644 --- a/Sources/RichTextKit/Export/RichTextExportMenu.swift +++ b/Sources/RichTextKit/Export/RichTextExportMenu.swift @@ -13,13 +13,13 @@ import SwiftUI This menu can be used to trigger various export actions for a list of ``RichTextDataFormat`` values. - This menu uses a ``RichTextDataFormatMenu`` that by default - is configured for exporting. It has customizable actions to - make it possible to use it in any custom way. + This menu uses a ``RichTextDataFormat/Menu`` configured for + exporting. It has customizable actions, to make it possible + to use it in any custom way. - If you have a ``RichTextDataFormat`` value, you can use its - ``RichTextDataFormat/convertibleFormats`` as init parameter - to get an export menu for all other formats. + You can use a custom ``RichTextDataFormat`` you can use its + `convertibleFormats` as `formats` init parameter, to get an + export menu for all other formats. */ public struct RichTextExportMenu: View { @@ -30,7 +30,7 @@ public struct RichTextExportMenu: View { formatAction: @escaping (RichTextDataFormat) -> Void, pdfAction: (() -> Void)? = nil ) { - self.menu = RichTextDataFormatMenu( + self.menu = RichTextDataFormat.Menu( title: title, icon: icon, formats: formats, @@ -38,8 +38,8 @@ public struct RichTextExportMenu: View { pdfAction: pdfAction ) } - - private let menu: RichTextDataFormatMenu + + private let menu: RichTextDataFormat.Menu public var body: some View { menu diff --git a/Sources/RichTextKit/Export/RichTextExportService.swift b/Sources/RichTextKit/Export/RichTextExportService.swift index 35823e7ce..23b277137 100644 --- a/Sources/RichTextKit/Export/RichTextExportService.swift +++ b/Sources/RichTextKit/Export/RichTextExportService.swift @@ -30,7 +30,8 @@ public protocol RichTextExportService: AnyObject { func generateExportFile( withName fileName: String, content: NSAttributedString, - format: RichTextDataFormat) throws -> URL + format: RichTextDataFormat + ) throws -> URL /** Generate a PDF export file with a certain name and rich @@ -42,5 +43,6 @@ public protocol RichTextExportService: AnyObject { */ func generatePdfExportFile( withName fileName: String, - content: NSAttributedString) throws -> URL + content: NSAttributedString + ) throws -> URL } diff --git a/Sources/RichTextKit/Export/StandardRichTextExportUrlResolver.swift b/Sources/RichTextKit/Export/StandardRichTextExportUrlResolver.swift index 3a4b3c19e..18a9e77ec 100644 --- a/Sources/RichTextKit/Export/StandardRichTextExportUrlResolver.swift +++ b/Sources/RichTextKit/Export/StandardRichTextExportUrlResolver.swift @@ -29,7 +29,8 @@ public extension FileManager { func fileUrl( withName fileName: String, extension: String, - in directory: FileManager.SearchPathDirectory) throws -> URL { + in directory: FileManager.SearchPathDirectory + ) throws -> URL { let url = self .urls(for: directory, in: .userDomainMask).first? .appendingPathComponent(fileName) @@ -54,7 +55,8 @@ public extension FileManager { func uniqueFileUrl( withName fileName: String, extension: String, - in directory: FileManager.SearchPathDirectory) throws -> URL { + in directory: FileManager.SearchPathDirectory + ) throws -> URL { let url = try fileUrl(withName: fileName, extension: `extension`, in: directory) let uniqueUrl = uniqueUrl(for: url) return uniqueUrl diff --git a/Sources/RichTextKit/RichTextEditor.swift b/Sources/RichTextKit/RichTextEditor.swift index a9703f953..7f5c3673e 100644 --- a/Sources/RichTextKit/RichTextEditor.swift +++ b/Sources/RichTextKit/RichTextEditor.swift @@ -50,8 +50,8 @@ public struct RichTextEditor: ViewRepresentable { - Parameters: - text: The rich text to edit. - context: The rich text context to use. - - config: The rich text configuration to use, by deafult `standard`. - - format: The rich text data format, by default ``RichTextDataFormat/archivedData``. + - config: The rich text configuration to use, by deafult `.standard`. + - format: The rich text data format, by default `.archivedData`. */ public init( text: Binding, diff --git a/Sources/RichTextKit/RichTextKit.docc/RichTextKit.md b/Sources/RichTextKit/RichTextKit.docc/RichTextKit.md index f1173a73e..bbabb041d 100644 --- a/Sources/RichTextKit/RichTextKit.docc/RichTextKit.md +++ b/Sources/RichTextKit/RichTextKit.docc/RichTextKit.md @@ -92,7 +92,6 @@ RichTextKit is available under the MIT license. See the [LICENSE][License] file - ``RichTextDataError`` - ``RichTextDataFormat`` -- ``RichTextDataFormatMenu`` - ``RichTextDataReader`` ### Export diff --git a/Sources/RichTextKit/Sharing/RichTextShareMenu.swift b/Sources/RichTextKit/Sharing/RichTextShareMenu.swift index 40f8ef218..8440fd24b 100644 --- a/Sources/RichTextKit/Sharing/RichTextShareMenu.swift +++ b/Sources/RichTextKit/Sharing/RichTextShareMenu.swift @@ -30,7 +30,7 @@ public struct RichTextShareMenu: View { formatAction: @escaping (RichTextDataFormat) -> Void, pdfAction: (() -> Void)? = nil ) { - self.menu = RichTextDataFormatMenu( + self.menu = RichTextDataFormat.Menu( title: title, icon: icon, formats: formats, @@ -39,7 +39,7 @@ public struct RichTextShareMenu: View { ) } - private let menu: RichTextDataFormatMenu + private let menu: RichTextDataFormat.Menu public var body: some View { menu diff --git a/Sources/RichTextKit/_Deprecated/RichTextData+Deprecated.swift b/Sources/RichTextKit/_Deprecated/RichTextData+Deprecated.swift new file mode 100644 index 000000000..8d6aab2ab --- /dev/null +++ b/Sources/RichTextKit/_Deprecated/RichTextData+Deprecated.swift @@ -0,0 +1,4 @@ +import SwiftUI + +@available(*, deprecated, renamed: "RichTextDataFormat.Menu") +public typealias RichTextDataFormatMenu = RichTextDataFormat.Menu