@apollohg/react-native-prose-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.md +143 -0
- package/android/build.gradle +39 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +228 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +254 -0
- package/ios/EditorTheme.swift +372 -0
- package/ios/Generated_editor_core.swift +1143 -0
- package/ios/NativeEditorExpoView.swift +1417 -0
- package/ios/NativeEditorModule.swift +263 -0
- package/ios/PositionBridge.swift +278 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +825 -0
- package/ios/RichTextEditorView.swift +1559 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +66 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
- package/src/EditorTheme.ts +130 -0
- package/src/EditorToolbar.tsx +620 -0
- package/src/NativeEditorBridge.ts +607 -0
- package/src/NativeRichTextEditor.tsx +951 -0
- package/src/addons.ts +158 -0
- package/src/index.ts +63 -0
- package/src/schemas.ts +153 -0
- package/src/useNativeEditor.ts +173 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import CoreText
|
|
3
|
+
|
|
4
|
+
/// Draws list markers visually in the gutter without inserting them into the
|
|
5
|
+
/// editable text storage. This keeps UIKit paragraph-start behaviors, such as
|
|
6
|
+
/// sentence auto-capitalization, working naturally inside list items.
|
|
7
|
+
final class EditorLayoutManager: NSLayoutManager {
|
|
8
|
+
|
|
9
|
+
override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
|
|
10
|
+
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
|
|
11
|
+
|
|
12
|
+
guard let textStorage, glyphsToShow.length > 0 else { return }
|
|
13
|
+
|
|
14
|
+
let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
|
|
15
|
+
let nsString = textStorage.string as NSString
|
|
16
|
+
var drawnParagraphStarts = Set<Int>()
|
|
17
|
+
|
|
18
|
+
textStorage.enumerateAttribute(
|
|
19
|
+
RenderBridgeAttributes.listMarkerContext,
|
|
20
|
+
in: characterRange,
|
|
21
|
+
options: []
|
|
22
|
+
) { value, range, _ in
|
|
23
|
+
guard range.length > 0, let listContext = value as? [String: Any] else { return }
|
|
24
|
+
|
|
25
|
+
let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
|
|
26
|
+
let paragraphStart = paragraphRange.location
|
|
27
|
+
guard !Self.isParagraphStartCreatedByHardBreak(paragraphStart, in: textStorage) else {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
guard drawnParagraphStarts.insert(paragraphStart).inserted else { return }
|
|
31
|
+
|
|
32
|
+
self.drawListMarker(
|
|
33
|
+
listContext: listContext,
|
|
34
|
+
paragraphStart: paragraphStart,
|
|
35
|
+
origin: origin,
|
|
36
|
+
textStorage: textStorage
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private func drawListMarker(
|
|
42
|
+
listContext: [String: Any],
|
|
43
|
+
paragraphStart: Int,
|
|
44
|
+
origin: CGPoint,
|
|
45
|
+
textStorage: NSTextStorage
|
|
46
|
+
) {
|
|
47
|
+
guard paragraphStart < textStorage.length else { return }
|
|
48
|
+
|
|
49
|
+
let glyphIndex = glyphIndexForCharacter(at: paragraphStart)
|
|
50
|
+
guard glyphIndex < numberOfGlyphs else { return }
|
|
51
|
+
|
|
52
|
+
var lineGlyphRange = NSRange()
|
|
53
|
+
let usedRect = lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: &lineGlyphRange)
|
|
54
|
+
let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
|
|
55
|
+
let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
|
|
56
|
+
|
|
57
|
+
let baseFont = attrs[.font] as? UIFont ?? .systemFont(ofSize: 16)
|
|
58
|
+
let textColor = attrs[RenderBridgeAttributes.listMarkerColor] as? UIColor
|
|
59
|
+
?? attrs[.foregroundColor] as? UIColor
|
|
60
|
+
?? .label
|
|
61
|
+
let markerScale = (attrs[RenderBridgeAttributes.listMarkerScale] as? NSNumber)
|
|
62
|
+
.map { CGFloat(truncating: $0) }
|
|
63
|
+
?? LayoutConstants.unorderedListMarkerFontScale
|
|
64
|
+
let markerWidth = (attrs[RenderBridgeAttributes.listMarkerWidth] as? NSNumber)
|
|
65
|
+
.map { CGFloat(truncating: $0) }
|
|
66
|
+
?? LayoutConstants.listMarkerWidth
|
|
67
|
+
let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
|
|
68
|
+
|
|
69
|
+
let glyphLocation = location(forGlyphAt: glyphIndex)
|
|
70
|
+
let baselineY = lineFragmentRect.minY + glyphLocation.y
|
|
71
|
+
|
|
72
|
+
if ordered {
|
|
73
|
+
let markerFont = markerFont(
|
|
74
|
+
for: listContext,
|
|
75
|
+
baseFont: baseFont,
|
|
76
|
+
markerScale: markerScale
|
|
77
|
+
)
|
|
78
|
+
let markerText = RenderBridge.listMarkerString(listContext: listContext)
|
|
79
|
+
let markerOrigin = Self.orderedMarkerDrawingOrigin(
|
|
80
|
+
usedRect: usedRect,
|
|
81
|
+
lineFragmentRect: lineFragmentRect,
|
|
82
|
+
markerWidth: markerWidth,
|
|
83
|
+
baselineY: baselineY,
|
|
84
|
+
markerFont: markerFont,
|
|
85
|
+
markerText: markerText,
|
|
86
|
+
origin: origin
|
|
87
|
+
)
|
|
88
|
+
let markerAttrs: [NSAttributedString.Key: Any] = [
|
|
89
|
+
.font: markerFont,
|
|
90
|
+
.foregroundColor: textColor,
|
|
91
|
+
]
|
|
92
|
+
NSAttributedString(string: markerText, attributes: markerAttrs).draw(at: markerOrigin)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let bulletRect = Self.unorderedBulletDrawingRect(
|
|
97
|
+
usedRect: usedRect,
|
|
98
|
+
lineFragmentRect: lineFragmentRect,
|
|
99
|
+
markerWidth: markerWidth,
|
|
100
|
+
baselineY: baselineY,
|
|
101
|
+
baseFont: baseFont,
|
|
102
|
+
markerScale: markerScale,
|
|
103
|
+
origin: origin
|
|
104
|
+
)
|
|
105
|
+
let path = UIBezierPath(ovalIn: bulletRect)
|
|
106
|
+
textColor.setFill()
|
|
107
|
+
path.fill()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static func markerParagraphStyle(from attrs: [NSAttributedString.Key: Any]) -> NSMutableParagraphStyle {
|
|
111
|
+
let markerStyle = NSMutableParagraphStyle()
|
|
112
|
+
let sourceStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
|
113
|
+
|
|
114
|
+
markerStyle.minimumLineHeight = sourceStyle?.minimumLineHeight ?? 0
|
|
115
|
+
markerStyle.maximumLineHeight = sourceStyle?.maximumLineHeight ?? 0
|
|
116
|
+
markerStyle.lineHeightMultiple = sourceStyle?.lineHeightMultiple ?? 0
|
|
117
|
+
markerStyle.baseWritingDirection = sourceStyle?.baseWritingDirection ?? .natural
|
|
118
|
+
markerStyle.alignment = .right
|
|
119
|
+
markerStyle.lineBreakMode = .byClipping
|
|
120
|
+
markerStyle.firstLineHeadIndent = 0
|
|
121
|
+
markerStyle.headIndent = 0
|
|
122
|
+
markerStyle.tailIndent = 0
|
|
123
|
+
|
|
124
|
+
return markerStyle
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static func markerDrawingRect(
|
|
128
|
+
usedRect: CGRect,
|
|
129
|
+
lineFragmentRect: CGRect,
|
|
130
|
+
markerWidth: CGFloat,
|
|
131
|
+
baselineY: CGFloat,
|
|
132
|
+
markerFont: UIFont,
|
|
133
|
+
origin: CGPoint
|
|
134
|
+
) -> CGRect {
|
|
135
|
+
let typographicHeight = markerFont.ascender - markerFont.descender
|
|
136
|
+
let leading = max(markerFont.lineHeight - typographicHeight, 0)
|
|
137
|
+
let topY = baselineY - markerFont.ascender - (leading / 2.0)
|
|
138
|
+
let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
|
|
139
|
+
return CGRect(
|
|
140
|
+
x: origin.x + referenceRect.minX - markerWidth,
|
|
141
|
+
y: origin.y + topY,
|
|
142
|
+
width: markerWidth - 4.0,
|
|
143
|
+
height: markerFont.lineHeight
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static func orderedMarkerDrawingOrigin(
|
|
148
|
+
usedRect: CGRect,
|
|
149
|
+
lineFragmentRect: CGRect,
|
|
150
|
+
markerWidth: CGFloat,
|
|
151
|
+
baselineY: CGFloat,
|
|
152
|
+
markerFont: UIFont,
|
|
153
|
+
markerText: String,
|
|
154
|
+
origin: CGPoint
|
|
155
|
+
) -> CGPoint {
|
|
156
|
+
let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
|
|
157
|
+
let visibleMarkerText = markerText.trimmingCharacters(in: .whitespaces)
|
|
158
|
+
let markerSize = (visibleMarkerText as NSString).size(withAttributes: [
|
|
159
|
+
.font: markerFont,
|
|
160
|
+
])
|
|
161
|
+
let rightInset: CGFloat = 4.0
|
|
162
|
+
let x = origin.x + referenceRect.minX - rightInset - ceil(markerSize.width)
|
|
163
|
+
let y = origin.y + baselineY - markerFont.ascender
|
|
164
|
+
return CGPoint(x: x, y: y)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static func markerBaselineOffset(
|
|
168
|
+
for listContext: [String: Any],
|
|
169
|
+
baseFont: UIFont,
|
|
170
|
+
markerFont: UIFont
|
|
171
|
+
) -> CGFloat {
|
|
172
|
+
let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
|
|
173
|
+
guard !ordered else { return 0 }
|
|
174
|
+
|
|
175
|
+
let targetMidline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
|
|
176
|
+
let glyphMidline = unorderedBulletGlyphMidline(for: markerFont)
|
|
177
|
+
return targetMidline - glyphMidline
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static func unorderedBulletDrawingRect(
|
|
181
|
+
usedRect: CGRect,
|
|
182
|
+
lineFragmentRect: CGRect,
|
|
183
|
+
markerWidth: CGFloat,
|
|
184
|
+
baselineY: CGFloat,
|
|
185
|
+
baseFont: UIFont,
|
|
186
|
+
markerScale: CGFloat,
|
|
187
|
+
origin: CGPoint
|
|
188
|
+
) -> CGRect {
|
|
189
|
+
let markerFont = baseFont.withSize(baseFont.pointSize * markerScale)
|
|
190
|
+
let bulletBounds = unorderedBulletGlyphBounds(for: markerFont)
|
|
191
|
+
let bulletDiameter = max(max(bulletBounds.width, bulletBounds.height), 1)
|
|
192
|
+
let targetCenterAboveBaseline = (baseFont.xHeight > 0 ? baseFont.xHeight : baseFont.capHeight) / 2.0
|
|
193
|
+
let centerY = baselineY - targetCenterAboveBaseline
|
|
194
|
+
let referenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
|
|
195
|
+
let rightInset = LayoutConstants.listMarkerTextGap
|
|
196
|
+
let x = origin.x + referenceRect.minX - rightInset - bulletDiameter
|
|
197
|
+
let y = origin.y + centerY - (bulletDiameter / 2.0)
|
|
198
|
+
|
|
199
|
+
return CGRect(
|
|
200
|
+
x: x,
|
|
201
|
+
y: y,
|
|
202
|
+
width: bulletDiameter,
|
|
203
|
+
height: bulletDiameter
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static func isParagraphStartCreatedByHardBreak(
|
|
208
|
+
_ paragraphStart: Int,
|
|
209
|
+
in textStorage: NSTextStorage
|
|
210
|
+
) -> Bool {
|
|
211
|
+
guard paragraphStart > 0, paragraphStart <= textStorage.length else { return false }
|
|
212
|
+
let previousVoidType = textStorage.attribute(
|
|
213
|
+
RenderBridgeAttributes.voidNodeType,
|
|
214
|
+
at: paragraphStart - 1,
|
|
215
|
+
effectiveRange: nil
|
|
216
|
+
) as? String
|
|
217
|
+
return previousVoidType == "hardBreak"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private func markerFont(
|
|
221
|
+
for listContext: [String: Any],
|
|
222
|
+
baseFont: UIFont,
|
|
223
|
+
markerScale: CGFloat
|
|
224
|
+
) -> UIFont {
|
|
225
|
+
let ordered = (listContext["ordered"] as? NSNumber)?.boolValue ?? false
|
|
226
|
+
if ordered {
|
|
227
|
+
return baseFont
|
|
228
|
+
}
|
|
229
|
+
return baseFont.withSize(baseFont.pointSize * markerScale)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static func unorderedBulletGlyphBounds(for font: UIFont) -> CGRect {
|
|
233
|
+
let ctFont = font as CTFont
|
|
234
|
+
let bullet = UniChar(0x2022)
|
|
235
|
+
var glyph = CGGlyph()
|
|
236
|
+
guard CTFontGetGlyphsForCharacters(ctFont, [bullet], &glyph, 1) else {
|
|
237
|
+
let fallbackDiameter = max(font.pointSize * 0.28, 1)
|
|
238
|
+
return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
var boundingRect = CGRect.zero
|
|
242
|
+
CTFontGetBoundingRectsForGlyphs(ctFont, .default, [glyph], &boundingRect, 1)
|
|
243
|
+
if boundingRect.isNull || boundingRect.isEmpty {
|
|
244
|
+
let fallbackDiameter = max(font.pointSize * 0.28, 1)
|
|
245
|
+
return CGRect(x: 0, y: 0, width: fallbackDiameter, height: fallbackDiameter)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return boundingRect
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private static func unorderedBulletGlyphMidline(for font: UIFont) -> CGFloat {
|
|
252
|
+
unorderedBulletGlyphBounds(for: font).midY
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct EditorTextStyle {
|
|
4
|
+
var fontFamily: String?
|
|
5
|
+
var fontSize: CGFloat?
|
|
6
|
+
var fontWeight: String?
|
|
7
|
+
var fontStyle: String?
|
|
8
|
+
var color: UIColor?
|
|
9
|
+
var lineHeight: CGFloat?
|
|
10
|
+
var spacingAfter: CGFloat?
|
|
11
|
+
|
|
12
|
+
init(
|
|
13
|
+
fontFamily: String? = nil,
|
|
14
|
+
fontSize: CGFloat? = nil,
|
|
15
|
+
fontWeight: String? = nil,
|
|
16
|
+
fontStyle: String? = nil,
|
|
17
|
+
color: UIColor? = nil,
|
|
18
|
+
lineHeight: CGFloat? = nil,
|
|
19
|
+
spacingAfter: CGFloat? = nil
|
|
20
|
+
) {
|
|
21
|
+
self.fontFamily = fontFamily
|
|
22
|
+
self.fontSize = fontSize
|
|
23
|
+
self.fontWeight = fontWeight
|
|
24
|
+
self.fontStyle = fontStyle
|
|
25
|
+
self.color = color
|
|
26
|
+
self.lineHeight = lineHeight
|
|
27
|
+
self.spacingAfter = spacingAfter
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
init(dictionary: [String: Any]) {
|
|
31
|
+
fontFamily = dictionary["fontFamily"] as? String
|
|
32
|
+
fontSize = EditorTheme.cgFloat(dictionary["fontSize"])
|
|
33
|
+
fontWeight = dictionary["fontWeight"] as? String
|
|
34
|
+
fontStyle = dictionary["fontStyle"] as? String
|
|
35
|
+
color = EditorTheme.color(from: dictionary["color"])
|
|
36
|
+
lineHeight = EditorTheme.cgFloat(dictionary["lineHeight"])
|
|
37
|
+
spacingAfter = EditorTheme.cgFloat(dictionary["spacingAfter"])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func merged(with override: EditorTextStyle?) -> EditorTextStyle {
|
|
41
|
+
guard let override else { return self }
|
|
42
|
+
return EditorTextStyle(
|
|
43
|
+
fontFamily: override.fontFamily ?? fontFamily,
|
|
44
|
+
fontSize: override.fontSize ?? fontSize,
|
|
45
|
+
fontWeight: override.fontWeight ?? fontWeight,
|
|
46
|
+
fontStyle: override.fontStyle ?? fontStyle,
|
|
47
|
+
color: override.color ?? color,
|
|
48
|
+
lineHeight: override.lineHeight ?? lineHeight,
|
|
49
|
+
spacingAfter: override.spacingAfter ?? spacingAfter
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func resolvedFont(fallback: UIFont) -> UIFont {
|
|
54
|
+
let size = fontSize ?? fallback.pointSize
|
|
55
|
+
var font = fallback.withSize(size)
|
|
56
|
+
|
|
57
|
+
if let fontFamily,
|
|
58
|
+
let familyFont = UIFont(name: fontFamily, size: size) {
|
|
59
|
+
font = familyFont
|
|
60
|
+
} else if let fontWeight {
|
|
61
|
+
font = UIFont.systemFont(ofSize: size, weight: EditorTheme.fontWeight(from: fontWeight))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var traits = font.fontDescriptor.symbolicTraits
|
|
65
|
+
if EditorTheme.shouldApplyBoldTrait(fontWeight) {
|
|
66
|
+
traits.insert(.traitBold)
|
|
67
|
+
}
|
|
68
|
+
if fontStyle == "italic" {
|
|
69
|
+
traits.insert(.traitItalic)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if traits != font.fontDescriptor.symbolicTraits,
|
|
73
|
+
let descriptor = font.fontDescriptor.withSymbolicTraits(traits) {
|
|
74
|
+
font = UIFont(descriptor: descriptor, size: size)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return font
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
struct EditorListTheme {
|
|
82
|
+
var indent: CGFloat?
|
|
83
|
+
var itemSpacing: CGFloat?
|
|
84
|
+
var markerColor: UIColor?
|
|
85
|
+
var markerScale: CGFloat?
|
|
86
|
+
|
|
87
|
+
init(dictionary: [String: Any]) {
|
|
88
|
+
indent = EditorTheme.cgFloat(dictionary["indent"])
|
|
89
|
+
itemSpacing = EditorTheme.cgFloat(dictionary["itemSpacing"])
|
|
90
|
+
markerColor = EditorTheme.color(from: dictionary["markerColor"])
|
|
91
|
+
markerScale = EditorTheme.cgFloat(dictionary["markerScale"])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
struct EditorHorizontalRuleTheme {
|
|
96
|
+
var color: UIColor?
|
|
97
|
+
var thickness: CGFloat?
|
|
98
|
+
var verticalMargin: CGFloat?
|
|
99
|
+
|
|
100
|
+
init(dictionary: [String: Any]) {
|
|
101
|
+
color = EditorTheme.color(from: dictionary["color"])
|
|
102
|
+
thickness = EditorTheme.cgFloat(dictionary["thickness"])
|
|
103
|
+
verticalMargin = EditorTheme.cgFloat(dictionary["verticalMargin"])
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
struct EditorMentionTheme {
|
|
108
|
+
var textColor: UIColor?
|
|
109
|
+
var backgroundColor: UIColor?
|
|
110
|
+
var borderColor: UIColor?
|
|
111
|
+
var borderWidth: CGFloat?
|
|
112
|
+
var borderRadius: CGFloat?
|
|
113
|
+
var fontWeight: String?
|
|
114
|
+
var popoverBackgroundColor: UIColor?
|
|
115
|
+
var popoverBorderColor: UIColor?
|
|
116
|
+
var popoverBorderWidth: CGFloat?
|
|
117
|
+
var popoverBorderRadius: CGFloat?
|
|
118
|
+
var popoverShadowColor: UIColor?
|
|
119
|
+
var optionTextColor: UIColor?
|
|
120
|
+
var optionSecondaryTextColor: UIColor?
|
|
121
|
+
var optionHighlightedBackgroundColor: UIColor?
|
|
122
|
+
var optionHighlightedTextColor: UIColor?
|
|
123
|
+
|
|
124
|
+
init(dictionary: [String: Any]) {
|
|
125
|
+
textColor = EditorTheme.color(from: dictionary["textColor"])
|
|
126
|
+
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|
|
127
|
+
borderColor = EditorTheme.color(from: dictionary["borderColor"])
|
|
128
|
+
borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
|
|
129
|
+
borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
|
|
130
|
+
fontWeight = dictionary["fontWeight"] as? String
|
|
131
|
+
popoverBackgroundColor = EditorTheme.color(from: dictionary["popoverBackgroundColor"])
|
|
132
|
+
popoverBorderColor = EditorTheme.color(from: dictionary["popoverBorderColor"])
|
|
133
|
+
popoverBorderWidth = EditorTheme.cgFloat(dictionary["popoverBorderWidth"])
|
|
134
|
+
popoverBorderRadius = EditorTheme.cgFloat(dictionary["popoverBorderRadius"])
|
|
135
|
+
popoverShadowColor = EditorTheme.color(from: dictionary["popoverShadowColor"])
|
|
136
|
+
optionTextColor = EditorTheme.color(from: dictionary["optionTextColor"])
|
|
137
|
+
optionSecondaryTextColor = EditorTheme.color(from: dictionary["optionSecondaryTextColor"])
|
|
138
|
+
optionHighlightedBackgroundColor = EditorTheme.color(from: dictionary["optionHighlightedBackgroundColor"])
|
|
139
|
+
optionHighlightedTextColor = EditorTheme.color(from: dictionary["optionHighlightedTextColor"])
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
struct EditorToolbarTheme {
|
|
144
|
+
var backgroundColor: UIColor?
|
|
145
|
+
var borderColor: UIColor?
|
|
146
|
+
var borderWidth: CGFloat?
|
|
147
|
+
var borderRadius: CGFloat?
|
|
148
|
+
var keyboardOffset: CGFloat?
|
|
149
|
+
var horizontalInset: CGFloat?
|
|
150
|
+
var separatorColor: UIColor?
|
|
151
|
+
var buttonColor: UIColor?
|
|
152
|
+
var buttonActiveColor: UIColor?
|
|
153
|
+
var buttonDisabledColor: UIColor?
|
|
154
|
+
var buttonActiveBackgroundColor: UIColor?
|
|
155
|
+
var buttonBorderRadius: CGFloat?
|
|
156
|
+
|
|
157
|
+
init(dictionary: [String: Any]) {
|
|
158
|
+
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|
|
159
|
+
borderColor = EditorTheme.color(from: dictionary["borderColor"])
|
|
160
|
+
borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
|
|
161
|
+
borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
|
|
162
|
+
keyboardOffset = EditorTheme.cgFloat(dictionary["keyboardOffset"])
|
|
163
|
+
horizontalInset = EditorTheme.cgFloat(dictionary["horizontalInset"])
|
|
164
|
+
separatorColor = EditorTheme.color(from: dictionary["separatorColor"])
|
|
165
|
+
buttonColor = EditorTheme.color(from: dictionary["buttonColor"])
|
|
166
|
+
buttonActiveColor = EditorTheme.color(from: dictionary["buttonActiveColor"])
|
|
167
|
+
buttonDisabledColor = EditorTheme.color(from: dictionary["buttonDisabledColor"])
|
|
168
|
+
buttonActiveBackgroundColor = EditorTheme.color(from: dictionary["buttonActiveBackgroundColor"])
|
|
169
|
+
buttonBorderRadius = EditorTheme.cgFloat(dictionary["buttonBorderRadius"])
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
struct EditorContentInsets {
|
|
174
|
+
var top: CGFloat?
|
|
175
|
+
var right: CGFloat?
|
|
176
|
+
var bottom: CGFloat?
|
|
177
|
+
var left: CGFloat?
|
|
178
|
+
|
|
179
|
+
init(dictionary: [String: Any]) {
|
|
180
|
+
top = EditorTheme.cgFloat(dictionary["top"])
|
|
181
|
+
right = EditorTheme.cgFloat(dictionary["right"])
|
|
182
|
+
bottom = EditorTheme.cgFloat(dictionary["bottom"])
|
|
183
|
+
left = EditorTheme.cgFloat(dictionary["left"])
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
struct EditorTheme {
|
|
188
|
+
var text: EditorTextStyle?
|
|
189
|
+
var paragraph: EditorTextStyle?
|
|
190
|
+
var headings: [String: EditorTextStyle] = [:]
|
|
191
|
+
var list: EditorListTheme?
|
|
192
|
+
var horizontalRule: EditorHorizontalRuleTheme?
|
|
193
|
+
var mentions: EditorMentionTheme?
|
|
194
|
+
var toolbar: EditorToolbarTheme?
|
|
195
|
+
var backgroundColor: UIColor?
|
|
196
|
+
var borderRadius: CGFloat?
|
|
197
|
+
var contentInsets: EditorContentInsets?
|
|
198
|
+
|
|
199
|
+
static func from(json: String?) -> EditorTheme? {
|
|
200
|
+
guard let json, !json.isEmpty,
|
|
201
|
+
let data = json.data(using: .utf8),
|
|
202
|
+
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
203
|
+
else {
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
206
|
+
return EditorTheme(dictionary: raw)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
init(dictionary: [String: Any]) {
|
|
210
|
+
if let text = dictionary["text"] as? [String: Any] {
|
|
211
|
+
self.text = EditorTextStyle(dictionary: text)
|
|
212
|
+
}
|
|
213
|
+
if let paragraph = dictionary["paragraph"] as? [String: Any] {
|
|
214
|
+
self.paragraph = EditorTextStyle(dictionary: paragraph)
|
|
215
|
+
}
|
|
216
|
+
if let headings = dictionary["headings"] as? [String: Any] {
|
|
217
|
+
for level in ["h1", "h2", "h3", "h4", "h5", "h6"] {
|
|
218
|
+
if let style = headings[level] as? [String: Any] {
|
|
219
|
+
self.headings[level] = EditorTextStyle(dictionary: style)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if let list = dictionary["list"] as? [String: Any] {
|
|
224
|
+
self.list = EditorListTheme(dictionary: list)
|
|
225
|
+
}
|
|
226
|
+
if let horizontalRule = dictionary["horizontalRule"] as? [String: Any] {
|
|
227
|
+
self.horizontalRule = EditorHorizontalRuleTheme(dictionary: horizontalRule)
|
|
228
|
+
}
|
|
229
|
+
if let mentions = dictionary["mentions"] as? [String: Any] {
|
|
230
|
+
self.mentions = EditorMentionTheme(dictionary: mentions)
|
|
231
|
+
}
|
|
232
|
+
if let toolbar = dictionary["toolbar"] as? [String: Any] {
|
|
233
|
+
self.toolbar = EditorToolbarTheme(dictionary: toolbar)
|
|
234
|
+
}
|
|
235
|
+
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|
|
236
|
+
borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
|
|
237
|
+
if let contentInsets = dictionary["contentInsets"] as? [String: Any] {
|
|
238
|
+
self.contentInsets = EditorContentInsets(dictionary: contentInsets)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func effectiveTextStyle(for nodeType: String) -> EditorTextStyle {
|
|
243
|
+
var style = text ?? EditorTextStyle()
|
|
244
|
+
if nodeType == "paragraph" {
|
|
245
|
+
style = style.merged(with: paragraph)
|
|
246
|
+
if paragraph?.lineHeight == nil {
|
|
247
|
+
style.lineHeight = nil
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
style = style.merged(with: headings[nodeType])
|
|
251
|
+
return style
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static func cgFloat(_ value: Any?) -> CGFloat? {
|
|
255
|
+
guard let number = value as? NSNumber else { return nil }
|
|
256
|
+
return CGFloat(truncating: number)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static func fontWeight(from value: String) -> UIFont.Weight {
|
|
260
|
+
switch value {
|
|
261
|
+
case "100": return .ultraLight
|
|
262
|
+
case "200": return .thin
|
|
263
|
+
case "300": return .light
|
|
264
|
+
case "500": return .medium
|
|
265
|
+
case "600": return .semibold
|
|
266
|
+
case "700", "bold": return .bold
|
|
267
|
+
case "800": return .heavy
|
|
268
|
+
case "900": return .black
|
|
269
|
+
default: return .regular
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
static func shouldApplyBoldTrait(_ value: String?) -> Bool {
|
|
274
|
+
guard let value else { return false }
|
|
275
|
+
return value == "bold" || Int(value).map { $0 >= 600 } == true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static func color(from value: Any?) -> UIColor? {
|
|
279
|
+
guard let raw = value as? String else { return nil }
|
|
280
|
+
let string = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
281
|
+
|
|
282
|
+
if let hexColor = colorFromHex(string) {
|
|
283
|
+
return hexColor
|
|
284
|
+
}
|
|
285
|
+
if let rgbColor = colorFromRGBFunction(string) {
|
|
286
|
+
return rgbColor
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
switch string {
|
|
290
|
+
case "black": return .black
|
|
291
|
+
case "white": return .white
|
|
292
|
+
case "red": return .red
|
|
293
|
+
case "green": return .green
|
|
294
|
+
case "blue": return .blue
|
|
295
|
+
case "gray", "grey": return .gray
|
|
296
|
+
case "clear", "transparent": return .clear
|
|
297
|
+
default: return nil
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private static func colorFromHex(_ string: String) -> UIColor? {
|
|
302
|
+
guard string.hasPrefix("#") else { return nil }
|
|
303
|
+
let hex = String(string.dropFirst())
|
|
304
|
+
|
|
305
|
+
switch hex.count {
|
|
306
|
+
case 3:
|
|
307
|
+
let chars = Array(hex)
|
|
308
|
+
return UIColor(
|
|
309
|
+
red: component(String(repeating: String(chars[0]), count: 2)),
|
|
310
|
+
green: component(String(repeating: String(chars[1]), count: 2)),
|
|
311
|
+
blue: component(String(repeating: String(chars[2]), count: 2)),
|
|
312
|
+
alpha: 1
|
|
313
|
+
)
|
|
314
|
+
case 4:
|
|
315
|
+
let chars = Array(hex)
|
|
316
|
+
return UIColor(
|
|
317
|
+
red: component(String(repeating: String(chars[0]), count: 2)),
|
|
318
|
+
green: component(String(repeating: String(chars[1]), count: 2)),
|
|
319
|
+
blue: component(String(repeating: String(chars[2]), count: 2)),
|
|
320
|
+
alpha: component(String(repeating: String(chars[3]), count: 2))
|
|
321
|
+
)
|
|
322
|
+
case 6:
|
|
323
|
+
return UIColor(
|
|
324
|
+
red: component(String(hex.prefix(2))),
|
|
325
|
+
green: component(String(hex.dropFirst(2).prefix(2))),
|
|
326
|
+
blue: component(String(hex.dropFirst(4).prefix(2))),
|
|
327
|
+
alpha: 1
|
|
328
|
+
)
|
|
329
|
+
case 8:
|
|
330
|
+
return UIColor(
|
|
331
|
+
red: component(String(hex.prefix(2))),
|
|
332
|
+
green: component(String(hex.dropFirst(2).prefix(2))),
|
|
333
|
+
blue: component(String(hex.dropFirst(4).prefix(2))),
|
|
334
|
+
alpha: component(String(hex.dropFirst(6).prefix(2)))
|
|
335
|
+
)
|
|
336
|
+
default:
|
|
337
|
+
return nil
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private static func colorFromRGBFunction(_ string: String) -> UIColor? {
|
|
342
|
+
let isRGBA = string.hasPrefix("rgba(") && string.hasSuffix(")")
|
|
343
|
+
let isRGB = string.hasPrefix("rgb(") && string.hasSuffix(")")
|
|
344
|
+
guard isRGBA || isRGB else { return nil }
|
|
345
|
+
|
|
346
|
+
let start = string.index(string.startIndex, offsetBy: isRGBA ? 5 : 4)
|
|
347
|
+
let end = string.index(before: string.endIndex)
|
|
348
|
+
let parts = string[start..<end]
|
|
349
|
+
.split(separator: ",")
|
|
350
|
+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
351
|
+
|
|
352
|
+
guard parts.count == (isRGBA ? 4 : 3),
|
|
353
|
+
let red = Double(parts[0]),
|
|
354
|
+
let green = Double(parts[1]),
|
|
355
|
+
let blue = Double(parts[2])
|
|
356
|
+
else {
|
|
357
|
+
return nil
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let alpha = isRGBA ? (Double(parts[3]) ?? 1) : 1
|
|
361
|
+
return UIColor(
|
|
362
|
+
red: red / 255,
|
|
363
|
+
green: green / 255,
|
|
364
|
+
blue: blue / 255,
|
|
365
|
+
alpha: alpha
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private static func component(_ hex: String) -> CGFloat {
|
|
370
|
+
CGFloat(Int(hex, radix: 16) ?? 0) / 255
|
|
371
|
+
}
|
|
372
|
+
}
|