@apollohg/react-native-prose-editor 0.4.0 → 0.4.1
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 +18 -0
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/NativeEditorBridge.d.ts +36 -1
- package/dist/NativeEditorBridge.js +173 -94
- package/dist/NativeRichTextEditor.d.ts +2 -0
- package/dist/NativeRichTextEditor.js +160 -53
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- 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 +3 -3
- package/ios/Generated_editor_core.swift +41 -0
- package/ios/NativeEditorExpoView.swift +43 -11
- package/ios/NativeEditorModule.swift +6 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +1983 -187
- package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
- package/package.json +11 -2
- 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 +63 -0
package/ios/PositionBridge.swift
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import UIKit
|
|
2
|
+
import ObjectiveC
|
|
2
3
|
|
|
3
4
|
// MARK: - PositionBridge
|
|
4
5
|
|
|
@@ -16,11 +17,34 @@ import UIKit
|
|
|
16
17
|
/// This bridge converts between those scalar offsets and UITextView UTF-16 offsets.
|
|
17
18
|
final class PositionBridge {
|
|
18
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
|
+
|
|
19
33
|
struct VirtualListMarker {
|
|
20
34
|
let paragraphStartUtf16: Int
|
|
21
35
|
let scalarLength: UInt32
|
|
22
36
|
}
|
|
23
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
|
+
|
|
24
48
|
// MARK: - UTF-16 <-> Scalar Conversion
|
|
25
49
|
|
|
26
50
|
/// Convert a UITextView cursor position (UTF-16 offset) to a Rust scalar offset.
|
|
@@ -58,36 +82,27 @@ final class PositionBridge {
|
|
|
58
82
|
|
|
59
83
|
static func utf16OffsetToScalar(_ utf16Offset: Int, in textView: UITextView) -> UInt32 {
|
|
60
84
|
let text = textView.text ?? ""
|
|
61
|
-
let
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
for placeholderOffset in syntheticPlaceholderOffsets(in: textView) where clampedOffset > placeholderOffset {
|
|
66
|
-
if scalarOffset > 0 {
|
|
67
|
-
scalarOffset -= 1
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for marker in virtualListMarkers(in: textView) where clampedOffset >= marker.paragraphStartUtf16 {
|
|
72
|
-
scalarOffset += marker.scalarLength
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return scalarOffset
|
|
85
|
+
let clampedOffset = min(max(utf16Offset, 0), (text as NSString).length)
|
|
86
|
+
let conversionTable = textViewConversionTable(for: textView)
|
|
87
|
+
return conversionTable.adjustedUtf16ToScalar[clampedOffset]
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
static func scalarToUtf16Offset(_ scalar: UInt32, in textView: UITextView) -> Int {
|
|
79
|
-
let
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
if scalar == 0 || maxUtf16 == 0 {
|
|
91
|
+
let conversionTable = textViewConversionTable(for: textView)
|
|
92
|
+
let utf16ToScalar = conversionTable.adjustedUtf16ToScalar
|
|
93
|
+
guard scalar > 0, !utf16ToScalar.isEmpty else {
|
|
83
94
|
return 0
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
if let last = utf16ToScalar.last, scalar > last {
|
|
98
|
+
return utf16ToScalar.count - 1
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
var low = 0
|
|
87
|
-
var high =
|
|
102
|
+
var high = utf16ToScalar.count - 1
|
|
88
103
|
while low < high {
|
|
89
104
|
let mid = (low + high) / 2
|
|
90
|
-
if
|
|
105
|
+
if utf16ToScalar[mid] < scalar {
|
|
91
106
|
low = mid + 1
|
|
92
107
|
} else {
|
|
93
108
|
high = mid
|
|
@@ -107,21 +122,9 @@ final class PositionBridge {
|
|
|
107
122
|
/// - text: The string to walk.
|
|
108
123
|
/// - Returns: The number of Unicode scalars from the start to the given UTF-16 offset.
|
|
109
124
|
static func utf16OffsetToScalar(_ utf16Offset: Int, in text: String) -> UInt32 {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let endIndex = min(utf16Offset, utf16View.count)
|
|
114
|
-
var scalarCount: UInt32 = 0
|
|
115
|
-
var utf16Pos = 0
|
|
116
|
-
|
|
117
|
-
for scalar in text.unicodeScalars {
|
|
118
|
-
if utf16Pos >= endIndex { break }
|
|
119
|
-
let scalarUtf16Len = scalar.utf16.count
|
|
120
|
-
utf16Pos += scalarUtf16Len
|
|
121
|
-
scalarCount += 1
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return scalarCount
|
|
125
|
+
let conversionTable = stringConversionTable(for: text)
|
|
126
|
+
let clampedOffset = min(max(utf16Offset, 0), conversionTable.utf16ToScalar.count - 1)
|
|
127
|
+
return conversionTable.utf16ToScalar[clampedOffset]
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/// Convert a Unicode scalar offset to a UTF-16 offset within a string.
|
|
@@ -134,18 +137,10 @@ final class PositionBridge {
|
|
|
134
137
|
/// - text: The string to walk.
|
|
135
138
|
/// - Returns: The number of UTF-16 code units from the start to the given scalar offset.
|
|
136
139
|
static func scalarToUtf16Offset(_ scalar: UInt32, in text: String) -> Int {
|
|
140
|
+
let conversionTable = stringConversionTable(for: text)
|
|
137
141
|
guard scalar > 0 else { return 0 }
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
var scalarsSeen: UInt32 = 0
|
|
141
|
-
|
|
142
|
-
for s in text.unicodeScalars {
|
|
143
|
-
if scalarsSeen >= scalar { break }
|
|
144
|
-
utf16Len += s.utf16.count
|
|
145
|
-
scalarsSeen += 1
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return utf16Len
|
|
142
|
+
let scalarIndex = min(Int(scalar), conversionTable.scalarToUtf16.count - 1)
|
|
143
|
+
return conversionTable.scalarToUtf16[scalarIndex]
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
// MARK: - Grapheme Boundary Snapping
|
|
@@ -238,31 +233,259 @@ final class PositionBridge {
|
|
|
238
233
|
atUtf16Offset utf16Offset: Int,
|
|
239
234
|
in textView: UITextView
|
|
240
235
|
) -> VirtualListMarker? {
|
|
241
|
-
virtualListMarkers(in: textView).first { $0.paragraphStartUtf16 == utf16Offset }
|
|
236
|
+
virtualListMarkers(in: textView.textStorage).first { $0.paragraphStartUtf16 == utf16Offset }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static func invalidateCache(for textView: UITextView) {
|
|
240
|
+
objc_setAssociatedObject(
|
|
241
|
+
textView,
|
|
242
|
+
&textViewConversionTableKey,
|
|
243
|
+
nil,
|
|
244
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
245
|
+
)
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
@discardableResult
|
|
249
|
+
static func applyAttributedPatchIfPossible(
|
|
250
|
+
for textView: UITextView,
|
|
251
|
+
replaceRange: NSRange,
|
|
252
|
+
replacement: NSAttributedString
|
|
253
|
+
) -> Bool {
|
|
254
|
+
guard let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable else {
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let oldAdjusted = cached.adjustedUtf16ToScalar
|
|
259
|
+
let oldUtf16Count = max(0, oldAdjusted.count - 1)
|
|
260
|
+
guard replaceRange.location >= 0,
|
|
261
|
+
replaceRange.length >= 0,
|
|
262
|
+
replaceRange.location + replaceRange.length <= oldUtf16Count
|
|
263
|
+
else {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
247
266
|
|
|
248
|
-
let
|
|
267
|
+
let startOffset = replaceRange.location
|
|
268
|
+
let endOffset = replaceRange.location + replaceRange.length
|
|
269
|
+
let replacementAdjusted = adjustedConversionTable(for: replacement)
|
|
270
|
+
let patched = patchedAdjustedConversionTable(
|
|
271
|
+
oldAdjusted: oldAdjusted,
|
|
272
|
+
startOffset: startOffset,
|
|
273
|
+
endOffset: endOffset,
|
|
274
|
+
replacementAdjusted: replacementAdjusted
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
objc_setAssociatedObject(
|
|
278
|
+
textView,
|
|
279
|
+
&textViewConversionTableKey,
|
|
280
|
+
TextViewConversionTable(adjustedUtf16ToScalar: patched),
|
|
281
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
282
|
+
)
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@discardableResult
|
|
287
|
+
static func applyPlainTextPatchIfPossible(
|
|
288
|
+
for textView: UITextView,
|
|
289
|
+
replaceRange: NSRange,
|
|
290
|
+
replacementText: String
|
|
291
|
+
) -> Bool {
|
|
292
|
+
guard let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable else {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let oldAdjusted = cached.adjustedUtf16ToScalar
|
|
297
|
+
let oldUtf16Count = max(0, oldAdjusted.count - 1)
|
|
298
|
+
guard replaceRange.location >= 0,
|
|
299
|
+
replaceRange.length >= 0,
|
|
300
|
+
replaceRange.location + replaceRange.length <= oldUtf16Count
|
|
301
|
+
else {
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let startOffset = replaceRange.location
|
|
306
|
+
let endOffset = replaceRange.location + replaceRange.length
|
|
307
|
+
let replacementBase = stringConversionTable(for: replacementText).utf16ToScalar
|
|
308
|
+
let patched = patchedAdjustedConversionTable(
|
|
309
|
+
oldAdjusted: oldAdjusted,
|
|
310
|
+
startOffset: startOffset,
|
|
311
|
+
endOffset: endOffset,
|
|
312
|
+
replacementAdjusted: replacementBase
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
objc_setAssociatedObject(
|
|
316
|
+
textView,
|
|
317
|
+
&textViewConversionTableKey,
|
|
318
|
+
TextViewConversionTable(adjustedUtf16ToScalar: patched),
|
|
319
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
320
|
+
)
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private static func patchedAdjustedConversionTable(
|
|
325
|
+
oldAdjusted: [UInt32],
|
|
326
|
+
startOffset: Int,
|
|
327
|
+
endOffset: Int,
|
|
328
|
+
replacementAdjusted: [UInt32]
|
|
329
|
+
) -> [UInt32] {
|
|
330
|
+
let startScalar = Int32(oldAdjusted[startOffset])
|
|
331
|
+
let deletedScalarCount = Int32(oldAdjusted[endOffset]) - startScalar
|
|
332
|
+
let replacementScalarCount = Int32(replacementAdjusted.last ?? 0)
|
|
333
|
+
let scalarDelta = replacementScalarCount - deletedScalarCount
|
|
334
|
+
let replacement = replacementAdjusted.map { value in
|
|
335
|
+
UInt32(max(0, Int32(value) + startScalar))
|
|
336
|
+
}
|
|
337
|
+
let prefix = Array(oldAdjusted[..<startOffset])
|
|
338
|
+
let suffix = oldAdjusted[(endOffset + 1)...].map { value in
|
|
339
|
+
UInt32(max(0, Int32(value) + scalarDelta))
|
|
340
|
+
}
|
|
341
|
+
return prefix + replacement + suffix
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private static func stringConversionTable(for text: String) -> StringConversionTable {
|
|
345
|
+
stringTableLock.lock()
|
|
346
|
+
if lastStringTableText == text, let lastStringTable {
|
|
347
|
+
stringTableLock.unlock()
|
|
348
|
+
return lastStringTable
|
|
349
|
+
}
|
|
350
|
+
stringTableLock.unlock()
|
|
351
|
+
|
|
352
|
+
let utf16Count = text.utf16.count
|
|
353
|
+
let scalarCount = text.unicodeScalars.count
|
|
354
|
+
var utf16ToScalar = Array(repeating: UInt32(0), count: utf16Count + 1)
|
|
355
|
+
var scalarToUtf16 = Array(repeating: 0, count: scalarCount + 1)
|
|
356
|
+
var utf16Pos = 0
|
|
357
|
+
var scalarPos = 0
|
|
358
|
+
|
|
359
|
+
for scalar in text.unicodeScalars {
|
|
360
|
+
let nextUtf16Pos = utf16Pos + scalar.utf16.count
|
|
361
|
+
scalarPos += 1
|
|
362
|
+
if nextUtf16Pos > utf16Pos {
|
|
363
|
+
for offset in (utf16Pos + 1)...nextUtf16Pos {
|
|
364
|
+
utf16ToScalar[offset] = UInt32(scalarPos)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
scalarToUtf16[scalarPos] = nextUtf16Pos
|
|
368
|
+
utf16Pos = nextUtf16Pos
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let conversionTable = StringConversionTable(
|
|
372
|
+
utf16ToScalar: utf16ToScalar,
|
|
373
|
+
scalarToUtf16: scalarToUtf16
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
stringTableLock.lock()
|
|
377
|
+
lastStringTableText = text
|
|
378
|
+
lastStringTable = conversionTable
|
|
379
|
+
stringTableLock.unlock()
|
|
380
|
+
|
|
381
|
+
return conversionTable
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private static func adjustedConversionTable(for attributedString: NSAttributedString) -> [UInt32] {
|
|
385
|
+
let baseTable = stringConversionTable(for: attributedString.string)
|
|
386
|
+
let adjustments = positionAdjustments(in: attributedString)
|
|
387
|
+
return adjustedUtf16ToScalar(
|
|
388
|
+
baseUtf16ToScalar: baseTable.utf16ToScalar,
|
|
389
|
+
placeholders: adjustments.placeholders,
|
|
390
|
+
listMarkers: adjustments.listMarkers
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private static func textViewConversionTable(for textView: UITextView) -> TextViewConversionTable {
|
|
395
|
+
if let cached = objc_getAssociatedObject(textView, &textViewConversionTableKey) as? TextViewConversionTable {
|
|
396
|
+
return cached
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let text = textView.text ?? ""
|
|
400
|
+
let baseTable = stringConversionTable(for: text)
|
|
401
|
+
let adjustments = positionAdjustments(in: textView.textStorage)
|
|
402
|
+
let adjustedUtf16ToScalar = adjustedUtf16ToScalar(
|
|
403
|
+
baseUtf16ToScalar: baseTable.utf16ToScalar,
|
|
404
|
+
placeholders: adjustments.placeholders,
|
|
405
|
+
listMarkers: adjustments.listMarkers
|
|
406
|
+
)
|
|
407
|
+
let conversionTable = TextViewConversionTable(adjustedUtf16ToScalar: adjustedUtf16ToScalar)
|
|
408
|
+
objc_setAssociatedObject(
|
|
409
|
+
textView,
|
|
410
|
+
&textViewConversionTableKey,
|
|
411
|
+
conversionTable,
|
|
412
|
+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
413
|
+
)
|
|
414
|
+
return conversionTable
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private static func adjustedUtf16ToScalar(
|
|
418
|
+
baseUtf16ToScalar: [UInt32],
|
|
419
|
+
placeholders: [Int],
|
|
420
|
+
listMarkers: [VirtualListMarker] = [],
|
|
421
|
+
) -> [UInt32] {
|
|
422
|
+
let utf16Count = max(0, baseUtf16ToScalar.count - 1)
|
|
423
|
+
var deltas = Array(repeating: Int32(0), count: utf16Count + 2)
|
|
424
|
+
|
|
425
|
+
for placeholderOffset in placeholders {
|
|
426
|
+
let startOffset = min(max(placeholderOffset + 1, 0), utf16Count + 1)
|
|
427
|
+
if startOffset <= utf16Count {
|
|
428
|
+
deltas[startOffset] -= 1
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for marker in listMarkers {
|
|
433
|
+
let startOffset = min(max(marker.paragraphStartUtf16, 0), utf16Count)
|
|
434
|
+
deltas[startOffset] += Int32(marker.scalarLength)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var adjustedUtf16ToScalar = Array(repeating: UInt32(0), count: utf16Count + 1)
|
|
438
|
+
var runningDelta: Int32 = 0
|
|
439
|
+
for offset in 0...utf16Count {
|
|
440
|
+
runningDelta += deltas[offset]
|
|
441
|
+
let adjustedValue = Int32(baseUtf16ToScalar[offset]) + runningDelta
|
|
442
|
+
adjustedUtf16ToScalar[offset] = UInt32(max(0, adjustedValue))
|
|
443
|
+
}
|
|
444
|
+
return adjustedUtf16ToScalar
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private static func adjustedUtf16ToScalar(
|
|
448
|
+
baseUtf16ToScalar: [UInt32],
|
|
449
|
+
listMarkers: [VirtualListMarker]
|
|
450
|
+
) -> [UInt32] {
|
|
451
|
+
adjustedUtf16ToScalar(baseUtf16ToScalar: baseUtf16ToScalar, placeholders: [], listMarkers: listMarkers)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private static func virtualListMarkers(in attributedString: NSAttributedString) -> [VirtualListMarker] {
|
|
455
|
+
positionAdjustments(in: attributedString).listMarkers
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private static func positionAdjustments(in attributedString: NSAttributedString) -> PositionAdjustments {
|
|
459
|
+
guard attributedString.length > 0 else {
|
|
460
|
+
return PositionAdjustments(placeholders: [], listMarkers: [])
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let nsString = attributedString.string as NSString
|
|
464
|
+
var placeholders: [Int] = []
|
|
249
465
|
var markers: [VirtualListMarker] = []
|
|
250
466
|
var seenStarts = Set<Int>()
|
|
251
|
-
let fullRange = NSRange(location: 0, length:
|
|
467
|
+
let fullRange = NSRange(location: 0, length: attributedString.length)
|
|
252
468
|
|
|
253
|
-
|
|
254
|
-
RenderBridgeAttributes.listMarkerContext,
|
|
469
|
+
attributedString.enumerateAttributes(
|
|
255
470
|
in: fullRange,
|
|
256
|
-
options: []
|
|
257
|
-
) {
|
|
258
|
-
guard range.length > 0
|
|
471
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
472
|
+
) { attrs, range, _ in
|
|
473
|
+
guard range.length > 0 else { return }
|
|
474
|
+
|
|
475
|
+
if attrs[RenderBridgeAttributes.syntheticPlaceholder] as? Bool == true {
|
|
476
|
+
placeholders.append(range.location)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
guard let listContext = attrs[RenderBridgeAttributes.listMarkerContext] as? [String: Any] else {
|
|
480
|
+
return
|
|
481
|
+
}
|
|
259
482
|
|
|
260
483
|
let paragraphStart = nsString.paragraphRange(
|
|
261
484
|
for: NSRange(location: range.location, length: 0)
|
|
262
485
|
).location
|
|
263
|
-
guard !
|
|
486
|
+
guard !isParagraphStartCreatedByHardBreak(
|
|
264
487
|
paragraphStart,
|
|
265
|
-
in:
|
|
488
|
+
in: attributedString
|
|
266
489
|
) else {
|
|
267
490
|
return
|
|
268
491
|
}
|
|
@@ -279,22 +502,34 @@ final class PositionBridge {
|
|
|
279
502
|
)
|
|
280
503
|
}
|
|
281
504
|
|
|
282
|
-
return
|
|
505
|
+
return PositionAdjustments(
|
|
506
|
+
placeholders: placeholders,
|
|
507
|
+
listMarkers: markers.sorted { $0.paragraphStartUtf16 < $1.paragraphStartUtf16 }
|
|
508
|
+
)
|
|
283
509
|
}
|
|
284
510
|
|
|
285
|
-
private static func
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
511
|
+
private static func virtualListMarkers(in textStorage: NSTextStorage) -> [VirtualListMarker] {
|
|
512
|
+
virtualListMarkers(in: textStorage as NSAttributedString)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private static func syntheticPlaceholderOffsets(in attributedString: NSAttributedString) -> [Int] {
|
|
516
|
+
positionAdjustments(in: attributedString).placeholders
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private static func syntheticPlaceholderOffsets(in textStorage: NSTextStorage) -> [Int] {
|
|
520
|
+
syntheticPlaceholderOffsets(in: textStorage as NSAttributedString)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private static func isParagraphStartCreatedByHardBreak(
|
|
524
|
+
_ paragraphStart: Int,
|
|
525
|
+
in attributedString: NSAttributedString
|
|
526
|
+
) -> Bool {
|
|
527
|
+
guard paragraphStart > 0, paragraphStart <= attributedString.length else { return false }
|
|
528
|
+
let previousVoidType = attributedString.attribute(
|
|
529
|
+
RenderBridgeAttributes.voidNodeType,
|
|
530
|
+
at: paragraphStart - 1,
|
|
531
|
+
effectiveRange: nil
|
|
532
|
+
) as? String
|
|
533
|
+
return previousVoidType == "hardBreak"
|
|
299
534
|
}
|
|
300
535
|
}
|