@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,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
|