@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.
Files changed (47) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +143 -0
  3. package/android/build.gradle +39 -0
  4. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  5. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  6. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
  7. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
  8. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
  10. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
  11. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
  12. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
  13. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
  14. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
  15. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
  16. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
  17. package/expo-module.config.json +9 -0
  18. package/ios/EditorAddons.swift +228 -0
  19. package/ios/EditorCore.xcframework/Info.plist +44 -0
  20. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  21. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  22. package/ios/EditorLayoutManager.swift +254 -0
  23. package/ios/EditorTheme.swift +372 -0
  24. package/ios/Generated_editor_core.swift +1143 -0
  25. package/ios/NativeEditorExpoView.swift +1417 -0
  26. package/ios/NativeEditorModule.swift +263 -0
  27. package/ios/PositionBridge.swift +278 -0
  28. package/ios/ReactNativeProseEditor.podspec +49 -0
  29. package/ios/RenderBridge.swift +825 -0
  30. package/ios/RichTextEditorView.swift +1559 -0
  31. package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
  32. package/ios/editor_coreFFI/module.modulemap +7 -0
  33. package/ios/editor_coreFFI.h +904 -0
  34. package/ios/editor_coreFFI.modulemap +7 -0
  35. package/package.json +66 -0
  36. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  37. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  38. package/rust/android/x86_64/libeditor_core.so +0 -0
  39. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
  40. package/src/EditorTheme.ts +130 -0
  41. package/src/EditorToolbar.tsx +620 -0
  42. package/src/NativeEditorBridge.ts +607 -0
  43. package/src/NativeRichTextEditor.tsx +951 -0
  44. package/src/addons.ts +158 -0
  45. package/src/index.ts +63 -0
  46. package/src/schemas.ts +153 -0
  47. package/src/useNativeEditor.ts +173 -0
@@ -0,0 +1,263 @@
1
+ import ExpoModulesCore
2
+
3
+ public class NativeEditorModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("NativeEditor")
6
+
7
+ Function("editorCreate") { (configJson: String) -> Int in
8
+ Int(editorCreate(configJson: configJson))
9
+ }
10
+ Function("editorDestroy") { (id: Int) in
11
+ editorDestroy(id: UInt64(id))
12
+ }
13
+ Function("editorSetHtml") { (id: Int, html: String) -> String in
14
+ editorSetHtml(id: UInt64(id), html: html)
15
+ }
16
+ Function("editorGetHtml") { (id: Int) -> String in
17
+ editorGetHtml(id: UInt64(id))
18
+ }
19
+ Function("editorSetJson") { (id: Int, json: String) -> String in
20
+ editorSetJson(id: UInt64(id), json: json)
21
+ }
22
+ Function("editorGetJson") { (id: Int) -> String in
23
+ editorGetJson(id: UInt64(id))
24
+ }
25
+ Function("editorInsertText") { (id: Int, pos: Int, text: String) -> String in
26
+ editorInsertText(id: UInt64(id), pos: UInt32(pos), text: text)
27
+ }
28
+ Function("editorInsertTextScalar") { (id: Int, scalarPos: Int, text: String) -> String in
29
+ editorInsertTextScalar(id: UInt64(id), scalarPos: UInt32(scalarPos), text: text)
30
+ }
31
+ Function("editorReplaceSelectionText") { (id: Int, text: String) -> String in
32
+ editorReplaceSelectionText(id: UInt64(id), text: text)
33
+ }
34
+ Function("editorDeleteRange") { (id: Int, from: Int, to: Int) -> String in
35
+ editorDeleteRange(id: UInt64(id), from: UInt32(from), to: UInt32(to))
36
+ }
37
+ Function("editorDeleteScalarRange") { (id: Int, scalarFrom: Int, scalarTo: Int) -> String in
38
+ editorDeleteScalarRange(
39
+ id: UInt64(id),
40
+ scalarFrom: UInt32(scalarFrom),
41
+ scalarTo: UInt32(scalarTo)
42
+ )
43
+ }
44
+ Function(
45
+ "editorReplaceTextScalar"
46
+ ) { (id: Int, scalarFrom: Int, scalarTo: Int, text: String) -> String in
47
+ editorReplaceTextScalar(
48
+ id: UInt64(id),
49
+ scalarFrom: UInt32(scalarFrom),
50
+ scalarTo: UInt32(scalarTo),
51
+ text: text
52
+ )
53
+ }
54
+ Function("editorSplitBlock") { (id: Int, pos: Int) -> String in
55
+ editorSplitBlock(id: UInt64(id), pos: UInt32(pos))
56
+ }
57
+ Function("editorSplitBlockScalar") { (id: Int, scalarPos: Int) -> String in
58
+ editorSplitBlockScalar(id: UInt64(id), scalarPos: UInt32(scalarPos))
59
+ }
60
+ Function("editorDeleteAndSplitScalar") { (id: Int, scalarFrom: Int, scalarTo: Int) -> String in
61
+ editorDeleteAndSplitScalar(
62
+ id: UInt64(id),
63
+ scalarFrom: UInt32(scalarFrom),
64
+ scalarTo: UInt32(scalarTo)
65
+ )
66
+ }
67
+ Function("editorInsertContentHtml") { (id: Int, html: String) -> String in
68
+ editorInsertContentHtml(id: UInt64(id), html: html)
69
+ }
70
+ Function("editorToggleMark") { (id: Int, markName: String) -> String in
71
+ editorToggleMark(id: UInt64(id), markName: markName)
72
+ }
73
+ Function("editorSetSelection") { (id: Int, anchor: Int, head: Int) in
74
+ editorSetSelection(id: UInt64(id), anchor: UInt32(anchor), head: UInt32(head))
75
+ }
76
+ Function("editorSetSelectionScalar") { (id: Int, scalarAnchor: Int, scalarHead: Int) in
77
+ editorSetSelectionScalar(
78
+ id: UInt64(id),
79
+ scalarAnchor: UInt32(scalarAnchor),
80
+ scalarHead: UInt32(scalarHead)
81
+ )
82
+ }
83
+ Function(
84
+ "editorToggleMarkAtSelectionScalar"
85
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int, markName: String) -> String in
86
+ editorToggleMarkAtSelectionScalar(
87
+ id: UInt64(id),
88
+ scalarAnchor: UInt32(scalarAnchor),
89
+ scalarHead: UInt32(scalarHead),
90
+ markName: markName
91
+ )
92
+ }
93
+ Function(
94
+ "editorWrapInListAtSelectionScalar"
95
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int, listType: String) -> String in
96
+ editorWrapInListAtSelectionScalar(
97
+ id: UInt64(id),
98
+ scalarAnchor: UInt32(scalarAnchor),
99
+ scalarHead: UInt32(scalarHead),
100
+ listType: listType
101
+ )
102
+ }
103
+ Function(
104
+ "editorUnwrapFromListAtSelectionScalar"
105
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int) -> String in
106
+ editorUnwrapFromListAtSelectionScalar(
107
+ id: UInt64(id),
108
+ scalarAnchor: UInt32(scalarAnchor),
109
+ scalarHead: UInt32(scalarHead)
110
+ )
111
+ }
112
+ Function(
113
+ "editorIndentListItemAtSelectionScalar"
114
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int) -> String in
115
+ editorIndentListItemAtSelectionScalar(
116
+ id: UInt64(id),
117
+ scalarAnchor: UInt32(scalarAnchor),
118
+ scalarHead: UInt32(scalarHead)
119
+ )
120
+ }
121
+ Function(
122
+ "editorOutdentListItemAtSelectionScalar"
123
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int) -> String in
124
+ editorOutdentListItemAtSelectionScalar(
125
+ id: UInt64(id),
126
+ scalarAnchor: UInt32(scalarAnchor),
127
+ scalarHead: UInt32(scalarHead)
128
+ )
129
+ }
130
+ Function(
131
+ "editorInsertNodeAtSelectionScalar"
132
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int, nodeType: String) -> String in
133
+ editorInsertNodeAtSelectionScalar(
134
+ id: UInt64(id),
135
+ scalarAnchor: UInt32(scalarAnchor),
136
+ scalarHead: UInt32(scalarHead),
137
+ nodeType: nodeType
138
+ )
139
+ }
140
+ Function("editorGetSelection") { (id: Int) -> String in
141
+ editorGetSelection(id: UInt64(id))
142
+ }
143
+ Function("editorDocToScalar") { (id: Int, docPos: Int) -> Int in
144
+ Int(editorDocToScalar(id: UInt64(id), docPos: UInt32(docPos)))
145
+ }
146
+ Function("editorScalarToDoc") { (id: Int, scalar: Int) -> Int in
147
+ Int(editorScalarToDoc(id: UInt64(id), scalar: UInt32(scalar)))
148
+ }
149
+ Function("editorGetCurrentState") { (id: Int) -> String in
150
+ editorGetCurrentState(id: UInt64(id))
151
+ }
152
+ Function("editorUndo") { (id: Int) -> String in
153
+ editorUndo(id: UInt64(id))
154
+ }
155
+ Function("editorRedo") { (id: Int) -> String in
156
+ editorRedo(id: UInt64(id))
157
+ }
158
+ Function("editorCanUndo") { (id: Int) -> Bool in
159
+ editorCanUndo(id: UInt64(id))
160
+ }
161
+ Function("editorCanRedo") { (id: Int) -> Bool in
162
+ editorCanRedo(id: UInt64(id))
163
+ }
164
+ Function("editorReplaceHtml") { (id: Int, html: String) -> String in
165
+ editorReplaceHtml(id: UInt64(id), html: html)
166
+ }
167
+ Function("editorReplaceJson") { (id: Int, json: String) -> String in
168
+ editorReplaceJson(id: UInt64(id), json: json)
169
+ }
170
+ Function("editorInsertContentJson") { (id: Int, json: String) -> String in
171
+ editorInsertContentJson(id: UInt64(id), json: json)
172
+ }
173
+ Function(
174
+ "editorInsertContentJsonAtSelectionScalar"
175
+ ) { (id: Int, scalarAnchor: Int, scalarHead: Int, json: String) -> String in
176
+ editorInsertContentJsonAtSelectionScalar(
177
+ id: UInt64(id),
178
+ scalarAnchor: UInt32(scalarAnchor),
179
+ scalarHead: UInt32(scalarHead),
180
+ json: json
181
+ )
182
+ }
183
+ Function("editorWrapInList") { (id: Int, listType: String) -> String in
184
+ editorWrapInList(id: UInt64(id), listType: listType)
185
+ }
186
+ Function("editorUnwrapFromList") { (id: Int) -> String in
187
+ editorUnwrapFromList(id: UInt64(id))
188
+ }
189
+ Function("editorIndentListItem") { (id: Int) -> String in
190
+ editorIndentListItem(id: UInt64(id))
191
+ }
192
+ Function("editorOutdentListItem") { (id: Int) -> String in
193
+ editorOutdentListItem(id: UInt64(id))
194
+ }
195
+ Function("editorInsertNode") { (id: Int, nodeType: String) -> String in
196
+ editorInsertNode(id: UInt64(id), nodeType: nodeType)
197
+ }
198
+
199
+ View(NativeEditorExpoView.self) {
200
+ Events(
201
+ "onEditorUpdate",
202
+ "onSelectionChange",
203
+ "onFocusChange",
204
+ "onContentHeightChange",
205
+ "onToolbarAction",
206
+ "onAddonEvent"
207
+ )
208
+
209
+ Prop("editorId") { (view: NativeEditorExpoView, id: Int) in
210
+ view.setEditorId(UInt64(id))
211
+ }
212
+ Prop("editable") { (view: NativeEditorExpoView, editable: Bool) in
213
+ view.setEditable(editable)
214
+ }
215
+ Prop("placeholder") { (view: NativeEditorExpoView, placeholder: String) in
216
+ view.richTextView.textView.placeholder = placeholder
217
+ }
218
+ Prop("autoFocus") { (view: NativeEditorExpoView, autoFocus: Bool) in
219
+ view.setAutoFocus(autoFocus)
220
+ }
221
+ Prop("showToolbar") { (view: NativeEditorExpoView, showToolbar: Bool) in
222
+ view.setShowToolbar(showToolbar)
223
+ }
224
+ Prop("toolbarPlacement") { (view: NativeEditorExpoView, toolbarPlacement: String?) in
225
+ view.setToolbarPlacement(toolbarPlacement)
226
+ }
227
+ Prop("heightBehavior") { (view: NativeEditorExpoView, heightBehavior: String) in
228
+ view.setHeightBehavior(heightBehavior)
229
+ }
230
+ Prop("themeJson") { (view: NativeEditorExpoView, themeJson: String?) in
231
+ view.setThemeJson(themeJson)
232
+ }
233
+ Prop("addonsJson") { (view: NativeEditorExpoView, addonsJson: String?) in
234
+ view.setAddonsJson(addonsJson)
235
+ }
236
+ Prop("toolbarItemsJson") { (view: NativeEditorExpoView, toolbarItemsJson: String?) in
237
+ view.setToolbarButtonsJson(toolbarItemsJson)
238
+ }
239
+ Prop("toolbarFrameJson") { (view: NativeEditorExpoView, toolbarFrameJson: String?) in
240
+ view.setToolbarFrameJson(toolbarFrameJson)
241
+ }
242
+ Prop("editorUpdateJson") { (view: NativeEditorExpoView, editorUpdateJson: String?) in
243
+ view.setPendingEditorUpdateJson(editorUpdateJson)
244
+ }
245
+ Prop("editorUpdateRevision") { (view: NativeEditorExpoView, editorUpdateRevision: Int) in
246
+ view.setPendingEditorUpdateRevision(editorUpdateRevision)
247
+ }
248
+ OnViewDidUpdateProps { (view: NativeEditorExpoView) in
249
+ view.applyPendingEditorUpdateIfNeeded()
250
+ }
251
+
252
+ AsyncFunction("applyEditorUpdate") { (view: NativeEditorExpoView, updateJson: String) in
253
+ view.applyEditorUpdate(updateJson)
254
+ }
255
+ AsyncFunction("focus") { (view: NativeEditorExpoView) in
256
+ view.focus()
257
+ }
258
+ AsyncFunction("blur") { (view: NativeEditorExpoView) in
259
+ view.blur()
260
+ }
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,278 @@
1
+ import UIKit
2
+
3
+ // MARK: - PositionBridge
4
+
5
+ /// Converts between UITextView cursor positions (UTF-16 code unit offsets, snapped
6
+ /// to grapheme cluster boundaries) and Rust editor-core scalar offsets (Unicode
7
+ /// scalar values = Unicode code points).
8
+ ///
9
+ /// UIKit's text system uses UTF-16 internally (NSString). Emoji like U+1F468
10
+ /// (man) occupy 2 UTF-16 code units (a surrogate pair) but 1 Unicode scalar.
11
+ /// Composed emoji sequences like 👨‍👩‍👧‍👦 are multiple scalars joined by
12
+ /// ZWJ but render as a single grapheme cluster.
13
+ ///
14
+ /// Rust's editor-core counts positions in Unicode scalars (what Rust calls `char`).
15
+ /// The PositionMap in Rust converts between doc positions and scalar offsets.
16
+ /// This bridge converts between those scalar offsets and UITextView UTF-16 offsets.
17
+ final class PositionBridge {
18
+
19
+ struct VirtualListMarker {
20
+ let paragraphStartUtf16: Int
21
+ let scalarLength: UInt32
22
+ }
23
+
24
+ // MARK: - UTF-16 <-> Scalar Conversion
25
+
26
+ /// Convert a UITextView cursor position (UTF-16 offset) to a Rust scalar offset.
27
+ ///
28
+ /// Walks the string from the beginning, counting Unicode scalars consumed as
29
+ /// we advance through UTF-16 code units. Surrogate pairs (code units > U+FFFF)
30
+ /// contribute 2 UTF-16 code units but only 1 scalar.
31
+ ///
32
+ /// - Parameters:
33
+ /// - position: A `UITextPosition` obtained from the text view.
34
+ /// - textView: The text view containing the text.
35
+ /// - Returns: The equivalent Unicode scalar offset.
36
+ static func textViewToScalar(_ position: UITextPosition, in textView: UITextView) -> UInt32 {
37
+ let utf16Offset = textView.offset(from: textView.beginningOfDocument, to: position)
38
+ return utf16OffsetToScalar(utf16Offset, in: textView)
39
+ }
40
+
41
+ /// Convert a Rust scalar offset to a UITextView position.
42
+ ///
43
+ /// Walks the string counting scalars until we reach the target, then returns
44
+ /// the corresponding UTF-16 offset as a UITextPosition.
45
+ ///
46
+ /// - Parameters:
47
+ /// - scalar: The Unicode scalar offset from Rust.
48
+ /// - textView: The text view containing the text.
49
+ /// - Returns: The equivalent `UITextPosition`, or the end of document if the
50
+ /// scalar offset exceeds the text length.
51
+ static func scalarToTextView(_ scalar: UInt32, in textView: UITextView) -> UITextPosition {
52
+ let utf16Offset = scalarToUtf16Offset(scalar, in: textView)
53
+ return textView.position(
54
+ from: textView.beginningOfDocument,
55
+ offset: utf16Offset
56
+ ) ?? textView.endOfDocument
57
+ }
58
+
59
+ static func utf16OffsetToScalar(_ utf16Offset: Int, in textView: UITextView) -> UInt32 {
60
+ let text = textView.text ?? ""
61
+ let nsString = text as NSString
62
+ let clampedOffset = min(max(utf16Offset, 0), nsString.length)
63
+ var scalarOffset = utf16OffsetToScalar(clampedOffset, in: text)
64
+
65
+ for marker in virtualListMarkers(in: textView) where clampedOffset >= marker.paragraphStartUtf16 {
66
+ scalarOffset += marker.scalarLength
67
+ }
68
+
69
+ return scalarOffset
70
+ }
71
+
72
+ static func scalarToUtf16Offset(_ scalar: UInt32, in textView: UITextView) -> Int {
73
+ let text = textView.text ?? ""
74
+ let maxUtf16 = (text as NSString).length
75
+
76
+ if scalar == 0 || maxUtf16 == 0 {
77
+ return 0
78
+ }
79
+
80
+ var low = 0
81
+ var high = maxUtf16
82
+ while low < high {
83
+ let mid = (low + high) / 2
84
+ if utf16OffsetToScalar(mid, in: textView) < scalar {
85
+ low = mid + 1
86
+ } else {
87
+ high = mid
88
+ }
89
+ }
90
+
91
+ return low
92
+ }
93
+
94
+ /// Convert a UTF-16 offset to a Unicode scalar offset within a string.
95
+ ///
96
+ /// This is the core conversion used by `textViewToScalar`. Exposed as a
97
+ /// static method for direct use and testing.
98
+ ///
99
+ /// - Parameters:
100
+ /// - utf16Offset: The UTF-16 code unit offset.
101
+ /// - text: The string to walk.
102
+ /// - Returns: The number of Unicode scalars from the start to the given UTF-16 offset.
103
+ static func utf16OffsetToScalar(_ utf16Offset: Int, in text: String) -> UInt32 {
104
+ guard utf16Offset > 0 else { return 0 }
105
+
106
+ let utf16View = text.utf16
107
+ let endIndex = min(utf16Offset, utf16View.count)
108
+ var scalarCount: UInt32 = 0
109
+ var utf16Pos = 0
110
+
111
+ for scalar in text.unicodeScalars {
112
+ if utf16Pos >= endIndex { break }
113
+ let scalarUtf16Len = scalar.utf16.count
114
+ utf16Pos += scalarUtf16Len
115
+ scalarCount += 1
116
+ }
117
+
118
+ return scalarCount
119
+ }
120
+
121
+ /// Convert a Unicode scalar offset to a UTF-16 offset within a string.
122
+ ///
123
+ /// This is the core conversion used by `scalarToTextView`. Exposed as a
124
+ /// static method for direct use and testing.
125
+ ///
126
+ /// - Parameters:
127
+ /// - scalar: The Unicode scalar offset.
128
+ /// - text: The string to walk.
129
+ /// - Returns: The number of UTF-16 code units from the start to the given scalar offset.
130
+ static func scalarToUtf16Offset(_ scalar: UInt32, in text: String) -> Int {
131
+ guard scalar > 0 else { return 0 }
132
+
133
+ var utf16Len = 0
134
+ var scalarsSeen: UInt32 = 0
135
+
136
+ for s in text.unicodeScalars {
137
+ if scalarsSeen >= scalar { break }
138
+ utf16Len += s.utf16.count
139
+ scalarsSeen += 1
140
+ }
141
+
142
+ return utf16Len
143
+ }
144
+
145
+ // MARK: - Grapheme Boundary Snapping
146
+
147
+ /// Snap a UTF-16 offset to the nearest grapheme cluster boundary.
148
+ ///
149
+ /// UITextView may report offsets in the middle of a grapheme cluster (e.g.
150
+ /// between the scalars of a flag emoji or a composed character sequence).
151
+ /// This method snaps the offset forward to the end of the current grapheme
152
+ /// cluster, since that is the position the user would perceive.
153
+ ///
154
+ /// - Parameters:
155
+ /// - utf16Offset: A UTF-16 code unit offset that may be mid-grapheme.
156
+ /// - text: The string to inspect.
157
+ /// - Returns: The nearest grapheme-aligned UTF-16 offset. If the input is
158
+ /// already on a boundary, it is returned unchanged.
159
+ static func snapToGraphemeBoundary(_ utf16Offset: Int, in text: String) -> Int {
160
+ guard !text.isEmpty else { return 0 }
161
+
162
+ let nsString = text as NSString
163
+ let clampedOffset = min(max(utf16Offset, 0), nsString.length)
164
+
165
+ // If we're at the very start or end, already on a boundary.
166
+ if clampedOffset == 0 || clampedOffset == nsString.length {
167
+ return clampedOffset
168
+ }
169
+
170
+ // composedCharacterSequence(at:) returns the full grapheme cluster range
171
+ // containing the given UTF-16 index. We snap to the end of that range
172
+ // (forward bias) since that's what a user moving the cursor expects.
173
+ let range = nsString.rangeOfComposedCharacterSequence(at: clampedOffset)
174
+
175
+ // If the offset is already at the start of a grapheme cluster, it's on a boundary.
176
+ if range.location == clampedOffset {
177
+ return clampedOffset
178
+ }
179
+
180
+ // Otherwise, snap to the end of this cluster.
181
+ return NSMaxRange(range)
182
+ }
183
+
184
+ // MARK: - UITextRange <-> Scalar Range
185
+
186
+ /// Convert a UITextRange to a (from, to) pair of Rust scalar offsets.
187
+ ///
188
+ /// - Parameters:
189
+ /// - range: A `UITextRange` from the text view.
190
+ /// - textView: The text view containing the text.
191
+ /// - Returns: A tuple of (from, to) scalar offsets where from <= to.
192
+ static func textRangeToScalarRange(
193
+ _ range: UITextRange,
194
+ in textView: UITextView
195
+ ) -> (from: UInt32, to: UInt32) {
196
+ let from = textViewToScalar(range.start, in: textView)
197
+ let to = textViewToScalar(range.end, in: textView)
198
+ return (from: min(from, to), to: max(from, to))
199
+ }
200
+
201
+ /// Convert a pair of Rust scalar offsets to a UITextRange.
202
+ ///
203
+ /// - Parameters:
204
+ /// - from: The start scalar offset.
205
+ /// - to: The end scalar offset.
206
+ /// - textView: The text view.
207
+ /// - Returns: The corresponding `UITextRange`, or nil if the positions are invalid.
208
+ static func scalarRangeToTextRange(
209
+ from: UInt32,
210
+ to: UInt32,
211
+ in textView: UITextView
212
+ ) -> UITextRange? {
213
+ let startPos = scalarToTextView(from, in: textView)
214
+ let endPos = scalarToTextView(to, in: textView)
215
+ return textView.textRange(from: startPos, to: endPos)
216
+ }
217
+
218
+ // MARK: - Cursor Scalar Offset (Convenience)
219
+
220
+ /// Get the current cursor position as a Rust scalar offset.
221
+ ///
222
+ /// If there is a range selection, returns the head (moving end) position.
223
+ ///
224
+ /// - Parameter textView: The text view.
225
+ /// - Returns: The scalar offset of the cursor, or 0 if no selection exists.
226
+ static func cursorScalarOffset(in textView: UITextView) -> UInt32 {
227
+ guard let selectedRange = textView.selectedTextRange else { return 0 }
228
+ return textViewToScalar(selectedRange.end, in: textView)
229
+ }
230
+
231
+ static func virtualListMarker(
232
+ atUtf16Offset utf16Offset: Int,
233
+ in textView: UITextView
234
+ ) -> VirtualListMarker? {
235
+ virtualListMarkers(in: textView).first { $0.paragraphStartUtf16 == utf16Offset }
236
+ }
237
+
238
+ private static func virtualListMarkers(in textView: UITextView) -> [VirtualListMarker] {
239
+ let textStorage = textView.textStorage
240
+ guard textStorage.length > 0 else { return [] }
241
+
242
+ let nsString = textStorage.string as NSString
243
+ var markers: [VirtualListMarker] = []
244
+ var seenStarts = Set<Int>()
245
+ let fullRange = NSRange(location: 0, length: textStorage.length)
246
+
247
+ textStorage.enumerateAttribute(
248
+ RenderBridgeAttributes.listMarkerContext,
249
+ in: fullRange,
250
+ options: []
251
+ ) { value, range, _ in
252
+ guard range.length > 0, let listContext = value as? [String: Any] else { return }
253
+
254
+ let paragraphStart = nsString.paragraphRange(
255
+ for: NSRange(location: range.location, length: 0)
256
+ ).location
257
+ guard !EditorLayoutManager.isParagraphStartCreatedByHardBreak(
258
+ paragraphStart,
259
+ in: textStorage
260
+ ) else {
261
+ return
262
+ }
263
+ guard seenStarts.insert(paragraphStart).inserted else { return }
264
+
265
+ let markerLength = UInt32(
266
+ RenderBridge.listMarkerString(listContext: listContext).unicodeScalars.count
267
+ )
268
+ markers.append(
269
+ VirtualListMarker(
270
+ paragraphStartUtf16: paragraphStart,
271
+ scalarLength: markerLength
272
+ )
273
+ )
274
+ }
275
+
276
+ return markers.sorted { $0.paragraphStartUtf16 < $1.paragraphStartUtf16 }
277
+ }
278
+ }
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ReactNativeProseEditor'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = { :type => 'MIT' }
11
+ s.author = 'Apollo HG'
12
+ s.homepage = 'https://github.com/apollohg/react-native-prose-editor'
13
+ s.platforms = { :ios => '15.1' }
14
+ s.swift_version = '5.9'
15
+ s.source = { git: 'https://github.com/apollohg/react-native-prose-editor.git' }
16
+ # UniFFI's generated Swift bindings import a companion Clang module
17
+ # (`editor_coreFFI`) via a custom modulemap. CocoaPods does not support
18
+ # custom module maps on Swift static libraries, so this pod must build as
19
+ # a framework.
20
+ s.static_framework = false
21
+
22
+ s.dependency 'ExpoModulesCore'
23
+
24
+ # Swift source files (including generated UniFFI bindings)
25
+ s.source_files = '**/*.{h,m,swift}'
26
+ s.exclude_files = 'Tests/**/*'
27
+
28
+ # Prebuilt Rust static library as XCFramework. CocoaPods only reliably
29
+ # picks up vendored binaries that live under the pod root, so build-ios.sh
30
+ # syncs the generated XCFramework into this ios/ directory.
31
+ xcframework_path = File.join(__dir__, 'EditorCore.xcframework')
32
+
33
+ if File.exist?(xcframework_path)
34
+ s.vendored_frameworks = 'EditorCore.xcframework'
35
+ end
36
+
37
+ # The UniFFI C header and modulemap for the Rust FFI layer
38
+ s.preserve_paths = [
39
+ 'editor_coreFFI/**/*',
40
+ 'EditorCore.xcframework/**/*',
41
+ ]
42
+
43
+ s.pod_target_xcconfig = {
44
+ 'DEFINES_MODULE' => 'YES',
45
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule',
46
+ 'SWIFT_INCLUDE_PATHS' => '$(PODS_TARGET_SRCROOT)/editor_coreFFI',
47
+ 'HEADER_SEARCH_PATHS' => '$(PODS_TARGET_SRCROOT)/editor_coreFFI',
48
+ }
49
+ end