@openeditor/react-native-prose-editor 0.0.9 โ 0.0.11
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/README.md +5 -9
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +476 -53
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +110 -169
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +124 -337
- package/dist/EditorTheme.d.ts +8 -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/Generated_editor_core.swift +15 -0
- package/ios/NativeBlockEditorSurface.swift +371 -60
- package/ios/NativeEditorExpoView.swift +114 -117
- package/ios/NativeInputSupport.swift +0 -302
- package/ios/PositionBridge.swift +22 -548
- package/ios/RichTextEditorView.swift +217 -4671
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
- package/package.json +1 -1
- 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 +31 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +0 -50
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +0 -4068
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +0 -1009
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +0 -6
package/ios/PositionBridge.swift
CHANGED
|
@@ -1,558 +1,32 @@
|
|
|
1
1
|
import UIKit
|
|
2
|
-
import ObjectiveC
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/// Converts between UITextView cursor positions (UTF-16 code unit offsets, snapped
|
|
7
|
-
/// to grapheme cluster boundaries) and Rust editor-core scalar offsets (Unicode
|
|
8
|
-
/// scalar values = Unicode code points).
|
|
9
|
-
///
|
|
10
|
-
/// UIKit's text system uses UTF-16 internally (NSString). Emoji like U+1F468
|
|
11
|
-
/// (man) occupy 2 UTF-16 code units (a surrogate pair) but 1 Unicode scalar.
|
|
12
|
-
/// Composed emoji sequences like ๐จโ๐ฉโ๐งโ๐ฆ are multiple scalars joined by
|
|
13
|
-
/// ZWJ but render as a single grapheme cluster.
|
|
14
|
-
///
|
|
15
|
-
/// Rust's editor-core counts positions in Unicode scalars (what Rust calls `char`).
|
|
16
|
-
/// The PositionMap in Rust converts between doc positions and scalar offsets.
|
|
17
|
-
/// This bridge converts between those scalar offsets and UITextView UTF-16 offsets.
|
|
3
|
+
/// Converts native UTF-16 offsets inside one structural text leaf to the
|
|
4
|
+
/// Unicode-scalar offsets used by the Rust editor engine.
|
|
18
5
|
final class PositionBridge {
|
|
19
|
-
|
|
20
|
-
private struct StringConversionTable {
|
|
21
|
-
let utf16ToScalar: [UInt32]
|
|
22
|
-
let scalarToUtf16: [Int]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private final class TextViewConversionTable: NSObject {
|
|
26
|
-
let adjustedUtf16ToScalar: [UInt32]
|
|
27
|
-
|
|
28
|
-
init(adjustedUtf16ToScalar: [UInt32]) {
|
|
29
|
-
self.adjustedUtf16ToScalar = adjustedUtf16ToScalar
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
struct VirtualListMarker {
|
|
34
|
-
let paragraphStartUtf16: Int
|
|
35
|
-
let scalarLength: UInt32
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private struct PositionAdjustments {
|
|
39
|
-
let placeholders: [Int]
|
|
40
|
-
let listMarkers: [VirtualListMarker]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private static var textViewConversionTableKey: UInt8 = 0
|
|
44
|
-
private static let stringTableLock = NSLock()
|
|
45
|
-
private static var lastStringTableText = ""
|
|
46
|
-
private static var lastStringTable: StringConversionTable?
|
|
47
|
-
|
|
48
|
-
// MARK: - UTF-16 <-> Scalar Conversion
|
|
49
|
-
|
|
50
|
-
/// Convert a UITextView cursor position (UTF-16 offset) to a Rust scalar offset.
|
|
51
|
-
///
|
|
52
|
-
/// Walks the string from the beginning, counting Unicode scalars consumed as
|
|
53
|
-
/// we advance through UTF-16 code units. Surrogate pairs (code units > U+FFFF)
|
|
54
|
-
/// contribute 2 UTF-16 code units but only 1 scalar.
|
|
55
|
-
///
|
|
56
|
-
/// - Parameters:
|
|
57
|
-
/// - position: A `UITextPosition` obtained from the text view.
|
|
58
|
-
/// - textView: The text view containing the text.
|
|
59
|
-
/// - Returns: The equivalent Unicode scalar offset.
|
|
60
6
|
static func textViewToScalar(_ position: UITextPosition, in textView: UITextView) -> UInt32 {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/// Convert a Rust scalar offset to a UITextView position.
|
|
66
|
-
///
|
|
67
|
-
/// Walks the string counting scalars until we reach the target, then returns
|
|
68
|
-
/// the corresponding UTF-16 offset as a UITextPosition.
|
|
69
|
-
///
|
|
70
|
-
/// - Parameters:
|
|
71
|
-
/// - scalar: The Unicode scalar offset from Rust.
|
|
72
|
-
/// - textView: The text view containing the text.
|
|
73
|
-
/// - Returns: The equivalent `UITextPosition`, or the end of document if the
|
|
74
|
-
/// scalar offset exceeds the text length.
|
|
75
|
-
static func scalarToTextView(_ scalar: UInt32, in textView: UITextView) -> UITextPosition {
|
|
76
|
-
let utf16Offset = scalarToUtf16Offset(scalar, in: textView)
|
|
77
|
-
return textView.position(
|
|
78
|
-
from: textView.beginningOfDocument,
|
|
79
|
-
offset: utf16Offset
|
|
80
|
-
) ?? textView.endOfDocument
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
static func utf16OffsetToScalar(_ utf16Offset: Int, in textView: UITextView) -> UInt32 {
|
|
84
|
-
let text = textView.text ?? ""
|
|
85
|
-
let conversionTable = textViewConversionTable(for: textView)
|
|
86
|
-
guard !conversionTable.adjustedUtf16ToScalar.isEmpty else { return 0 }
|
|
87
|
-
let clampedOffset = min(
|
|
88
|
-
max(utf16Offset, 0),
|
|
89
|
-
min((text as NSString).length, conversionTable.adjustedUtf16ToScalar.count - 1)
|
|
7
|
+
utf16OffsetToScalar(
|
|
8
|
+
textView.offset(from: textView.beginningOfDocument, to: position),
|
|
9
|
+
in: textView.text ?? ""
|
|
90
10
|
)
|
|
91
|
-
return conversionTable.adjustedUtf16ToScalar[clampedOffset]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static func utf16OffsetToScalar(_ utf16Offset: Int, in attributedString: NSAttributedString) -> UInt32 {
|
|
95
|
-
let conversionTable = adjustedConversionTable(for: attributedString)
|
|
96
|
-
guard !conversionTable.isEmpty else { return 0 }
|
|
97
|
-
let clampedOffset = min(max(utf16Offset, 0), conversionTable.count - 1)
|
|
98
|
-
return conversionTable[clampedOffset]
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
static func scalarToUtf16Offset(_ scalar: UInt32, in textView: UITextView) -> Int {
|
|
102
|
-
let conversionTable = textViewConversionTable(for: textView)
|
|
103
|
-
let utf16ToScalar = conversionTable.adjustedUtf16ToScalar
|
|
104
|
-
return scalarToUtf16Offset(scalar, inAdjustedUtf16ToScalarTable: utf16ToScalar)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
static func scalarToUtf16Offset(_ scalar: UInt32, in attributedString: NSAttributedString) -> Int {
|
|
108
|
-
let utf16ToScalar = adjustedConversionTable(for: attributedString)
|
|
109
|
-
return scalarToUtf16Offset(scalar, inAdjustedUtf16ToScalarTable: utf16ToScalar)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private static func scalarToUtf16Offset(
|
|
113
|
-
_ scalar: UInt32,
|
|
114
|
-
inAdjustedUtf16ToScalarTable utf16ToScalar: [UInt32]
|
|
115
|
-
) -> Int {
|
|
116
|
-
guard scalar > 0, !utf16ToScalar.isEmpty else {
|
|
117
|
-
return 0
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if let last = utf16ToScalar.last, scalar > last {
|
|
121
|
-
return utf16ToScalar.count - 1
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
var low = 0
|
|
125
|
-
var high = utf16ToScalar.count - 1
|
|
126
|
-
while low < high {
|
|
127
|
-
let mid = (low + high) / 2
|
|
128
|
-
if utf16ToScalar[mid] < scalar {
|
|
129
|
-
low = mid + 1
|
|
130
|
-
} else {
|
|
131
|
-
high = mid
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return low
|
|
136
11
|
}
|
|
137
12
|
|
|
138
|
-
/// Convert a UTF-16 offset to a Unicode scalar offset within a string.
|
|
139
|
-
///
|
|
140
|
-
/// This is the core conversion used by `textViewToScalar`. Exposed as a
|
|
141
|
-
/// static method for direct use and testing.
|
|
142
|
-
///
|
|
143
|
-
/// - Parameters:
|
|
144
|
-
/// - utf16Offset: The UTF-16 code unit offset.
|
|
145
|
-
/// - text: The string to walk.
|
|
146
|
-
/// - Returns: The number of Unicode scalars from the start to the given UTF-16 offset.
|
|
147
13
|
static func utf16OffsetToScalar(_ utf16Offset: Int, in text: String) -> UInt32 {
|
|
148
|
-
let
|
|
149
|
-
let
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
let scalarIndex = min(Int(scalar), conversionTable.scalarToUtf16.count - 1)
|
|
166
|
-
return conversionTable.scalarToUtf16[scalarIndex]
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// MARK: - Grapheme Boundary Snapping
|
|
170
|
-
|
|
171
|
-
/// Snap a UTF-16 offset to the nearest grapheme cluster boundary.
|
|
172
|
-
///
|
|
173
|
-
/// UITextView may report offsets in the middle of a grapheme cluster (e.g.
|
|
174
|
-
/// between the scalars of a flag emoji or a composed character sequence).
|
|
175
|
-
/// This method snaps the offset forward to the end of the current grapheme
|
|
176
|
-
/// cluster, since that is the position the user would perceive.
|
|
177
|
-
///
|
|
178
|
-
/// - Parameters:
|
|
179
|
-
/// - utf16Offset: A UTF-16 code unit offset that may be mid-grapheme.
|
|
180
|
-
/// - text: The string to inspect.
|
|
181
|
-
/// - Returns: The nearest grapheme-aligned UTF-16 offset. If the input is
|
|
182
|
-
/// already on a boundary, it is returned unchanged.
|
|
183
|
-
static func snapToGraphemeBoundary(_ utf16Offset: Int, in text: String) -> Int {
|
|
184
|
-
guard !text.isEmpty else { return 0 }
|
|
185
|
-
|
|
186
|
-
let nsString = text as NSString
|
|
187
|
-
let clampedOffset = min(max(utf16Offset, 0), nsString.length)
|
|
188
|
-
|
|
189
|
-
// If we're at the very start or end, already on a boundary.
|
|
190
|
-
if clampedOffset == 0 || clampedOffset == nsString.length {
|
|
191
|
-
return clampedOffset
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// composedCharacterSequence(at:) returns the full grapheme cluster range
|
|
195
|
-
// containing the given UTF-16 index. We snap to the end of that range
|
|
196
|
-
// (forward bias) since that's what a user moving the cursor expects.
|
|
197
|
-
let range = nsString.rangeOfComposedCharacterSequence(at: clampedOffset)
|
|
198
|
-
|
|
199
|
-
// If the offset is already at the start of a grapheme cluster, it's on a boundary.
|
|
200
|
-
if range.location == clampedOffset {
|
|
201
|
-
return clampedOffset
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Otherwise, snap to the end of this cluster.
|
|
205
|
-
return NSMaxRange(range)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// MARK: - UITextRange <-> Scalar Range
|
|
209
|
-
|
|
210
|
-
/// Convert a UITextRange to a (from, to) pair of Rust scalar offsets.
|
|
211
|
-
///
|
|
212
|
-
/// - Parameters:
|
|
213
|
-
/// - range: A `UITextRange` from the text view.
|
|
214
|
-
/// - textView: The text view containing the text.
|
|
215
|
-
/// - Returns: A tuple of (from, to) scalar offsets where from <= to.
|
|
216
|
-
static func textRangeToScalarRange(
|
|
217
|
-
_ range: UITextRange,
|
|
218
|
-
in textView: UITextView
|
|
219
|
-
) -> (from: UInt32, to: UInt32) {
|
|
220
|
-
let from = textViewToScalar(range.start, in: textView)
|
|
221
|
-
let to = textViewToScalar(range.end, in: textView)
|
|
222
|
-
return (from: min(from, to), to: max(from, to))
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/// Convert a pair of Rust scalar offsets to a UITextRange.
|
|
226
|
-
///
|
|
227
|
-
/// - Parameters:
|
|
228
|
-
/// - from: The start scalar offset.
|
|
229
|
-
/// - to: The end scalar offset.
|
|
230
|
-
/// - textView: The text view.
|
|
231
|
-
/// - Returns: The corresponding `UITextRange`, or nil if the positions are invalid.
|
|
232
|
-
static func scalarRangeToTextRange(
|
|
233
|
-
from: UInt32,
|
|
234
|
-
to: UInt32,
|
|
235
|
-
in textView: UITextView
|
|
236
|
-
) -> UITextRange? {
|
|
237
|
-
let startPos = scalarToTextView(from, in: textView)
|
|
238
|
-
let endPos = scalarToTextView(to, in: textView)
|
|
239
|
-
return textView.textRange(from: startPos, to: endPos)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// MARK: - Cursor Scalar Offset (Convenience)
|
|
243
|
-
|
|
244
|
-
/// Get the current cursor position as a Rust scalar offset.
|
|
245
|
-
///
|
|
246
|
-
/// If there is a range selection, returns the head (moving end) position.
|
|
247
|
-
///
|
|
248
|
-
/// - Parameter textView: The text view.
|
|
249
|
-
/// - Returns: The scalar offset of the cursor, or 0 if no selection exists.
|
|
250
|
-
static func cursorScalarOffset(in textView: UITextView) -> UInt32 {
|
|
251
|
-
guard let selectedRange = textView.selectedTextRange else { return 0 }
|
|
252
|
-
return textViewToScalar(selectedRange.end, in: textView)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
static func virtualListMarker(
|
|
256
|
-
atUtf16Offset utf16Offset: Int,
|
|
257
|
-
in textView: UITextView
|
|
258
|
-
) -> VirtualListMarker? {
|
|
259
|
-
virtualListMarkers(in: textView.textStorage).first { $0.paragraphStartUtf16 == utf16Offset }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
static func invalidateCache(for textView: UITextView) {
|
|
263
|
-
objc_setAssociatedObject(
|
|
264
|
-
textView,
|
|
265
|
-
&textViewConversionTableKey,
|
|
266
|
-
nil,
|
|
267
|
-
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
@discardableResult
|
|
272
|
-
static func applyAttributedPatchIfPossible(
|
|
273
|
-
for textView: UITextView,
|
|
274
|
-
replaceRange: NSRange,
|
|
275
|
-
replacement: NSAttributedString
|
|
276
|
-
) -> Bool {
|
|
277
|
-
guard let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable else {
|
|
278
|
-
return false
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
let oldAdjusted = cached.adjustedUtf16ToScalar
|
|
282
|
-
let oldUtf16Count = max(0, oldAdjusted.count - 1)
|
|
283
|
-
guard replaceRange.location >= 0,
|
|
284
|
-
replaceRange.length >= 0,
|
|
285
|
-
replaceRange.location + replaceRange.length <= oldUtf16Count
|
|
286
|
-
else {
|
|
287
|
-
return false
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
let startOffset = replaceRange.location
|
|
291
|
-
let endOffset = replaceRange.location + replaceRange.length
|
|
292
|
-
let replacementAdjusted = adjustedConversionTable(for: replacement)
|
|
293
|
-
let patched = patchedAdjustedConversionTable(
|
|
294
|
-
oldAdjusted: oldAdjusted,
|
|
295
|
-
startOffset: startOffset,
|
|
296
|
-
endOffset: endOffset,
|
|
297
|
-
replacementAdjusted: replacementAdjusted
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
objc_setAssociatedObject(
|
|
301
|
-
textView,
|
|
302
|
-
&textViewConversionTableKey,
|
|
303
|
-
TextViewConversionTable(adjustedUtf16ToScalar: patched),
|
|
304
|
-
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
305
|
-
)
|
|
306
|
-
return true
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
@discardableResult
|
|
310
|
-
static func applyPlainTextPatchIfPossible(
|
|
311
|
-
for textView: UITextView,
|
|
312
|
-
replaceRange: NSRange,
|
|
313
|
-
replacementText: String
|
|
314
|
-
) -> Bool {
|
|
315
|
-
guard let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable else {
|
|
316
|
-
return false
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
let oldAdjusted = cached.adjustedUtf16ToScalar
|
|
320
|
-
let oldUtf16Count = max(0, oldAdjusted.count - 1)
|
|
321
|
-
guard replaceRange.location >= 0,
|
|
322
|
-
replaceRange.length >= 0,
|
|
323
|
-
replaceRange.location + replaceRange.length <= oldUtf16Count
|
|
324
|
-
else {
|
|
325
|
-
return false
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
let startOffset = replaceRange.location
|
|
329
|
-
let endOffset = replaceRange.location + replaceRange.length
|
|
330
|
-
let replacementBase = stringConversionTable(for: replacementText).utf16ToScalar
|
|
331
|
-
let patched = patchedAdjustedConversionTable(
|
|
332
|
-
oldAdjusted: oldAdjusted,
|
|
333
|
-
startOffset: startOffset,
|
|
334
|
-
endOffset: endOffset,
|
|
335
|
-
replacementAdjusted: replacementBase
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
objc_setAssociatedObject(
|
|
339
|
-
textView,
|
|
340
|
-
&textViewConversionTableKey,
|
|
341
|
-
TextViewConversionTable(adjustedUtf16ToScalar: patched),
|
|
342
|
-
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
343
|
-
)
|
|
344
|
-
return true
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private static func patchedAdjustedConversionTable(
|
|
348
|
-
oldAdjusted: [UInt32],
|
|
349
|
-
startOffset: Int,
|
|
350
|
-
endOffset: Int,
|
|
351
|
-
replacementAdjusted: [UInt32]
|
|
352
|
-
) -> [UInt32] {
|
|
353
|
-
let startScalar = Int32(oldAdjusted[startOffset])
|
|
354
|
-
let deletedScalarCount = Int32(oldAdjusted[endOffset]) - startScalar
|
|
355
|
-
let replacementScalarCount = Int32(replacementAdjusted.last ?? 0)
|
|
356
|
-
let scalarDelta = replacementScalarCount - deletedScalarCount
|
|
357
|
-
let replacement = replacementAdjusted.map { value in
|
|
358
|
-
UInt32(max(0, Int32(value) + startScalar))
|
|
359
|
-
}
|
|
360
|
-
let prefix = Array(oldAdjusted[..<startOffset])
|
|
361
|
-
let suffix = oldAdjusted[(endOffset + 1)...].map { value in
|
|
362
|
-
UInt32(max(0, Int32(value) + scalarDelta))
|
|
363
|
-
}
|
|
364
|
-
return prefix + replacement + suffix
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
private static func stringConversionTable(for text: String) -> StringConversionTable {
|
|
368
|
-
stringTableLock.lock()
|
|
369
|
-
if lastStringTableText == text, let lastStringTable {
|
|
370
|
-
stringTableLock.unlock()
|
|
371
|
-
return lastStringTable
|
|
372
|
-
}
|
|
373
|
-
stringTableLock.unlock()
|
|
374
|
-
|
|
375
|
-
let utf16Count = text.utf16.count
|
|
376
|
-
let scalarCount = text.unicodeScalars.count
|
|
377
|
-
var utf16ToScalar = Array(repeating: UInt32(0), count: utf16Count + 1)
|
|
378
|
-
var scalarToUtf16 = Array(repeating: 0, count: scalarCount + 1)
|
|
379
|
-
var utf16Pos = 0
|
|
380
|
-
var scalarPos = 0
|
|
381
|
-
|
|
382
|
-
for scalar in text.unicodeScalars {
|
|
383
|
-
let nextUtf16Pos = utf16Pos + scalar.utf16.count
|
|
384
|
-
scalarPos += 1
|
|
385
|
-
if nextUtf16Pos > utf16Pos {
|
|
386
|
-
for offset in (utf16Pos + 1)...nextUtf16Pos {
|
|
387
|
-
utf16ToScalar[offset] = UInt32(scalarPos)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
scalarToUtf16[scalarPos] = nextUtf16Pos
|
|
391
|
-
utf16Pos = nextUtf16Pos
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let conversionTable = StringConversionTable(
|
|
395
|
-
utf16ToScalar: utf16ToScalar,
|
|
396
|
-
scalarToUtf16: scalarToUtf16
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
stringTableLock.lock()
|
|
400
|
-
lastStringTableText = text
|
|
401
|
-
lastStringTable = conversionTable
|
|
402
|
-
stringTableLock.unlock()
|
|
403
|
-
|
|
404
|
-
return conversionTable
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private static func adjustedConversionTable(for attributedString: NSAttributedString) -> [UInt32] {
|
|
408
|
-
let baseTable = stringConversionTable(for: attributedString.string)
|
|
409
|
-
let adjustments = positionAdjustments(in: attributedString)
|
|
410
|
-
return adjustedUtf16ToScalar(
|
|
411
|
-
baseUtf16ToScalar: baseTable.utf16ToScalar,
|
|
412
|
-
placeholders: adjustments.placeholders,
|
|
413
|
-
listMarkers: adjustments.listMarkers
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
private static func textViewConversionTable(for textView: UITextView) -> TextViewConversionTable {
|
|
418
|
-
if let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable {
|
|
419
|
-
return cached
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
let text = textView.text ?? ""
|
|
423
|
-
let baseTable = stringConversionTable(for: text)
|
|
424
|
-
let adjustments = positionAdjustments(in: textView.textStorage)
|
|
425
|
-
let adjustedUtf16ToScalar = adjustedUtf16ToScalar(
|
|
426
|
-
baseUtf16ToScalar: baseTable.utf16ToScalar,
|
|
427
|
-
placeholders: adjustments.placeholders,
|
|
428
|
-
listMarkers: adjustments.listMarkers
|
|
429
|
-
)
|
|
430
|
-
let conversionTable = TextViewConversionTable(adjustedUtf16ToScalar: adjustedUtf16ToScalar)
|
|
431
|
-
objc_setAssociatedObject(
|
|
432
|
-
textView,
|
|
433
|
-
&textViewConversionTableKey,
|
|
434
|
-
conversionTable,
|
|
435
|
-
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
436
|
-
)
|
|
437
|
-
return conversionTable
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private static func adjustedUtf16ToScalar(
|
|
441
|
-
baseUtf16ToScalar: [UInt32],
|
|
442
|
-
placeholders: [Int],
|
|
443
|
-
listMarkers: [VirtualListMarker] = [],
|
|
444
|
-
) -> [UInt32] {
|
|
445
|
-
let utf16Count = max(0, baseUtf16ToScalar.count - 1)
|
|
446
|
-
var deltas = Array(repeating: Int32(0), count: utf16Count + 2)
|
|
447
|
-
|
|
448
|
-
for placeholderOffset in placeholders {
|
|
449
|
-
let startOffset = min(max(placeholderOffset + 1, 0), utf16Count + 1)
|
|
450
|
-
if startOffset <= utf16Count {
|
|
451
|
-
deltas[startOffset] -= 1
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
for marker in listMarkers {
|
|
456
|
-
let startOffset = min(max(marker.paragraphStartUtf16, 0), utf16Count)
|
|
457
|
-
deltas[startOffset] += Int32(marker.scalarLength)
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
var adjustedUtf16ToScalar = Array(repeating: UInt32(0), count: utf16Count + 1)
|
|
461
|
-
var runningDelta: Int32 = 0
|
|
462
|
-
for offset in 0...utf16Count {
|
|
463
|
-
runningDelta += deltas[offset]
|
|
464
|
-
let adjustedValue = Int32(baseUtf16ToScalar[offset]) + runningDelta
|
|
465
|
-
adjustedUtf16ToScalar[offset] = UInt32(max(0, adjustedValue))
|
|
466
|
-
}
|
|
467
|
-
return adjustedUtf16ToScalar
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
private static func adjustedUtf16ToScalar(
|
|
471
|
-
baseUtf16ToScalar: [UInt32],
|
|
472
|
-
listMarkers: [VirtualListMarker]
|
|
473
|
-
) -> [UInt32] {
|
|
474
|
-
adjustedUtf16ToScalar(baseUtf16ToScalar: baseUtf16ToScalar, placeholders: [], listMarkers: listMarkers)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private static func virtualListMarkers(in attributedString: NSAttributedString) -> [VirtualListMarker] {
|
|
478
|
-
positionAdjustments(in: attributedString).listMarkers
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
private static func positionAdjustments(in attributedString: NSAttributedString) -> PositionAdjustments {
|
|
482
|
-
guard attributedString.length > 0 else {
|
|
483
|
-
return PositionAdjustments(placeholders: [], listMarkers: [])
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
let nsString = attributedString.string as NSString
|
|
487
|
-
var placeholders: [Int] = []
|
|
488
|
-
var markers: [VirtualListMarker] = []
|
|
489
|
-
var seenStarts = Set<Int>()
|
|
490
|
-
let fullRange = NSRange(location: 0, length: attributedString.length)
|
|
491
|
-
|
|
492
|
-
attributedString.enumerateAttributes(
|
|
493
|
-
in: fullRange,
|
|
494
|
-
options: [.longestEffectiveRangeNotRequired]
|
|
495
|
-
) { attrs, range, _ in
|
|
496
|
-
guard range.length > 0 else { return }
|
|
497
|
-
|
|
498
|
-
if attrs[InputCoordinatorAttributes.syntheticPlaceholder] as? Bool == true {
|
|
499
|
-
placeholders.append(range.location)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
guard let listContext = attrs[InputCoordinatorAttributes.listMarkerContext] as? [String: Any] else {
|
|
503
|
-
return
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let paragraphStart = nsString.paragraphRange(
|
|
507
|
-
for: NSRange(location: range.location, length: 0)
|
|
508
|
-
).location
|
|
509
|
-
guard !isParagraphStartCreatedByHardBreak(
|
|
510
|
-
paragraphStart,
|
|
511
|
-
in: attributedString
|
|
512
|
-
) else {
|
|
513
|
-
return
|
|
514
|
-
}
|
|
515
|
-
guard seenStarts.insert(paragraphStart).inserted else { return }
|
|
516
|
-
|
|
517
|
-
let markerLength = UInt32(
|
|
518
|
-
NativeListMarker.text(for: listContext).unicodeScalars.count
|
|
519
|
-
)
|
|
520
|
-
markers.append(
|
|
521
|
-
VirtualListMarker(
|
|
522
|
-
paragraphStartUtf16: paragraphStart,
|
|
523
|
-
scalarLength: markerLength
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
return PositionAdjustments(
|
|
529
|
-
placeholders: placeholders,
|
|
530
|
-
listMarkers: markers.sorted { $0.paragraphStartUtf16 < $1.paragraphStartUtf16 }
|
|
531
|
-
)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private static func virtualListMarkers(in textStorage: NSTextStorage) -> [VirtualListMarker] {
|
|
535
|
-
virtualListMarkers(in: textStorage as NSAttributedString)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
private static func syntheticPlaceholderOffsets(in attributedString: NSAttributedString) -> [Int] {
|
|
539
|
-
positionAdjustments(in: attributedString).placeholders
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private static func syntheticPlaceholderOffsets(in textStorage: NSTextStorage) -> [Int] {
|
|
543
|
-
syntheticPlaceholderOffsets(in: textStorage as NSAttributedString)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private static func isParagraphStartCreatedByHardBreak(
|
|
547
|
-
_ paragraphStart: Int,
|
|
548
|
-
in attributedString: NSAttributedString
|
|
549
|
-
) -> Bool {
|
|
550
|
-
guard paragraphStart > 0, paragraphStart <= attributedString.length else { return false }
|
|
551
|
-
let previousVoidType = attributedString.attribute(
|
|
552
|
-
InputCoordinatorAttributes.voidNodeType,
|
|
553
|
-
at: paragraphStart - 1,
|
|
554
|
-
effectiveRange: nil
|
|
555
|
-
) as? String
|
|
556
|
-
return previousVoidType == "hardBreak"
|
|
14
|
+
let utf16 = text.utf16
|
|
15
|
+
let clamped = min(max(utf16Offset, 0), utf16.count)
|
|
16
|
+
let index = utf16.index(utf16.startIndex, offsetBy: clamped)
|
|
17
|
+
guard let scalarIndex = String.Index(index, within: text) else {
|
|
18
|
+
let safe = max(0, clamped - 1)
|
|
19
|
+
let safeIndex = utf16.index(utf16.startIndex, offsetBy: safe)
|
|
20
|
+
return UInt32(text[..<(String.Index(safeIndex, within: text) ?? text.startIndex)].unicodeScalars.count)
|
|
21
|
+
}
|
|
22
|
+
return UInt32(text[..<scalarIndex].unicodeScalars.count)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static func scalarToUtf16Offset(_ scalarOffset: UInt32, in text: String) -> Int {
|
|
26
|
+
let scalars = text.unicodeScalars
|
|
27
|
+
let clamped = min(Int(scalarOffset), scalars.count)
|
|
28
|
+
let scalarIndex = scalars.index(scalars.startIndex, offsetBy: clamped)
|
|
29
|
+
let stringIndex = scalarIndex.samePosition(in: text) ?? text.endIndex
|
|
30
|
+
return text[..<stringIndex].utf16.count
|
|
557
31
|
}
|
|
558
32
|
}
|