@apollohg/react-native-prose-editor 0.1.1 → 0.3.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.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -5,6 +5,54 @@ import CoreText
5
5
  /// editable text storage. This keeps UIKit paragraph-start behaviors, such as
6
6
  /// sentence auto-capitalization, working naturally inside list items.
7
7
  final class EditorLayoutManager: NSLayoutManager {
8
+ private(set) var blockquoteStripeDrawPassesForTesting: [[CGRect]] = []
9
+
10
+ func blockquoteStripeRectsForTesting(
11
+ in textStorage: NSTextStorage,
12
+ visibleGlyphRange: NSRange? = nil,
13
+ origin: CGPoint = .zero
14
+ ) -> [CGRect] {
15
+ let glyphsToShow = visibleGlyphRange ?? NSRange(location: 0, length: numberOfGlyphs)
16
+ guard glyphsToShow.length > 0 else { return [] }
17
+
18
+ let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
19
+ let nsString = textStorage.string as NSString
20
+ var drawnBlockquoteStarts = Set<Int>()
21
+ var rects: [CGRect] = []
22
+
23
+ textStorage.enumerateAttribute(
24
+ RenderBridgeAttributes.blockquoteBorderColor,
25
+ in: characterRange,
26
+ options: []
27
+ ) { value, range, _ in
28
+ guard range.length > 0, let color = value as? UIColor else { return }
29
+
30
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
31
+ let paragraphStart = paragraphRange.location
32
+ let groupRange = Self.blockquoteGroupCharacterRange(
33
+ containing: paragraphStart,
34
+ in: textStorage,
35
+ nsString: nsString
36
+ )
37
+ let groupStart = groupRange.location
38
+ guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
39
+ guard let rect = blockquoteStripeRect(
40
+ characterRange: groupRange,
41
+ color: color,
42
+ textStorage: textStorage,
43
+ origin: origin
44
+ ) else {
45
+ return
46
+ }
47
+ rects.append(rect)
48
+ }
49
+
50
+ return rects
51
+ }
52
+
53
+ func resetBlockquoteStripeDrawPassesForTesting() {
54
+ blockquoteStripeDrawPassesForTesting.removeAll()
55
+ }
8
56
 
9
57
  override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
10
58
  super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
@@ -14,6 +62,8 @@ final class EditorLayoutManager: NSLayoutManager {
14
62
  let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
15
63
  let nsString = textStorage.string as NSString
16
64
  var drawnParagraphStarts = Set<Int>()
65
+ var drawnBlockquoteStarts = Set<Int>()
66
+ var drawnStripeRects: [CGRect] = []
17
67
 
18
68
  textStorage.enumerateAttribute(
19
69
  RenderBridgeAttributes.listMarkerContext,
@@ -36,6 +86,42 @@ final class EditorLayoutManager: NSLayoutManager {
36
86
  textStorage: textStorage
37
87
  )
38
88
  }
89
+
90
+ textStorage.enumerateAttribute(
91
+ RenderBridgeAttributes.blockquoteBorderColor,
92
+ in: characterRange,
93
+ options: []
94
+ ) { value, range, _ in
95
+ guard range.length > 0, let color = value as? UIColor else { return }
96
+
97
+ let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
98
+ let paragraphStart = paragraphRange.location
99
+ let groupRange = Self.blockquoteGroupCharacterRange(
100
+ containing: paragraphStart,
101
+ in: textStorage,
102
+ nsString: nsString
103
+ )
104
+ let groupStart = groupRange.location
105
+ guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
106
+
107
+ guard let stripeRect = self.blockquoteStripeRect(
108
+ characterRange: groupRange,
109
+ color: color,
110
+ textStorage: textStorage,
111
+ origin: origin
112
+ ) else {
113
+ return
114
+ }
115
+ self.drawBlockquoteBorder(
116
+ stripeRect: stripeRect,
117
+ color: color
118
+ )
119
+ drawnStripeRects.append(stripeRect)
120
+ }
121
+
122
+ if !drawnStripeRects.isEmpty {
123
+ blockquoteStripeDrawPassesForTesting.append(drawnStripeRects)
124
+ }
39
125
  }
40
126
 
41
127
  private func drawListMarker(
@@ -107,6 +193,156 @@ final class EditorLayoutManager: NSLayoutManager {
107
193
  path.fill()
108
194
  }
109
195
 
196
+ private func blockquoteStripeRect(
197
+ characterRange: NSRange,
198
+ color: UIColor,
199
+ textStorage: NSTextStorage,
200
+ origin: CGPoint
201
+ ) -> CGRect? {
202
+ guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
203
+ return nil
204
+ }
205
+
206
+ ensureLayout(forCharacterRange: characterRange)
207
+ let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
208
+ guard glyphRange.length > 0 else { return nil }
209
+
210
+ var topEdge: CGFloat?
211
+ var bottomEdge: CGFloat?
212
+ var textLeadingEdge: CGFloat?
213
+ enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
214
+ let verticalReferenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
215
+ if let currentTop = topEdge {
216
+ topEdge = min(currentTop, lineFragmentRect.minY)
217
+ } else {
218
+ topEdge = lineFragmentRect.minY
219
+ }
220
+ if let currentBottom = bottomEdge {
221
+ bottomEdge = max(currentBottom, verticalReferenceRect.maxY)
222
+ } else {
223
+ bottomEdge = verticalReferenceRect.maxY
224
+ }
225
+ let referenceMinX = usedRect.width > 0 ? usedRect.minX : lineFragmentRect.minX
226
+ if let current = textLeadingEdge {
227
+ textLeadingEdge = min(current, referenceMinX)
228
+ } else {
229
+ textLeadingEdge = referenceMinX
230
+ }
231
+ }
232
+ guard let topEdge, let bottomEdge, bottomEdge > topEdge, let textLeadingEdge else { return nil }
233
+
234
+ let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
235
+ let borderWidth = (attrs[RenderBridgeAttributes.blockquoteBorderWidth] as? NSNumber)
236
+ .map { CGFloat(truncating: $0) }
237
+ ?? LayoutConstants.blockquoteBorderWidth
238
+ let gap = (attrs[RenderBridgeAttributes.blockquoteMarkerGap] as? NSNumber)
239
+ .map { CGFloat(truncating: $0) }
240
+ ?? LayoutConstants.blockquoteMarkerGap
241
+
242
+ let stripeX = origin.x + textLeadingEdge - gap - borderWidth
243
+ let stripeRect = CGRect(
244
+ x: stripeX,
245
+ y: origin.y + topEdge,
246
+ width: borderWidth,
247
+ height: bottomEdge - topEdge
248
+ )
249
+ return stripeRect
250
+ }
251
+
252
+ private func drawBlockquoteBorder(
253
+ stripeRect: CGRect,
254
+ color: UIColor
255
+ ) {
256
+ color.setFill()
257
+ UIBezierPath(rect: stripeRect).fill()
258
+ }
259
+
260
+ private static func blockquoteGroupCharacterRange(
261
+ containing paragraphStart: Int,
262
+ in textStorage: NSTextStorage,
263
+ nsString: NSString
264
+ ) -> NSRange {
265
+ let initialParagraphRange = nsString.paragraphRange(
266
+ for: NSRange(location: paragraphStart, length: 0)
267
+ )
268
+ var groupStart = initialParagraphRange.location
269
+ var groupEnd = NSMaxRange(initialParagraphRange)
270
+
271
+ var probeStart = groupStart
272
+ while probeStart > 0 {
273
+ let previousParagraphRange = nsString.paragraphRange(
274
+ for: NSRange(location: probeStart - 1, length: 0)
275
+ )
276
+ let previousStart = previousParagraphRange.location
277
+ guard paragraphHasBlockquoteBorder(
278
+ previousParagraphRange,
279
+ in: textStorage
280
+ )
281
+ else {
282
+ break
283
+ }
284
+
285
+ groupStart = previousStart
286
+ probeStart = previousStart
287
+ }
288
+
289
+ var nextParagraphLocation = groupEnd
290
+ while nextParagraphLocation < textStorage.length {
291
+ let nextParagraphRange = nsString.paragraphRange(
292
+ for: NSRange(location: nextParagraphLocation, length: 0)
293
+ )
294
+ guard paragraphHasBlockquoteBorder(
295
+ nextParagraphRange,
296
+ in: textStorage
297
+ )
298
+ else {
299
+ break
300
+ }
301
+
302
+ groupEnd = NSMaxRange(nextParagraphRange)
303
+ nextParagraphLocation = groupEnd
304
+ }
305
+
306
+ return NSRange(location: groupStart, length: groupEnd - groupStart)
307
+ }
308
+
309
+ private static func paragraphHasBlockquoteBorder(
310
+ _ paragraphRange: NSRange,
311
+ in textStorage: NSTextStorage
312
+ ) -> Bool {
313
+ guard paragraphRange.length > 0 else { return false }
314
+ let nsString = textStorage.string as NSString
315
+ var sawQuotedContent = false
316
+ var sawAnyQuotedCharacter = false
317
+
318
+ for offset in 0..<paragraphRange.length {
319
+ let index = paragraphRange.location + offset
320
+ guard index < textStorage.length else { break }
321
+
322
+ let hasBorder = textStorage.attribute(
323
+ RenderBridgeAttributes.blockquoteBorderColor,
324
+ at: index,
325
+ effectiveRange: nil
326
+ ) != nil
327
+ guard hasBorder else { continue }
328
+ sawAnyQuotedCharacter = true
329
+
330
+ let scalar = nsString.character(at: index)
331
+ if scalar != 0x000A, scalar != 0x000D {
332
+ sawQuotedContent = true
333
+ break
334
+ }
335
+ }
336
+
337
+ if sawQuotedContent {
338
+ return true
339
+ }
340
+
341
+ let trimmed = nsString.substring(with: paragraphRange)
342
+ .trimmingCharacters(in: .newlines)
343
+ return trimmed.isEmpty && sawAnyQuotedCharacter
344
+ }
345
+
110
346
  static func markerParagraphStyle(from attrs: [NSAttributedString.Key: Any]) -> NSMutableParagraphStyle {
111
347
  let markerStyle = NSMutableParagraphStyle()
112
348
  let sourceStyle = attrs[.paragraphStyle] as? NSParagraphStyle
@@ -104,6 +104,24 @@ struct EditorHorizontalRuleTheme {
104
104
  }
105
105
  }
106
106
 
107
+ struct EditorBlockquoteTheme {
108
+ var text: EditorTextStyle?
109
+ var indent: CGFloat?
110
+ var borderColor: UIColor?
111
+ var borderWidth: CGFloat?
112
+ var markerGap: CGFloat?
113
+
114
+ init(dictionary: [String: Any]) {
115
+ if let text = dictionary["text"] as? [String: Any] {
116
+ self.text = EditorTextStyle(dictionary: text)
117
+ }
118
+ indent = EditorTheme.cgFloat(dictionary["indent"])
119
+ borderColor = EditorTheme.color(from: dictionary["borderColor"])
120
+ borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
121
+ markerGap = EditorTheme.cgFloat(dictionary["markerGap"])
122
+ }
123
+ }
124
+
107
125
  struct EditorMentionTheme {
108
126
  var textColor: UIColor?
109
127
  var backgroundColor: UIColor?
@@ -140,7 +158,13 @@ struct EditorMentionTheme {
140
158
  }
141
159
  }
142
160
 
161
+ enum EditorToolbarAppearance: String {
162
+ case custom
163
+ case native
164
+ }
165
+
143
166
  struct EditorToolbarTheme {
167
+ var appearance: EditorToolbarAppearance?
144
168
  var backgroundColor: UIColor?
145
169
  var borderColor: UIColor?
146
170
  var borderWidth: CGFloat?
@@ -155,6 +179,7 @@ struct EditorToolbarTheme {
155
179
  var buttonBorderRadius: CGFloat?
156
180
 
157
181
  init(dictionary: [String: Any]) {
182
+ appearance = (dictionary["appearance"] as? String).flatMap(EditorToolbarAppearance.init(rawValue:))
158
183
  backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
159
184
  borderColor = EditorTheme.color(from: dictionary["borderColor"])
160
185
  borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
@@ -168,6 +193,26 @@ struct EditorToolbarTheme {
168
193
  buttonActiveBackgroundColor = EditorTheme.color(from: dictionary["buttonActiveBackgroundColor"])
169
194
  buttonBorderRadius = EditorTheme.cgFloat(dictionary["buttonBorderRadius"])
170
195
  }
196
+
197
+ var resolvedKeyboardOffset: CGFloat {
198
+ keyboardOffset ?? (appearance == .native ? 6 : 0)
199
+ }
200
+
201
+ var resolvedHorizontalInset: CGFloat {
202
+ horizontalInset ?? (appearance == .native ? 10 : 0)
203
+ }
204
+
205
+ var resolvedBorderRadius: CGFloat {
206
+ borderRadius ?? (appearance == .native ? 20 : 0)
207
+ }
208
+
209
+ var resolvedBorderWidth: CGFloat {
210
+ borderWidth ?? (appearance == .native ? 0 : 0.5)
211
+ }
212
+
213
+ var resolvedButtonBorderRadius: CGFloat {
214
+ buttonBorderRadius ?? (appearance == .native ? 10 : 8)
215
+ }
171
216
  }
172
217
 
173
218
  struct EditorContentInsets {
@@ -187,6 +232,7 @@ struct EditorContentInsets {
187
232
  struct EditorTheme {
188
233
  var text: EditorTextStyle?
189
234
  var paragraph: EditorTextStyle?
235
+ var blockquote: EditorBlockquoteTheme?
190
236
  var headings: [String: EditorTextStyle] = [:]
191
237
  var list: EditorListTheme?
192
238
  var horizontalRule: EditorHorizontalRuleTheme?
@@ -213,6 +259,9 @@ struct EditorTheme {
213
259
  if let paragraph = dictionary["paragraph"] as? [String: Any] {
214
260
  self.paragraph = EditorTextStyle(dictionary: paragraph)
215
261
  }
262
+ if let blockquote = dictionary["blockquote"] as? [String: Any] {
263
+ self.blockquote = EditorBlockquoteTheme(dictionary: blockquote)
264
+ }
216
265
  if let headings = dictionary["headings"] as? [String: Any] {
217
266
  for level in ["h1", "h2", "h3", "h4", "h5", "h6"] {
218
267
  if let style = headings[level] as? [String: Any] {
@@ -239,8 +288,9 @@ struct EditorTheme {
239
288
  }
240
289
  }
241
290
 
242
- func effectiveTextStyle(for nodeType: String) -> EditorTextStyle {
291
+ func effectiveTextStyle(for nodeType: String, inBlockquote: Bool = false) -> EditorTextStyle {
243
292
  var style = text ?? EditorTextStyle()
293
+ style = style.merged(with: inBlockquote ? blockquote?.text : nil)
244
294
  if nodeType == "paragraph" {
245
295
  style = style.merged(with: paragraph)
246
296
  if paragraph?.lineHeight == nil {