@openeditor/react-native-prose-editor 0.0.2

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.
Files changed (91) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +180 -0
  3. package/android/build.gradle +69 -0
  4. package/android/consumer-rules.pro +18 -0
  5. package/android/src/androidTest/AndroidManifest.xml +8 -0
  6. package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
  7. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
  8. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
  9. package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
  10. package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
  11. package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
  12. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  13. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
  15. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
  16. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
  20. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  21. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
  22. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
  23. package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
  24. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
  25. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
  26. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
  27. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
  28. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
  29. package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
  30. package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
  31. package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
  32. package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
  33. package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
  34. package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
  35. package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
  36. package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
  37. package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
  38. package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
  39. package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
  40. package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
  41. package/app.plugin.js +62 -0
  42. package/dist/EditorTheme.d.ts +113 -0
  43. package/dist/EditorTheme.js +29 -0
  44. package/dist/EditorToolbar.d.ts +191 -0
  45. package/dist/EditorToolbar.js +1095 -0
  46. package/dist/NativeEditorBridge.d.ts +325 -0
  47. package/dist/NativeEditorBridge.js +933 -0
  48. package/dist/NativeProseViewer.d.ts +53 -0
  49. package/dist/NativeProseViewer.js +480 -0
  50. package/dist/NativeRichTextEditor.d.ts +183 -0
  51. package/dist/NativeRichTextEditor.js +2574 -0
  52. package/dist/YjsCollaboration.d.ts +90 -0
  53. package/dist/YjsCollaboration.js +743 -0
  54. package/dist/addons.d.ts +100 -0
  55. package/dist/addons.js +82 -0
  56. package/dist/heightCache.d.ts +6 -0
  57. package/dist/heightCache.js +45 -0
  58. package/dist/index.d.ts +10 -0
  59. package/dist/index.js +30 -0
  60. package/dist/schemas.d.ts +37 -0
  61. package/dist/schemas.js +263 -0
  62. package/dist/useNativeEditor.d.ts +42 -0
  63. package/dist/useNativeEditor.js +124 -0
  64. package/expo-module.config.json +9 -0
  65. package/ios/EditorAddons.swift +327 -0
  66. package/ios/EditorCore.xcframework/Info.plist +44 -0
  67. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  68. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  69. package/ios/EditorLayoutManager.swift +774 -0
  70. package/ios/EditorTheme.swift +515 -0
  71. package/ios/Generated_editor_core.swift +1513 -0
  72. package/ios/NativeEditorExpoView.swift +3593 -0
  73. package/ios/NativeEditorModule.swift +624 -0
  74. package/ios/NativeProseViewerExpoView.swift +276 -0
  75. package/ios/PositionBridge.swift +558 -0
  76. package/ios/ReactNativeProseEditor.podspec +49 -0
  77. package/ios/RenderBridge.swift +1708 -0
  78. package/ios/RichTextEditorView.swift +6150 -0
  79. package/ios/Tests/NativePerformanceTests.swift +687 -0
  80. package/ios/Tests/PositionBridgeTests.swift +706 -0
  81. package/ios/Tests/RenderBridgeTests.swift +2236 -0
  82. package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
  83. package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
  84. package/ios/editor_coreFFI/module.modulemap +7 -0
  85. package/ios/editor_coreFFI.h +904 -0
  86. package/ios/editor_coreFFI.modulemap +7 -0
  87. package/package.json +74 -0
  88. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  89. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  90. package/rust/android/x86_64/libeditor_core.so +0 -0
  91. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
@@ -0,0 +1,515 @@
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 baseIndentMultiplier: CGFloat?
84
+ var itemSpacing: CGFloat?
85
+ var markerColor: UIColor?
86
+ var markerScale: CGFloat?
87
+
88
+ init(dictionary: [String: Any]) {
89
+ indent = EditorTheme.cgFloat(dictionary["indent"])
90
+ baseIndentMultiplier = EditorTheme.cgFloat(dictionary["baseIndentMultiplier"])
91
+ itemSpacing = EditorTheme.cgFloat(dictionary["itemSpacing"])
92
+ markerColor = EditorTheme.color(from: dictionary["markerColor"])
93
+ markerScale = EditorTheme.cgFloat(dictionary["markerScale"])
94
+ }
95
+ }
96
+
97
+ struct EditorHorizontalRuleTheme {
98
+ var color: UIColor?
99
+ var thickness: CGFloat?
100
+ var verticalMargin: CGFloat?
101
+
102
+ init(dictionary: [String: Any]) {
103
+ color = EditorTheme.color(from: dictionary["color"])
104
+ thickness = EditorTheme.cgFloat(dictionary["thickness"])
105
+ verticalMargin = EditorTheme.cgFloat(dictionary["verticalMargin"])
106
+ }
107
+ }
108
+
109
+ struct EditorBlockquoteTheme {
110
+ var text: EditorTextStyle?
111
+ var indent: CGFloat?
112
+ var borderColor: UIColor?
113
+ var borderWidth: CGFloat?
114
+ var markerGap: CGFloat?
115
+
116
+ init(dictionary: [String: Any]) {
117
+ if let text = dictionary["text"] as? [String: Any] {
118
+ self.text = EditorTextStyle(dictionary: text)
119
+ }
120
+ indent = EditorTheme.cgFloat(dictionary["indent"])
121
+ borderColor = EditorTheme.color(from: dictionary["borderColor"])
122
+ borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
123
+ markerGap = EditorTheme.cgFloat(dictionary["markerGap"])
124
+ }
125
+ }
126
+
127
+ struct EditorCodeBlockTheme {
128
+ var text: EditorTextStyle?
129
+ var backgroundColor: UIColor?
130
+ var borderRadius: CGFloat?
131
+ var paddingHorizontal: CGFloat?
132
+ var paddingVertical: CGFloat?
133
+
134
+ init(dictionary: [String: Any]) {
135
+ if let text = dictionary["text"] as? [String: Any] {
136
+ self.text = EditorTextStyle(dictionary: text)
137
+ }
138
+ backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
139
+ borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
140
+ paddingHorizontal = EditorTheme.cgFloat(dictionary["paddingHorizontal"])
141
+ paddingVertical = EditorTheme.cgFloat(dictionary["paddingVertical"])
142
+ }
143
+ }
144
+
145
+ struct EditorLinkTheme {
146
+ var fontFamily: String?
147
+ var fontSize: CGFloat?
148
+ var fontWeight: String?
149
+ var fontStyle: String?
150
+ var color: UIColor?
151
+ var backgroundColor: UIColor?
152
+ var underline: Bool?
153
+
154
+ init(dictionary: [String: Any]) {
155
+ fontFamily = dictionary["fontFamily"] as? String
156
+ fontSize = EditorTheme.cgFloat(dictionary["fontSize"])
157
+ fontWeight = dictionary["fontWeight"] as? String
158
+ fontStyle = dictionary["fontStyle"] as? String
159
+ color = EditorTheme.color(from: dictionary["color"])
160
+ backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
161
+ underline = dictionary["underline"] as? Bool
162
+ }
163
+
164
+ func resolvedFont(fallback: UIFont) -> UIFont {
165
+ EditorTextStyle(
166
+ fontFamily: fontFamily,
167
+ fontSize: fontSize,
168
+ fontWeight: fontWeight,
169
+ fontStyle: fontStyle
170
+ ).resolvedFont(fallback: fallback)
171
+ }
172
+ }
173
+
174
+ struct EditorMentionTheme {
175
+ var textColor: UIColor?
176
+ var backgroundColor: UIColor?
177
+ var borderColor: UIColor?
178
+ var borderWidth: CGFloat?
179
+ var borderRadius: CGFloat?
180
+ var fontWeight: String?
181
+ var popoverBackgroundColor: UIColor?
182
+ var popoverBorderColor: UIColor?
183
+ var popoverBorderWidth: CGFloat?
184
+ var popoverBorderRadius: CGFloat?
185
+ var popoverShadowColor: UIColor?
186
+ var optionTextColor: UIColor?
187
+ var optionSecondaryTextColor: UIColor?
188
+ var optionHighlightedBackgroundColor: UIColor?
189
+ var optionHighlightedTextColor: UIColor?
190
+
191
+ func merged(with override: EditorMentionTheme?) -> EditorMentionTheme {
192
+ guard let override else { return self }
193
+ var merged = self
194
+ merged.textColor = override.textColor ?? merged.textColor
195
+ merged.backgroundColor = override.backgroundColor ?? merged.backgroundColor
196
+ merged.borderColor = override.borderColor ?? merged.borderColor
197
+ merged.borderWidth = override.borderWidth ?? merged.borderWidth
198
+ merged.borderRadius = override.borderRadius ?? merged.borderRadius
199
+ merged.fontWeight = override.fontWeight ?? merged.fontWeight
200
+ merged.popoverBackgroundColor =
201
+ override.popoverBackgroundColor ?? merged.popoverBackgroundColor
202
+ merged.popoverBorderColor = override.popoverBorderColor ?? merged.popoverBorderColor
203
+ merged.popoverBorderWidth = override.popoverBorderWidth ?? merged.popoverBorderWidth
204
+ merged.popoverBorderRadius = override.popoverBorderRadius ?? merged.popoverBorderRadius
205
+ merged.popoverShadowColor = override.popoverShadowColor ?? merged.popoverShadowColor
206
+ merged.optionTextColor = override.optionTextColor ?? merged.optionTextColor
207
+ merged.optionSecondaryTextColor =
208
+ override.optionSecondaryTextColor ?? merged.optionSecondaryTextColor
209
+ merged.optionHighlightedBackgroundColor =
210
+ override.optionHighlightedBackgroundColor ?? merged.optionHighlightedBackgroundColor
211
+ merged.optionHighlightedTextColor =
212
+ override.optionHighlightedTextColor ?? merged.optionHighlightedTextColor
213
+ return merged
214
+ }
215
+
216
+ init(dictionary: [String: Any]) {
217
+ textColor = EditorTheme.color(from: dictionary["textColor"])
218
+ backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
219
+ borderColor = EditorTheme.color(from: dictionary["borderColor"])
220
+ borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
221
+ borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
222
+ fontWeight = dictionary["fontWeight"] as? String
223
+ popoverBackgroundColor = EditorTheme.color(from: dictionary["popoverBackgroundColor"])
224
+ popoverBorderColor = EditorTheme.color(from: dictionary["popoverBorderColor"])
225
+ popoverBorderWidth = EditorTheme.cgFloat(dictionary["popoverBorderWidth"])
226
+ popoverBorderRadius = EditorTheme.cgFloat(dictionary["popoverBorderRadius"])
227
+ popoverShadowColor = EditorTheme.color(from: dictionary["popoverShadowColor"])
228
+ optionTextColor = EditorTheme.color(from: dictionary["optionTextColor"])
229
+ optionSecondaryTextColor = EditorTheme.color(from: dictionary["optionSecondaryTextColor"])
230
+ optionHighlightedBackgroundColor = EditorTheme.color(from: dictionary["optionHighlightedBackgroundColor"])
231
+ optionHighlightedTextColor = EditorTheme.color(from: dictionary["optionHighlightedTextColor"])
232
+ }
233
+ }
234
+
235
+ enum EditorToolbarAppearance: String {
236
+ case custom
237
+ case native
238
+ }
239
+
240
+ struct EditorToolbarTheme {
241
+ var appearance: EditorToolbarAppearance?
242
+ var height: CGFloat?
243
+ var backgroundColor: UIColor?
244
+ var borderColor: UIColor?
245
+ var borderWidth: CGFloat?
246
+ var borderRadius: CGFloat?
247
+ var marginTop: CGFloat?
248
+ var showTopBorder: Bool?
249
+ var keyboardOffset: CGFloat?
250
+ var horizontalInset: CGFloat?
251
+ var separatorColor: UIColor?
252
+ var buttonColor: UIColor?
253
+ var buttonActiveColor: UIColor?
254
+ var buttonDisabledColor: UIColor?
255
+ var buttonActiveBackgroundColor: UIColor?
256
+ var buttonBorderRadius: CGFloat?
257
+
258
+ init(dictionary: [String: Any]) {
259
+ appearance = (dictionary["appearance"] as? String).flatMap(EditorToolbarAppearance.init(rawValue:))
260
+ height = EditorTheme.cgFloat(dictionary["height"])
261
+ backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
262
+ borderColor = EditorTheme.color(from: dictionary["borderColor"])
263
+ borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
264
+ borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
265
+ marginTop = EditorTheme.cgFloat(dictionary["marginTop"])
266
+ showTopBorder = dictionary["showTopBorder"] as? Bool
267
+ keyboardOffset = EditorTheme.cgFloat(dictionary["keyboardOffset"])
268
+ horizontalInset = EditorTheme.cgFloat(dictionary["horizontalInset"])
269
+ separatorColor = EditorTheme.color(from: dictionary["separatorColor"])
270
+ buttonColor = EditorTheme.color(from: dictionary["buttonColor"])
271
+ buttonActiveColor = EditorTheme.color(from: dictionary["buttonActiveColor"])
272
+ buttonDisabledColor = EditorTheme.color(from: dictionary["buttonDisabledColor"])
273
+ buttonActiveBackgroundColor = EditorTheme.color(from: dictionary["buttonActiveBackgroundColor"])
274
+ buttonBorderRadius = EditorTheme.cgFloat(dictionary["buttonBorderRadius"])
275
+ }
276
+
277
+ var resolvedKeyboardOffset: CGFloat {
278
+ keyboardOffset ?? (appearance == .native ? 6 : 0)
279
+ }
280
+
281
+ var resolvedHorizontalInset: CGFloat {
282
+ horizontalInset ?? (appearance == .native ? 10 : 0)
283
+ }
284
+
285
+ var resolvedBorderRadius: CGFloat {
286
+ borderRadius ?? (appearance == .native ? 20 : 0)
287
+ }
288
+
289
+ var resolvedBorderWidth: CGFloat {
290
+ borderWidth ?? (appearance == .native ? 0 : 0.5)
291
+ }
292
+
293
+ var resolvedButtonBorderRadius: CGFloat {
294
+ buttonBorderRadius ?? (appearance == .native ? 10 : 8)
295
+ }
296
+ }
297
+
298
+ struct EditorContentInsets {
299
+ var top: CGFloat?
300
+ var right: CGFloat?
301
+ var bottom: CGFloat?
302
+ var left: CGFloat?
303
+
304
+ init(dictionary: [String: Any]) {
305
+ top = EditorTheme.cgFloat(dictionary["top"])
306
+ right = EditorTheme.cgFloat(dictionary["right"])
307
+ bottom = EditorTheme.cgFloat(dictionary["bottom"])
308
+ left = EditorTheme.cgFloat(dictionary["left"])
309
+ }
310
+ }
311
+
312
+ struct EditorTheme {
313
+ var text: EditorTextStyle?
314
+ var paragraph: EditorTextStyle?
315
+ var blockquote: EditorBlockquoteTheme?
316
+ var codeBlock: EditorCodeBlockTheme?
317
+ var headings: [String: EditorTextStyle] = [:]
318
+ var list: EditorListTheme?
319
+ var horizontalRule: EditorHorizontalRuleTheme?
320
+ var mentions: EditorMentionTheme?
321
+ var links: EditorLinkTheme?
322
+ var toolbar: EditorToolbarTheme?
323
+ var placeholderColor: UIColor?
324
+ var backgroundColor: UIColor?
325
+ var borderRadius: CGFloat?
326
+ var contentInsets: EditorContentInsets?
327
+
328
+ static func from(json: String?) -> EditorTheme? {
329
+ guard let json, !json.isEmpty,
330
+ let data = json.data(using: .utf8),
331
+ let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
332
+ else {
333
+ return nil
334
+ }
335
+ return EditorTheme(dictionary: raw)
336
+ }
337
+
338
+ init(dictionary: [String: Any]) {
339
+ if let text = dictionary["text"] as? [String: Any] {
340
+ self.text = EditorTextStyle(dictionary: text)
341
+ }
342
+ if let paragraph = dictionary["paragraph"] as? [String: Any] {
343
+ self.paragraph = EditorTextStyle(dictionary: paragraph)
344
+ }
345
+ if let blockquote = dictionary["blockquote"] as? [String: Any] {
346
+ self.blockquote = EditorBlockquoteTheme(dictionary: blockquote)
347
+ }
348
+ if let codeBlock = dictionary["codeBlock"] as? [String: Any] {
349
+ self.codeBlock = EditorCodeBlockTheme(dictionary: codeBlock)
350
+ }
351
+ if let headings = dictionary["headings"] as? [String: Any] {
352
+ for level in ["h1", "h2", "h3", "h4", "h5", "h6"] {
353
+ if let style = headings[level] as? [String: Any] {
354
+ self.headings[level] = EditorTextStyle(dictionary: style)
355
+ }
356
+ }
357
+ }
358
+ if let list = dictionary["list"] as? [String: Any] {
359
+ self.list = EditorListTheme(dictionary: list)
360
+ }
361
+ if let horizontalRule = dictionary["horizontalRule"] as? [String: Any] {
362
+ self.horizontalRule = EditorHorizontalRuleTheme(dictionary: horizontalRule)
363
+ }
364
+ if let mentions = dictionary["mentions"] as? [String: Any] {
365
+ self.mentions = EditorMentionTheme(dictionary: mentions)
366
+ }
367
+ if let links = dictionary["links"] as? [String: Any] {
368
+ self.links = EditorLinkTheme(dictionary: links)
369
+ }
370
+ if let toolbar = dictionary["toolbar"] as? [String: Any] {
371
+ self.toolbar = EditorToolbarTheme(dictionary: toolbar)
372
+ }
373
+ placeholderColor = EditorTheme.color(from: dictionary["placeholderColor"])
374
+ backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
375
+ borderRadius = EditorTheme.cgFloat(dictionary["borderRadius"])
376
+ if let contentInsets = dictionary["contentInsets"] as? [String: Any] {
377
+ self.contentInsets = EditorContentInsets(dictionary: contentInsets)
378
+ }
379
+ }
380
+
381
+ func effectiveTextStyle(for nodeType: String, inBlockquote: Bool = false) -> EditorTextStyle {
382
+ var style = text ?? EditorTextStyle()
383
+ style = style.merged(with: inBlockquote ? blockquote?.text : nil)
384
+ if nodeType == "paragraph" {
385
+ style = style.merged(with: paragraph)
386
+ if paragraph?.lineHeight == nil {
387
+ style.lineHeight = nil
388
+ }
389
+ }
390
+ if nodeType == "codeBlock" {
391
+ style = style.merged(with: codeBlock?.text)
392
+ }
393
+ style = style.merged(with: headings[nodeType])
394
+ return style
395
+ }
396
+
397
+ static func cgFloat(_ value: Any?) -> CGFloat? {
398
+ guard let number = value as? NSNumber else { return nil }
399
+ return CGFloat(truncating: number)
400
+ }
401
+
402
+ static func fontWeight(from value: String) -> UIFont.Weight {
403
+ switch value {
404
+ case "100": return .ultraLight
405
+ case "200": return .thin
406
+ case "300": return .light
407
+ case "500": return .medium
408
+ case "600": return .semibold
409
+ case "700", "bold": return .bold
410
+ case "800": return .heavy
411
+ case "900": return .black
412
+ default: return .regular
413
+ }
414
+ }
415
+
416
+ static func shouldApplyBoldTrait(_ value: String?) -> Bool {
417
+ guard let value else { return false }
418
+ return value == "bold" || Int(value).map { $0 >= 600 } == true
419
+ }
420
+
421
+ static func color(from value: Any?) -> UIColor? {
422
+ guard let raw = value as? String else { return nil }
423
+ let string = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
424
+
425
+ if let hexColor = colorFromHex(string) {
426
+ return hexColor
427
+ }
428
+ if let rgbColor = colorFromRGBFunction(string) {
429
+ return rgbColor
430
+ }
431
+
432
+ switch string {
433
+ case "black": return .black
434
+ case "white": return .white
435
+ case "red": return .red
436
+ case "green": return .green
437
+ case "blue": return .blue
438
+ case "gray", "grey": return .gray
439
+ case "clear", "transparent": return .clear
440
+ default: return nil
441
+ }
442
+ }
443
+
444
+ private static func colorFromHex(_ string: String) -> UIColor? {
445
+ guard string.hasPrefix("#") else { return nil }
446
+ let hex = String(string.dropFirst())
447
+
448
+ switch hex.count {
449
+ case 3:
450
+ let chars = Array(hex)
451
+ return UIColor(
452
+ red: component(String(repeating: String(chars[0]), count: 2)),
453
+ green: component(String(repeating: String(chars[1]), count: 2)),
454
+ blue: component(String(repeating: String(chars[2]), count: 2)),
455
+ alpha: 1
456
+ )
457
+ case 4:
458
+ let chars = Array(hex)
459
+ return UIColor(
460
+ red: component(String(repeating: String(chars[0]), count: 2)),
461
+ green: component(String(repeating: String(chars[1]), count: 2)),
462
+ blue: component(String(repeating: String(chars[2]), count: 2)),
463
+ alpha: component(String(repeating: String(chars[3]), count: 2))
464
+ )
465
+ case 6:
466
+ return UIColor(
467
+ red: component(String(hex.prefix(2))),
468
+ green: component(String(hex.dropFirst(2).prefix(2))),
469
+ blue: component(String(hex.dropFirst(4).prefix(2))),
470
+ alpha: 1
471
+ )
472
+ case 8:
473
+ return UIColor(
474
+ red: component(String(hex.prefix(2))),
475
+ green: component(String(hex.dropFirst(2).prefix(2))),
476
+ blue: component(String(hex.dropFirst(4).prefix(2))),
477
+ alpha: component(String(hex.dropFirst(6).prefix(2)))
478
+ )
479
+ default:
480
+ return nil
481
+ }
482
+ }
483
+
484
+ private static func colorFromRGBFunction(_ string: String) -> UIColor? {
485
+ let isRGBA = string.hasPrefix("rgba(") && string.hasSuffix(")")
486
+ let isRGB = string.hasPrefix("rgb(") && string.hasSuffix(")")
487
+ guard isRGBA || isRGB else { return nil }
488
+
489
+ let start = string.index(string.startIndex, offsetBy: isRGBA ? 5 : 4)
490
+ let end = string.index(before: string.endIndex)
491
+ let parts = string[start..<end]
492
+ .split(separator: ",")
493
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
494
+
495
+ guard parts.count == (isRGBA ? 4 : 3),
496
+ let red = Double(parts[0]),
497
+ let green = Double(parts[1]),
498
+ let blue = Double(parts[2])
499
+ else {
500
+ return nil
501
+ }
502
+
503
+ let alpha = isRGBA ? (Double(parts[3]) ?? 1) : 1
504
+ return UIColor(
505
+ red: red / 255,
506
+ green: green / 255,
507
+ blue: blue / 255,
508
+ alpha: alpha
509
+ )
510
+ }
511
+
512
+ private static func component(_ hex: String) -> CGFloat {
513
+ CGFloat(Int(hex, radix: 16) ?? 0) / 255
514
+ }
515
+ }