├── docs
├── editor_mac_dark.png
├── editor_mac_light.png
├── editor_iphone_dark.png
├── editor_iphone_light.png
├── editor_toolbar_iphone_dark.png
└── editor_toolbar_iphone_light.png
├── RichEditorDemo
├── RichEditorDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── RichEditorDemoApp.swift
│ ├── RichEditorDemo.entitlements
│ └── JsonUtils.swift
└── RichEditorDemo.xcodeproj
│ └── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── .gitignore
├── Sources
└── RichEditorSwiftUI
│ ├── Fonts
│ ├── RichTextFont.swift
│ ├── StandardFontSizeProvider.swift
│ ├── RichTextFont+PickerItem.swift
│ ├── FontRepresentable.swift
│ ├── FontTraitsRepresentable.swift
│ ├── RichTextFont+SizePickerConfig.swift
│ ├── FontDescriptorRepresentable.swift
│ ├── RichTextFont+ListPicker.swift
│ ├── RichTextFont+SizePicker.swift
│ ├── RichTextFontSizePickerStack.swift
│ └── RichTextFont+PickerConfig.swift
│ ├── Format
│ ├── RichTextFormat.swift
│ ├── RichTextFormat+ToolbarStyle.swift
│ ├── RichTextFormat+ToolbarConfig.swift
│ └── RichTextFormat+Toolbar.swift
│ ├── Headers
│ ├── RichTextHeader.swift
│ └── RichTextHeader+Picker.swift
│ ├── Pdf
│ ├── PdfDataError.swift
│ ├── PdfPageMargins.swift
│ └── PdfPageConfiguration.swift
│ ├── UI
│ ├── Extensions
│ │ ├── Array+Extension.swift
│ │ ├── Character+Extension.swift
│ │ ├── NSAttributedString+Empty.swift
│ │ ├── Image+Label.swift
│ │ ├── View+BackportSupportExtension.swift
│ │ ├── NSMutableParagraphStyle+Custom.swift
│ │ ├── NSRange+Extension.swift
│ │ └── String+Characters.swift
│ ├── TextViewUI
│ │ ├── RichTextView+Config.swift
│ │ ├── TextViewEvents.swift
│ │ ├── RichTextViewRepresentable.swift
│ │ ├── RichTextView+Setup.swift
│ │ ├── RichTextEditor+Style.swift
│ │ ├── RichTextView+Config_AppKit.swift
│ │ ├── RichTextEditor+Config.swift
│ │ ├── RichEditorStateFocusedValueKey.swift
│ │ ├── RichTextView+Theme.swift
│ │ └── RichTextView+Config_UIKit.swift
│ ├── Views
│ │ ├── ViewRepresentable.swift
│ │ ├── ListPickerItem.swift
│ │ ├── RichTextLabelValue.swift
│ │ ├── ListPickerSection.swift
│ │ ├── ListPicker.swift
│ │ └── ForEachPicker.swift
│ ├── Context
│ │ ├── RichEditorState+Header.swift
│ │ ├── RichTextContext+Actions.swift
│ │ ├── RichEditorState+TextAlignment.swift
│ │ ├── RichEditorState+Styles.swift
│ │ ├── RichEditorState+Link.swift
│ │ └── RichTextContext+Color.swift
│ ├── Parser
│ │ └── EditorAdapter.swift
│ └── Helper
│ │ ├── RichTextReader.swift
│ │ └── RichEditorState+LineInfo.swift
│ ├── ExportData
│ ├── RichTextDataError.swift
│ ├── UTType+RichText.swift
│ ├── RichTextDataFormat+Menu.swift
│ ├── RichTextDataReader.swift
│ └── NSAttributedString+Init.swift
│ ├── Attributes
│ ├── RichTextAttribute.swift
│ ├── RichTextAttributes+RichTextStyle.swift
│ ├── RichTextWriter.swift
│ └── RichTextAttributeReader.swift
│ ├── Export
│ ├── RichTextExportError.swift
│ ├── NSAttributedString+Export.swift
│ ├── RichTextExportOption.swift
│ ├── RichTextExportMenu.swift
│ ├── RichTextExportUrlResolver.swift
│ ├── RichTextExportService.swift
│ └── RichTextExportUrlResolver+FileManager.swift
│ ├── Helper
│ └── Buildable.swift
│ ├── Colors
│ ├── ColorRepresentable.swift
│ └── RichTextColor.swift
│ ├── Data
│ └── Models
│ │ ├── RichText.swift
│ │ ├── RichTextSpanInternal.swift
│ │ ├── HeaderType.swift
│ │ └── RichAttributes+RichTextAttributes.swift
│ ├── Styles
│ ├── View+RichTextStyle.swift
│ ├── RichTextHighlightingStyle.swift
│ ├── RichTextAction+ButtonStack.swift
│ ├── RichTextStyle+ToggleStack.swift
│ ├── RichTextStyle+ToggleGroup.swift
│ ├── RichTextStyle+Toggle.swift
│ ├── RichTextStyle+Button.swift
│ └── RichTextStyle.swift
│ ├── Components
│ ├── RichTextViewComponent+Alignment.swift
│ ├── RichTextViewComponent+Paragraph.swift
│ ├── RichTextViewComponent+Colors.swift
│ ├── RichTextViewComponent+Styles.swift
│ ├── RichTextViewComponent+Link.swift
│ ├── RichTextViewComponent+Pasting.swift
│ ├── RichTextViewComponent+Ranges.swift
│ └── RichTextViewComponent+Attributes.swift
│ ├── Alignment
│ ├── RichTextAlignment+Picker.swift
│ └── RichTextAlignment.swift
│ ├── RichTextOtherMenu
│ ├── RichTextOtherMenu+ToggleStack.swift
│ ├── RichTextOtherMenu.swift
│ ├── RichTextOtherMenu+ToggleGroup.swift
│ ├── RichTextOtherMenu+Button.swift
│ └── RichTextOtherMenu+Toggle.swift
│ ├── Actions
│ ├── RichTextActionButton.swift
│ └── RichTextAction+KeyboardShortcutModifier.swift
│ ├── BaseFoundation
│ ├── RichTextPresenter.swift
│ └── RichTextCoordinator+Subscriptions.swift
│ ├── ListStyle
│ └── ListType.swift
│ ├── Keyboard
│ ├── RichTextKeyboardToolbar+Config.swift
│ └── RichTextKeyboardToolbar+Style.swift
│ └── Localization
│ └── RTEL10n.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── RichEditorSwiftUITests.xcscheme
│ └── RichEditorSwiftUI.xcscheme
├── .github
├── pull_request_template.md
└── workflows
│ ├── release.yaml
│ ├── build.yaml
│ └── codeql.yaml
├── Tests
└── RichEditorSwiftUITests
│ └── RichEditorSwiftUITests.swift
├── .pre-commit-config.yaml
├── RichEditorSwiftUI.podspec
├── LICENSE.md
├── Package.swift
└── CONTRIBUTING.md
/docs/editor_mac_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_mac_dark.png
--------------------------------------------------------------------------------
/docs/editor_mac_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_mac_light.png
--------------------------------------------------------------------------------
/docs/editor_iphone_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_iphone_dark.png
--------------------------------------------------------------------------------
/docs/editor_iphone_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_iphone_light.png
--------------------------------------------------------------------------------
/docs/editor_toolbar_iphone_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_toolbar_iphone_dark.png
--------------------------------------------------------------------------------
/docs/editor_toolbar_iphone_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/canopas/rich-editor-swiftui/HEAD/docs/editor_toolbar_iphone_light.png
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This is a namespace for font-related types and views.
11 | public struct RichTextFont {}
12 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Format/RichTextFormat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextLine.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This is a namespace for format-related types and views.
11 | public struct RichTextFormat {}
12 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Headers/RichTextHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextHeader.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is a namespace for header-related types and views.
11 | public struct RichTextHeader {}
12 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/RichEditorDemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorDemoApp.swift
3 | // RichEditorDemo
4 | //
5 | // Created by Divyesh Vekariya on 11/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct RichEditorDemoApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | ContentView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Pdf/PdfDataError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PdfDataError.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This error can be thrown when creating PDF data.
11 | public enum PdfDataError: Error {
12 |
13 | /// The platform is not supported
14 | case unsupportedPlatform
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/Array+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Extension.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 04/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Array where Element: Comparable {
11 | func containsSameElements(as other: [Element]) -> Bool {
12 | return self.count == other.count && self.sorted() == other.sorted()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/Character+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Character+Extension.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 09/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Character {
11 |
12 | /// Check if a character is a new line separator.
13 | var isNewLineSeparator: Bool {
14 | self == .newLine || self == .carriageReturn
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/RichEditorDemo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/NSAttributedString+Empty.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Empty.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 13/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension NSAttributedString {
11 |
12 | /// Create an empty attributed string.
13 | public static var empty: NSAttributedString {
14 | .init(string: "")
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | [Quick description and/or mentions to other PRs]
4 |
5 | ## Breaking Changes
6 |
7 | - [List breaking changes]
8 |
9 | ## New Features
10 |
11 | - [List what was introduced]
12 |
13 | ## Bug Fixes
14 |
15 | - [List of issues fixed]
16 |
17 | ## Enhancements
18 |
19 | - [List non-breaking changes]
20 |
21 | ## Maintenance
22 |
23 | - [General refactoring, deployment related changes, etc.]
24 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/Image+Label.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Image+Label.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Image {
11 |
12 | /// Create a label from the icon.
13 | func label(_ title: String) -> some View {
14 | Label {
15 | Text(title)
16 | } icon: {
17 | self
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ExportData/RichTextDataError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextDataError.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This enum represents rich text data-related errors.
11 | public enum RichTextDataError: Error {
12 |
13 | case invalidArchivedData(in: Data)
14 | case invalidPlainTextData(in: Data)
15 | case invalidData(in: String)
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/RichEditorSwiftUITests/RichEditorSwiftUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import RichEditorSwiftUI
3 |
4 | final class RichEditorSwiftUITests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 | //
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Attributes/RichTextAttribute.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAttribute.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /**
11 | This typealias represents a rich text dictionary key.
12 | */
13 |
14 | public typealias RichTextAttribute = NSAttributedString.Key
15 |
16 | /// This typealias represents a ``RichTextAttribute`` keyed and
17 | /// `Any` valued dictionary.
18 | public typealias RichTextAttributes = [RichTextAttribute: Any]
19 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextView+Config.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
11 | extension RichTextView.Configuration {
12 |
13 | /// The standard rich text view configuration.
14 | ///
15 | /// You can set a new value to change the global default.
16 | public static var standard = Self()
17 | }
18 | #endif
19 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/TextViewEvents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextViewEvents.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 23/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | //MARK: - TextView Events
11 | public enum TextViewEvents {
12 | case didChangeSelection(selectedRange: NSRange, text: NSAttributedString)
13 | case didBeginEditing(selectedRange: NSRange, text: NSAttributedString)
14 | case didChange(selectedRange: NSRange, text: NSAttributedString)
15 | case didEndEditing(selectedRange: NSRange, text: NSAttributedString)
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewRepresentable.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(macOS)
9 | import AppKit
10 |
11 | /// This typealias bridges UIKit & AppKit native text views.
12 | public typealias RichTextViewRepresentable = NSTextView
13 | #endif
14 |
15 | #if os(iOS) || os(tvOS) || os(visionOS)
16 | import UIKit
17 |
18 | /// This typealias bridges UIKit & AppKit native text views.
19 | public typealias RichTextViewRepresentable = UITextView
20 | #endif
21 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextExportError.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This enum defines errors that can be thrown when failing to
11 | /// export rich text.
12 | public enum RichTextExportError: Error {
13 |
14 | /// This error occurs when no file could be generated at a certain url.
15 | case cantCreateFile(at: URL)
16 |
17 | /// This error occurs when no file could be generated in a certain directory.
18 | case cantCreateFileUrl(in: FileManager.SearchPathDirectory)
19 | }
20 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: local
3 | hooks:
4 | - id: swiftformat
5 | name: Swift Format
6 | description: Enforces formatting guidelines for Swift files before committing.
7 | language: system
8 | entry: swiftformat --swiftversion 5
9 | stages:
10 | - pre-commit
11 |
12 | - id: swiftlint
13 | name: Swift Linter
14 | description: Runs a linter before committing to ensure code quality.
15 | language: system
16 | always_run: true
17 | entry: swiftlint lint --lenient --config .swiftlint.yml
18 | stages:
19 | - pre-commit
20 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | # This workflow release package on Cocoapods.
2 |
3 | name: Release
4 | on:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: macOS-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 |
16 | - name: Install CocoaPods
17 | run: gem install cocoapods
18 |
19 | - name: Prepare and Publish
20 | env:
21 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
22 | run: |
23 | set -eo pipefail
24 | pod lib lint --allow-warnings
25 | pod trunk push RichEditorSwiftUI.podspec --allow-warnings
26 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewRepresentable.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS) || os(tvOS) || os(visionOS)
11 | import UIKit
12 |
13 | /// This typealias bridges platform-specific view representable
14 | /// types to simplify multi-platform support.
15 | typealias ViewRepresentable = UIViewRepresentable
16 | #endif
17 |
18 | #if os(macOS)
19 | import AppKit
20 |
21 | /// This typealias bridges platform-specific view representable
22 | /// types to simplify multi-platform support.
23 | typealias ViewRepresentable = NSViewRepresentable
24 | #endif
25 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/NSAttributedString+Export.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Export.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | @MainActor
11 | extension NSAttributedString {
12 |
13 | /// Make all text black to account for dark mode.
14 | func withBlackText() -> NSAttributedString {
15 | let mutable = NSMutableAttributedString(attributedString: self)
16 | let range = mutable.safeRange(
17 | for: NSRange(location: 0, length: mutable.length))
18 | mutable.setRichTextAttribute(
19 | .foregroundColor, to: ColorRepresentable.black, at: range)
20 | return mutable
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/ListPickerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListPickerItem.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is an internal version of the original that is defined
11 | /// and available in https://github.com/danielsaidi/swiftuikit.
12 | /// This will not be made public or documented for this library.
13 | protocol ListPickerItem: View {
14 |
15 | associatedtype Item: Equatable
16 |
17 | var item: Item { get }
18 | var isSelected: Bool { get }
19 | }
20 |
21 | extension ListPickerItem {
22 |
23 | var checkmark: some View {
24 | Image(systemName: "checkmark")
25 | .opacity(isSelected ? 1 : 0)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Attributes/RichTextAttributes+RichTextStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAttributes+RichTextStyle.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextAttributes {
11 | /**
12 | Whether or not the attributes has a strikethrough style.
13 | */
14 | public var isStrikethrough: Bool {
15 | get { self[.strikethroughStyle] as? Int == 1 }
16 | set { self[.strikethroughStyle] = newValue ? 1 : 0 }
17 | }
18 |
19 | /**
20 | Whether or not the attributes has an underline style.
21 | */
22 | public var isUnderlined: Bool {
23 | get { self[.underlineStyle] as? Int == 1 }
24 | set { self[.underlineStyle] = newValue ? 1 : 0 }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/RichTextLabelValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextLabelValue.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 30/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This protocol can be implemented by any rich text values
11 | /// that can be represented as a label.
12 | public protocol RichTextLabelValue: Hashable {
13 |
14 | /// The value icon.
15 | var icon: Image { get }
16 |
17 | /// The value display title.
18 | var title: String { get }
19 | }
20 |
21 | extension RichTextLabelValue {
22 |
23 | /// The standard label to use for the value.
24 | public var label: some View {
25 | Label(
26 | title: { Text(title) },
27 | icon: { icon }
28 | )
29 | .tag(self)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ExportData/UTType+RichText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UTType+RichText.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import UniformTypeIdentifiers
9 |
10 | extension UTType {
11 |
12 | /// Uniform rich text types that RichTextKit supports.
13 | public static let richTextTypes: [UTType] = [
14 | .archivedData,
15 | .rtf,
16 | .text,
17 | .plainText,
18 | .data,
19 | ]
20 |
21 | /// The uniform type for ``RichTextDataFormat/archivedData``.
22 | public static let archivedData = UTType(
23 | exportedAs: "com.richtextkit.archiveddata")
24 | }
25 |
26 | extension Collection where Element == UTType {
27 |
28 | /// The uniforum types that rich text documents support.
29 | public static var richTextTypes: [UTType] { UTType.richTextTypes }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Helper/Buildable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Buildable.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 24/09/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Adds a helper function to mutate a properties and help implement _Builder_ pattern
11 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
12 | protocol Buildable {}
13 |
14 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
15 | extension Buildable {
16 | /// Mutates a property of the instance
17 | ///
18 | /// - Parameter keyPath: `WritableKeyPath` to the instance property to be modified
19 | /// - Parameter value: value to overwrite the instance property
20 | func mutating(keyPath: WritableKeyPath, value: T) -> Self {
21 | var newSelf = self
22 | newSelf[keyPath: keyPath] = value
23 | return newSelf
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorRepresentable.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(macOS)
9 | import AppKit
10 |
11 | /// This typealias bridges platform-specific colors to simplify
12 | /// multi-platform support.
13 | public typealias ColorRepresentable = NSColor
14 | #endif
15 |
16 | #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
17 | import UIKit
18 |
19 | /// This typealias bridges platform-specific colors to simplify
20 | /// multi-platform support.
21 | public typealias ColorRepresentable = UIColor
22 | #endif
23 |
24 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
25 | extension ColorRepresentable {
26 |
27 | #if os(iOS) || os(tvOS) || os(visionOS)
28 | public static var textColor: ColorRepresentable { .label }
29 | #endif
30 | }
31 | #endif
32 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/ListPickerSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListPickerSection.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is an internal version of the original that is defined
11 | /// and available in https://github.com/danielsaidi/swiftuikit.
12 | /// This will not be made public or documented for this library.
13 | struct ListPickerSection: Identifiable {
14 |
15 | init(title: String, items: [Item]) {
16 | self.id = UUID()
17 | self.title = title
18 | self.items = items
19 | }
20 |
21 | let id: UUID
22 | let title: String
23 | let items: [Item]
24 |
25 | @ViewBuilder
26 | var header: some View {
27 | if title.trimmingCharacters(in: .whitespaces).isEmpty {
28 | EmptyView()
29 | } else {
30 | Text(title)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Data/Models/RichText.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichText.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 12/02/24.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias RichTextSpans = [RichTextSpan]
11 |
12 | public struct RichText: Codable {
13 | public let spans: [RichTextSpan]
14 |
15 | public init(spans: [RichTextSpan] = []) {
16 | self.spans = spans
17 | }
18 |
19 | func encodeToData() throws -> Data {
20 | return try JSONEncoder().encode(self)
21 | }
22 | }
23 |
24 | public struct RichTextSpan: Codable {
25 | // public var id: String = UUID().uuidString
26 | public let insert: String
27 | public let attributes: RichAttributes?
28 |
29 | public init(
30 | insert: String,
31 | attributes: RichAttributes? = nil
32 | ) {
33 | // self.id = id
34 | self.insert = insert
35 | self.attributes = attributes
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+RichTextStyle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 |
12 | /**
13 | Add a keyboard shortcut that toggles a certain style.
14 |
15 | This modifier only has effect on platforms that support
16 | keyboard shortcuts.
17 | */
18 | @ViewBuilder
19 | public func keyboardShortcut(for style: RichTextStyle) -> some View {
20 | #if os(iOS) || os(macOS) || os(visionOS)
21 | switch style {
22 | case .bold: keyboardShortcut("b", modifiers: .command)
23 | case .italic: keyboardShortcut("i", modifiers: .command)
24 | case .strikethrough: self
25 | case .underline: keyboardShortcut("u", modifiers: .command)
26 | }
27 | #else
28 | self
29 | #endif
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Setup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextView+Setup.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextView {
12 |
13 | func setupSharedBehavior(
14 | with text: NSAttributedString,
15 | _ format: RichTextDataFormat?
16 | ) {
17 | attributedString = .empty
18 | attributedString = text
19 |
20 | setContentCompressionResistancePriority(
21 | .defaultLow, for: .horizontal)
22 | }
23 |
24 | func setup(_ theme: RichTextView.Theme) {
25 | guard richText.string.isEmpty else { return }
26 | font = theme.font
27 | textColor = theme.fontColor
28 | backgroundColor = theme.backgroundColor
29 | }
30 | }
31 | #endif
32 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Header.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorState+Header.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// Get a binding for a certain style.
13 | public func headerBinding() -> Binding {
14 | Binding(
15 | get: { self.currentHeader() },
16 | set: { self.setHeaderStyle($0) }
17 | )
18 | }
19 |
20 | /// Check whether or not the context has a certain header style.
21 | public func currentHeader() -> HeaderType {
22 | return headerType
23 | }
24 |
25 | /// Set whether or not the context has a certain header style.
26 | public func setHeaderStyle(
27 | _ header: HeaderType
28 | ) {
29 | guard header != headerType else { return }
30 | updateStyle(style: header.getTextSpanStyle())
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/View+BackportSupportExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+BackportSupportExtension.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 30/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | nonisolated public func onChangeBackPort(
12 | of value: V, _ action: @escaping (_ newValue: V) -> Void
13 | ) -> some View where V: Equatable {
14 | Group {
15 | if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) {
16 | self
17 | //iOS17~
18 | .onChange(of: value) { oldValue, newValue in
19 | action(newValue)
20 | }
21 | } else {
22 | //up to iOS16
23 | self
24 | .onChange(of: value) { newValue in
25 | action(newValue)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportOption.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextExportOption.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 27/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum RichTextExportOption: CaseIterable, Equatable, Identifiable {
11 | case pdf
12 | case json
13 | }
14 |
15 | extension RichTextExportOption {
16 | /// The format's unique identifier.
17 | public var id: String {
18 | switch self {
19 | case .pdf: "pdf"
20 | case .json: "json"
21 | }
22 | }
23 |
24 | /// The format's file format display text.
25 | public var fileFormatText: String {
26 | switch self {
27 | case .pdf: RTEL10n.fileFormatPdf.text
28 | case .json: RTEL10n.fileFormatJson.text
29 | }
30 | }
31 | }
32 |
33 | extension Collection where Element == RichTextExportOption {
34 |
35 | public static var all: [Element] { RichTextExportOption.allCases }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/NSMutableParagraphStyle+Custom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSMutableParagraphStyle+Custom.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
13 | import AppKit
14 | #endif
15 |
16 | extension NSMutableParagraphStyle {
17 |
18 | convenience init(
19 | from style: NSMutableParagraphStyle? = nil,
20 | alignment: RichTextAlignment? = nil,
21 | indent: CGFloat? = nil,
22 | lineSpacing: CGFloat? = nil
23 | ) {
24 | let style = style ?? .init()
25 | self.init()
26 | self.alignment = alignment?.nativeAlignment ?? style.alignment
27 | self.lineSpacing = lineSpacing ?? style.lineSpacing
28 | self.headIndent = indent ?? style.headIndent
29 | self.firstLineHeadIndent = indent ?? style.headIndent
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Alignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Alignment.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst)
13 | import AppKit
14 | #endif
15 |
16 | extension RichTextViewComponent {
17 |
18 | /// Get the text alignment.
19 | public var richTextAlignment: RichTextAlignment? {
20 | guard let style = richTextParagraphStyle else { return nil }
21 | return RichTextAlignment(style.alignment)
22 | }
23 |
24 | /// Set the text alignment.
25 | public func setRichTextAlignment(_ alignment: RichTextAlignment) {
26 | // if richTextAlignment == alignment { return }
27 | let style = NSMutableParagraphStyle(
28 | from: richTextParagraphStyle,
29 | alignment: alignment
30 | )
31 | setRichTextParagraphStyle(style)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/RichEditorSwiftUI.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "RichEditorSwiftUI"
3 | s.version = "1.1.1"
4 | s.summary = "Rich text editing, SwiftUI rich text editor library."
5 |
6 | s.description = <<-DESC
7 | Wrapper around UITextView to support Rich text editing in SwiftUI.
8 | DESC
9 |
10 | s.homepage = "https://github.com/canopas/RichEditorSwiftUI"
11 | s.license = { :type => "MIT", :file => "LICENSE.md" }
12 | s.author = { "Jimmy" => "jimmy@canopas.com" }
13 | s.source = { :git => "https://github.com/canopas/rich-editor-swiftui.git", :tag => s.version.to_s }
14 | s.social_media_url = "https://x.com/canopas_eng"
15 |
16 | s.source_files = "Sources/**/*.swift"
17 |
18 | s.module_name = "RichEditorSwiftUI"
19 | s.requires_arc = true
20 |
21 | s.swift_version = "5.9"
22 |
23 | s.ios.deployment_target = "15.0"
24 | s.osx.deployment_target = "12.0"
25 | s.tvos.deployment_target = "17.0"
26 | s.watchos.deployment_target = "8.0"
27 |
28 | s.preserve_paths = "README.md"
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) 2022 Canopas Software LLP
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "RichEditorSwiftUI",
7 | platforms: [
8 | .iOS(.v15),
9 | .macOS(.v12),
10 | .tvOS(.v17),
11 | .watchOS(.v8),
12 | .visionOS(.v1)
13 | ],
14 | products: [
15 | .library(
16 | name: "RichEditorSwiftUI",
17 | targets: ["RichEditorSwiftUI"]),
18 | ],
19 | dependencies: [],
20 | targets: [
21 | .target(
22 | name: "RichEditorSwiftUI",
23 | dependencies: [],
24 | resources: [],
25 | swiftSettings: [
26 | .define("macOS", .when(platforms: [.macOS])),
27 | .define("iOS", .when(platforms: [.iOS, .macCatalyst]))
28 | ]
29 | ),
30 | .testTarget(
31 | name: "RichEditorSwiftUITests",
32 | dependencies: ["RichEditorSwiftUI"],
33 | swiftSettings: [
34 | .define("macOS", .when(platforms: [.macOS])),
35 | .define("iOS", .when(platforms: [.iOS, .macCatalyst]))
36 | ]
37 | )
38 | ]
39 | )
40 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/StandardFontSizeProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardFontSizeProvider.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 12/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol StandardFontSizeProvider {}
11 |
12 | extension CGFloat: StandardFontSizeProvider {}
13 |
14 | extension Double: StandardFontSizeProvider {}
15 |
16 | extension RichEditorState: StandardFontSizeProvider {}
17 |
18 | #if os(iOS) || os(macOS) || os(tvOS)
19 | extension RichTextEditor: StandardFontSizeProvider {}
20 |
21 | extension RichTextView: StandardFontSizeProvider {}
22 | #endif
23 |
24 | extension StandardFontSizeProvider {
25 |
26 | /**
27 | The standard font size to use for rich text.
28 |
29 | You can change this value to affect all types that make
30 | use of this value.
31 | */
32 | public static var standardRichTextFontSize: CGFloat {
33 | get { StandardFontSizeProviderStorage.standardRichTextFontSize }
34 | set {
35 | StandardFontSizeProviderStorage.standardRichTextFontSize = newValue
36 | }
37 | }
38 | }
39 |
40 | private class StandardFontSizeProviderStorage {
41 |
42 | static var standardRichTextFontSize: CGFloat = 16
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont+PickerItem.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextFont {
11 |
12 | /**
13 | This struct is used by the various library font pickers.
14 | */
15 | struct PickerItem: View, ListPickerItem {
16 |
17 | init(
18 | font: Item,
19 | fontSize: CGFloat = 20,
20 | isSelected: Bool
21 | ) {
22 | self.font = font
23 | self.fontSize = fontSize
24 | self.isSelected = isSelected
25 | }
26 |
27 | typealias Item = RichTextFont.PickerFont
28 |
29 | let font: Item
30 | let fontSize: CGFloat
31 | let isSelected: Bool
32 |
33 | var item: Item { font }
34 |
35 | var body: some View {
36 | HStack {
37 | Text(font.fontDisplayName)
38 | .font(.custom(font.fontName, size: fontSize))
39 | Spacer()
40 | if isSelected {
41 | checkmark
42 | }
43 | }.contentShape(Rectangle())
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Style.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextEditor+Style.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | /// This struct can be used to style a ``RichTextEditor``.
12 | public typealias RichTextEditorStyle = RichTextView.Theme
13 |
14 | extension View {
15 |
16 | /// Apply a ``RichTextEditor`` style.
17 | public func richTextEditorStyle(
18 | _ style: RichTextEditorStyle
19 | ) -> some View {
20 | self.environment(\.richTextEditorStyle, style)
21 | }
22 | }
23 |
24 | extension RichTextEditorStyle {
25 |
26 | fileprivate struct Key: EnvironmentKey {
27 |
28 | static var defaultValue: RichTextEditorStyle = .standard
29 | }
30 | }
31 |
32 | extension EnvironmentValues {
33 |
34 | /// This value can bind to a rich text editor style.
35 | public var richTextEditorStyle: RichTextEditorStyle {
36 | get { self[RichTextEditorStyle.Key.self] }
37 | set { self[RichTextEditorStyle.Key.self] = newValue }
38 | }
39 | }
40 | #endif
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextView+Config_AppKit.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(macOS)
9 | import Foundation
10 |
11 | extension RichTextView {
12 |
13 | /**
14 | This type can be used to configure a ``RichTextEditor``.
15 | */
16 | public struct Configuration {
17 |
18 | /// Create a custom configuration
19 | /// - Parameters:
20 | /// - isScrollingEnabled: Whether or not the editor should scroll, by default `true`.
21 | /// - isContinuousSpellCheckingEnabled: Whether the editor spell-checks in realtime. Defaults to `true`.
22 | public init(
23 | isScrollingEnabled: Bool = true,
24 | isContinuousSpellCheckingEnabled: Bool = true
25 | ) {
26 | self.isScrollingEnabled = isScrollingEnabled
27 | self.isContinuousSpellCheckingEnabled =
28 | isContinuousSpellCheckingEnabled
29 | }
30 |
31 | /// Whether or not the editor should scroll.
32 | public var isScrollingEnabled: Bool
33 |
34 | /// Whether the editor spell-checks in realtime.
35 | public var isContinuousSpellCheckingEnabled: Bool
36 | }
37 | }
38 | #endif
39 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/FontRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontRepresentable.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | #if canImport(UIKit)
9 | import UIKit
10 |
11 | /// This typealias bridges platform-specific fonts, to simplify
12 | /// multi-platform support.
13 | public typealias FontRepresentable = UIFont
14 | #endif
15 |
16 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
17 | import AppKit
18 |
19 | /// This typealias bridges platform-specific fonts, to simplify
20 | /// multi-platform support.
21 | public typealias FontRepresentable = NSFont
22 | #endif
23 |
24 | extension FontRepresentable {
25 |
26 | /**
27 | The standard font to use for rich text.
28 |
29 | You can change this value to affect all types that make
30 | use of the value.
31 | */
32 | public static var standardRichTextFont = systemFont(
33 | ofSize: .standardRichTextFontSize)
34 |
35 | /// Create a new font by toggling a certain style.
36 | public func toggling(
37 | _ style: RichTextStyle
38 | ) -> FontRepresentable? {
39 | .init(
40 | descriptor: fontDescriptor.byTogglingStyle(style),
41 | size: pointSize
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextEditor+Config.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | /// This struct be used to configure a ``RichTextEditor``.
12 | public typealias RichTextEditorConfig = RichTextView.Configuration
13 |
14 | extension View {
15 |
16 | /// Apply a ``RichTextEditor`` configuration.
17 | public func richTextEditorConfig(
18 | _ config: RichTextEditorConfig
19 | ) -> some View {
20 | self.environment(\.richTextEditorConfig, config)
21 | }
22 | }
23 |
24 | extension RichTextEditorConfig {
25 |
26 | fileprivate struct Key: EnvironmentKey {
27 |
28 | public static var defaultValue: RichTextEditorConfig = .standard
29 | }
30 | }
31 |
32 | extension EnvironmentValues {
33 |
34 | /// This value can bind to a rich text editor config.
35 | public var richTextEditorConfig: RichTextEditorConfig {
36 | get { self[RichTextEditorConfig.Key.self] }
37 | set { self[RichTextEditorConfig.Key.self] = newValue }
38 | }
39 | }
40 | #endif
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Paragraph.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Paragraph.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 | #endif
13 |
14 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
15 | import AppKit
16 | #endif
17 |
18 | extension RichTextViewComponent {
19 |
20 | /// Get the paragraph style.
21 | public var richTextParagraphStyle: NSMutableParagraphStyle? {
22 | richTextAttribute(.paragraphStyle)
23 | }
24 |
25 | /// Set the paragraph style.
26 | ///
27 | /// > Todo: The function currently can't handle multiple
28 | /// selected paragraphs. If many paragraphs are selected,
29 | /// it will only affect the first one.
30 | public func setRichTextParagraphStyle(_ style: NSParagraphStyle) {
31 | let range = lineRange(for: selectedRange)
32 | guard range.length > 0 else { return }
33 | #if os(watchOS)
34 | setRichTextAttribute(.paragraphStyle, to: style, at: range)
35 | #else
36 | textStorageWrapper?.addAttribute(
37 | .paragraphStyle, value: style, range: range)
38 | #endif
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Pdf/PdfPageMargins.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PdfPageMargins.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | /// This error can be thrown when creating PDF data.
11 | public struct PdfPageMargins: Equatable {
12 |
13 | /// Create PDF page margins.
14 | public init(top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) {
15 | self.top = top
16 | self.left = left
17 | self.bottom = bottom
18 | self.right = right
19 | }
20 |
21 | /// Create PDF page margins.
22 | public init(horizontal: CGFloat, vertical: CGFloat) {
23 | self.top = vertical
24 | self.left = horizontal
25 | self.bottom = vertical
26 | self.right = horizontal
27 | }
28 |
29 | /// Create PDF page margins.
30 | public init(all: CGFloat) {
31 | self.top = all
32 | self.left = all
33 | self.bottom = all
34 | self.right = all
35 | }
36 |
37 | /// The top margins.
38 | public var top: CGFloat
39 |
40 | /// The left margins.
41 | public var left: CGFloat
42 |
43 | /// The bottom margins.
44 | public var bottom: CGFloat
45 |
46 | /// The right margins.
47 | public var right: CGFloat
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextHighlightingStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextHighlightingStyle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This struct can be used to style rich text highlighting.
11 | public struct RichTextHighlightingStyle: Equatable, Hashable {
12 |
13 | /**
14 | Create a style instance.
15 |
16 | - Parameters:
17 | - backgroundColor: The background color to use for highlighted text.
18 | - foregroundColor: The foreground color to use for highlighted text.
19 | */
20 | public init(
21 | backgroundColor: Color = .clear,
22 | foregroundColor: Color = .accentColor
23 | ) {
24 | self.backgroundColor = backgroundColor
25 | self.foregroundColor = foregroundColor
26 | }
27 |
28 | /// The background color to use for highlighted text.
29 | public let backgroundColor: Color
30 |
31 | /// The foreground color to use for highlighted text.
32 | public let foregroundColor: Color
33 | }
34 |
35 | extension RichTextHighlightingStyle {
36 |
37 | /// The standard rich text highlighting style.
38 | ///
39 | /// You can set a new value to change the global default.
40 | public static var standard = Self()
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Actions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextContext+Actions.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// Handle a certain rich text action.
13 | public func handle(_ action: RichTextAction) {
14 | switch action {
15 | // case .stepFontSize(let size):
16 | // fontSize += CGFloat(size)
17 | // updateStyle(style: .size(Int(fontSize)))
18 | default: actionPublisher.send(action)
19 | }
20 | }
21 |
22 | /// Check if the context can handle a certain action.
23 | public func canHandle(_ action: RichTextAction) -> Bool {
24 | switch action {
25 | case .copy: canCopy
26 | // case .pasteImage: true
27 | // case .pasteImages: true
28 | // case .pasteText: true
29 | case .print: false
30 | case .redoLatestChange: canRedoLatestChange
31 | case .undoLatestChange: canUndoLatestChange
32 | default: true
33 | }
34 | }
35 |
36 | /// Trigger a certain rich text action.
37 | public func trigger(_ action: RichTextAction) {
38 | handle(action)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Parser/EditorAdapter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditorAdapter.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 11/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EditorAdapter {
11 | func encodeToString(type model: T) throws -> String?
12 | func decode(from jsonString: String) throws -> T?
13 | func encode(type model: T) throws -> Data
14 | }
15 |
16 | class DefaultAdapter: EditorAdapter {
17 | func encodeToString(type model: T) throws -> String? {
18 | do {
19 | let jsonData = try JSONEncoder().encode(model)
20 | let jsonString = String(data: jsonData, encoding: .utf8)
21 | return jsonString
22 | } catch {
23 | throw error
24 | }
25 | }
26 |
27 | func decode(from jsonString: String) throws -> T? {
28 | guard let data = jsonString.data(using: .utf8) else { return nil }
29 | do {
30 | let content = try JSONDecoder().decode(T.self, from: data)
31 | return content
32 | } catch {
33 | throw error
34 | }
35 | }
36 |
37 | func encode(type model: T) throws -> Data where T: Encodable {
38 | return try JSONEncoder().encode(model)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichEditorStateFocusedValueKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorStateFocusedValueKey.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// This key can be used to keep track of a context in a
13 | /// multi-windowed app.
14 | public struct FocusedValueKey: SwiftUI.FocusedValueKey {
15 |
16 | public typealias Value = RichEditorState
17 | }
18 | }
19 |
20 | extension FocusedValues {
21 |
22 | /// This value can be used to keep track of a context in
23 | /// a multi-windowed app.
24 | ///
25 | /// You can bind a context to a view with `focusedValue`:
26 | ///
27 | /// ```swift
28 | /// RichTextEditor(...)
29 | /// .focusedValue(\.richTextContext, richTextContext)
30 | /// ```
31 | ///
32 | /// You can then access the context as a `@FocusedValue`:
33 | ///
34 | /// ```swift
35 | /// @FocusedValue(\.richEditorState)
36 | /// var richEditorState: RichEditorState?
37 | /// ```
38 | public var richEditorState: RichEditorState.FocusedValueKey.Value? {
39 | get { self[RichEditorState.FocusedValueKey.self] }
40 | set { self[RichEditorState.FocusedValueKey.self] = newValue }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+TextAlignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorState+TextAlignment.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// Get a binding for a certain TextAlignment style.
13 | public func textAlignmentBinding() -> Binding {
14 | Binding(
15 | get: { self.currentTextAlignment() },
16 | set: { self.setTextAlignmentStyle($0) }
17 | )
18 | }
19 |
20 | /// Check whether or not the context has a certain TextAlignment style.
21 | public func currentTextAlignment() -> RichTextAlignment {
22 | return textAlignment
23 | }
24 |
25 | /// Set whether or not the context has a certain TextAlignment style.
26 | public func setTextAlignmentStyle(
27 | _ alignment: RichTextAlignment
28 | ) {
29 | guard alignment != textAlignment else { return }
30 | updateStyle(style: alignment.getTextSpanStyle())
31 | setTextAlignmentInternal(alignment: alignment)
32 | }
33 |
34 | public func setTextAlignmentInternal(
35 | alignment: RichTextAlignment
36 | ) {
37 | guard alignment != textAlignment else { return }
38 | textAlignment = alignment
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/NSRange+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSRange+Extension.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 11/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | extension NSRange {
11 | var isCollapsed: Bool {
12 | return self.length == 0 || self.upperBound == self.lowerBound
13 | }
14 |
15 | var closedRange: ClosedRange {
16 | return lowerBound...(upperBound - (length > 0 ? 1 : 0))
17 | }
18 | }
19 |
20 | extension ClosedRange {
21 | var nsRange: NSRange {
22 | return NSRange(location: lowerBound, length: upperBound - lowerBound)
23 | }
24 |
25 | func isInRange(_ range: ClosedRange) -> Bool {
26 | return range.contains(self.lowerBound)
27 | && range.contains(self.upperBound)
28 | }
29 |
30 | func isPartialOverlap(_ range: ClosedRange) -> Bool {
31 | return self.contains(range.lowerBound)
32 | != self.contains(range.upperBound)
33 | }
34 |
35 | func isSameAs(_ range: ClosedRange) -> Bool {
36 | return (self.lowerBound == range.lowerBound)
37 | && (self.upperBound == range.upperBound)
38 | }
39 | }
40 |
41 | extension Range {
42 | var nsRange: NSRange {
43 | return NSRange(location: lowerBound, length: upperBound - lowerBound)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment+Picker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAlignment+Picker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextAlignment {
11 |
12 | /// This picker can be used to pick a text alignment.
13 | ///
14 | /// This view returns a plain SwiftUI `Picker` view that
15 | /// can be styled and configured with a `PickerStyle`.
16 | public struct Picker: View {
17 |
18 | /// Create a rich text alignment picker.
19 | ///
20 | /// - Parameters:
21 | /// - selection: The binding to update with the picker.
22 | /// - values: The pickable alignments, by default `.allCases`.
23 | public init(
24 | selection: Binding,
25 | values: [RichTextAlignment] = RichTextAlignment.allCases
26 | ) {
27 | self._selection = selection
28 | self.values = values
29 | }
30 |
31 | let values: [RichTextAlignment]
32 |
33 | @Binding
34 | private var selection: RichTextAlignment
35 |
36 | public var body: some View {
37 | SwiftUI.Picker(RTEL10n.textAlignment.text, selection: $selection) {
38 | ForEach(values) { value in
39 | value.label
40 | .labelStyle(.iconOnly)
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/FontTraitsRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontTraitsRepresentable.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
9 | import AppKit
10 |
11 | /// This typealias bridges platform-specific symbolic traits to
12 | /// simplify multi-platform support.
13 | ///
14 | /// The typealias also defines additional functionality as type
15 | /// extensions for the platform-specific types.
16 | public typealias FontTraitsRepresentable = NSFontDescriptor.SymbolicTraits
17 | #endif
18 |
19 | #if canImport(UIKit)
20 | import UIKit
21 |
22 | /// This typealias bridges platform-specific symbolic traits to
23 | /// simplify multi-platform support.
24 | ///
25 | /// The typealias also defines additional functionality as type
26 | /// extensions for the platform-specific types.
27 | public typealias FontTraitsRepresentable = UIFontDescriptor.SymbolicTraits
28 | #endif
29 |
30 | extension FontTraitsRepresentable {
31 |
32 | /**
33 | Get the rich text styles that are enabled in the traits.
34 |
35 | Note that the traits only contain some of the available
36 | rich text styles.
37 | */
38 | public var enabledRichTextStyles: [RichTextStyle] {
39 | RichTextStyle.allCases.filter {
40 | guard let trait = $0.symbolicTraits else { return false }
41 | return contains(trait)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextExportMenu.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | /// This menu can be used to trigger various export actions for
12 | /// a list of ``RichTextDataFormat`` values.
13 | ///
14 | /// This menu uses a ``RichTextDataFormat/Menu`` configured for
15 | /// exporting, with customizable actions and data formats.
16 | public struct RichTextExportMenu: View {
17 |
18 | public init(
19 | title: String = RTEL10n.menuExportAs.text,
20 | icon: Image = .richTextExport,
21 | formats: [RichTextDataFormat] = RichTextDataFormat.libraryFormats,
22 | otherFormats: [RichTextExportOption] = .all,
23 | formatAction: @escaping (RichTextDataFormat) -> Void,
24 | otherOptionAction: ((RichTextExportOption) -> Void)? = nil
25 | ) {
26 | self.menu = RichTextDataFormat.Menu(
27 | title: title,
28 | icon: icon,
29 | formats: formats,
30 | otherFormats: otherFormats,
31 | formatAction: formatAction,
32 | otherOptionAction: otherOptionAction
33 | )
34 | }
35 |
36 | private let menu: RichTextDataFormat.Menu
37 |
38 | public var body: some View {
39 | menu
40 | }
41 | }
42 | #endif
43 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | # This workflow builds and tests the project.
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift
3 |
4 | name: Build Runner
5 |
6 | on:
7 | push:
8 | branches: ["main"]
9 | pull_request:
10 | branches: ["main"]
11 |
12 | env:
13 | SCHEME: RichEditorSwiftUI
14 | TEST_SCHEME: RichEditorSwiftUITests
15 |
16 | jobs:
17 | build:
18 | runs-on: macos-15
19 | steps:
20 | - uses: actions/checkout@v3
21 | - uses: maxim-lobanov/setup-xcode@v1
22 | with:
23 | xcode-version: '16.0'
24 | - name: Build iOS
25 | run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=iOS' | xcpretty --color;
26 | - name: Build macOS
27 | run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=OS X' | xcpretty --color;
28 | - name: Build tvOS
29 | run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=tvOS' | xcpretty --color;
30 | - name: Build watchOS
31 | run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=watchOS' | xcpretty --color;
32 | - name: Build visionOS
33 | run: xcodebuild -scheme $SCHEME -derivedDataPath .build -destination 'generic/platform=xrOS' | xcpretty --color;
34 | - name: Test iOS
35 | run: xcodebuild test -scheme $SCHEME -derivedDataPath .build -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.0' -enableCodeCoverage YES | xcpretty --color;
36 |
--------------------------------------------------------------------------------
/RichEditorDemo/RichEditorDemo/JsonUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JsonUtils.swift
3 | // RichEditorDemo
4 | //
5 | // Created by Divyesh Vekariya on 05/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | internal func readJSONFromFile(
11 | fileName: String,
12 | type: T.Type,
13 | bundle: Bundle? = nil
14 | ) -> T? {
15 | if let url = (bundle ?? Bundle.main)
16 | .url(forResource: fileName, withExtension: "json")
17 | {
18 | do {
19 | let data = try Data(contentsOf: url)
20 | let decoder = JSONDecoder()
21 | let jsonData = try decoder.decode(T.self, from: data)
22 | return jsonData
23 | } catch {
24 | print("JSONUtils: error - \(error)")
25 | }
26 | }
27 | return nil
28 | }
29 |
30 | internal class RichBundleFakeClass {}
31 |
32 | extension Bundle {
33 | static var richBundle: Bundle {
34 | return Bundle(for: RichBundleFakeClass.self)
35 | }
36 | }
37 |
38 | func encode(model: T?) throws -> String? {
39 | guard let model else { return nil }
40 | do {
41 | let jsonData = try JSONEncoder().encode(model)
42 | let jsonString = String(data: jsonData, encoding: .utf8)
43 | return jsonString
44 | } catch {
45 | throw error
46 | }
47 | }
48 |
49 | func decode(json string: String) throws -> T? {
50 | guard let data = string.data(using: .utf8) else { return nil }
51 | do {
52 | let content = try JSONDecoder().decode(T.self, from: data)
53 | return content
54 | } catch {
55 | throw error
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Colors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Colors.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextViewComponent {
11 |
12 | /// Get a certain color.
13 | public func richTextColor(
14 | _ color: RichTextColor
15 | ) -> ColorRepresentable? {
16 | guard let attribute = color.attribute else { return nil }
17 | return richTextAttribute(attribute)
18 | }
19 |
20 | /// Get a certain color at a certain range.
21 | public func richTextColor(
22 | _ color: RichTextColor,
23 | at range: NSRange
24 | ) -> ColorRepresentable? {
25 | guard let attribute = color.attribute else { return nil }
26 | return richTextAttribute(attribute, at: range)
27 | }
28 |
29 | /// Set a certain color.
30 | public func setRichTextColor(
31 | _ color: RichTextColor,
32 | to val: ColorRepresentable
33 | ) {
34 | if richTextColor(color) == val { return }
35 | guard let attribute = color.attribute else { return }
36 | setRichTextAttribute(attribute, to: val)
37 | }
38 |
39 | /// Set a certain colors at a certain range.
40 | public func setRichTextColor(
41 | _ color: RichTextColor,
42 | to val: ColorRepresentable,
43 | at range: NSRange
44 | ) {
45 | guard let attribute = color.attribute else { return }
46 | if richTextColor(color, at: range) == val { return }
47 | setRichTextAttribute(attribute, to: val, at: range)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Theme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextView+Theme.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextView {
12 |
13 | /**
14 | This type can be used to configure a ``RichTextEditor``'s current color properties.
15 | */
16 | public struct Theme {
17 |
18 | /**
19 | Create a custom configuration.
20 |
21 | - Parameters:
22 | - font: default `.systemFont` of point size `16` (this differs on iOS and macOS).
23 | - fontColor: default `.textColor`.
24 | - backgroundColor: Color of whole textView default `.clear`.
25 | */
26 | public init(
27 | font: FontRepresentable = .systemFont(ofSize: 16),
28 | fontColor: ColorRepresentable = .textColor,
29 | backgroundColor: ColorRepresentable = .clear
30 | ) {
31 | self.font = font
32 | self.fontColor = fontColor
33 | self.backgroundColor = backgroundColor
34 | }
35 |
36 | public let font: FontRepresentable
37 | public let fontColor: ColorRepresentable
38 | public let backgroundColor: ColorRepresentable
39 | }
40 | }
41 |
42 | extension RichTextView.Theme {
43 |
44 | /// The standard rich text view theme.
45 | ///
46 | /// You can set a new value to change the global default.
47 | public static var standard = Self()
48 | }
49 | #endif
50 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Pdf/PdfPageConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PdfPageConfiguration.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | /// This error can be thrown when creating PDF data.
11 | public struct PdfPageConfiguration: Equatable {
12 |
13 | /**
14 | Create a PDF page configuration.
15 |
16 | - Parameters:
17 | - pageSize: The page size in points.
18 | - pageMargins: The page margins, by default `72`.
19 | */
20 | public init(
21 | pageSize: CGSize = CGSize(width: 595.2, height: 841.8),
22 | pageMargins: PdfPageMargins = .init(all: 72)
23 | ) {
24 | self.pageSize = pageSize
25 | self.pageMargins = pageMargins
26 | }
27 |
28 | /// The page size in points.
29 | public var pageSize: CGSize
30 |
31 | /// The page margins.
32 | public var pageMargins: PdfPageMargins
33 | }
34 |
35 | extension PdfPageConfiguration {
36 |
37 | /// The standard PDF page configuration.
38 | public static var standard: Self { .init() }
39 | }
40 |
41 | extension PdfPageConfiguration {
42 |
43 | /// Get the paper rectangle.
44 | public var paperRect: CGRect {
45 | CGRect(x: 0, y: 0, width: pageSize.width, height: pageSize.height)
46 | }
47 |
48 | /// Get the printable rectangle.
49 | public var printableRect: CGRect {
50 | CGRect(
51 | x: pageMargins.left,
52 | y: pageMargins.top,
53 | width: pageSize.width - pageMargins.left - pageMargins.right,
54 | height: pageSize.height - pageMargins.top - pageMargins.bottom)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorState+Styles.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// Get a binding for a certain style.
13 | public func binding(for style: RichTextStyle) -> Binding {
14 | Binding(
15 | get: { Bool(self.hasStyle(style)) },
16 | set: { [weak self] _ in self?.setStyle(style) }
17 | )
18 | }
19 |
20 | /// Check whether or not the context has a certain style.
21 | public func hasStyle(_ style: RichTextStyle) -> Bool {
22 | styles[style] == true
23 | }
24 |
25 | /// Set whether or not the context has a certain style.
26 | public func setStyle(
27 | _ style: RichTextStyle,
28 | to val: Bool
29 | ) {
30 | guard styles[style] != val else { return }
31 | actionPublisher.send(.setStyle(style, val))
32 | setStyleInternal(style, to: val)
33 | }
34 |
35 | /// Toggle a certain style for the context.
36 | public func toggleStyle(_ style: RichTextStyle) {
37 | setStyle(style, to: !hasStyle(style))
38 | }
39 |
40 | public func setStyle(_ style: RichTextStyle) {
41 | toggleStyle(style: style.richTextSpanStyle)
42 | }
43 | }
44 |
45 | extension RichEditorState {
46 |
47 | /// Set the value for a certain color, or remove it.
48 | func setStyleInternal(
49 | _ style: RichTextStyle,
50 | to val: Bool?
51 | ) {
52 | guard let val else {
53 | styles[style] = nil
54 | return
55 | }
56 | styles[style] = val
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorState+Link.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS)
11 | extension RichEditorState {
12 | func insertLink(value: Bool) {
13 | if link != nil {
14 | alertController.showAlert(
15 | title: "Remove link", message: "It will remove link from selected text",
16 | onOk: { [weak self] in
17 | guard let self else { return }
18 | self.updateStyle(style: .link())
19 | },
20 | onCancel: {
21 | return
22 | })
23 | } else {
24 | alertController.showAlert(
25 | title: "Enter url", message: "", placeholder: "Enter link",
26 | defaultText: "",
27 | onTextChange: { text in
28 | },
29 | completion: { [weak self] finalText in
30 | self?.updateStyle(style: .link(finalText))
31 | })
32 | }
33 | }
34 | }
35 |
36 | extension RichEditorState {
37 |
38 | /// Get a binding for a certain style.
39 | public func bindingForManu(for menu: RichTextOtherMenu) -> Binding {
40 | Binding(
41 | get: { Bool(self.hasStyle(menu)) },
42 | set: { self.setLink(to: $0) }
43 | )
44 | }
45 |
46 | /// Check whether or not the context has a certain style.
47 | public func hasStyle(_ style: RichTextOtherMenu) -> Bool {
48 | link != nil
49 | }
50 |
51 | /// Set whether or not the context has a certain style.
52 | public func setLink(
53 | to val: Bool
54 | ) {
55 | insertLink(value: val)
56 | }
57 | }
58 | #endif
59 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextExportUrlResolver.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol can be implemented by types that can generate
11 | /// file urls, for instance when exporting rich text files.
12 | ///
13 | /// The protocol is implemented by `FileManager`, which is used
14 | /// by default by the library.
15 | public protocol RichTextExportUrlResolver {
16 |
17 | /**
18 | Try to generate a file url in a certain directory.
19 |
20 | - Parameters:
21 | - fileName: The preferred file name.
22 | - extensions: The file extension.
23 | - directory: The directory in which to generate an url.
24 | */
25 | func fileUrl(
26 | withName fileName: String,
27 | extension: String,
28 | in directory: FileManager.SearchPathDirectory
29 | ) throws -> URL
30 |
31 | /**
32 | Try to generate a unique file url in a certain directory.
33 |
34 | - Parameters:
35 | - fileName: The preferred file name.
36 | - extensions: The file extension.
37 | - directory: The directory in which to generate an url.
38 | */
39 | func uniqueFileUrl(
40 | withName fileName: String,
41 | extension: String,
42 | in directory: FileManager.SearchPathDirectory
43 | ) throws -> URL
44 |
45 | /**
46 | Get a unique url for the provided url, to ensure that a
47 | file with the same name doesn't exist.
48 |
49 | - Parameters:
50 | - url: The url to generate a unique url for.
51 | - separator: The separator to use for separating the counter.
52 | */
53 | func uniqueUrl(for url: URL) -> URL
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS) || os(macOS) || os(visionOS)
11 |
12 | extension RichTextOtherMenu {
13 |
14 | /**
15 | This view can list ``RichTextOtherMenu/Toggle``s for a list
16 | of ``RichTextOtherMenu`` values, in a horizontal stack.
17 |
18 | Since this view uses multiple styles, it binds directly
19 | to a ``RichTextContext`` instead of individual values.
20 | */
21 | public struct ToggleStack: View {
22 |
23 | /**
24 | Create a rich text style toggle button group.
25 |
26 | - Parameters:
27 | - context: The context to affect.
28 | - styles: The styles to list, by default ``RichTextOtherMenu/all``.
29 | - spacing: The spacing to apply to stack items, by default `5`.
30 | */
31 | public init(
32 | context: RichEditorState,
33 | styles: [RichTextOtherMenu] = .all,
34 | spacing: Double = 5
35 | ) {
36 | self._context = ObservedObject(wrappedValue: context)
37 | self.styles = styles
38 | self.spacing = spacing
39 | }
40 |
41 | private let styles: [RichTextOtherMenu]
42 | private let spacing: Double
43 |
44 | @ObservedObject
45 | private var context: RichEditorState
46 |
47 | public var body: some View {
48 | HStack(spacing: spacing) {
49 | ForEach(styles) {
50 | RichTextOtherMenu.Toggle(
51 | style: $0,
52 | context: context,
53 | fillVertically: true
54 | )
55 | }
56 | }
57 | .fixedSize(horizontal: false, vertical: true)
58 | }
59 | }
60 | }
61 | #endif
62 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextAction+ButtonStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAction+ButtonStack.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextAction {
11 |
12 | /**
13 | This view lists ``RichTextAction`` buttons in a stack.
14 |
15 | Since this view uses multiple values, it binds directly
16 | to a ``RichTextContext`` instead of individual values.
17 | */
18 | public struct ButtonStack: View {
19 |
20 | /**
21 | Create a rich text action button stack.
22 |
23 | - Parameters:
24 | - context: The context to affect.
25 | - actions: The actions to list, by default all non-size actions.
26 | - spacing: The spacing to apply to stack items, by default `5`.
27 | */
28 | public init(
29 | context: RichEditorState,
30 | actions: [RichTextAction],
31 | spacing: Double = 5
32 | ) {
33 | self._context = ObservedObject(wrappedValue: context)
34 | self.actions = actions
35 | self.spacing = spacing
36 | }
37 |
38 | private let actions: [RichTextAction]
39 | private let spacing: Double
40 |
41 | @ObservedObject
42 | private var context: RichEditorState
43 |
44 | public var body: some View {
45 | HStack(spacing: spacing) {
46 | ForEach(actions) {
47 | RichTextAction.Button(
48 | action: $0,
49 | context: context,
50 | fillVertically: true
51 | )
52 | .frame(maxHeight: .infinity)
53 | }
54 | }
55 | .fixedSize(horizontal: false, vertical: true)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Headers/RichTextHeader+Picker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextHeader+Picker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 25/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextHeader {
11 |
12 | /**
13 | This picker can be used to pick a Header type.
14 |
15 | The view returns a plain SwiftUI `Picker` view that can
16 | be styled and configured with plain SwiftUI.
17 |
18 | You can configure this picker by applying a config view
19 | modifier to your view hierarchy:
20 |
21 | ```swift
22 | VStack {
23 | RichTextHeader.HeaderTypePicker(...)
24 | ...
25 | }
26 | ```
27 | */
28 | public struct Picker: View {
29 |
30 | /**
31 | Create a font size picker.
32 |
33 | - Parameters:
34 | - selection: The selected font size.
35 | */
36 | public init(
37 | context: RichEditorState,
38 | values: [HeaderType]
39 | ) {
40 | self._selection = context.headerBinding()
41 | self.values = values
42 | }
43 |
44 | @Binding
45 | private var selection: HeaderType
46 |
47 | private let values: [HeaderType]
48 |
49 | public var body: some View {
50 | SwiftUI.Picker("", selection: $selection) {
51 | ForEach(
52 | values,
53 | id: \.self
54 | ) {
55 | text(for: $0)
56 | .tag($0)
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | extension RichTextHeader.Picker {
64 |
65 | fileprivate func text(
66 | for headerType: HeaderType
67 | ) -> some View {
68 | Text(headerType.titleLabel)
69 | .fixedSize(horizontal: true, vertical: false)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextStyle+ToggleStack.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextStyle {
11 |
12 | /**
13 | This view can list ``RichTextStyle/Toggle``s for a list
14 | of ``RichTextStyle`` values, in a horizontal stack.
15 |
16 | Since this view uses multiple styles, it binds directly
17 | to a ``RichTextContext`` instead of individual values.
18 | */
19 | public struct ToggleStack: View {
20 |
21 | /**
22 | Create a rich text style toggle button group.
23 |
24 | - Parameters:
25 | - context: The context to affect.
26 | - styles: The styles to list, by default ``RichTextStyle/all``.
27 | - spacing: The spacing to apply to stack items, by default `5`.
28 | */
29 | public init(
30 | context: RichEditorState,
31 | styles: [RichTextStyle] = .all,
32 | spacing: Double = 5
33 | ) {
34 | self._context = ObservedObject(wrappedValue: context)
35 | self.styles = styles
36 | self.spacing = spacing
37 | }
38 |
39 | private let styles: [RichTextStyle]
40 | private let spacing: Double
41 |
42 | @ObservedObject
43 | private var context: RichEditorState
44 |
45 | public var body: some View {
46 | HStack(spacing: spacing) {
47 | ForEach(styles) {
48 | RichTextStyle.Toggle(
49 | style: $0,
50 | context: context,
51 | fillVertically: true
52 | )
53 | }
54 | }
55 | .fixedSize(horizontal: false, vertical: true)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Actions/RichTextActionButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextActionButton.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextAction {
11 |
12 | /**
13 | This button can be used to trigger a ``RichTextAction``.
14 |
15 | This renders a plain `Button`, which means that you can
16 | use and configure it as a normal button.
17 | */
18 | public struct Button: View {
19 | /**
20 | Create a rich text action button.
21 |
22 | - Parameters:
23 | - action: The action to trigger.
24 | - context: The context to affect.
25 | - fillVertically: WhetherP or not fill up vertical space, by default `false`.
26 | */
27 | public init(
28 | action: RichTextAction,
29 | context: RichEditorState,
30 | fillVertically: Bool = false
31 | ) {
32 | self.action = action
33 | self._context = ObservedObject(wrappedValue: context)
34 | self.fillVertically = fillVertically
35 | }
36 |
37 | private let action: RichTextAction
38 | private let fillVertically: Bool
39 |
40 | @ObservedObject
41 | private var context: RichEditorState
42 |
43 | public var body: some View {
44 | SwiftUI.Button(action: triggerAction) {
45 | action.label
46 | .labelStyle(.iconOnly)
47 | .frame(maxHeight: fillVertically ? .infinity : nil)
48 | .contentShape(Rectangle())
49 | }
50 | .keyboardShortcut(for: action)
51 | .disabled(!context.canHandle(action))
52 | }
53 | }
54 | }
55 |
56 | extension RichTextAction.Button {
57 |
58 | fileprivate func triggerAction() {
59 | context.handle(action)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFormat+ToolbarStyle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextFormat {
12 |
13 | /// This type can be used to style a format toolbar.
14 | public struct ToolbarStyle {
15 |
16 | public init(
17 | padding: Double = 10,
18 | spacing: Double = 10
19 | ) {
20 | self.padding = padding
21 | self.spacing = spacing
22 | }
23 |
24 | public var padding: Double
25 | public var spacing: Double
26 | }
27 | }
28 |
29 | extension RichTextFormat.ToolbarStyle {
30 |
31 | /// The standard rich text format toolbar style.
32 | public static var standard: Self { .init() }
33 | }
34 |
35 | extension View {
36 |
37 | /// Apply a rich text format toolbar style.
38 | public func richTextFormatToolbarStyle(
39 | _ style: RichTextFormat.ToolbarStyle
40 | ) -> some View {
41 | self.environment(\.richTextFormatToolbarStyle, style)
42 | }
43 | }
44 |
45 | extension RichTextFormat.ToolbarStyle {
46 |
47 | fileprivate struct Key: EnvironmentKey {
48 |
49 | public static var defaultValue: RichTextFormat.ToolbarStyle {
50 | .standard
51 | }
52 | }
53 | }
54 |
55 | extension EnvironmentValues {
56 |
57 | /// This value can bind to a format toolbar style.
58 | public var richTextFormatToolbarStyle: RichTextFormat.ToolbarStyle {
59 | get { self[RichTextFormat.ToolbarStyle.Key.self] }
60 | set { self[RichTextFormat.ToolbarStyle.Key.self] = newValue }
61 | }
62 | }
63 | #endif
64 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Styles.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextViewComponent {
11 |
12 | /// Get all styles.
13 | public var richTextStyles: [RichTextStyle] {
14 | let attributes = richTextAttributes
15 | let traits = richTextFont?.fontDescriptor.symbolicTraits
16 | var styles = traits?.enabledRichTextStyles ?? []
17 | if attributes.isStrikethrough { styles.append(.strikethrough) }
18 | if attributes.isUnderlined { styles.append(.underline) }
19 | return styles
20 | }
21 |
22 | /// Whether or not the current range has a certain style.
23 | public func hasRichTextStyle(_ style: RichTextStyle) -> Bool {
24 | richTextStyles.contains(style)
25 | }
26 |
27 | /// Set a certain style.
28 | public func setRichTextStyle(
29 | _ style: RichTextStyle,
30 | to newValue: Bool
31 | ) {
32 | let value = newValue ? 1 : 0
33 | switch style {
34 | case .bold, .italic:
35 | let styles = richTextStyles
36 | guard styles.shouldAddOrRemove(style, newValue) else { return }
37 | guard let font = richTextFont else { return }
38 | guard let newFont = font.toggling(style) else { return }
39 | setRichTextFont(newFont)
40 | case .underline:
41 | setRichTextAttribute(.underlineStyle, to: value)
42 | case .strikethrough:
43 | setRichTextAttribute(.strikethroughStyle, to: value)
44 | }
45 | }
46 |
47 | /// Toggle a certain style.
48 | public func toggleRichTextStyle(
49 | _ style: RichTextStyle
50 | ) {
51 | let hasStyle = hasRichTextStyle(style)
52 | setRichTextStyle(style, to: !hasStyle)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextContext+Color.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichEditorState {
11 |
12 | /// Get a binding for a certain color.
13 | public func binding(for color: RichTextColor) -> Binding {
14 | Binding(
15 | get: { Color(self.color(for: color) ?? .clear) },
16 | set: { self.updateStyleFor(color, to: .init($0)) }
17 | )
18 | }
19 |
20 | /// Get the value for a certain color.
21 | public func color(for color: RichTextColor) -> ColorRepresentable? {
22 | colors[color]
23 | }
24 |
25 | /// Set the value for a certain color.
26 | public func setColor(
27 | _ color: RichTextColor,
28 | to val: ColorRepresentable
29 | ) {
30 | guard self.color(for: color) != val else { return }
31 | actionPublisher.send(.setColor(color, val))
32 | setColorInternal(color, to: val)
33 | }
34 |
35 | public func updateStyleFor(
36 | _ color: RichTextColor, to val: ColorRepresentable
37 | ) {
38 | let value = Color(val)
39 | switch color {
40 | case .foreground:
41 | updateStyle(style: .color(value))
42 | case .background:
43 | updateStyle(style: .background(value))
44 | case .strikethrough:
45 | return
46 | case .stroke:
47 | return
48 | case .underline:
49 | return
50 | }
51 | }
52 | }
53 |
54 | extension RichEditorState {
55 |
56 | /// Set the value for a certain color, or remove it.
57 | func setColorInternal(
58 | _ color: RichTextColor,
59 | to val: ColorRepresentable?
60 | ) {
61 | guard let val else {
62 | colors[color] = nil
63 | return
64 | }
65 | colors[color] = val
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Link.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Link.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/12/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextViewComponent {
11 | /// Get the paragraph style.
12 | public var richTextLink: String? {
13 | richTextAttribute(.link)
14 | }
15 |
16 | /// Get a certain link.
17 | public func richTextLink(
18 | _ style: RichTextSpanStyle
19 | ) -> String? {
20 | return richTextAttribute(style.attributedStringKey)
21 | }
22 |
23 | /// Get a certain link at a certain range.
24 | public func richTextLink(
25 | _ style: RichTextSpanStyle,
26 | at range: NSRange
27 | ) -> String? {
28 | return richTextAttribute(style.attributedStringKey, at: range)
29 | }
30 |
31 | /// Set a certain link.
32 | public func setRichTextLink(
33 | _ style: RichTextSpanStyle
34 | ) {
35 | guard let val = style.getRichAttribute()?.link,
36 | richTextLink(style) != val
37 | else { return }
38 | setRichTextAttribute(style.attributedStringKey, to: val)
39 |
40 | }
41 |
42 | /// Set a certain link at a certain range.
43 | public func setRichTextLink(
44 | _ style: RichTextSpanStyle,
45 | at range: NSRange
46 | ) {
47 | let val = style.getRichAttribute()?.link
48 | guard let val = val, richTextLink(style, at: range) != val else {
49 | return
50 | }
51 | setRichTextAttribute(style.attributedStringKey, to: val, at: range)
52 | }
53 |
54 | public func removeRichTextLink(_ style: RichTextSpanStyle) {
55 | removeRichTextAttribute(style.attributedStringKey)
56 | }
57 |
58 | public func removeRichTextLink(
59 | _ style: RichTextSpanStyle, at range: NSRange
60 | ) {
61 | removeRichTextAttribute(style.attributedStringKey, at: range)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Pasting.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 | #endif
13 |
14 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
15 | import AppKit
16 | #endif
17 |
18 | extension RichTextViewComponent {
19 |
20 | /**
21 | Paste text into the text view, at a certain index.
22 |
23 | - Parameters:
24 | - text: The text to paste.
25 | - index: The text index to paste at.
26 | - moveCursorToPastedContent: Whether or not the input
27 | cursor should be moved to the end of the pasted content,
28 | by default `false`.
29 | */
30 | public func pasteText(
31 | _ text: String,
32 | at index: Int,
33 | moveCursorToPastedContent: Bool = false
34 | ) {
35 | let selected = selectedRange
36 | let isSelectedRange = (index == selected.location)
37 | let content = NSMutableAttributedString(attributedString: richText)
38 | let insertString = NSMutableAttributedString(string: text)
39 | let insertRange = NSRange(location: index, length: 0)
40 | let safeInsertRange = safeRange(for: insertRange)
41 | let safeMoveIndex = safeInsertRange.location + insertString.length
42 | let attributes = content.richTextAttributes(at: safeInsertRange)
43 | let attributeRange = NSRange(location: 0, length: insertString.length)
44 | let safeAttributeRange = safeRange(for: attributeRange)
45 | insertString.setRichTextAttributes(attributes, at: safeAttributeRange)
46 | content.insert(insertString, at: index)
47 | setRichText(content)
48 | if moveCursorToPastedContent {
49 | moveInputCursor(to: safeMoveIndex)
50 | } else if isSelectedRange {
51 | moveInputCursor(to: selected.location + text.count)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePickerConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont+SizePickerConfig.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextFont {
11 |
12 | /// This type can configure a ``RichTextFont/SizePicker``.
13 | public struct SizePickerConfig {
14 |
15 | /// Create a custom font size picker config.
16 | ///
17 | /// - Parameters:
18 | /// - values: The values to display in the list, by default a standard list.
19 | public init(
20 | values: [CGFloat] = [
21 | 10, 12, 14, 16, 18, 20, 22, 24, 28, 36, 48, 64, 72, 96, 144,
22 | ]
23 | ) {
24 | self.values = values
25 | }
26 |
27 | /// The values to display in the list.
28 | public var values: [CGFloat]
29 | }
30 | }
31 |
32 | extension RichTextFont.SizePickerConfig {
33 |
34 | /// The standard font size picker configuration.
35 | ///
36 | /// You can set a new value to change the global default.
37 | public static var standard = Self()
38 | }
39 |
40 | extension View {
41 |
42 | /// Apply a ``RichTextFont`` size picker configuration.
43 | public func richTextFontSizePickerConfig(
44 | _ config: RichTextFont.SizePickerConfig
45 | ) -> some View {
46 | self.environment(\.richTextFontSizePickerConfig, config)
47 | }
48 | }
49 |
50 | extension RichTextFont.SizePickerConfig {
51 |
52 | fileprivate struct Key: EnvironmentKey {
53 |
54 | public static var defaultValue: RichTextFont.SizePickerConfig =
55 | .standard
56 | }
57 | }
58 |
59 | extension EnvironmentValues {
60 |
61 | /// This value can bind to a font size picker config.
62 | public var richTextFontSizePickerConfig: RichTextFont.SizePickerConfig {
63 | get { self[RichTextFont.SizePickerConfig.Key.self] }
64 | set { self[RichTextFont.SizePickerConfig.Key.self] = newValue }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/BaseFoundation/RichTextPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextPresenter.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol can be implemented any types that can present
11 | /// a rich text and provide a ``selectedRange``.
12 | ///
13 | /// This protocol is implemented by ``RichTextEditor`` since it
14 | /// can both present and select text. It is also implemented by
15 | /// the platform-specific ``RichTextView`` components.
16 | public protocol RichTextPresenter: RichTextReader {
17 |
18 | /// Get the currently selected range.
19 | var selectedRange: NSRange { get }
20 | }
21 |
22 | extension RichTextPresenter {
23 |
24 | /// Whether or not the presenter has a selected range.
25 | public var hasSelectedRange: Bool {
26 | selectedRange.length > 0
27 | }
28 |
29 | /// Whether or not the rich text contains trimmed text.
30 | public var hasTrimmedText: Bool {
31 | let string = richText.string
32 | let trimmed = string.trimmingCharacters(in: .whitespaces)
33 | return !trimmed.isEmpty
34 | }
35 |
36 | /// Get the range after the input cursor.
37 | public var rangeAfterInputCursor: NSRange {
38 | let location = selectedRange.location
39 | let length = richText.length - location
40 | return NSRange(location: location, length: length)
41 | }
42 |
43 | /// Get the range before the input cursor.
44 | public var rangeBeforeInputCursor: NSRange {
45 | let location = selectedRange.location
46 | return NSRange(location: 0, length: location)
47 | }
48 |
49 | /// Get the rich text after the input cursor.
50 | public var richTextAfterInputCursor: NSAttributedString {
51 | richText(at: rangeAfterInputCursor)
52 | }
53 |
54 | /// Get the rich text before the input cursor.
55 | public var richTextBeforeInputCursor: NSAttributedString {
56 | richText(at: rangeBeforeInputCursor)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextOtherMenu.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public enum RichTextOtherMenu: String, CaseIterable, Identifiable,
11 | RichTextLabelValue
12 | {
13 | case link
14 |
15 | }
16 |
17 | extension RichTextOtherMenu {
18 |
19 | /// All available rich text styles.
20 | public static var all: [Self] { allCases }
21 | }
22 |
23 | extension Collection where Element == RichTextOtherMenu {
24 |
25 | /// All available rich text styles.
26 | public static var all: [RichTextOtherMenu] { RichTextOtherMenu.allCases }
27 | }
28 |
29 | extension RichTextOtherMenu {
30 |
31 | public var id: String { rawValue }
32 |
33 | /// The standard icon to use for the trait.
34 | public var icon: Image {
35 | switch self {
36 | case .link: .richTextLink
37 | }
38 | }
39 |
40 | /// The localized style title.
41 | public var title: String {
42 | titleKey.text
43 | }
44 |
45 | /// The localized style title key.
46 | public var titleKey: RTEL10n {
47 | switch self {
48 | case .link: .link
49 | }
50 | }
51 | }
52 |
53 | extension Collection where Element == RichTextOtherMenu {
54 |
55 | /// Check if the collection contains a certain style.
56 | public func hasStyle(_ style: RichTextOtherMenu) -> Bool {
57 | contains(style)
58 | }
59 |
60 | /// Check if a certain style change should be applied.
61 | public func shouldAddOrRemove(
62 | _ style: RichTextOtherMenu,
63 | _ newValue: Bool
64 | ) -> Bool {
65 | let shouldAdd = newValue && !hasStyle(style)
66 | let shouldRemove = !newValue && hasStyle(style)
67 | return shouldAdd || shouldRemove
68 | }
69 | }
70 |
71 | extension RichTextOtherMenu {
72 | func richTextSpanStyle() -> RichTextSpanStyle {
73 | switch self {
74 | case .link: .link()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Attributes/RichTextWriter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextWriter.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol extends ``RichTextReader`` and is implemented
11 | /// by types that can provide a writable rich text string.
12 | ///
13 | /// This protocol is implemented by `NSMutableAttributedString`
14 | /// as well as other types in the library.
15 | public protocol RichTextWriter: RichTextReader {
16 |
17 | /// Get the writable attributed string for the type.
18 | var mutableAttributedString: NSMutableAttributedString? { get }
19 | }
20 |
21 | extension NSMutableAttributedString: RichTextWriter {
22 |
23 | /// This type returns itself as the attributed string.
24 | public var mutableAttributedString: NSMutableAttributedString? {
25 | self
26 | }
27 | }
28 |
29 | extension RichTextWriter {
30 |
31 | /**
32 | Get the writable rich text provided by the implementing
33 | type.
34 |
35 | This is an alias for ``mutableAttributedString`` and is
36 | used to get a property that uses the rich text naming.
37 | */
38 | public var mutableRichText: NSMutableAttributedString? {
39 | mutableAttributedString
40 | }
41 |
42 | /**
43 | Replace the text in a certain range with a new string.
44 |
45 | - Parameters:
46 | - range: The range to replace text in.
47 | - string: The string to replace the current text with.
48 | */
49 | public func replaceText(in range: NSRange, with string: String) {
50 | mutableRichText?.replaceCharacters(in: range, with: string)
51 | }
52 |
53 | /**
54 | Replace the text in a certain range with a new string.
55 |
56 | - Parameters:
57 | - range: The range to replace text in.
58 | - string: The string to replace the current text with.
59 | */
60 | public func replaceText(in range: NSRange, with string: NSAttributedString)
61 | {
62 | mutableRichText?.replaceCharacters(in: range, with: string)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_UIKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextView+Config_UIKit.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextView {
12 |
13 | /**
14 | This type can be used to configure a ``RichTextEditor``.
15 | */
16 | public struct Configuration {
17 |
18 | /**
19 | Create a custom configuration.
20 |
21 | - Parameters:
22 | - isScrollingEnabled: Whether or not the editor should scroll, by default `true`.
23 | - allowsEditingTextAttributes: If editor allows editing text attributes, by default `true`.
24 | - autocapitalizationType: Type of Auto capitalization, default is to `.sentences`.
25 | - spellCheckingType: Whether textView spell-Checks, default is `.no`.
26 | */
27 | public init(
28 | isScrollingEnabled: Bool = true,
29 | allowsEditingTextAttributes: Bool = true,
30 | autocapitalizationType: UITextAutocapitalizationType =
31 | .sentences,
32 | spellCheckingType: UITextSpellCheckingType = .no
33 | ) {
34 | self.isScrollingEnabled = isScrollingEnabled
35 | self.allowsEditingTextAttributes = allowsEditingTextAttributes
36 | self.autocapitalizationType = autocapitalizationType
37 | self.spellCheckingType = spellCheckingType
38 | }
39 |
40 | /// Whether or not the editor should scroll.
41 | public var isScrollingEnabled: Bool
42 |
43 | /// Whether textView allows editting text attributes
44 | public var allowsEditingTextAttributes: Bool
45 |
46 | /// Kind of auto capitalization
47 | public var autocapitalizationType: UITextAutocapitalizationType
48 |
49 | /// If TextView spell-checks the text.
50 | public var spellCheckingType: UITextSpellCheckingType
51 | }
52 | }
53 | #endif
54 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/RichEditorSwiftUITests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
17 |
19 |
25 |
26 |
27 |
28 |
29 |
39 |
40 |
46 |
47 |
49 |
50 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Ranges.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Ranges.swift
3 | // RichTextKit
4 | //
5 | // Created by Dominik Bucher
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextViewComponent {
11 |
12 | var notFoundRange: NSRange {
13 | .init(location: NSNotFound, length: 0)
14 | }
15 |
16 | /// Get the line range at a certain text location.
17 | func lineRange(at location: Int) -> NSRange {
18 | #if os(watchOS)
19 | return notFoundRange
20 | #else
21 | guard
22 | let manager = layoutManagerWrapper,
23 | let storage = textStorageWrapper
24 | else { return NSRange(location: NSNotFound, length: 0) }
25 | let string = storage.string as NSString
26 | let locationRange = NSRange(location: location, length: 0)
27 | let lineRange = string.lineRange(for: locationRange)
28 | return manager.characterRange(
29 | forGlyphRange: lineRange, actualGlyphRange: nil)
30 | #endif
31 | }
32 |
33 | /// Get the line range for a certain text range.
34 | func lineRange(for range: NSRange) -> NSRange {
35 | #if os(watchOS)
36 | return notFoundRange
37 | #else
38 | // Use the location-based logic if range is empty
39 | if range.length == 0 {
40 | return lineRange(at: range.location)
41 | }
42 |
43 | guard let manager = layoutManagerWrapper else {
44 | return NSRange(location: NSNotFound, length: 0)
45 | }
46 |
47 | var lineRange = NSRange(location: NSNotFound, length: 0)
48 | manager.enumerateLineFragments(
49 | forGlyphRange: range
50 | ) { (_, _, _, glyphRange, stop) in
51 | lineRange = glyphRange
52 | stop.pointee = true
53 | }
54 |
55 | // Convert glyph range to character range
56 | return manager.characterRange(
57 | forGlyphRange: lineRange, actualGlyphRange: nil)
58 | #endif
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Actions/RichTextAction+KeyboardShortcutModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAction+KeyboardShortcutModifier.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 30/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextAction {
11 |
12 | /**
13 | This view modifier can apply keyboard shortcuts for any
14 | ``RichTextAction`` to any view.
15 |
16 | You can also apply it with the `.keyboardShortcut(for:)`
17 | view modifier.
18 | */
19 | public struct KeyboardShortcutModifier: ViewModifier {
20 |
21 | public init(_ action: RichTextAction) {
22 | self.action = action
23 | }
24 |
25 | private let action: RichTextAction
26 |
27 | public func body(content: Content) -> some View {
28 | content.keyboardShortcut(for: action)
29 | }
30 | }
31 | }
32 |
33 | extension View {
34 |
35 | /// Apply a ``RichTextAction/KeyboardShortcutModifier``.
36 | @ViewBuilder
37 | public func keyboardShortcut(for action: RichTextAction) -> some View {
38 | #if os(iOS) || os(macOS) || os(visionOS)
39 | switch action {
40 | case .copy: keyboardShortcut("c", modifiers: .command)
41 | case .dismissKeyboard: self
42 | case .print: keyboardShortcut("p", modifiers: .command)
43 | case .redoLatestChange:
44 | keyboardShortcut("z", modifiers: [.command, .shift])
45 | // case .setAlignment(let align): keyboardShortcut(for: align)
46 | case .stepFontSize(let points):
47 | keyboardShortcut(points < 0 ? "-" : "+", modifiers: .command)
48 | case .stepIndent(let steps):
49 | keyboardShortcut(steps < 0 ? "Ö" : "Ä", modifiers: .command)
50 | case .stepSuperscript: self
51 | // case .toggleStyle(let style): keyboardShortcut(for: style)
52 | case .undoLatestChange: keyboardShortcut("z", modifiers: .command)
53 | default: self // TODO: Probably not defined, object to discuss.
54 | }
55 | #else
56 | self
57 | #endif
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/FontDescriptorRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontDescriptorRepresentable.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 17/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
11 | import AppKit
12 |
13 | /// This typealias bridges platform-specific font descriptors.
14 | ///
15 | /// The typealias also defines additional functionality as type
16 | /// extensions for the platform-specific types.
17 | public typealias FontDescriptorRepresentable = NSFontDescriptor
18 |
19 | extension FontDescriptorRepresentable {
20 |
21 | /// Get a new font descriptor by toggling a text style.
22 | public func byTogglingStyle(_ style: RichTextStyle)
23 | -> FontDescriptorRepresentable
24 | {
25 | guard let traits = style.symbolicTraits else { return self }
26 | if symbolicTraits.contains(traits) {
27 | return withSymbolicTraits(symbolicTraits.subtracting(traits))
28 | } else {
29 | return withSymbolicTraits(symbolicTraits.union(traits))
30 | }
31 | }
32 | }
33 | #endif
34 |
35 | #if canImport(UIKit)
36 | import UIKit
37 |
38 | /// This typealias bridges platform-specific font descriptors.
39 | ///
40 | /// The typealias also defines additional functionality as type
41 | /// extensions for the platform-specific types.
42 | public typealias FontDescriptorRepresentable = UIFontDescriptor
43 |
44 | extension FontDescriptorRepresentable {
45 |
46 | /// Get a new font descriptor by toggling a text style.
47 | public func byTogglingStyle(_ style: RichTextStyle)
48 | -> FontDescriptorRepresentable
49 | {
50 | guard let traits = style.symbolicTraits else { return self }
51 | if symbolicTraits.contains(traits) {
52 | return withSymbolicTraits(symbolicTraits.subtracting(traits))
53 | ?? self
54 | } else {
55 | return withSymbolicTraits(symbolicTraits.union(traits)) ?? self
56 | }
57 | }
58 | }
59 | #endif
60 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextExportService.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol can be implemented by any classes that can be
11 | /// used to export rich text to files.
12 | @preconcurrency @MainActor
13 | public protocol RichTextExportService: AnyObject {
14 |
15 | /**
16 | Generate an export file with a certain name and content,
17 | that uses a certain rich text data format.
18 |
19 | - Parameters:
20 | - fileName: The preferred file name.
21 | - content: The rich text content to export.
22 | - format: The rich text format to use when exporting.
23 | */
24 | func generateExportFile(
25 | withName fileName: String,
26 | content: NSAttributedString,
27 | format: RichTextDataFormat
28 | ) throws -> URL
29 |
30 | /**
31 | Generate a PDF export file with a certain name and rich
32 | text content.
33 |
34 | - Parameters:
35 | - fileName: The preferred file name.
36 | - content: The rich text content to export.
37 | */
38 | func generatePdfExportFile(
39 | withName fileName: String,
40 | content: NSAttributedString
41 | ) throws -> URL
42 |
43 | /**
44 | Generate a JSON export file with a certain name and rich
45 | text content.
46 |
47 | - Parameters:
48 | - fileName: The preferred file name.
49 | - content: The rich text (`RichText`) content to export.
50 | */
51 | func generateJsonExportFile(
52 | withName fileName: String,
53 | content: RichText
54 | ) throws -> URL
55 |
56 | /**
57 | Get `Data` for with provided `RichTextDataFormat`
58 | */
59 | func getDataFor(_ string: NSAttributedString, format: RichTextDataFormat)
60 | throws -> Data
61 |
62 | /**
63 | Get `Data` for `PDF` format.
64 | */
65 | func getDataForPdfFormat(_ string: NSAttributedString) throws -> Data
66 | /**
67 | Get `Data` for `JSON` format.
68 | */
69 | func getDataForJsonFormat(_ richText: RichText) throws -> Data
70 | }
71 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yaml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL Advanced"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | branches: [ "main" ]
19 | schedule:
20 | - cron: '30 19 * * 3'
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze (${{ matrix.language }})
25 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
26 | permissions:
27 | # required for all workflows
28 | security-events: write
29 |
30 | # required to fetch internal or private CodeQL packs
31 | packages: read
32 |
33 | # only required for workflows in private repositories
34 | actions: read
35 | contents: read
36 |
37 | strategy:
38 | fail-fast: false
39 | matrix:
40 | include:
41 | - language: swift
42 | build-mode: autobuild
43 | steps:
44 | - name: Checkout repository
45 | uses: actions/checkout@v4
46 |
47 | # Initializes the CodeQL tools for scanning.
48 | - name: Initialize CodeQL
49 | uses: github/codeql-action/init@v3
50 | with:
51 | languages: ${{ matrix.language }}
52 | build-mode: ${{ matrix.build-mode }}
53 | - if: matrix.build-mode == 'manual'
54 | shell: bash
55 | run: |
56 | echo 'If you are using a "manual" build mode for one or more of the' \
57 | 'languages you are analyzing, replace this with the commands to build' \
58 | 'your code, for example:'
59 | echo ' make bootstrap'
60 | echo ' make release'
61 | exit 1
62 |
63 | - name: Perform CodeQL Analysis
64 | uses: github/codeql-action/analyze@v3
65 | with:
66 | category: "/language:${{matrix.language}}"
67 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont+ListPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont+ListPicker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextFont {
11 |
12 | /**
13 | This view uses a `List` to list a set of fonts of which
14 | one can be selected.
15 |
16 | Unlike ``RichTextFont/Picker`` this picker presents all
17 | pickers with proper previews on all platforms. You must
18 | therefore add it ina way that gives it space.
19 |
20 | You can configure this picker by applying a config view
21 | modifier to your view hierarchy:
22 |
23 | ```swift
24 | VStack {
25 | RichTextFont.ListPicker(...)
26 | ...
27 | }
28 | .richTextFontPickerConfig(...)
29 | ```
30 | */
31 | public struct ListPicker: View {
32 |
33 | /**
34 | Create a font list picker.
35 |
36 | - Parameters:
37 | - selection: The selected font name.
38 | */
39 | public init(
40 | selection: Binding
41 | ) {
42 | self._selection = selection
43 | }
44 |
45 | public typealias Config = RichTextFont.PickerConfig
46 | public typealias Font = Config.Font
47 | public typealias FontName = Config.FontName
48 |
49 | @Binding
50 | private var selection: FontName
51 |
52 | @Environment(\.richTextFontPickerConfig)
53 | private var config
54 |
55 | public var body: some View {
56 | let font = Binding(
57 | get: { Font(fontName: selection) },
58 | set: { selection = $0.fontName }
59 | )
60 |
61 | RichEditorSwiftUI.ListPicker(
62 | items: config.fontsToList(for: selection),
63 | selection: font,
64 | dismissAfterPick: config.dismissAfterPick
65 | ) { font, isSelected in
66 | RichTextFont.PickerItem(
67 | font: font,
68 | fontSize: config.fontSize,
69 | isSelected: isSelected
70 | )
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextViewComponent+Attributes.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichTextViewComponent {
11 |
12 | /// Get all attributes.
13 | public var richTextAttributes: RichTextAttributes {
14 | if hasSelectedRange {
15 | return richTextAttributes(at: selectedRange)
16 | }
17 |
18 | #if os(macOS)
19 | let range = NSRange(location: selectedRange.location - 1, length: 1)
20 | let safeRange = safeRange(for: range)
21 | return richTextAttributes(at: safeRange)
22 | #else
23 | return typingAttributes
24 | #endif
25 | }
26 |
27 | /// Get a certain attribute.
28 | public func richTextAttribute(
29 | _ attribute: RichTextAttribute
30 | ) -> Value? {
31 | richTextAttributes[attribute] as? Value
32 | }
33 |
34 | /// Set a certain attribute.
35 | public func setRichTextAttribute(
36 | _ attribute: RichTextAttribute,
37 | to value: Any
38 | ) {
39 | if hasSelectedRange {
40 | setRichTextAttribute(attribute, to: value, at: selectedRange)
41 | } else {
42 | typingAttributes[attribute] = value
43 | }
44 | }
45 |
46 | /// Set certain attributes.
47 | public func setRichTextAttributes(
48 | _ attributes: RichTextAttributes
49 | ) {
50 | attributes.forEach { attribute, value in
51 | setRichTextAttribute(attribute, to: value)
52 | }
53 | }
54 |
55 | public func setNewRichTextAttributes(
56 | _ attributes: RichTextAttributes
57 | ) {
58 | typingAttributes = attributes
59 | }
60 |
61 | /// Remove a certain attribute.
62 | public func removeRichTextAttribute(
63 | _ attribute: RichTextAttribute
64 | ) {
65 | if hasSelectedRange {
66 | removeRichTextAttribute(attribute, at: selectedRange)
67 | } else {
68 | typingAttributes[attribute] = nil
69 | }
70 | }
71 |
72 | /// Remove certain attributes.
73 | public func removeRichTextAttributes(
74 | _ attributes: RichTextAttributes
75 | ) {
76 | attributes.forEach { attribute, value in
77 | removeRichTextAttribute(attribute)
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing
3 | Thank you so much for your interest in contributing! All types of contributions are encouraged and valued. The Project Team looks forward to your contributions.
4 |
5 | ## Filing issues
6 | When in doubt, file an issue. We'd rather close a few duplicate issues than let a problem go unnoticed.
7 | Similarly, if you support a particular feature request, please let us know by commenting on the issue or [subscribing](https://help.github.com/articles/subscribing-to-conversations/) to the issue.
8 |
9 | If you are reporting a bug, please help speed up problem diagnosis by providing as much information as possible. Ideally, that would include a small sample project (or gist) that reproduces the problem.
10 |
11 |
12 | ## Contributing code
13 | We actively welcome your pull requests. You can find instructions on building the project in [README.md](https://github.com/canopas/rich-editor-swiftui).
14 | 1. Fork the repo and create your branch from `main`.
15 | 2. If you've added code that should be tested, add tests
16 | 4. Make sure your code lints.
17 |
18 | ## Labels
19 | Labels on issues are managed by contributors, you don't have to worry about them. Here's a list of what they mean:
20 |
21 | * **bug**: feature that should work, but doesn't
22 | * **enhancement**: minor tweak/addition to existing behaviour
23 | * **feature**: new behaviour, bigger than enhancement
24 | * **question**: no need of any fix, usually a usage problem
25 | * **reproducible**: has enough information to very easily reproduce, mostly in the form of a small project in a GitHub repo
26 | * **repro-needed**: we need some code to be able to reproduce and debug locally, otherwise there's not much we can do
27 | * **duplicate**: there's another issue which already covers/tracks this
28 | * **wontfix**: working as intended, or won't be fixed due to compatibility or other reasons
29 | * **invalid**: there isn't enough information to make a verdict, or unrelated
30 | * **non-library**: issue is not in the core library code, but rather in documentation, samples, build process, releases
31 |
32 | ## License
33 | By contributing to RichEditorSwiftUI, you agree that your contributions will be licensed under its Apache License, Version 2.0. See LICENSE file for details.
34 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Extensions/String+Characters.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Characters.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 01/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String.Element {
11 |
12 | /// Get the string element for a `\r` carriage return.
13 | static var carriageReturn: String.Element { "\r" }
14 |
15 | /// Get the string element for a `\n` newline.
16 | static var newLine: String.Element { "\n" }
17 |
18 | /// Get the string element for a `\t` tab.
19 | static var tab: String.Element { "\t" }
20 |
21 | /// Get the string element for a ` ` space.
22 | static var space: String.Element { " " }
23 | }
24 |
25 | extension String {
26 |
27 | /// Get the string for a `\r` carriage return.
28 | static let carriageReturn = String(.carriageReturn)
29 |
30 | /// Get the string for a `\n` newline.
31 | static let newLine = String(.newLine)
32 |
33 | /// Get the string for a `\t` tab.
34 | static let tab = String(.tab)
35 |
36 | /// Get the string for a ` ` space.
37 | static let space = String(.space)
38 | }
39 |
40 | extension String {
41 | func getHeaderRangeFor(_ range: NSRange) -> NSRange {
42 | let text = self
43 | guard !text.isEmpty else { return range }
44 |
45 | let fromIndex = range.lowerBound
46 | let toIndex = range.isCollapsed ? fromIndex : range.upperBound
47 |
48 | let newLineStartIndex =
49 | text.utf16.prefix(fromIndex).map({ $0 }).lastIndex(
50 | of: "\n".utf16.last) ?? 0
51 | let newLineEndIndex = text.utf16.suffix(
52 | from: text.utf16.index(
53 | text.utf16.startIndex, offsetBy: max(0, toIndex - 1))
54 | ).map({ $0 }).firstIndex(of: "\n".utf16.last)
55 |
56 | let shouldAddOneIndex = newLineStartIndex != 0
57 | let startIndex = min(
58 | max(0, self.utf16Length),
59 | max(0, newLineStartIndex + (shouldAddOneIndex ? 1 : 0)))
60 | var endIndex = (toIndex) + (newLineEndIndex ?? 0)
61 |
62 | if newLineEndIndex == nil {
63 | endIndex = (text.utf16Length)
64 | }
65 |
66 | let range = startIndex...endIndex
67 | return range.nsRange
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont+SizePicker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextFont {
11 |
12 | /**
13 | This picker can be used to pick a font size.
14 |
15 | The view returns a plain SwiftUI `Picker` view that can
16 | be styled and configured with plain SwiftUI.
17 |
18 | You can configure this picker by applying a config view
19 | modifier to your view hierarchy:
20 |
21 | ```swift
22 | VStack {
23 | RichTextFont.SizePicker(...)
24 | ...
25 | }
26 | .richTextFontSizePickerConfig(...)
27 | ```
28 | */
29 | public struct SizePicker: View {
30 |
31 | /**
32 | Create a font size picker.
33 |
34 | - Parameters:
35 | - selection: The selected font size.
36 | */
37 | public init(
38 | selection: Binding
39 | ) {
40 | self._selection = selection
41 | }
42 |
43 | @Binding
44 | private var selection: CGFloat
45 |
46 | @Environment(\.richTextFontSizePickerConfig)
47 | private var config
48 |
49 | public var body: some View {
50 | SwiftUI.Picker("", selection: $selection) {
51 | ForEach(
52 | values(
53 | for: config.values,
54 | selection: selection
55 | ), id: \.self
56 | ) {
57 | text(for: $0)
58 | .tag($0)
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | extension RichTextFont.SizePicker {
66 |
67 | /// Get a list of values for a certain selection.
68 | public func values(
69 | for values: [CGFloat],
70 | selection: CGFloat
71 | ) -> [CGFloat] {
72 | let values = values + [selection]
73 | return Array(Set(values)).sorted()
74 | }
75 | }
76 |
77 | extension RichTextFont.SizePicker {
78 |
79 | fileprivate func text(
80 | for fontSize: CGFloat
81 | ) -> some View {
82 | Text("\(Int(fontSize))")
83 | .fixedSize(horizontal: true, vertical: false)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/ListPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListPicker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is an internal version of the original that is defined
11 | /// and available in https://github.com/danielsaidi/swiftuikit.
12 | /// This will not be made public or documented for this library.
13 | struct ListPicker: View {
14 |
15 | init(
16 | items: [Item],
17 | selection: Binding- ,
18 | animatedSelection: Bool = false,
19 | dismissAfterPick: Bool = true,
20 | listItem: @escaping ItemViewBuilder
21 | ) {
22 | self.init(
23 | sections: [ListPickerSection(title: "", items: items)],
24 | selection: selection,
25 | animatedSelection: animatedSelection,
26 | dismissAfterPick: dismissAfterPick,
27 | listItem: listItem)
28 | }
29 |
30 | init(
31 | sections: [ListPickerSection
- ],
32 | selection: Binding
- ,
33 | animatedSelection: Bool = false,
34 | dismissAfterPick: Bool = true,
35 | listItem: @escaping ItemViewBuilder
36 | ) {
37 | self.sections = sections
38 | self.selection = selection
39 | self.animatedSelection = animatedSelection
40 | self.dismissAfterPick = dismissAfterPick
41 | self.listItem = listItem
42 | }
43 |
44 | private let sections: [ListPickerSection
- ]
45 | private let selection: Binding
-
46 | private let animatedSelection: Bool
47 | private let dismissAfterPick: Bool
48 | private let listItem: ItemViewBuilder
49 |
50 | typealias ItemViewBuilder = (_ item: Item, _ isSelected: Bool) -> ItemView
51 |
52 | var body: some View {
53 | List {
54 | ForEach(sections) { section in
55 | Section(header: section.header) {
56 | ForEachPicker(
57 | items: section.items,
58 | selection: selection,
59 | animatedSelection: animatedSelection,
60 | dismissAfterPick: dismissAfterPick,
61 | listItem: listItem
62 | )
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAttributeReader.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol extends ``RichTextReader`` with functionality
11 | /// for reading rich text attributes for the current rich text.
12 | ///
13 | /// The protocol is implemented by `NSAttributedString` as well
14 | /// as other types in the library.
15 | public protocol RichTextAttributeReader: RichTextReader {}
16 |
17 | extension NSAttributedString: RichTextAttributeReader {}
18 |
19 | extension RichTextAttributeReader {
20 |
21 | /// Get a rich text attribute at a certain range.
22 | public func richTextAttribute(
23 | _ attribute: RichTextAttribute,
24 | at range: NSRange
25 | ) -> Value? {
26 | richTextAttributes(at: range)[attribute] as? Value
27 | }
28 |
29 | /// Get all rich text attributes at a certain range.
30 | public func richTextAttributes(
31 | at range: NSRange
32 | ) -> RichTextAttributes {
33 | if richText.string.utf16Length == 0 { return [:] }
34 | let range = safeRange(for: range, isAttributeOperation: true)
35 | return richText.attributes(at: range.location, effectiveRange: nil)
36 | }
37 | }
38 |
39 | // RichTextAttributeReader+Font
40 | extension RichTextAttributeReader {
41 |
42 | /// Get the font at a certain range.
43 | public func richTextFont(at range: NSRange) -> FontRepresentable? {
44 | richTextAttribute(.font, at: range)
45 | }
46 |
47 | /// Get the font size (in points) at a certain range.
48 | public func richTextFontSize(at range: NSRange) -> CGFloat? {
49 | richTextFont(at: range)?.pointSize
50 | }
51 | }
52 |
53 | // RichTextAttributeReader+Style
54 | extension RichTextAttributeReader {
55 |
56 | /// Get the text styles at a certain range.
57 | public func richTextStyles(at range: NSRange) -> [RichTextSpanStyle] {
58 | let attributes = richTextAttributes(at: range)
59 | let traits = richTextFont(at: range)?.fontDescriptor.symbolicTraits
60 | var styles = traits?.enabledRichTextStyles ?? []
61 | if attributes.isStrikethrough { styles.append(.strikethrough) }
62 | if attributes.isUnderlined { styles.append(.underline) }
63 | return styles.map({ $0.richTextSpanStyle })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextStyle+ToggleGroup.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextStyle {
12 |
13 | /**
14 | This view can list ``RichTextStyle/Toggle``s for a list
15 | of ``RichTextStyle`` values, in a bordered button group.
16 |
17 | Since this view uses multiple styles, it binds directly
18 | to a ``RichTextContext`` instead of individual values.
19 |
20 | > Important: Since the `ControlGroup` doesn't highlight
21 | buttons in iOS, we use a `ToggleStack` for iOS.
22 | */
23 | public struct ToggleGroup: View {
24 |
25 | /**
26 | Create a rich text style toggle button group.
27 |
28 | - Parameters:
29 | - context: The context to affect.
30 | - styles: The styles to list, by default ``RichTextStyle/all``.
31 | - greedy: Whether or not the group is horizontally greedy, by default `true`.
32 | */
33 | public init(
34 | context: RichEditorState,
35 | styles: [RichTextStyle] = .all,
36 | greedy: Bool = true
37 | ) {
38 | self._context = ObservedObject(wrappedValue: context)
39 | self.isGreedy = greedy
40 | self.styles = styles
41 | }
42 |
43 | private let styles: [RichTextStyle]
44 | private let isGreedy: Bool
45 |
46 | private var groupWidth: CGFloat? {
47 | if isGreedy { return nil }
48 | let count = Double(styles.count)
49 | #if os(macOS)
50 | return 30 * count
51 | #else
52 | return 50 * count
53 | #endif
54 | }
55 |
56 | @ObservedObject
57 | private var context: RichEditorState
58 |
59 | public var body: some View {
60 | #if os(macOS)
61 | ControlGroup {
62 | ForEach(styles) {
63 | RichTextStyle.Toggle(
64 | style: $0,
65 | context: context,
66 | fillVertically: true
67 | )
68 | }
69 | }
70 | .frame(width: groupWidth)
71 | #else
72 | RichTextStyle.ToggleStack(
73 | context: context,
74 | styles: styles
75 | )
76 | #endif
77 | }
78 | }
79 | }
80 | #endif
81 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextSpanInternal.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 12/10/23.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct RichTextSpanInternal {
11 | public let id: String
12 | public let from: Int
13 | public let to: Int
14 | // public let insert: String
15 | public let attributes: RichAttributes?
16 |
17 | public init(
18 | id: String = UUID().uuidString,
19 | from: Int,
20 | to: Int,
21 | // insert: String,
22 | attributes: RichAttributes? = RichAttributes()
23 | ) {
24 | self.id = id
25 | self.from = from
26 | self.to = to
27 | // self.insert = insert
28 | self.attributes = attributes
29 | }
30 | }
31 |
32 | extension RichTextSpanInternal: Equatable {
33 | public static func == (
34 | lhs: RichTextSpanInternal,
35 | rhs: RichTextSpanInternal
36 | ) -> Bool {
37 | return lhs.from == rhs.from
38 | && lhs.to == rhs.to
39 | // && lhs.insert == rhs.insert
40 | && lhs.attributes == rhs.attributes
41 | }
42 | }
43 |
44 | extension RichTextSpanInternal: Hashable {
45 | public func hash(into hasher: inout Hasher) {
46 | hasher.combine(from)
47 | hasher.combine(to)
48 | // hasher.combine(insert)
49 | hasher.combine(attributes)
50 | }
51 | }
52 |
53 | extension RichTextSpanInternal {
54 | public var spanRange: NSRange {
55 | let range = NSRange(location: from, length: max(((to - from) + 1), 0))
56 | return range
57 | }
58 |
59 | public var closedRange: ClosedRange {
60 | return from...to
61 | }
62 |
63 | public var length: Int {
64 | return to - from
65 | }
66 | }
67 |
68 | extension RichTextSpanInternal {
69 | public func copy(
70 | from: Int? = nil,
71 | to: Int? = nil,
72 | // insert: String? = nil,
73 | attributes: RichAttributes? = nil
74 | ) -> RichTextSpanInternal {
75 | return RichTextSpanInternal(
76 | from: (from != nil ? from! : self.from),
77 | to: (to != nil ? to! : self.to),
78 | // insert: (insert != nil ? insert! : self.insert),
79 | attributes: (attributes != nil ? attributes! : self.attributes)
80 | )
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Views/ForEachPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForEachPicker.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is an internal version of the original that is defined
11 | /// and available in https://github.com/danielsaidi/swiftuikit.
12 | /// This will not be made public or documented for this library.
13 | struct ForEachPicker: View {
14 |
15 | init(
16 | items: [Item],
17 | selection: Binding
- ,
18 | animatedSelection: Bool = false,
19 | dismissAfterPick: Bool = false,
20 | listItem: @escaping ItemViewBuilder
21 | ) {
22 | self.items = items
23 | self.selection = selection
24 | self.animatedSelection = animatedSelection
25 | self.dismissAfterPick = dismissAfterPick
26 | self.listItem = listItem
27 | }
28 |
29 | private let items: [Item]
30 | private let selection: Binding
-
31 | private let animatedSelection: Bool
32 | private let dismissAfterPick: Bool
33 | private let listItem: ItemViewBuilder
34 |
35 | typealias ItemViewBuilder = (_ item: Item, _ isSelected: Bool) -> ItemView
36 |
37 | @Environment(\.dismiss)
38 | var dismiss
39 |
40 | var body: some View {
41 | ForEach(items) { item in
42 | Button {
43 | select(item)
44 | } label: {
45 | listItem(item, isSelected(item))
46 | }
47 | .buttonStyle(.plain)
48 | }
49 | }
50 | }
51 |
52 | extension ForEachPicker {
53 |
54 | fileprivate var selectedId: Item.ID {
55 | selection.wrappedValue.id
56 | }
57 | }
58 |
59 | extension ForEachPicker {
60 |
61 | fileprivate func isSelected(_ item: Item) -> Bool {
62 | selectedId == item.id
63 | }
64 |
65 | fileprivate func select(_ item: Item) {
66 | if animatedSelection {
67 | selectWithAnimation(item)
68 | } else {
69 | selectWithoutAnimation(item)
70 | }
71 | }
72 |
73 | fileprivate func selectWithAnimation(_ item: Item) {
74 | withAnimation {
75 | selectWithoutAnimation(item)
76 | }
77 | }
78 |
79 | fileprivate func selectWithoutAnimation(_ item: Item) {
80 | selection.wrappedValue = item
81 | if dismissAfterPick {
82 | dismiss()
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeaderType.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/04/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | public enum HeaderType: Int, CaseIterable, Codable, Equatable, Identifiable,
12 | RichTextLabelValue
13 | {
14 | case `default` = 0
15 | case h1 = 1
16 | case h2 = 2
17 | case h3 = 3
18 | case h4 = 4
19 | case h5 = 5
20 | case h6 = 6
21 |
22 | var titleLabel: String {
23 | switch self {
24 | case .default:
25 | return "Body"
26 | case .h1:
27 | return "H1"
28 | case .h2:
29 | return "H2"
30 | case .h3:
31 | return "H3"
32 | case .h4:
33 | return "H4"
34 | case .h5:
35 | return "H5"
36 | case .h6:
37 | return "H6"
38 | }
39 | }
40 |
41 | func getTextSpanStyle() -> RichTextSpanStyle {
42 | switch self {
43 | case .default: return .default
44 | case .h1: return .h1
45 | case .h2: return .h2
46 | case .h3: return .h3
47 | case .h4: return .h4
48 | case .h5: return .h5
49 | case .h6: return .h6
50 | }
51 | }
52 | }
53 |
54 | extension Collection where Element == HeaderType {
55 |
56 | public static var all: [Element] { HeaderType.allCases }
57 | }
58 |
59 | extension HeaderType {
60 |
61 | /// The unique header ID.
62 | public var id: String { "\(rawValue)" }
63 |
64 | /// The standard icon to use for the header.
65 | public var icon: Image {
66 | switch self {
67 | case .default: .richTextHeaderDefault
68 | case .h1: .richTextHeader1
69 | case .h2: .richTextHeader2
70 | case .h3: .richTextHeader3
71 | case .h4: .richTextHeader4
72 | case .h5: .richTextHeader5
73 | case .h6: .richTextHeader6
74 | }
75 | }
76 |
77 | /// standard title to use for the headers.
78 | public var title: String { titleKey.text }
79 |
80 | /// The standard title key to use for the header.
81 | public var titleKey: RTEL10n {
82 | switch self {
83 | case .default: .headerDefault
84 | case .h1: .header1
85 | case .h2: .header2
86 | case .h3: .header3
87 | case .h4: .header4
88 | case .h5: .header5
89 | case .h6: .header6
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextOtherMenu+ToggleGroup.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextOtherMenu {
12 |
13 | /**
14 | This view can list ``RichTextOtherMenu/Toggle``s for a list
15 | of ``RichTextOtherMenu`` values, in a bordered button group.
16 |
17 | Since this view uses multiple styles, it binds directly
18 | to a ``RichTextContext`` instead of individual values.
19 |
20 | > Important: Since the `ControlGroup` doesn't highlight
21 | buttons in iOS, we use a `ToggleStack` for iOS.
22 | */
23 | public struct ToggleGroup: View {
24 |
25 | /**
26 | Create a rich text style toggle button group.
27 |
28 | - Parameters:
29 | - context: The context to affect.
30 | - styles: The styles to list, by default ``RichTextOtherMenu/all``.
31 | - greedy: Whether or not the group is horizontally greedy, by default `true`.
32 | */
33 | public init(
34 | context: RichEditorState,
35 | styles: [RichTextOtherMenu] = .all,
36 | greedy: Bool = true
37 | ) {
38 | self._context = ObservedObject(wrappedValue: context)
39 | self.isGreedy = greedy
40 | self.styles = styles
41 | }
42 |
43 | private let styles: [RichTextOtherMenu]
44 | private let isGreedy: Bool
45 |
46 | private var groupWidth: CGFloat? {
47 | if isGreedy { return nil }
48 | let count = Double(styles.count)
49 | #if os(macOS)
50 | return 30 * count
51 | #else
52 | return 50 * count
53 | #endif
54 | }
55 |
56 | @ObservedObject
57 | private var context: RichEditorState
58 |
59 | public var body: some View {
60 | #if os(macOS)
61 | ControlGroup {
62 | ForEach(styles) {
63 | RichTextOtherMenu.Toggle(
64 | style: $0,
65 | context: context,
66 | fillVertically: true
67 | )
68 | }
69 | }
70 | .frame(width: groupWidth)
71 | #else
72 | RichTextOtherMenu.ToggleStack(
73 | context: context,
74 | styles: styles
75 | )
76 | #endif
77 | }
78 | }
79 | }
80 | #endif
81 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Colors/RichTextColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextColor.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This enum defines supported rich text color types.
11 | ///
12 | /// The enum makes the colors identifiable and diffable.
13 | public enum RichTextColor: String, CaseIterable, Codable, Equatable,
14 | Identifiable
15 | {
16 |
17 | /// Foreground color.
18 | case foreground
19 |
20 | /// Background color.
21 | case background
22 |
23 | /// Strikethrough color.
24 | case strikethrough
25 |
26 | /// Stroke color.
27 | case stroke
28 |
29 | /// Underline color.
30 | case underline
31 | }
32 |
33 | extension RichTextColor {
34 |
35 | /// The unique color ID.
36 | public var id: String { rawValue }
37 |
38 | /// The corresponding rich text attribute, if any.
39 | public var attribute: NSAttributedString.Key? {
40 | switch self {
41 | case .foreground: .foregroundColor
42 | case .background: .backgroundColor
43 | case .strikethrough: .strikethroughColor
44 | case .stroke: .strokeColor
45 | case .underline: .underlineColor
46 | }
47 | }
48 |
49 | /// The standard icon to use for the color.
50 | public var icon: Image {
51 | switch self {
52 | case .foreground: .richTextColorForeground
53 | case .background: .richTextColorBackground
54 | case .strikethrough: .richTextColorStrikethrough
55 | case .stroke: .richTextColorStroke
56 | case .underline: .richTextColorUnderline
57 | }
58 | }
59 |
60 | /// The localized color title key.
61 | public var titleKey: RTEL10n {
62 | switch self {
63 | case .foreground: .foregroundColor
64 | case .background: .backgroundColor
65 | case .strikethrough: .strikethroughColor
66 | case .stroke: .strokeColor
67 | case .underline: .underlineColor
68 | }
69 | }
70 |
71 | /// Adjust a `color` for a certain `colorScheme`.
72 | public func adjust(
73 | _ color: Color?,
74 | for scheme: ColorScheme
75 | ) -> Color {
76 | switch self {
77 | case .background: color ?? .clear
78 | default: color ?? .primary
79 | }
80 | }
81 | }
82 |
83 | extension Collection where Element == RichTextColor {
84 |
85 | public static var allCases: [RichTextColor] { Element.allCases }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextOtherMenu+Button.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS) || os(macOS) || os(visionOS)
11 | extension RichTextOtherMenu {
12 |
13 | /**
14 | This button can be used to toggle a ``RichTextOtherMenu``.
15 |
16 | This view renders a plain `Button`, which means you can
17 | use and configure with plain SwiftUI.
18 | */
19 | public struct Button: View {
20 |
21 | /**
22 | Create a rich text style button.
23 |
24 | - Parameters:
25 | - style: The style to toggle.
26 | - value: The value to bind to.
27 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
28 | */
29 | public init(
30 | style: RichTextOtherMenu,
31 | value: Binding,
32 | fillVertically: Bool = false
33 | ) {
34 | self.style = style
35 | self.value = value
36 | self.fillVertically = fillVertically
37 | }
38 |
39 | /**
40 | Create a rich text style button.
41 |
42 | - Parameters:
43 | - style: The style to toggle.
44 | - context: The context to affect.
45 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
46 | */
47 | public init(
48 | style: RichTextOtherMenu,
49 | context: RichEditorState,
50 | fillVertically: Bool = false
51 | ) {
52 | self.init(
53 | style: style,
54 | value: context.bindingForManu(for: style),
55 | fillVertically: fillVertically
56 | )
57 | }
58 |
59 | private let style: RichTextOtherMenu
60 | private let value: Binding
61 | private let fillVertically: Bool
62 |
63 | public var body: some View {
64 | SwiftUI.Button(action: toggle) {
65 | style.label
66 | .labelStyle(.iconOnly)
67 | .frame(maxHeight: fillVertically ? .infinity : nil)
68 | .contentShape(Rectangle())
69 | }
70 | .tint(.accentColor, if: isOn)
71 | .foreground(.accentColor, if: isOn)
72 | // .keyboardShortcut(for: style)
73 | .accessibilityLabel(style.title)
74 | }
75 | }
76 | }
77 |
78 | extension RichTextOtherMenu.Button {
79 |
80 | fileprivate var isOn: Bool {
81 | value.wrappedValue
82 | }
83 |
84 | fileprivate func toggle() {
85 | value.wrappedValue.toggle()
86 | }
87 | }
88 | #endif
89 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextOtherMenu+Toggle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 19/12/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if os(iOS) || os(macOS) || os(visionOS)
11 | extension RichTextOtherMenu {
12 |
13 | /**
14 | This toggle can be used to toggle a ``RichTextOtherMenu``.
15 |
16 | This view renders a plain `Toggle`, which means you can
17 | use and configure with plain SwiftUI. The one exception
18 | is the tint color, which is set with a style.
19 | */
20 | public struct Toggle: View {
21 |
22 | /**
23 | Create a rich text style toggle toggle.
24 |
25 | - Parameters:
26 | - style: The style to toggle.
27 | - value: The value to bind to.
28 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
29 | */
30 | public init(
31 | style: RichTextOtherMenu,
32 | value: Binding,
33 | fillVertically: Bool = false
34 | ) {
35 | self.style = style
36 | self.value = value
37 | self.fillVertically = fillVertically
38 | }
39 |
40 | /**
41 | Create a rich text style toggle.
42 |
43 | - Parameters:
44 | - style: The style to toggle.
45 | - context: The context to affect.
46 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
47 | */
48 | public init(
49 | style: RichTextOtherMenu,
50 | context: RichEditorState,
51 | fillVertically: Bool = false
52 | ) {
53 | self.init(
54 | style: style,
55 | value: context.bindingForManu(for: style),
56 | fillVertically: fillVertically
57 | )
58 | }
59 |
60 | private let style: RichTextOtherMenu
61 | private let value: Binding
62 | private let fillVertically: Bool
63 |
64 | public var body: some View {
65 | #if os(tvOS) || os(watchOS)
66 | toggle
67 | #else
68 | toggle.toggleStyle(.button)
69 | #endif
70 | }
71 |
72 | private var toggle: some View {
73 | SwiftUI.Toggle(isOn: value) {
74 | style.icon
75 | .frame(maxHeight: fillVertically ? .infinity : nil)
76 | }
77 | // .keyboardShortcut(for: style)
78 | .accessibilityLabel(style.title)
79 | }
80 | }
81 | }
82 |
83 | extension RichTextOtherMenu.Toggle {
84 |
85 | fileprivate var isOn: Bool {
86 | value.wrappedValue
87 | }
88 | }
89 | #endif
90 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextStyle+Toggle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextStyle {
11 |
12 | /**
13 | This toggle can be used to toggle a ``RichTextStyle``.
14 |
15 | This view renders a plain `Toggle`, which means you can
16 | use and configure with plain SwiftUI. The one exception
17 | is the tint color, which is set with a style.
18 | */
19 | public struct Toggle: View {
20 |
21 | /**
22 | Create a rich text style toggle toggle.
23 |
24 | - Parameters:
25 | - style: The style to toggle.
26 | - value: The value to bind to.
27 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
28 | */
29 | public init(
30 | style: RichTextStyle,
31 | value: Binding,
32 | fillVertically: Bool = false
33 | ) {
34 | self.style = style
35 | self.value = value
36 | self.fillVertically = fillVertically
37 | }
38 |
39 | /**
40 | Create a rich text style toggle.
41 |
42 | - Parameters:
43 | - style: The style to toggle.
44 | - context: The context to affect.
45 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
46 | */
47 | public init(
48 | style: RichTextStyle,
49 | context: RichEditorState,
50 | fillVertically: Bool = false
51 | ) {
52 | self.init(
53 | style: style,
54 | value: context.binding(for: style),
55 | fillVertically: fillVertically
56 | )
57 | }
58 |
59 | private let style: RichTextStyle
60 | private let value: Binding
61 | private let fillVertically: Bool
62 |
63 | public var body: some View {
64 | #if os(tvOS) || os(watchOS)
65 | toggle
66 | #else
67 | toggle.toggleStyle(.button)
68 | #endif
69 | }
70 |
71 | private var toggle: some View {
72 | SwiftUI.Toggle(isOn: value) {
73 | style.icon
74 | .frame(maxHeight: fillVertically ? .infinity : nil)
75 | }
76 | .keyboardShortcut(for: style)
77 | .accessibilityLabel(style.title)
78 | }
79 | }
80 | }
81 |
82 | extension RichTextStyle.Toggle {
83 |
84 | fileprivate var isOn: Bool {
85 | value.wrappedValue
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ListStyle/ListType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListType.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 29/04/24.
6 | //
7 |
8 | import Foundation
9 |
10 | //public enum ListType: Codable, Identifiable, CaseIterable, Hashable {
11 | // public static var allCases: [ListType] = [.bullet()]
12 | //
13 | // public var id: String {
14 | // return key
15 | // }
16 | //
17 | // public func hash(into hasher: inout Hasher) {
18 | // hasher.combine(key)
19 | // hasher.combine(getIndent())
20 | // }
21 | //
22 | // case bullet(_ indent: Int? = nil)
23 | // // case ordered(_ indent: Int? = nil)
24 | //
25 | // enum CodingKeys: String, CodingKey {
26 | // case bullet = "bullet"
27 | // // case ordered = "ordered"
28 | // }
29 | //
30 | // var key: String {
31 | // switch self {
32 | // case .bullet:
33 | // return "bullet"
34 | // // case .ordered:
35 | // // return "ordered"
36 | // }
37 | // }
38 | //}
39 | //
40 | //extension ListType {
41 | // func getTextSpanStyle() -> RichTextSpanStyle {
42 | // switch self {
43 | // case .bullet(let indent):
44 | // return .bullet(indent)
45 | // // case .ordered:
46 | // // return .ordered
47 | // }
48 | // }
49 | //
50 | // func getMarkerFormat() -> TextList.MarkerFormat {
51 | // switch self {
52 | // case .bullet:
53 | // return .disc
54 | // // case .ordered:
55 | // // return .decimal
56 | // }
57 | // }
58 | //
59 | // func getIndent() -> Int {
60 | // switch self {
61 | // case .bullet(let indent):
62 | // return indent ?? 0
63 | // // case .ordered(let indent):
64 | // // return indent ?? 0
65 | // }
66 | // }
67 | //
68 | // // func moveIndentForward() -> ListType {
69 | // // switch self {
70 | // // case .bullet(let indent):
71 | // // let newIndent = (indent ?? 0) + 1
72 | // // return .bullet(newIndent)
73 | // // }
74 | // // }
75 | // //
76 | // // func moveIndentBackward() -> ListType {
77 | // // switch self {
78 | // // case .bullet(let indent):
79 | // // let newIndent = max(0, ((indent ?? 0) - 1))
80 | // // return .bullet(newIndent)
81 | // // }
82 | // // }
83 | //}
84 | //
85 | //#if canImport(UIKit)
86 | // import UIKit
87 | //
88 | // typealias TextList = NSTextList
89 | //#endif
90 | //
91 | //#if canImport(AppKit)
92 | // import AppKit
93 | //
94 | // typealias TextList = NSTextList
95 | //#endif
96 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat+Menu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextDataFormat+Menu.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextDataFormat {
12 |
13 | /**
14 | This menu can be used to trigger custom actions for any
15 | list of ``RichTextDataFormat`` values.
16 |
17 | The menu uses customizable actions, which means that it
18 | can be used in toolbars, menu bar commands etc. It also
19 | has an optional `pdf` action, which for instance can be
20 | used when exporting or sharing rich text.
21 | */
22 | public struct Menu: View {
23 |
24 | public init(
25 | title: String,
26 | icon: Image,
27 | formats: [Format] = Format.libraryFormats,
28 | otherFormats: [RichTextExportOption] = .all,
29 | formatAction: @escaping (Format) -> Void,
30 | otherOptionAction: ((RichTextExportOption) -> Void)? = nil
31 | ) {
32 | self.title = title
33 | self.icon = icon
34 | self.formats = formats
35 | self.otherFormats = otherFormats
36 | self.formatAction = formatAction
37 | self.otherOptionAction = otherOptionAction
38 | }
39 |
40 | public typealias Format = RichTextDataFormat
41 |
42 | private let title: String
43 | private let icon: Image
44 | private let formats: [Format]
45 | private let otherFormats: [RichTextExportOption]
46 | private let formatAction: (Format) -> Void
47 | private let otherOptionAction: ((RichTextExportOption) -> Void)?
48 |
49 | public var body: some View {
50 | SwiftUI.Menu {
51 | ForEach(formats) { format in
52 | Button {
53 | formatAction(format)
54 | } label: {
55 | icon.label(format.fileFormatText)
56 | }
57 | }
58 | if let action = otherOptionAction {
59 | ForEach(otherFormats) { format in
60 | Button {
61 | action(format)
62 | } label: {
63 | icon.label(format.fileFormatText)
64 | }
65 | }
66 | }
67 | } label: {
68 | icon.label(title)
69 | }
70 | }
71 | }
72 | }
73 | #endif
74 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Subscriptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextCoordinator+Subscriptions.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextCoordinator {
12 |
13 | /// Subscribe to observable context state changes.
14 | ///
15 | /// The coordinator subscribes to both actions triggered
16 | /// by various buttons via the context, but also to some
17 | /// context value that are changed through view bindings.
18 | func subscribeToUserActions() {
19 | context.actionPublisher.sink { [weak self] action in
20 | self?.handle(action)
21 | }
22 | .store(in: &cancellables)
23 |
24 | subscribeToAlignment()
25 | subscribeToFontName()
26 | subscribeToFontSize()
27 | subscribeToIsEditable()
28 | subscribeToIsEditingText()
29 | subscribeToLineSpacing()
30 | }
31 | }
32 |
33 | extension RichTextCoordinator {
34 |
35 | fileprivate func subscribe(
36 | to publisher: Published.Publisher,
37 | action: @escaping (T) -> Void
38 | ) {
39 | publisher
40 | .sink(receiveValue: action)
41 | .store(in: &cancellables)
42 | }
43 |
44 | fileprivate func subscribeToAlignment() {
45 | subscribe(to: context.$textAlignment) { [weak self] in
46 | self?.handle(.setAlignment($0))
47 | }
48 | }
49 |
50 | fileprivate func subscribeToFontName() {
51 | subscribe(to: context.$fontName) { [weak self] in
52 | self?.textView.setRichTextFontName($0)
53 | }
54 | }
55 |
56 | fileprivate func subscribeToFontSize() {
57 | subscribe(to: context.$fontSize) { [weak self] in
58 | self?.textView.setRichTextFontSize($0)
59 | }
60 | }
61 |
62 | fileprivate func subscribeToIsEditable() {
63 | subscribe(to: context.$isEditable) { [weak self] in
64 | self?.setIsEditable(to: $0)
65 | }
66 | }
67 |
68 | fileprivate func subscribeToIsEditingText() {
69 | subscribe(to: context.$isEditingText) { [weak self] in
70 | self?.setIsEditing(to: $0)
71 | }
72 | }
73 |
74 | // TODO: Not done yet
75 | fileprivate func subscribeToLineSpacing() {
76 | // subscribe(to: context.$lineSpacing) { [weak self] in
77 | // self?.textView.setRichTextLineSpacing($0)
78 | // }
79 | }
80 | }
81 | #endif
82 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextKeyboardToolbar+Config.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | /// This struct can configure a ``RichTextKeyboardToolbar``.
12 | public struct RichTextKeyboardToolbarConfig {
13 |
14 | /// Create a custom keyboard toolbar configuration.
15 | ///
16 | /// - Parameters:
17 | /// - alwaysDisplayToolbar: Whether or not to always show the toolbar, by default `false`.
18 | /// - leadingActions: The leading actions, by default `.undo` and `.redo`.
19 | /// - trailingActions: The trailing actions, by default `.dismissKeyboard`.
20 | public init(
21 | alwaysDisplayToolbar: Bool = false,
22 | leadingActions: [RichTextAction] = [.undo, .redo],
23 | trailingActions: [RichTextAction] = [.dismissKeyboard]
24 | ) {
25 | self.alwaysDisplayToolbar = alwaysDisplayToolbar
26 | self.leadingActions = leadingActions
27 | self.trailingActions = trailingActions
28 | }
29 |
30 | /// Whether or not to always show the toolbar.
31 | public var alwaysDisplayToolbar: Bool
32 |
33 | /// The leading toolbar actions.
34 | public var leadingActions: [RichTextAction]
35 |
36 | /// The trailing toolbar actions.
37 | public var trailingActions: [RichTextAction]
38 | }
39 |
40 | extension RichTextKeyboardToolbarConfig {
41 |
42 | /// The standard rich text keyboard toolbar config.
43 | ///
44 | /// You can override this to change the global default.
45 | public static var standard = RichTextKeyboardToolbarConfig()
46 | }
47 |
48 | extension View {
49 |
50 | /// Apply a ``RichTextKeyboardToolbar`` configuration.
51 | public func richTextKeyboardToolbarConfig(
52 | _ config: RichTextKeyboardToolbarConfig
53 | ) -> some View {
54 | self.environment(\.richTextKeyboardToolbarConfig, config)
55 | }
56 | }
57 |
58 | extension RichTextKeyboardToolbarConfig {
59 |
60 | fileprivate struct Key: EnvironmentKey {
61 |
62 | public static var defaultValue: RichTextKeyboardToolbarConfig =
63 | .standard
64 | }
65 | }
66 |
67 | extension EnvironmentValues {
68 |
69 | /// This value can bind to a keyboard toolbar config.
70 | public var richTextKeyboardToolbarConfig: RichTextKeyboardToolbarConfig
71 | {
72 | get { self[RichTextKeyboardToolbarConfig.Key.self] }
73 | set { self[RichTextKeyboardToolbarConfig.Key.self] = newValue }
74 | }
75 | }
76 | #endif
77 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Style.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextKeyboardToolbar+Style.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | /// This struct can style a ``RichTextKeyboardToolbar``.
12 | public struct RichTextKeyboardToolbarStyle {
13 |
14 | /// Create a custom toolbar style
15 | ///
16 | /// - Parameters:
17 | /// - toolbarHeight: The height of the toolbar, by default `50`.
18 | /// - itemSpacing: The spacing between toolbar items, by default `15`.
19 | /// - shadowColor: The toolbar's shadow color, by default transparent black.
20 | /// - shadowRadius: The toolbar's shadow radius, by default `3`.
21 | public init(
22 | toolbarHeight: Double = 50,
23 | itemSpacing: Double = 15,
24 | shadowColor: Color = .black.opacity(0.1),
25 | shadowRadius: Double = 3
26 | ) {
27 | self.toolbarHeight = toolbarHeight
28 | self.itemSpacing = itemSpacing
29 | self.shadowColor = shadowColor
30 | self.shadowRadius = shadowRadius
31 | }
32 |
33 | /// The height of the toolbar.
34 | public var toolbarHeight: Double
35 |
36 | /// The spacing between toolbar items.
37 | public var itemSpacing: Double
38 |
39 | /// The toolbar's shadow color.
40 | public var shadowColor: Color
41 |
42 | /// The toolbar's shadow radius.
43 | public var shadowRadius: Double
44 | }
45 |
46 | extension RichTextKeyboardToolbarStyle {
47 |
48 | /// The standard rich text keyboard toolbar style.
49 | ///
50 | /// You can set a new value to change the global default.
51 | public static var standard = Self()
52 | }
53 |
54 | extension View {
55 |
56 | /// Apply a ``RichTextKeyboardToolbar`` style.
57 | public func richTextKeyboardToolbarStyle(
58 | _ style: RichTextKeyboardToolbarStyle
59 | ) -> some View {
60 | self.environment(\.richTextKeyboardToolbarStyle, style)
61 | }
62 | }
63 |
64 | extension RichTextKeyboardToolbarStyle {
65 |
66 | fileprivate struct Key: EnvironmentKey {
67 |
68 | static var defaultValue: RichTextKeyboardToolbarStyle = .standard
69 | }
70 | }
71 |
72 | extension EnvironmentValues {
73 |
74 | /// This value can bind to a keyboard toolbar style.
75 | public var richTextKeyboardToolbarStyle: RichTextKeyboardToolbarStyle {
76 | get { self[RichTextKeyboardToolbarStyle.Key.self] }
77 | set { self[RichTextKeyboardToolbarStyle.Key.self] = newValue }
78 | }
79 | }
80 |
81 | #endif
82 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Helper/RichTextReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextReader.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 28/12/23.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol can be implemented any types that can provide
11 | /// a rich text string.
12 | ///
13 | /// The protocol is implemented by `NSAttributedString` as well
14 | /// as other types in the library.
15 | public protocol RichTextReader {
16 |
17 | /// The attributed string to use as rich text.
18 | var attributedString: NSAttributedString { get }
19 | }
20 |
21 | extension NSAttributedString: RichTextReader {
22 |
23 | /// This type returns itself as the attributed string.
24 | public var attributedString: NSAttributedString { self }
25 | }
26 |
27 | extension RichTextReader {
28 |
29 | /**
30 | The rich text to use.
31 |
32 | This is a convenience name alias for ``attributedString``
33 | to provide this type with a property that uses the rich
34 | text naming convention.
35 | */
36 | public var richText: NSAttributedString {
37 | attributedString
38 | }
39 |
40 | /**
41 | Get the range of the entire ``richText``.
42 |
43 | This uses `safeRange(for:)` to return a range that willö
44 | always be valid for the current rich text.
45 | */
46 | public var richTextRange: NSRange {
47 | let range = NSRange(location: 0, length: richText.string.utf16Length)
48 | let safeRange = safeRange(for: range)
49 | return safeRange
50 | }
51 |
52 | /**
53 | Get the rich text at a certain range.
54 |
55 | Since this function uses `safeRange(for:)` to not crash
56 | for invalid ranges, always use this function instead of
57 | the unsafe `attributedSubstring`.
58 |
59 | - Parameters:
60 | - range: The range for which to get the rich text.
61 | */
62 | public func richText(at range: NSRange) -> NSAttributedString {
63 | let range = safeRange(for: range)
64 | return attributedString.attributedSubstring(from: range)
65 | }
66 |
67 | /**
68 | Get a safe range for the provided range.
69 |
70 | A safe range is limited to the bounds of the attributed
71 | string and helps protecting against range overflow.
72 |
73 | - Parameters:
74 | - range: The range for which to get a safe range.
75 | - isAttributeOperation: Set this to `true` to avoid last position.
76 | */
77 | public func safeRange(
78 | for range: NSRange,
79 | isAttributeOperation: Bool = false
80 | ) -> NSRange {
81 | let length = attributedString.string.utf16Length
82 | let subtract = isAttributeOperation ? 1 : 0
83 | return NSRange(
84 | location: max(0, min(length - subtract, range.location)),
85 | length: min(range.length, max(0, length - range.location)))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextDataReader.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This protocol extends ``RichTextReader`` with functionality
11 | /// for reading rich text data for the current rich text.
12 | ///
13 | /// The protocol is implemented by `NSAttributedString` as well
14 | /// as other types in the library.
15 | public protocol RichTextDataReader: RichTextReader {}
16 |
17 | extension NSAttributedString: RichTextDataReader {}
18 |
19 | extension RichTextDataReader {
20 |
21 | /**
22 | Generate rich text data from the current rich text.
23 |
24 | - Parameters:
25 | - format: The data format to use.
26 | */
27 | public func richTextData(
28 | for format: RichTextDataFormat
29 | ) throws -> Data {
30 | switch format {
31 | case .archivedData: try richTextArchivedData()
32 | case .plainText: try richTextPlainTextData()
33 | case .rtf: try richTextRtfData()
34 | case .vendorArchivedData: try richTextArchivedData()
35 | }
36 | }
37 | }
38 |
39 | extension RichTextDataReader {
40 |
41 | /// The full text range.
42 | fileprivate var textRange: NSRange {
43 | NSRange(location: 0, length: richText.length)
44 | }
45 |
46 | /// The full text range.
47 | fileprivate func documentAttributes(
48 | for documentType: NSAttributedString.DocumentType
49 | ) -> [NSAttributedString.DocumentAttributeKey: Any] {
50 | [.documentType: documentType]
51 | }
52 |
53 | /// Generate archived formatted data.
54 | fileprivate func richTextArchivedData() throws -> Data {
55 | try NSKeyedArchiver.archivedData(
56 | withRootObject: richText,
57 | requiringSecureCoding: false
58 | )
59 | }
60 |
61 | /// Generate plain text formatted data.
62 | fileprivate func richTextPlainTextData() throws -> Data {
63 | let string = richText.string
64 | guard let data = string.data(using: .utf8) else {
65 | throw
66 | RichTextDataError
67 | .invalidData(in: string)
68 | }
69 | return data
70 | }
71 |
72 | /// Generate RTF formatted data.
73 | fileprivate func richTextRtfData() throws -> Data {
74 | try richText.data(
75 | from: textRange,
76 | documentAttributes: documentAttributes(for: .rtf)
77 | )
78 | }
79 |
80 | /// Generate RTFD formatted data.
81 | fileprivate func richTextRtfdData() throws -> Data {
82 | try richText.data(
83 | from: textRange,
84 | documentAttributes: documentAttributes(for: .rtfd)
85 | )
86 | }
87 |
88 | #if os(macOS)
89 | /// Generate Word formatted data.
90 | func richTextWordData() throws -> Data {
91 | try richText.data(
92 | from: textRange,
93 | documentAttributes: documentAttributes(for: .docFormat)
94 | )
95 | }
96 | #endif
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichAttributes+RichTextAttributes.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 24/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | #if canImport(UIKit)
11 | import UIKit
12 | #elseif canImport(AppKit)
13 | import AppKit
14 | #endif
15 |
16 | extension RichAttributes {
17 | func toAttributes(font: FontRepresentable? = nil) -> RichTextAttributes {
18 | var attributes: RichTextAttributes = [:]
19 |
20 | var defaultFont: FontRepresentable = .standardRichTextFont
21 | #if !os(watchOS)
22 | defaultFont = RichTextView.Theme.standard.font
23 | #endif
24 | // Set the font size and handle headers
25 | var font = font ?? defaultFont
26 | if let headerType = self.header?.getTextSpanStyle() {
27 | font =
28 | font
29 | .updateFontSize(
30 | size: font.pointSize * headerType.fontSizeMultiplier
31 | )
32 | }
33 |
34 | if let size = size {
35 | font = font.updateFontSize(size: CGFloat(size))
36 | }
37 |
38 | if let fontName = self.font {
39 | let size = font.pointSize
40 | let newFont = FontRepresentable(name: fontName, size: size)
41 | if let newFont {
42 | font = newFont
43 | }
44 | }
45 |
46 | // Apply bold and italic styles
47 | if let isBold = bold, isBold {
48 | font = font.makeBold()
49 | }
50 |
51 | if let isItalic = italic, isItalic {
52 | font = font.makeItalic()
53 | }
54 |
55 | attributes[.font] = font
56 |
57 | // Apply underline
58 | if let isUnderline = underline, isUnderline {
59 | attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
60 | }
61 |
62 | // Apply strikethrough
63 | if let isStrike = strike, isStrike {
64 | attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
65 | }
66 |
67 | if let color {
68 | attributes[.foregroundColor] = ColorRepresentable(Color(hex: color))
69 | }
70 |
71 | if let background {
72 | attributes[.backgroundColor] = ColorRepresentable(
73 | Color(hex: background))
74 | }
75 |
76 | if let align {
77 | let style = NSMutableParagraphStyle(from: nil, alignment: align)
78 | attributes[.paragraphStyle] = style
79 | }
80 |
81 | // Handle indent and paragraph styles
82 | // if let indentLevel = indent {
83 | // let paragraphStyle = NSMutableParagraphStyle()
84 | // paragraphStyle.headIndent = CGFloat(indentLevel * 10) // Adjust indentation as needed
85 | // attributes[.paragraphStyle] = paragraphStyle
86 | // }
87 |
88 | return attributes
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFormat+ToolbarConfig.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextFormat {
12 |
13 | /// This type can be used to configure a format toolbar.
14 | public struct ToolbarConfig {
15 |
16 | public init(
17 | headers: [HeaderType] = .all,
18 | alignments: [RichTextAlignment] = .all,
19 | colorPickers: [RichTextColor] = [.foreground],
20 | colorPickersDisclosed: [RichTextColor] = [],
21 | fontPicker: Bool = true,
22 | fontSizePicker: Bool = true,
23 | indentButtons: Bool = true,
24 | lineSpacingPicker: Bool = false,
25 | styles: [RichTextStyle] = .all,
26 | otherMenu: [RichTextOtherMenu] = .all,
27 | superscriptButtons: Bool = true
28 | ) {
29 | self.headers = headers
30 | self.alignments = alignments
31 | self.colorPickers = colorPickers
32 | self.colorPickersDisclosed = colorPickersDisclosed
33 | self.fontPicker = fontPicker
34 | self.fontSizePicker = fontSizePicker
35 | self.indentButtons = indentButtons
36 | self.lineSpacingPicker = lineSpacingPicker
37 | self.styles = styles
38 | self.otherMenu = otherMenu
39 | #if os(macOS)
40 | self.superscriptButtons = superscriptButtons
41 | #else
42 | self.superscriptButtons = false
43 | #endif
44 | }
45 |
46 | public var headers: [HeaderType]
47 | public var alignments: [RichTextAlignment]
48 | public var colorPickers: [RichTextColor]
49 | public var colorPickersDisclosed: [RichTextColor]
50 | public var fontPicker: Bool
51 | public var fontSizePicker: Bool
52 | public var indentButtons: Bool
53 | public var lineSpacingPicker: Bool
54 | public var styles: [RichTextStyle]
55 | public var otherMenu: [RichTextOtherMenu]
56 | public var superscriptButtons: Bool
57 | }
58 | }
59 |
60 | extension RichTextFormat.ToolbarConfig {
61 |
62 | /// The standard rich text format toolbar configuration.
63 | public static var standard: Self { .init() }
64 | }
65 |
66 | extension View {
67 |
68 | /// Apply a rich text format toolbar style.
69 | public func richTextFormatToolbarConfig(
70 | _ value: RichTextFormat.ToolbarConfig
71 | ) -> some View {
72 | self.environment(\.richTextFormatToolbarConfig, value)
73 | }
74 | }
75 |
76 | extension RichTextFormat.ToolbarConfig {
77 |
78 | fileprivate struct Key: EnvironmentKey {
79 |
80 | public static var defaultValue: RichTextFormat.ToolbarConfig {
81 | .init()
82 | }
83 | }
84 | }
85 |
86 | extension EnvironmentValues {
87 |
88 | /// This value can bind to a format toolbar config.
89 | public var richTextFormatToolbarConfig: RichTextFormat.ToolbarConfig {
90 | get { self[RichTextFormat.ToolbarConfig.Key.self] }
91 | set { self[RichTextFormat.ToolbarConfig.Key.self] = newValue }
92 | }
93 | }
94 | #endif
95 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/RichEditorSwiftUI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
35 |
41 |
42 |
43 |
44 |
45 |
55 |
56 |
62 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextStyle+Button.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextStyle {
11 |
12 | /**
13 | This button can be used to toggle a ``RichTextStyle``.
14 |
15 | This view renders a plain `Button`, which means you can
16 | use and configure with plain SwiftUI.
17 | */
18 | public struct Button: View {
19 |
20 | /**
21 | Create a rich text style button.
22 |
23 | - Parameters:
24 | - style: The style to toggle.
25 | - value: The value to bind to.
26 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
27 | */
28 | public init(
29 | style: RichTextStyle,
30 | value: Binding,
31 | fillVertically: Bool = false
32 | ) {
33 | self.style = style
34 | self.value = value
35 | self.fillVertically = fillVertically
36 | }
37 |
38 | /**
39 | Create a rich text style button.
40 |
41 | - Parameters:
42 | - style: The style to toggle.
43 | - context: The context to affect.
44 | - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`.
45 | */
46 | public init(
47 | style: RichTextStyle,
48 | context: RichEditorState,
49 | fillVertically: Bool = false
50 | ) {
51 | self.init(
52 | style: style,
53 | value: context.binding(for: style),
54 | fillVertically: fillVertically
55 | )
56 | }
57 |
58 | private let style: RichTextStyle
59 | private let value: Binding
60 | private let fillVertically: Bool
61 |
62 | public var body: some View {
63 | SwiftUI.Button(action: toggle) {
64 | style.label
65 | .labelStyle(.iconOnly)
66 | .frame(maxHeight: fillVertically ? .infinity : nil)
67 | .contentShape(Rectangle())
68 | }
69 | .tint(.accentColor, if: isOn)
70 | .foreground(.accentColor, if: isOn)
71 | .keyboardShortcut(for: style)
72 | .accessibilityLabel(style.title)
73 | }
74 | }
75 | }
76 |
77 | extension View {
78 |
79 | @ViewBuilder
80 | func foreground(_ color: Color, if isOn: Bool) -> some View {
81 | if isOn {
82 | self.foregroundStyle(color)
83 | } else {
84 | self
85 | }
86 | }
87 |
88 | @ViewBuilder
89 | func tint(_ color: Color, if isOn: Bool) -> some View {
90 | if isOn {
91 | self.tint(color)
92 | } else {
93 | self
94 | }
95 | }
96 | }
97 |
98 | extension RichTextStyle.Button {
99 |
100 | fileprivate var isOn: Bool {
101 | value.wrappedValue
102 | }
103 |
104 | fileprivate func toggle() {
105 | value.wrappedValue.toggle()
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextAlignment.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 21/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This enum defines supported rich text alignments, like left,
11 | /// right, center, and justified.
12 | public enum RichTextAlignment: String, CaseIterable, Codable, Equatable,
13 | Identifiable, RichTextLabelValue
14 | {
15 |
16 | /**
17 | Initialize a rich text alignment with a native alignment.
18 |
19 | - Parameters:
20 | - alignment: The native alignment to use.
21 | */
22 | public init(_ alignment: NSTextAlignment) {
23 | switch alignment {
24 | case .left: self = .left
25 | case .right: self = .right
26 | case .center: self = .center
27 | case .justified: self = .justify
28 | default: self = .left
29 | }
30 | }
31 |
32 | /// Left text alignment.
33 | case left
34 |
35 | /// Center text alignment.
36 | case center
37 |
38 | /// Justified text alignment.
39 | case justify
40 |
41 | /// Right text alignment.
42 | case right
43 | }
44 |
45 | extension RichTextAlignment {
46 | public func getTextSpanStyle() -> RichTextSpanStyle {
47 | return .align(self)
48 | }
49 | }
50 |
51 | extension Collection where Element == RichTextAlignment {
52 |
53 | public static var all: [Element] { RichTextAlignment.allCases }
54 | }
55 |
56 | extension RichTextAlignment {
57 |
58 | /// The unique alignment ID.
59 | public var id: String { rawValue }
60 |
61 | /// The standard icon to use for the alignment.
62 | public var icon: Image { nativeAlignment.icon }
63 |
64 | /// The standard title to use for the alignment.
65 | public var title: String { nativeAlignment.title }
66 |
67 | /// The standard title key to use for the alignment.
68 | public var titleKey: RTEL10n { nativeAlignment.titleKey }
69 |
70 | /// The native alignment of the alignment.
71 | public var nativeAlignment: NSTextAlignment {
72 | switch self {
73 | case .left: .left
74 | case .right: .right
75 | case .center: .center
76 | case .justify: .justified
77 | }
78 | }
79 | }
80 |
81 | extension NSTextAlignment: RichTextLabelValue {}
82 |
83 | extension NSTextAlignment {
84 |
85 | /// The standard icon to use for the alignment.
86 | public var icon: Image {
87 | switch self {
88 | case .left: .richTextAlignmentLeft
89 | case .right: .richTextAlignmentRight
90 | case .center: .richTextAlignmentCenter
91 | case .justified: .richTextAlignmentJustified
92 | default: .richTextAlignmentLeft
93 | }
94 | }
95 |
96 | /// The standard title to use for the alignment.
97 | public var title: String {
98 | titleKey.text
99 | }
100 |
101 | /// The standard title key to use for the alignment.
102 | public var titleKey: RTEL10n {
103 | switch self {
104 | case .left: .textAlignmentLeft
105 | case .right: .textAlignmentRight
106 | case .center: .textAlignmentCentered
107 | case .justified: .textAlignmentJustified
108 | default: .textAlignmentLeft
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFontSizePickerStack.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFontSizePickerStack.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextFont {
12 |
13 | /**
14 | This view uses a ``RichTextFont/SizePicker`` and button
15 | steppers to increment and a decrement the font size.
16 |
17 | You can configure this picker by applying a config view
18 | modifier to your view hierarchy:
19 |
20 | ```swift
21 | VStack {
22 | RichTextFont.SizePickerStack(...)
23 | ...
24 | }
25 | .richTextFontSizePickerConfig(...)
26 | ```
27 | */
28 | public struct SizePickerStack: View {
29 |
30 | /**
31 | Create a rich text font size picker stack.
32 |
33 | - Parameters:
34 | - context: The context to affect.
35 | */
36 | public init(
37 | context: RichEditorState
38 | ) {
39 | self._context = ObservedObject(wrappedValue: context)
40 | }
41 |
42 | private let step = 1
43 |
44 | @ObservedObject
45 | private var context: RichEditorState
46 |
47 | public var body: some View {
48 | #if os(iOS) || os(visionOS)
49 | stack
50 | .fixedSize(horizontal: false, vertical: true)
51 | #else
52 | HStack(spacing: 3) {
53 | picker
54 | stepper
55 | }
56 | .overlay(macShortcutOverlay)
57 | #endif
58 | }
59 | }
60 | }
61 |
62 | extension RichTextFont.SizePickerStack {
63 |
64 | fileprivate var macShortcutOverlay: some View {
65 | stack
66 | .opacity(0)
67 | .allowsHitTesting(false)
68 | }
69 |
70 | fileprivate var stack: some View {
71 | HStack(spacing: 2) {
72 | stepButton(-step)
73 | picker
74 | stepButton(step)
75 | }
76 | }
77 |
78 | fileprivate func stepButton(_ points: Int) -> some View {
79 | RichTextAction.Button(
80 | action: .stepFontSize(points: points),
81 | context: context,
82 | fillVertically: true
83 | )
84 | }
85 |
86 | fileprivate var picker: some View {
87 | RichTextFont.SizePicker(
88 | selection: $context.fontSize
89 | )
90 | .onChangeBackPort(of: context.fontSize) { newValue in
91 | context.updateStyle(style: .size(Int(context.fontSize)))
92 | }
93 | }
94 |
95 | fileprivate var stepper: some View {
96 | Stepper(
97 | "",
98 | onIncrement: increment,
99 | onDecrement: decrement
100 | )
101 | }
102 |
103 | fileprivate func decrement() {
104 | context.fontSize -= CGFloat(step)
105 | }
106 |
107 | fileprivate func increment() {
108 | context.fontSize += CGFloat(step)
109 | }
110 | }
111 | #endif
112 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver+FileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StandardRichTextExportUrlResolver.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This is a typealias for the `FileManager` class, since it's
11 | /// the standard way to resolve export file urls.
12 | public typealias StandardRichTextExportUrlResolver = FileManager
13 |
14 | extension FileManager: RichTextExportUrlResolver {}
15 |
16 | extension FileManager {
17 |
18 | /**
19 | Try to generate a file url in a certain directory.
20 |
21 | - Parameters:
22 | - fileName: The preferred file name.
23 | - extensions: The file extension.
24 | - directory: The directory in which to generate an url.
25 | */
26 | public func fileUrl(
27 | withName fileName: String,
28 | extension: String,
29 | in directory: FileManager.SearchPathDirectory
30 | ) throws -> URL {
31 | let url =
32 | self
33 | .urls(for: directory, in: .userDomainMask).first?
34 | .appendingPathComponent(fileName)
35 | .appendingPathExtension(`extension`)
36 | guard let fileUrl = url else {
37 | throw RichTextExportError.cantCreateFileUrl(in: directory)
38 | }
39 | return fileUrl
40 | }
41 |
42 | /**
43 | Try to generate a unique file url in a certain directory.
44 |
45 | If needed, the function appends a counter until the url
46 | is unique. This means that the resulting url for a file
47 | url that has the file name `myFile.txt` may result in a
48 | url that has the file name `myFile-1.txt`.
49 |
50 | - Parameters:
51 | - fileName: The preferred file name.
52 | - extensions: The file extension.
53 | - directory: The directory in which to generate an url.
54 | */
55 | public func uniqueFileUrl(
56 | withName fileName: String,
57 | extension: String,
58 | in directory: FileManager.SearchPathDirectory
59 | ) throws -> URL {
60 | let url = try fileUrl(
61 | withName: fileName, extension: `extension`, in: directory)
62 | let uniqueUrl = uniqueUrl(for: url)
63 | return uniqueUrl
64 | }
65 |
66 | /**
67 | Get a unique url for the provided `url`, to ensure that
68 | no existing folder or file exists there.
69 |
70 | If needed, the function appends a counter until the url
71 | is unique. This means that the resulting url for a file
72 | url that has the file name `myFile.txt` may result in a
73 | url that has the file name `myFile-1.txt`.
74 |
75 | - Parameters:
76 | - url: The url to generate a unique url for.
77 | */
78 | public func uniqueUrl(for url: URL) -> URL {
79 | if !fileExists(atPath: url.path) { return url }
80 | let fileExtension = url.pathExtension
81 | let noExtension = url.deletingPathExtension()
82 | let fileName = noExtension.lastPathComponent
83 | var counter = 1
84 | repeat {
85 | let newUrl =
86 | noExtension
87 | .deletingLastPathComponent()
88 | .appendingPathComponent(fileName.appending("-\(counter)"))
89 | .appendingPathExtension(fileExtension)
90 | if !fileExists(atPath: newUrl.path) { return newUrl }
91 | counter += 1
92 | } while true
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFont+PickerConfig.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension RichTextFont {
11 |
12 | /// This type can configure a ``RichTextFont/Picker``.
13 | ///
14 | /// This configuration contains configuration properties
15 | /// for many different font pickers types. Some of these
16 | /// properties are not used in some pickers.
17 | public struct PickerConfig {
18 |
19 | /// Create a custom font picker config.
20 | ///
21 | /// - Parameters:
22 | /// - fonts: The fonts to display in the list, by default `all`.
23 | /// - fontSize: The font size to use in the list items, by default `20`.
24 | /// - dismissAfterPick: Whether or not to dismiss the picker after a font is selected, by default `false`.
25 | /// - moveSelectionTopmost: Whether or not to place the selected font topmost, by default `true`.
26 | public init(
27 | fonts: [RichTextFont.PickerFont] = .all,
28 | fontSize: CGFloat = 20,
29 | dismissAfterPick: Bool = false,
30 | moveSelectionTopmost: Bool = true
31 | ) {
32 | self.fonts = fonts
33 | self.fontSize = fontSize
34 | self.dismissAfterPick = dismissAfterPick
35 | self.moveSelectionTopmost = moveSelectionTopmost
36 | }
37 |
38 | public typealias Font = RichTextFont.PickerFont
39 | public typealias FontName = String
40 |
41 | /// The fonts to display in the list.
42 | public var fonts: [RichTextFont.PickerFont]
43 |
44 | /// The font size to use in the list items.
45 | public var fontSize: CGFloat
46 |
47 | /// Whether or not to dismiss the picker after a font is selected.
48 | public var dismissAfterPick: Bool
49 |
50 | /// Whether or not to move the selected font topmost
51 | public var moveSelectionTopmost: Bool
52 | }
53 | }
54 |
55 | extension RichTextFont.PickerConfig {
56 |
57 | /// The standard font picker configuration.
58 | public static var standard: Self { .init() }
59 | }
60 |
61 | extension RichTextFont.PickerConfig {
62 |
63 | /// The fonts to list for a given selection.
64 | public func fontsToList(for selection: FontName) -> [Font] {
65 | if moveSelectionTopmost {
66 | return fonts.moveTopmost(selection)
67 | } else {
68 | return fonts
69 | }
70 | }
71 | }
72 |
73 | extension View {
74 |
75 | /// Apply a ``RichTextFont`` picker configuration.
76 | public func richTextFontPickerConfig(
77 | _ config: RichTextFont.PickerConfig
78 | ) -> some View {
79 | self.environment(\.richTextFontPickerConfig, config)
80 | }
81 | }
82 |
83 | extension RichTextFont.PickerConfig {
84 |
85 | fileprivate struct Key: EnvironmentKey {
86 |
87 | public static var defaultValue: RichTextFont.PickerConfig {
88 | .standard
89 | }
90 | }
91 | }
92 |
93 | extension EnvironmentValues {
94 |
95 | /// This value can bind to a font picker config.
96 | public var richTextFontPickerConfig: RichTextFont.PickerConfig {
97 | get { self[RichTextFont.PickerConfig.Key.self] }
98 | set { self[RichTextFont.PickerConfig.Key.self] = newValue }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextStyle.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 22/11/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public enum RichTextStyle: String, CaseIterable, Identifiable,
11 | RichTextLabelValue
12 | {
13 |
14 | case bold
15 | case italic
16 | case underline
17 | case strikethrough
18 | }
19 |
20 | extension RichTextStyle {
21 |
22 | /// All available rich text styles.
23 | public static var all: [Self] { allCases }
24 | }
25 |
26 | extension Collection where Element == RichTextStyle {
27 |
28 | /// All available rich text styles.
29 | public static var all: [RichTextStyle] { RichTextStyle.allCases }
30 | }
31 |
32 | extension RichTextStyle {
33 |
34 | public var id: String { rawValue }
35 |
36 | /// The standard icon to use for the trait.
37 | public var icon: Image {
38 | switch self {
39 | case .bold: .richTextStyleBold
40 | case .italic: .richTextStyleItalic
41 | case .strikethrough: .richTextStyleStrikethrough
42 | case .underline: .richTextStyleUnderline
43 | }
44 | }
45 |
46 | /// The localized style title.
47 | public var title: String {
48 | titleKey.text
49 | }
50 |
51 | /// The localized style title key.
52 | public var titleKey: RTEL10n {
53 | switch self {
54 | case .bold: .styleBold
55 | case .italic: .styleItalic
56 | case .underline: .styleUnderlined
57 | case .strikethrough: .styleStrikethrough
58 | }
59 | }
60 |
61 | /**
62 | Get the rich text styles that are enabled in a provided
63 | set of traits and attributes.
64 |
65 | - Parameters:
66 | - traits: The symbolic traits to inspect.
67 | - attributes: The rich text attributes to inspect.
68 | */
69 | public static func styles(
70 | in traits: FontTraitsRepresentable?,
71 | attributes: RichTextAttributes?
72 | ) -> [RichTextStyle] {
73 | var styles = traits?.enabledRichTextStyles ?? []
74 | if attributes?.isStrikethrough == true { styles.append(.strikethrough) }
75 | if attributes?.isUnderlined == true { styles.append(.underline) }
76 | return styles
77 | }
78 | }
79 |
80 | extension Collection where Element == RichTextStyle {
81 |
82 | /// Check if the collection contains a certain style.
83 | public func hasStyle(_ style: RichTextStyle) -> Bool {
84 | contains(style)
85 | }
86 |
87 | /// Check if a certain style change should be applied.
88 | public func shouldAddOrRemove(
89 | _ style: RichTextStyle,
90 | _ newValue: Bool
91 | ) -> Bool {
92 | let shouldAdd = newValue && !hasStyle(style)
93 | let shouldRemove = !newValue && hasStyle(style)
94 | return shouldAdd || shouldRemove
95 | }
96 | }
97 |
98 | #if canImport(UIKit)
99 | extension RichTextStyle {
100 |
101 | /// The symbolic font traits for the style, if any.
102 | public var symbolicTraits: UIFontDescriptor.SymbolicTraits? {
103 | switch self {
104 | case .bold: .traitBold
105 | case .italic: .traitItalic
106 | case .strikethrough: nil
107 | case .underline: nil
108 | }
109 | }
110 | }
111 | #endif
112 |
113 | #if os(macOS)
114 | extension RichTextStyle {
115 |
116 | /// The symbolic font traits for the trait, if any.
117 | public var symbolicTraits: NSFontDescriptor.SymbolicTraits? {
118 | switch self {
119 | case .bold: .bold
120 | case .italic: .italic
121 | case .strikethrough: nil
122 | case .underline: nil
123 | }
124 | }
125 | }
126 | #endif
127 |
128 | extension RichTextStyle {
129 | var richTextSpanStyle: RichTextSpanStyle {
130 | switch self {
131 | case .bold: .bold
132 | case .italic: .italic
133 | case .strikethrough: .strikethrough
134 | case .underline: .underline
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/UI/Helper/RichEditorState+LineInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichEditorState+LineInfo.swift
3 | //
4 | //
5 | // Created by Divyesh Vekariya on 31/05/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension RichEditorState {
11 |
12 | internal struct LineInfo {
13 | let lineNumber: Int
14 | let lineRange: NSRange
15 | let lineString: String
16 | let caretLocation: Int
17 | }
18 |
19 | /**
20 | Calculates which line number (using a 0 based index) our caret is on, the range of this line (in comparison to the whole string), and the string that makes up that line of text.
21 | Will return nil if there is no caret present, and a portion of text is highlighted instead.
22 |
23 | A pain point of this function is that it cannot return the current line number when it's found, but rather
24 | has to wait for every single line to be iterated through first. This is because the enumerateSubstrings() function
25 | on the String is not an actual loop, and as such we cannot return or break within it.
26 |
27 | - returns: The line number that the caret is on, the range of our line, and the string that makes up that line of text
28 | */
29 | var currentLine: LineInfo {
30 | return getCurrentLineInfo(rawText, selectedRange: selectedRange)
31 | }
32 |
33 | internal func getCurrentLineInfo(_ string: String, selectedRange: NSRange)
34 | -> LineInfo
35 | {
36 |
37 | /// Determines if the user has selected (ie. highlighted) any text
38 | var hasSelectedText: Bool {
39 | !selectedRange.isCollapsed
40 | }
41 |
42 | /// The location of our caret within the textview
43 | var caretLocation: Int {
44 | selectedRange.location
45 | }
46 |
47 | //The line number that we're currently iterating on
48 | var lineNumber = 0
49 |
50 | //The line number & line of text that we believe the caret to be on
51 | var selectedLineNumber = 0
52 | var selectedLineRange = selectedRange
53 | var selectedLineOfText = ""
54 | var caretLocationInLine = 0
55 |
56 | var foundSelectedLine = false
57 |
58 | //Iterate over every line in our TextView
59 | string.enumerateSubstrings(
60 | in: string.startIndex..= startOfLine && caretLocation <= endOfLine {
71 | // MARK the line number
72 | selectedLineNumber = lineNumber
73 | selectedLineOfText = substring ?? ""
74 | selectedLineRange = range
75 | caretLocationInLine = caretLocation - startOfLine
76 |
77 | foundSelectedLine = true
78 | }
79 |
80 | lineNumber += 1
81 | }
82 |
83 | //If we're not at the starting point, and we didn't find a current line, then we're at the end of our TextView
84 | if caretLocation > 0 && !foundSelectedLine {
85 | selectedLineNumber = lineNumber
86 | selectedLineOfText = ""
87 | selectedLineRange = NSRange(location: caretLocation, length: 0)
88 | }
89 | return LineInfo(
90 | lineNumber: selectedLineNumber, lineRange: selectedLineRange,
91 | lineString: selectedLineOfText, caretLocation: caretLocationInLine)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RichTextFormat+Toolbar.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 18/11/24.
6 | //
7 |
8 | #if os(iOS) || os(macOS) || os(visionOS)
9 | import SwiftUI
10 |
11 | extension RichTextFormat {
12 |
13 | /**
14 | This horizontal toolbar provides text format controls.
15 |
16 | This toolbar adapts the layout based on horizontal size
17 | class. The control row is split in two for compact size,
18 | while os(macOS) and regular sizes get a single row.
19 |
20 | You can configure and style the view by applying config
21 | and style view modifiers to your view hierarchy:
22 |
23 | ```swift
24 | VStack {
25 | ...
26 | }
27 | .richTextFormatToolbarStyle(...)
28 | .richTextFormatToolbarConfig(...)
29 | ```
30 | */
31 | public struct Toolbar: RichTextFormatToolbarBase {
32 |
33 | /**
34 | Create a rich text format sheet.
35 |
36 | - Parameters:
37 | - context: The context to apply changes to.
38 | */
39 | public init(
40 | context: RichEditorState
41 | ) {
42 | self._context = ObservedObject(wrappedValue: context)
43 | }
44 |
45 | @ObservedObject
46 | private var context: RichEditorState
47 |
48 | @Environment(\.richTextFormatToolbarConfig)
49 | var config
50 |
51 | @Environment(\.richTextFormatToolbarStyle)
52 | var style
53 |
54 | @Environment(\.horizontalSizeClass)
55 | private var horizontalSizeClass
56 |
57 | public var body: some View {
58 | VStack(spacing: style.spacing) {
59 | controls
60 | if hasColorPickers {
61 | Divider()
62 | colorPickers(for: context)
63 | }
64 | }
65 | .labelsHidden()
66 | .padding(.vertical, style.padding)
67 | .environment(\.sizeCategory, .medium)
68 | // .background(background)
69 | #if os(macOS)
70 | .frame(minWidth: 650)
71 | #endif
72 | }
73 | }
74 | }
75 |
76 | // MARK: - Views
77 |
78 | extension RichTextFormat.Toolbar {
79 |
80 | fileprivate var useSingleLine: Bool {
81 | #if os(macOS)
82 | true
83 | #else
84 | horizontalSizeClass == .regular
85 | #endif
86 | }
87 | }
88 |
89 | extension RichTextFormat.Toolbar {
90 |
91 | fileprivate var background: some View {
92 | Color.clear
93 | .overlay(Color.primary.opacity(0.1))
94 | .shadow(color: .black.opacity(0.1), radius: 5)
95 | .edgesIgnoringSafeArea(.all)
96 | }
97 |
98 | @ViewBuilder
99 | fileprivate var controls: some View {
100 | if useSingleLine {
101 | HStack {
102 | controlsContent
103 | }
104 | .padding(.horizontal, style.padding)
105 | } else {
106 | VStack(spacing: style.spacing) {
107 | controlsContent
108 | }
109 | .padding(.horizontal, style.padding)
110 | }
111 | }
112 |
113 | @ViewBuilder
114 | fileprivate var controlsContent: some View {
115 | HStack {
116 | #if os(macOS)
117 | headerPicker(context: context)
118 | fontPicker(value: $context.fontName)
119 | .onChangeBackPort(of: context.fontName) { newValue in
120 | context.updateStyle(style: .font(newValue))
121 | }
122 | #endif
123 | styleToggleGroup(for: context)
124 | otherMenuToggleGroup(for: context)
125 | if !useSingleLine {
126 | Spacer()
127 | }
128 | fontSizePicker(for: context)
129 | if horizontalSizeClass == .regular {
130 | Spacer()
131 | }
132 | }
133 | HStack {
134 | #if !macOS
135 | headerPicker(context: context)
136 | #endif
137 | alignmentPicker(context: context)
138 | // superscriptButtons(for: context, greedy: false)
139 | // indentButtons(for: context, greedy: false)
140 | }
141 | }
142 | }
143 | #endif
144 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/Localization/RTEL10n.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RTEL10n.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 29/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This enum defines RichTextKit-specific, localized texts.
11 | public enum RTEL10n: String, CaseIterable, Identifiable {
12 |
13 | case
14 | done,
15 | more,
16 |
17 | font,
18 | fontSize,
19 | fontSizeIncrease,
20 | fontSizeIncreaseDescription,
21 | fontSizeDecrease,
22 | fontSizeDecreaseDescription,
23 |
24 | setHeaderStyle,
25 |
26 | color,
27 | foregroundColor,
28 | backgroundColor,
29 | underlineColor,
30 | strikethroughColor,
31 | strokeColor,
32 |
33 | actionCopy,
34 | actionDismissKeyboard,
35 | actionPrint,
36 | actionRedoLatestChange,
37 | actionUndoLatestChange,
38 |
39 | fileFormatRtk,
40 | fileFormatPdf,
41 | fileFormatRtf,
42 | fileFormatTxt,
43 | fileFormatJson,
44 |
45 | indent,
46 | indentIncrease,
47 | indentIncreaseDescription,
48 | indentDecrease,
49 | indentDecreaseDescription,
50 |
51 | lineSpacing,
52 | lineSpacingIncrease,
53 | lineSpacingIncreaseDescription,
54 | lineSpacingDecrease,
55 | lineSpacingDecreaseDescription,
56 |
57 | menuExport,
58 | menuExportAs,
59 | menuFormat,
60 | menuPrint,
61 | menuSave,
62 | menuSaveAs,
63 | menuShare,
64 | menuShareAs,
65 | menuText,
66 |
67 | highlightedRange,
68 | highlightingStyle,
69 |
70 | pasteImage,
71 | pasteImages,
72 | pasteText,
73 | selectRange,
74 |
75 | setAttributedString,
76 |
77 | styleBold,
78 | styleItalic,
79 | styleStrikethrough,
80 | styleUnderlined,
81 |
82 | superscript,
83 | superscriptIncrease,
84 | superscriptIncreaseDescription,
85 | superscriptDecrease,
86 | superscriptDecreaseDescription,
87 |
88 | textAlignment,
89 | textAlignmentLeft,
90 | textAlignmentRight,
91 | textAlignmentCentered,
92 | textAlignmentJustified,
93 |
94 | headerDefault,
95 | header1,
96 | header2,
97 | header3,
98 | header4,
99 | header5,
100 | header6,
101 |
102 | link,
103 |
104 | ignoreIt
105 | }
106 |
107 | extension RTEL10n {
108 |
109 | public static func actionStepFontSize(
110 | _ points: Int
111 | ) -> RTEL10n {
112 | points < 0 ? .fontSizeDecreaseDescription : .fontSizeIncreaseDescription
113 | }
114 |
115 | public static func actionStepIndent(
116 | _ points: Double
117 | ) -> RTEL10n {
118 | points < 0 ? .indentDecreaseDescription : .indentIncreaseDescription
119 | }
120 |
121 | public static func actionStepLineSpacing(
122 | _ points: CGFloat
123 | ) -> RTEL10n {
124 | points < 0
125 | ? .lineSpacingDecreaseDescription : .lineSpacingIncreaseDescription
126 | }
127 |
128 | public static func actionStepSuperscript(
129 | _ steps: Int
130 | ) -> RTEL10n {
131 | steps < 0
132 | ? .superscriptDecreaseDescription : .superscriptIncreaseDescription
133 | }
134 |
135 | public static func menuIndent(_ points: Double) -> RTEL10n {
136 | points < 0 ? .indentDecrease : .indentIncrease
137 | }
138 | }
139 |
140 | extension RTEL10n {
141 |
142 | /// The item's unique identifier.
143 | public var id: String { rawValue }
144 |
145 | /// The item's localization key.
146 | public var key: String { rawValue }
147 |
148 | /// The item's localized text.
149 | public var text: String {
150 | rawValue
151 | }
152 |
153 | /// Get the localized text for a certain `Locale`.
154 | // func text(for locale: Locale) -> String {
155 | // guard let bundle = Bundle.module.bundle(for: locale) else { return "" }
156 | // return NSLocalizedString(key, bundle: bundle, comment: "")
157 | // }
158 | }
159 |
--------------------------------------------------------------------------------
/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Init.swift
3 | // RichEditorSwiftUI
4 | //
5 | // Created by Divyesh Vekariya on 26/11/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension NSAttributedString {
11 |
12 | /**
13 | Try to parse ``RichTextDataFormat`` formatted data.
14 |
15 | - Parameters:
16 | - data: The data to initialize the string with.
17 | - format: The data format to use.
18 | */
19 | public convenience init(
20 | data: Data,
21 | format: RichTextDataFormat
22 | ) throws {
23 | switch format {
24 | case .archivedData: try self.init(archivedData: data)
25 | case .plainText: try self.init(plainTextData: data)
26 | case .rtf: try self.init(rtfData: data)
27 | case .vendorArchivedData: try self.init(archivedData: data)
28 | }
29 | }
30 | }
31 |
32 | extension NSAttributedString {
33 |
34 | /// Try to parse ``RichTextDataFormat/archivedData``.
35 | fileprivate convenience init(archivedData data: Data) throws {
36 | let unarchived = try NSKeyedUnarchiver.unarchivedObject(
37 | ofClass: NSAttributedString.self,
38 | from: data)
39 | guard let string = unarchived else {
40 | throw RichTextDataError.invalidArchivedData(in: data)
41 | }
42 | self.init(attributedString: string)
43 | }
44 |
45 | /// Try to parse ``RichTextDataFormat/plainText`` data.
46 | fileprivate convenience init(plainTextData data: Data) throws {
47 | let decoded = String(data: data, encoding: .utf8)
48 | guard let string = decoded else {
49 | throw RichTextDataError.invalidPlainTextData(in: data)
50 | }
51 | let attributed = NSAttributedString(string: string)
52 | self.init(attributedString: attributed)
53 | }
54 |
55 | /// Try to parse ``RichTextDataFormat/rtf`` data.
56 | fileprivate convenience init(rtfData data: Data) throws {
57 | var attributes = Self.rtfDataAttributes as NSDictionary?
58 | try self.init(
59 | data: data,
60 | options: [.characterEncoding: Self.utf8],
61 | documentAttributes: &attributes
62 | )
63 | }
64 |
65 | /// Try to parse ``RichTextDataFormat/rtfd`` data.
66 | fileprivate convenience init(rtfdData data: Data) throws {
67 | var attributes = Self.rtfdDataAttributes as NSDictionary?
68 | try self.init(
69 | data: data,
70 | options: [.characterEncoding: Self.utf8],
71 | documentAttributes: &attributes
72 | )
73 | }
74 |
75 | #if os(macOS)
76 | /// Try to parse ``RichTextDataFormat/word`` data.
77 | convenience init(wordData data: Data) throws {
78 | var attributes = Self.wordDataAttributes as NSDictionary?
79 | try self.init(
80 | data: data,
81 | options: [.characterEncoding: Self.utf8],
82 | documentAttributes: &attributes
83 | )
84 | }
85 | #endif
86 |
87 | fileprivate convenience init(jsonData data: Data) throws {
88 | let decoder = JSONDecoder()
89 | let richText = try? decoder.decode(RichText.self, from: data)
90 | guard let richText = richText else {
91 | throw RichTextDataError.invalidPlainTextData(in: data)
92 | }
93 |
94 | var tempSpans: [RichTextSpanInternal] = []
95 | var text = ""
96 | richText.spans.forEach({
97 | let span = RichTextSpanInternal(
98 | from: text.utf16Length,
99 | to: (text.utf16Length + $0.insert.utf16Length - 1),
100 | attributes: $0.attributes)
101 | tempSpans.append(span)
102 | text += $0.insert
103 | })
104 |
105 | let attributedString = NSMutableAttributedString(string: text)
106 |
107 | tempSpans.forEach { span in
108 | attributedString.addAttributes(
109 | span.attributes?.toAttributes() ?? [:], range: span.spanRange)
110 | }
111 | self.init(attributedString: attributedString)
112 | }
113 | }
114 |
115 | extension NSAttributedString {
116 |
117 | fileprivate static var utf8: UInt {
118 | String.Encoding.utf8.rawValue
119 | }
120 |
121 | fileprivate static var rtfDataAttributes: [DocumentAttributeKey: Any] {
122 | [.documentType: NSAttributedString.DocumentType.rtf]
123 | }
124 |
125 | fileprivate static var rtfdDataAttributes: [DocumentAttributeKey: Any] {
126 | [.documentType: NSAttributedString.DocumentType.rtfd]
127 | }
128 |
129 | #if os(macOS)
130 | static var wordDataAttributes: [DocumentAttributeKey: Any] {
131 | [.documentType: NSAttributedString.DocumentType.docFormat]
132 | }
133 | #endif
134 | }
135 |
--------------------------------------------------------------------------------