Skip to content

Commit

Permalink
feat: First working version.
Browse files Browse the repository at this point in the history
  • Loading branch information
laosb committed Feb 20, 2024
1 parent a7c210f commit e3d7af9
Show file tree
Hide file tree
Showing 28 changed files with 1,176 additions and 95 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ concurrency:

jobs:
docs:
runs-on: macOS-12
runs-on: macOS-14

steps:
- uses: actions/checkout@v3
- name: Generate Docs
env:
OTHER_DOCC_FLAGS: "--transform-for-static-hosting --output-path=./public"
run: xcodebuild docbuild -scheme EasyRichText -destination 'platform=iOS Simulator,name=iPhone 13'
run: xcodebuild docbuild -scheme EasyRichText-Package -destination 'platform=iOS Simulator,name=iPhone 15'
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@1
with:
Expand Down
16 changes: 15 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "EasyRichText",
platforms: [
.iOS(.v15),
.macOS(.v12),
.macCatalyst(.v15),
.tvOS(.v15),
.watchOS(.v10),
.visionOS(.v1)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "EasyRichText",
targets: ["EasyRichText"]),
.library(
name: "EasyRichTextUI",
targets: ["EasyRichTextUI"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
Expand All @@ -21,6 +32,9 @@ let package = Package(
.target(
name: "EasyRichText",
dependencies: []),
.target(
name: "EasyRichTextUI",
dependencies: ["EasyRichText"]),
.testTarget(
name: "EasyRichTextTests",
dependencies: ["EasyRichText"]),
Expand Down
14 changes: 6 additions & 8 deletions Sources/EasyRichText/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
# ``EasyRichText``

A simple library & format for simple rich text.
A set of tools and helpers for building your own rich text editor and encoding upon.

## Overview

This package is a collection of two libraries:

- **EasyRichText**: Utilities and helpers for creating your own rich text format, converting between old `NSAttributedString`, new `AttributedString`, and your rich text format.
- [**EasyRichTextUI**](./EasyRichTextUI): A SwiftUI view wrapping `UITextView` / `NSTextView` for editing rich text. A context object is provided for writing your own UI for common style operations.

## Topics

### Codable Models
### Essentials

- ``ERTTextSegment``
- <doc:Features>
- ``ERTRichText``

### Styling

- ``ERTTextSegment/Style``
- ``ERTTextSegment/Color``
28 changes: 28 additions & 0 deletions Sources/EasyRichText/Documentation.docc/Features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Features

Distinct style component for a segment of text.

## Overview

In EasyRichText, rich texts are expressed as an array of text segments (*runs*) with different styles (*features*) attached to each of the segments.

Features are the smallest unit of style in EasyRichText. They are used to represent a single style property, such as font, color, or underline. Each feature is represented by a `struct` conforming to `ERTFeature` protocol.

## Topics

### Basic Protocols

- ``ERTFeature``
- ``ERTSingleKeyFeature``
- ``ERTSymbolicTraitFeature-fpvl``

### Font Features

- ``ERTBoldFeature``
- ``ERTItalicFeature``
- ``ERTUnderlineFeature``

### Color Features

- ``ERTForegroundColorFeature``
- ``ERTBackgroundColorFeature``
83 changes: 83 additions & 0 deletions Sources/EasyRichText/ERTAttributedStringBridge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// ERTAttributedStringBridge.swift
// RichTextTest
//
// Created by Shibo Lyu on 2024/2/2.
//

import Foundation
import SwiftUI

@_spi(Internal) public extension NSAttributedString.Key {
static let ertSynthesizedItalic = Self("sb.lao.packages.easyrichtext.SynthesizedItalic")
}

public struct ERTAttributedStringBridge {
public static var `default` = ERTAttributedStringBridge()

public var bridgeColor: Bool
public var bridgeInternalAttributes: Bool

public init(bridgeColor: Bool = true, bridgeInternalAttributes: Bool = true) {
self.bridgeColor = bridgeColor
self.bridgeInternalAttributes = bridgeInternalAttributes
}

public func nsAttributedString(for attributedString: AttributedString) -> NSAttributedString {
let mutableString = NSMutableAttributedString()

for run in attributedString.runs {
let mutableSubstring = NSMutableAttributedString(AttributedString(attributedString[run.range]))
if bridgeInternalAttributes {
if run.synthesizedItalic ?? false {
mutableSubstring.addAttribute(.ertSynthesizedItalic, value: true, range: NSRange(location: 0, length: mutableSubstring.length))
}
}
mutableString.append(mutableSubstring)
}

return mutableString
}

public func attributedString(for nsAttributedString: NSAttributedString) -> AttributedString {
var attributedString = AttributedString()

nsAttributedString.enumerateAttributes(in: .init(location: 0, length: nsAttributedString.length)) { attributes, range, _ in
let substring = nsAttributedString.attributedSubstring(from: range)
var attributedSubstring = AttributedString(substring)

if bridgeInternalAttributes {
if let synthesizedItalicValue = attributes[.ertSynthesizedItalic], let synthesizedItalic = synthesizedItalicValue as? Bool, synthesizedItalic {
attributedSubstring.synthesizedItalic = true
}
}
attributedString += attributedSubstring
}

if bridgeColor {
#if canImport(AppKit)
for run in attributedString.runs {
if let nsColor = run.attributes.appKit.foregroundColor {
attributedString[run.range].swiftUI.foregroundColor = Color(nsColor: nsColor)
}
if let nsColor = run.attributes.appKit.backgroundColor {
attributedString[run.range].swiftUI.backgroundColor = Color(nsColor: nsColor)
}
}
#elseif canImport(UIKit)
for run in attributedString.runs {
if let uiColor = run.attributes.uiKit.foregroundColor {
attributedString[run.range].swiftUI.foregroundColor = Color(uiColor: uiColor)
}
if let uiColor = run.attributes.uiKit.backgroundColor {
attributedString[run.range].swiftUI.backgroundColor = Color(uiColor: uiColor)
}
}
#endif
}

print("ERTAttributedStringBridge attributedString: NSAttributedString: \(nsAttributedString), AttributedString: \(attributedString)")

return attributedString
}
}
18 changes: 18 additions & 0 deletions Sources/EasyRichText/ERTAttributes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// ERTAttributes.swift
// RichTextTest
//
// Created by Shibo Lyu on 2024/2/2.
//

import Foundation

@_spi(Internal) public struct ERTAttributes: AttributeScope {
@_spi(Internal) let synthesizedItalic: ERTItalicSynthesizer.SynthesizedItalicKey
}

@_spi(Internal) public extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<ERTAttributes, T>) -> T {
return self[T.self]
}
}
55 changes: 55 additions & 0 deletions Sources/EasyRichText/ERTFontUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// ERTFontUtils.swift
// RichTextTest
//
// Created by Shibo Lyu on 2024/2/1.
//

import Foundation
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif

public struct ERTFontUtils {
public static let `default` = ERTFontUtils()

#if canImport(AppKit)
public func font(_ font: NSFont, with descriptor: NSFontDescriptor) -> NSFont {
return .init(descriptor: descriptor, size: font.pointSize) ?? font
}

public func font(_ font: NSFont, modifyingDescriptor: (NSFontDescriptor) -> NSFontDescriptor) -> NSFont {
return self.font(font, with: modifyingDescriptor(font.fontDescriptor))
}

public func font(_ font: NSFont, with symbolicTraits: NSFontDescriptor.SymbolicTraits) -> NSFont {
return self.font(font, with: font.fontDescriptor.withSymbolicTraits(symbolicTraits))
}

public func font(_ font: NSFont, modifySymbolicTraits: (inout NSFontDescriptor.SymbolicTraits) -> Void) -> NSFont {
var traits = font.fontDescriptor.symbolicTraits
modifySymbolicTraits(&traits)
return self.font(font, with: traits)
}
#elseif canImport(UIKit)
public func font(_ font: UIFont, with descriptor: UIFontDescriptor) -> UIFont {
return .init(descriptor: descriptor, size: font.pointSize)
}

public func font(_ font: UIFont, modifyingDescriptor: (UIFontDescriptor) -> UIFontDescriptor) -> UIFont {
return self.font(font, with: modifyingDescriptor(font.fontDescriptor))
}

public func font(_ font: UIFont, with symbolicTraits: UIFontDescriptor.SymbolicTraits) -> UIFont {
return self.font(font, with: font.fontDescriptor.withSymbolicTraits(symbolicTraits) ?? font.fontDescriptor)
}

public func font(_ font: UIFont, modifySymbolicTraits: (inout UIFontDescriptor.SymbolicTraits) -> Void) -> UIFont {
var traits = font.fontDescriptor.symbolicTraits
modifySymbolicTraits(&traits)
return self.font(font, with: traits)
}
#endif
}
111 changes: 111 additions & 0 deletions Sources/EasyRichText/ERTItalicSynthesizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// ERTItalicSynthesizer.swift
// RichTextTest
//
// Created by Shibo Lyu on 2024/1/31.
//

import Foundation
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif

public struct ERTItalicSynthesizer {
public struct SynthesizedItalicKey: AttributedStringKey {
public typealias Value = Bool
public static let name = "sb.lao.packages.easyrichtext.SynthesizedItalic"
}

public static let `default` = ERTItalicSynthesizer()

public var fontUtils: ERTFontUtils
#if canImport(AppKit)
public var matrix: AffineTransform

public init(matrix: AffineTransform = .init(m11: 1, m12: 0, m21: 0.2, m22: 1, tX: 0, tY: 0), fontUtils: ERTFontUtils = .default) {
self.matrix = matrix
self.fontUtils = fontUtils
}
#elseif canImport(UIKit)
public var matrix: CGAffineTransform

public init(matrix: CGAffineTransform = .init(a: 1, b: 0, c: 0.2, d: 1, tx: 0, ty: 0), fontUtils: ERTFontUtils = .default) {
self.matrix = matrix
self.fontUtils = fontUtils
}
#endif

#if canImport(AppKit)
@_spi(Internal) public func synthesize(_ font: NSFont) -> NSFont {
fontUtils.font(font) { descriptor in
let newTraits = font.fontDescriptor.symbolicTraits.subtracting(.italic)
let newDescriptor = font.fontDescriptor.withSymbolicTraits(newTraits).withMatrix(matrix)
return newDescriptor
}
}
#elseif canImport(UIKit)
@_spi(Internal) public func synthesize(_ font: UIFont) -> UIFont {
fontUtils.font(font) { descriptor in
let newTraits = font.fontDescriptor.symbolicTraits.subtracting(.traitItalic)
let newDescriptor = font.fontDescriptor.withSymbolicTraits(newTraits)?.withMatrix(matrix)
return newDescriptor ?? descriptor
}
}
#endif

#if canImport(AppKit)
func desynthesize(_ font: NSFont) -> NSFont {
fontUtils.font(font) { descriptor in
let newTraits = font.fontDescriptor.symbolicTraits.union(.italic)
let newDescriptor = font.fontDescriptor.withSymbolicTraits(newTraits).withMatrix(.identity)
return newDescriptor
}
}
#elseif canImport(UIKit)
func desynthesize(_ font: UIFont) -> UIFont {
fontUtils.font(font) { descriptor in
let newTraits = font.fontDescriptor.symbolicTraits.union(.traitItalic)
let newDescriptor = font.fontDescriptor.withSymbolicTraits(newTraits)?.withMatrix(.identity)
return newDescriptor ?? descriptor
}
}
#endif

public func synthesize(_ attributedString: AttributedString) -> AttributedString {
var attributedString = attributedString

for run in attributedString.runs {
#if canImport(AppKit)
guard let font = run.appKit.font, font.fontDescriptor.symbolicTraits.contains(.italic) else { continue }
#elseif canImport(UIKit)
guard let font = run.uiKit.font, font.fontDescriptor.symbolicTraits.contains(.traitItalic) else { continue }
#endif

attributedString[run.range].font = synthesize(font)
attributedString[run.range].synthesizedItalic = true
}

return attributedString
}

public func desynthesize(_ attributedString: AttributedString) -> AttributedString {
var attributedString = attributedString

for run in attributedString.runs {
guard attributedString[run.range].synthesizedItalic ?? false else { continue }

#if canImport(AppKit)
guard let font: NSFont = run.font else { continue }
#elseif canImport(UIKit)
guard let font: UIFont = run.font else { continue }
#endif

attributedString[run.range].font = desynthesize(font)
attributedString[run.range].synthesizedItalic = false
}

return attributedString
}
}
6 changes: 0 additions & 6 deletions Sources/EasyRichText/EasyRichText.swift

This file was deleted.

Loading

0 comments on commit e3d7af9

Please sign in to comment.