@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/RenderBridge.swift
CHANGED
|
@@ -8,7 +8,53 @@ extension Notification.Name {
|
|
|
8
8
|
|
|
9
9
|
private enum RenderImageCache {
|
|
10
10
|
static let cache = NSCache<NSString, UIImage>()
|
|
11
|
-
static let
|
|
11
|
+
static let stateQueue = DispatchQueue(label: "com.apollohg.editor.image-loader-state")
|
|
12
|
+
static let queue: OperationQueue = {
|
|
13
|
+
let queue = OperationQueue()
|
|
14
|
+
queue.name = "com.apollohg.editor.image-loader"
|
|
15
|
+
queue.qualityOfService = .userInitiated
|
|
16
|
+
queue.maxConcurrentOperationCount = 2
|
|
17
|
+
return queue
|
|
18
|
+
}()
|
|
19
|
+
private static var inFlight: [String: [(UIImage?) -> Void]] = [:]
|
|
20
|
+
|
|
21
|
+
static func load(
|
|
22
|
+
source: String,
|
|
23
|
+
url: URL,
|
|
24
|
+
completion: @escaping (UIImage?) -> Void
|
|
25
|
+
) {
|
|
26
|
+
if let cached = cache.object(forKey: source as NSString) {
|
|
27
|
+
completion(cached)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var shouldStartLoad = false
|
|
32
|
+
stateQueue.sync {
|
|
33
|
+
if inFlight[source] != nil {
|
|
34
|
+
inFlight[source]?.append(completion)
|
|
35
|
+
} else {
|
|
36
|
+
inFlight[source] = [completion]
|
|
37
|
+
shouldStartLoad = true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
guard shouldStartLoad else { return }
|
|
41
|
+
|
|
42
|
+
queue.addOperation {
|
|
43
|
+
let data = try? Data(contentsOf: url)
|
|
44
|
+
let image = data.flatMap(UIImage.init(data:))
|
|
45
|
+
if let image {
|
|
46
|
+
cache.setObject(image, forKey: source as NSString)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let callbacks: [(UIImage?) -> Void] = stateQueue.sync {
|
|
50
|
+
let callbacks = inFlight.removeValue(forKey: source) ?? []
|
|
51
|
+
return callbacks
|
|
52
|
+
}
|
|
53
|
+
DispatchQueue.main.async {
|
|
54
|
+
callbacks.forEach { $0(image) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
12
58
|
}
|
|
13
59
|
|
|
14
60
|
// MARK: - Constants
|
|
@@ -57,6 +103,9 @@ enum RenderBridgeAttributes {
|
|
|
57
103
|
|
|
58
104
|
/// Marks synthetic zero-width placeholders used only for UIKit layout.
|
|
59
105
|
static let syntheticPlaceholder = NSAttributedString.Key("com.apollohg.editor.syntheticPlaceholder")
|
|
106
|
+
|
|
107
|
+
/// Stores the owning top-level document child index for partial native patching.
|
|
108
|
+
static let topLevelChildIndex = NSAttributedString.Key("com.apollohg.editor.topLevelChildIndex")
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
/// Layout constants for paragraph styles.
|
|
@@ -169,6 +218,7 @@ final class RenderBridge {
|
|
|
169
218
|
|
|
170
219
|
for element in elements {
|
|
171
220
|
guard let type = element["type"] as? String else { continue }
|
|
221
|
+
let topLevelChildIndex = jsonInt(element["topLevelChildIndex"])
|
|
172
222
|
|
|
173
223
|
switch type {
|
|
174
224
|
case "textRun":
|
|
@@ -194,7 +244,14 @@ final class RenderBridge {
|
|
|
194
244
|
blockStack: blockStack,
|
|
195
245
|
theme: theme
|
|
196
246
|
)
|
|
197
|
-
|
|
247
|
+
let attributedText = NSAttributedString(string: text, attributes: attrs)
|
|
248
|
+
result.append(
|
|
249
|
+
attributedStringApplyingLeadingTopLevelChildIndexIfNeeded(
|
|
250
|
+
attributedText,
|
|
251
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
252
|
+
resultIsEmpty: result.length == 0
|
|
253
|
+
)
|
|
254
|
+
)
|
|
198
255
|
|
|
199
256
|
case "voidInline":
|
|
200
257
|
let nodeType = element["nodeType"] as? String ?? ""
|
|
@@ -210,9 +267,16 @@ final class RenderBridge {
|
|
|
210
267
|
baseFont: baseFont,
|
|
211
268
|
textColor: textColor,
|
|
212
269
|
blockStack: blockStack,
|
|
270
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
213
271
|
theme: theme
|
|
214
272
|
)
|
|
215
|
-
result.append(
|
|
273
|
+
result.append(
|
|
274
|
+
attributedStringApplyingLeadingTopLevelChildIndexIfNeeded(
|
|
275
|
+
attrStr,
|
|
276
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
277
|
+
resultIsEmpty: result.length == 0
|
|
278
|
+
)
|
|
279
|
+
)
|
|
216
280
|
|
|
217
281
|
case "voidBlock":
|
|
218
282
|
let nodeType = element["nodeType"] as? String ?? ""
|
|
@@ -221,6 +285,12 @@ final class RenderBridge {
|
|
|
221
285
|
|
|
222
286
|
// Add inter-block newline if not the first block.
|
|
223
287
|
if !isFirstBlock {
|
|
288
|
+
collapseTrailingSpacingBeforeHorizontalRuleIfNeeded(
|
|
289
|
+
in: result,
|
|
290
|
+
pendingParagraphSpacing: &pendingTrailingParagraphSpacing,
|
|
291
|
+
nodeType: nodeType,
|
|
292
|
+
theme: theme
|
|
293
|
+
)
|
|
224
294
|
applyPendingTrailingParagraphSpacing(
|
|
225
295
|
in: result,
|
|
226
296
|
pendingParagraphSpacing: &pendingTrailingParagraphSpacing
|
|
@@ -230,7 +300,8 @@ final class RenderBridge {
|
|
|
230
300
|
baseFont: baseFont,
|
|
231
301
|
textColor: textColor,
|
|
232
302
|
blockStack: [],
|
|
233
|
-
theme: theme
|
|
303
|
+
theme: theme,
|
|
304
|
+
topLevelChildIndex: topLevelChildIndex
|
|
234
305
|
)
|
|
235
306
|
)
|
|
236
307
|
}
|
|
@@ -242,6 +313,7 @@ final class RenderBridge {
|
|
|
242
313
|
elementAttrs: attrs,
|
|
243
314
|
baseFont: baseFont,
|
|
244
315
|
textColor: textColor,
|
|
316
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
245
317
|
theme: theme
|
|
246
318
|
)
|
|
247
319
|
result.append(attrStr)
|
|
@@ -257,9 +329,16 @@ final class RenderBridge {
|
|
|
257
329
|
baseFont: baseFont,
|
|
258
330
|
textColor: textColor,
|
|
259
331
|
blockStack: blockStack,
|
|
332
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
260
333
|
theme: theme
|
|
261
334
|
)
|
|
262
|
-
result.append(
|
|
335
|
+
result.append(
|
|
336
|
+
attributedStringApplyingLeadingTopLevelChildIndexIfNeeded(
|
|
337
|
+
attrStr,
|
|
338
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
339
|
+
resultIsEmpty: result.length == 0
|
|
340
|
+
)
|
|
341
|
+
)
|
|
263
342
|
|
|
264
343
|
case "opaqueBlockAtom":
|
|
265
344
|
let nodeType = element["nodeType"] as? String ?? ""
|
|
@@ -276,7 +355,8 @@ final class RenderBridge {
|
|
|
276
355
|
baseFont: baseFont,
|
|
277
356
|
textColor: textColor,
|
|
278
357
|
blockStack: [],
|
|
279
|
-
theme: theme
|
|
358
|
+
theme: theme,
|
|
359
|
+
topLevelChildIndex: topLevelChildIndex
|
|
280
360
|
)
|
|
281
361
|
)
|
|
282
362
|
}
|
|
@@ -288,6 +368,7 @@ final class RenderBridge {
|
|
|
288
368
|
docPos: docPos,
|
|
289
369
|
baseFont: baseFont,
|
|
290
370
|
textColor: textColor,
|
|
371
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
291
372
|
theme: theme
|
|
292
373
|
)
|
|
293
374
|
result.append(attrStr)
|
|
@@ -302,6 +383,7 @@ final class RenderBridge {
|
|
|
302
383
|
nodeType: nodeType,
|
|
303
384
|
depth: depth,
|
|
304
385
|
listContext: listContext,
|
|
386
|
+
topLevelChildIndex: topLevelChildIndex,
|
|
305
387
|
markerPending: isListItemContainer
|
|
306
388
|
)
|
|
307
389
|
let nestedListItemContainer =
|
|
@@ -323,12 +405,20 @@ final class RenderBridge {
|
|
|
323
405
|
} else {
|
|
324
406
|
newlineBlockStack = blockStack + [ctx]
|
|
325
407
|
}
|
|
408
|
+
let collapsedSeparatorSpacing = collapsedParagraphSpacingAfterHorizontalRule(
|
|
409
|
+
in: result,
|
|
410
|
+
separatorBlockStack: newlineBlockStack,
|
|
411
|
+
theme: theme,
|
|
412
|
+
baseFont: baseFont
|
|
413
|
+
)
|
|
326
414
|
result.append(
|
|
327
415
|
interBlockNewline(
|
|
328
416
|
baseFont: baseFont,
|
|
329
417
|
textColor: textColor,
|
|
330
418
|
blockStack: newlineBlockStack,
|
|
331
|
-
theme: theme
|
|
419
|
+
theme: theme,
|
|
420
|
+
paragraphSpacingOverride: collapsedSeparatorSpacing,
|
|
421
|
+
topLevelChildIndex: topLevelChildIndex
|
|
332
422
|
)
|
|
333
423
|
)
|
|
334
424
|
}
|
|
@@ -392,6 +482,54 @@ final class RenderBridge {
|
|
|
392
482
|
return result
|
|
393
483
|
}
|
|
394
484
|
|
|
485
|
+
static func renderBlocks(
|
|
486
|
+
fromArray blocks: [[[String: Any]]],
|
|
487
|
+
startIndex: Int = 0,
|
|
488
|
+
includeLeadingInterBlockSeparator: Bool = false,
|
|
489
|
+
baseFont: UIFont,
|
|
490
|
+
textColor: UIColor,
|
|
491
|
+
theme: EditorTheme? = nil
|
|
492
|
+
) -> NSAttributedString {
|
|
493
|
+
var flattened: [[String: Any]] = []
|
|
494
|
+
flattened.reserveCapacity(blocks.reduce(0) { $0 + $1.count })
|
|
495
|
+
|
|
496
|
+
for (offset, block) in blocks.enumerated() {
|
|
497
|
+
let topLevelChildIndex = startIndex + offset
|
|
498
|
+
for element in block {
|
|
499
|
+
var tagged = element
|
|
500
|
+
tagged["topLevelChildIndex"] = topLevelChildIndex
|
|
501
|
+
flattened.append(tagged)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let renderedBlocks = renderElements(
|
|
506
|
+
fromArray: flattened,
|
|
507
|
+
baseFont: baseFont,
|
|
508
|
+
textColor: textColor,
|
|
509
|
+
theme: theme
|
|
510
|
+
)
|
|
511
|
+
guard includeLeadingInterBlockSeparator, startIndex > 0, !blocks.isEmpty else {
|
|
512
|
+
return renderedBlocks
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let separatorReadyBlocks = removingLeadingTopLevelChildIndex(
|
|
516
|
+
from: renderedBlocks,
|
|
517
|
+
topLevelChildIndex: startIndex
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
let result = NSMutableAttributedString(
|
|
521
|
+
attributedString: interBlockNewline(
|
|
522
|
+
baseFont: baseFont,
|
|
523
|
+
textColor: textColor,
|
|
524
|
+
blockStack: [],
|
|
525
|
+
theme: theme,
|
|
526
|
+
topLevelChildIndex: startIndex
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
result.append(separatorReadyBlocks)
|
|
530
|
+
return result
|
|
531
|
+
}
|
|
532
|
+
|
|
395
533
|
// MARK: - Mark Handling
|
|
396
534
|
|
|
397
535
|
/// Build NSAttributedString attributes for a set of render marks.
|
|
@@ -486,6 +624,7 @@ final class RenderBridge {
|
|
|
486
624
|
baseFont: UIFont,
|
|
487
625
|
textColor: UIColor,
|
|
488
626
|
blockStack: [BlockContext],
|
|
627
|
+
topLevelChildIndex _: Int?,
|
|
489
628
|
theme: EditorTheme?
|
|
490
629
|
) -> NSAttributedString {
|
|
491
630
|
let blockFont = resolvedFont(for: blockStack, baseFont: baseFont, theme: theme)
|
|
@@ -530,18 +669,22 @@ final class RenderBridge {
|
|
|
530
669
|
elementAttrs: [String: Any],
|
|
531
670
|
baseFont: UIFont,
|
|
532
671
|
textColor: UIColor,
|
|
672
|
+
topLevelChildIndex: Int?,
|
|
533
673
|
theme: EditorTheme?
|
|
534
674
|
) -> NSAttributedString {
|
|
535
675
|
var attrs = defaultAttributes(baseFont: baseFont, textColor: textColor)
|
|
536
676
|
attrs[RenderBridgeAttributes.voidNodeType] = nodeType
|
|
537
677
|
attrs[RenderBridgeAttributes.docPos] = docPos
|
|
678
|
+
if let topLevelChildIndex {
|
|
679
|
+
attrs[RenderBridgeAttributes.topLevelChildIndex] = NSNumber(value: topLevelChildIndex)
|
|
680
|
+
}
|
|
538
681
|
|
|
539
682
|
switch nodeType {
|
|
540
683
|
case "horizontalRule":
|
|
541
684
|
let attachment = HorizontalRuleAttachment()
|
|
542
685
|
attachment.lineColor = theme?.horizontalRule?.color ?? textColor.withAlphaComponent(0.3)
|
|
543
686
|
attachment.lineHeight = theme?.horizontalRule?.thickness ?? LayoutConstants.horizontalRuleHeight
|
|
544
|
-
attachment.verticalPadding = theme
|
|
687
|
+
attachment.verticalPadding = resolvedHorizontalRuleVerticalMargin(theme: theme)
|
|
545
688
|
let attrStr = NSMutableAttributedString(
|
|
546
689
|
attachment: attachment
|
|
547
690
|
)
|
|
@@ -587,6 +730,7 @@ final class RenderBridge {
|
|
|
587
730
|
baseFont: UIFont,
|
|
588
731
|
textColor: UIColor,
|
|
589
732
|
blockStack: [BlockContext],
|
|
733
|
+
topLevelChildIndex _: Int?,
|
|
590
734
|
theme: EditorTheme?
|
|
591
735
|
) -> NSAttributedString {
|
|
592
736
|
let blockFont = resolvedFont(for: blockStack, baseFont: baseFont, theme: theme)
|
|
@@ -620,12 +764,16 @@ final class RenderBridge {
|
|
|
620
764
|
docPos: UInt32,
|
|
621
765
|
baseFont: UIFont,
|
|
622
766
|
textColor: UIColor,
|
|
767
|
+
topLevelChildIndex: Int?,
|
|
623
768
|
theme: EditorTheme?
|
|
624
769
|
) -> NSAttributedString {
|
|
625
770
|
var attrs = defaultAttributes(baseFont: baseFont, textColor: textColor)
|
|
626
771
|
attrs[RenderBridgeAttributes.voidNodeType] = nodeType
|
|
627
772
|
attrs[RenderBridgeAttributes.docPos] = docPos
|
|
628
773
|
attrs[.backgroundColor] = UIColor.systemGray5
|
|
774
|
+
if let topLevelChildIndex {
|
|
775
|
+
attrs[RenderBridgeAttributes.topLevelChildIndex] = NSNumber(value: topLevelChildIndex)
|
|
776
|
+
}
|
|
629
777
|
|
|
630
778
|
return NSAttributedString(string: "[\(label)]", attributes: attrs)
|
|
631
779
|
}
|
|
@@ -734,6 +882,18 @@ final class RenderBridge {
|
|
|
734
882
|
return 0
|
|
735
883
|
}
|
|
736
884
|
|
|
885
|
+
static func jsonInt(_ value: Any?) -> Int? {
|
|
886
|
+
if let number = value as? NSNumber {
|
|
887
|
+
return number.intValue
|
|
888
|
+
}
|
|
889
|
+
if let string = value as? String,
|
|
890
|
+
let resolved = Int(string.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
891
|
+
{
|
|
892
|
+
return resolved
|
|
893
|
+
}
|
|
894
|
+
return nil
|
|
895
|
+
}
|
|
896
|
+
|
|
737
897
|
/// Extract a positive `CGFloat` from a JSON value produced by `JSONSerialization`.
|
|
738
898
|
static func jsonCGFloat(_ value: Any?) -> CGFloat? {
|
|
739
899
|
if let number = value as? NSNumber {
|
|
@@ -810,16 +970,79 @@ final class RenderBridge {
|
|
|
810
970
|
baseFont: UIFont,
|
|
811
971
|
textColor: UIColor,
|
|
812
972
|
blockStack: [BlockContext],
|
|
813
|
-
theme: EditorTheme
|
|
973
|
+
theme: EditorTheme?,
|
|
974
|
+
paragraphSpacingOverride: CGFloat? = nil,
|
|
975
|
+
topLevelChildIndex: Int? = nil
|
|
814
976
|
) -> NSAttributedString {
|
|
815
|
-
|
|
977
|
+
var attrs = applyBlockStyle(
|
|
816
978
|
to: defaultAttributes(baseFont: baseFont, textColor: textColor),
|
|
817
979
|
blockStack: blockStack,
|
|
818
980
|
theme: theme
|
|
819
981
|
)
|
|
982
|
+
if let topLevelChildIndex {
|
|
983
|
+
attrs[RenderBridgeAttributes.topLevelChildIndex] = NSNumber(value: topLevelChildIndex)
|
|
984
|
+
}
|
|
985
|
+
if let paragraphSpacingOverride,
|
|
986
|
+
let paragraphStyle = (attrs[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy()
|
|
987
|
+
as? NSMutableParagraphStyle
|
|
988
|
+
{
|
|
989
|
+
paragraphStyle.paragraphSpacing = paragraphSpacingOverride
|
|
990
|
+
attrs[.paragraphStyle] = paragraphStyle
|
|
991
|
+
}
|
|
820
992
|
return NSAttributedString(string: "\n", attributes: attrs)
|
|
821
993
|
}
|
|
822
994
|
|
|
995
|
+
private static func attributedStringApplyingLeadingTopLevelChildIndexIfNeeded(
|
|
996
|
+
_ attributedString: NSAttributedString,
|
|
997
|
+
topLevelChildIndex: Int?,
|
|
998
|
+
resultIsEmpty: Bool
|
|
999
|
+
) -> NSAttributedString {
|
|
1000
|
+
guard resultIsEmpty,
|
|
1001
|
+
let topLevelChildIndex,
|
|
1002
|
+
attributedString.length > 0
|
|
1003
|
+
else {
|
|
1004
|
+
return attributedString
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let tagged = NSMutableAttributedString(attributedString: attributedString)
|
|
1008
|
+
tagged.addAttribute(
|
|
1009
|
+
RenderBridgeAttributes.topLevelChildIndex,
|
|
1010
|
+
value: NSNumber(value: topLevelChildIndex),
|
|
1011
|
+
range: NSRange(location: 0, length: 1)
|
|
1012
|
+
)
|
|
1013
|
+
return tagged
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private static func removingLeadingTopLevelChildIndex(
|
|
1017
|
+
from attributedString: NSAttributedString,
|
|
1018
|
+
topLevelChildIndex: Int
|
|
1019
|
+
) -> NSAttributedString {
|
|
1020
|
+
guard attributedString.length > 0 else { return attributedString }
|
|
1021
|
+
|
|
1022
|
+
let firstValue = attributedString.attribute(
|
|
1023
|
+
RenderBridgeAttributes.topLevelChildIndex,
|
|
1024
|
+
at: 0,
|
|
1025
|
+
effectiveRange: nil
|
|
1026
|
+
) as? NSNumber
|
|
1027
|
+
guard firstValue?.intValue == topLevelChildIndex else {
|
|
1028
|
+
return attributedString
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
let adjusted = NSMutableAttributedString(attributedString: attributedString)
|
|
1032
|
+
var effectiveRange = NSRange(location: 0, length: 0)
|
|
1033
|
+
adjusted.attribute(
|
|
1034
|
+
RenderBridgeAttributes.topLevelChildIndex,
|
|
1035
|
+
at: 0,
|
|
1036
|
+
longestEffectiveRange: &effectiveRange,
|
|
1037
|
+
in: NSRange(location: 0, length: adjusted.length)
|
|
1038
|
+
)
|
|
1039
|
+
adjusted.removeAttribute(
|
|
1040
|
+
RenderBridgeAttributes.topLevelChildIndex,
|
|
1041
|
+
range: effectiveRange
|
|
1042
|
+
)
|
|
1043
|
+
return adjusted
|
|
1044
|
+
}
|
|
1045
|
+
|
|
823
1046
|
private static func effectiveBlockContext(_ blockStack: [BlockContext]) -> BlockContext? {
|
|
824
1047
|
guard let currentBlock = blockStack.last else { return nil }
|
|
825
1048
|
if currentBlock.listContext != nil {
|
|
@@ -883,7 +1106,11 @@ final class RenderBridge {
|
|
|
883
1106
|
|
|
884
1107
|
let nsString = result.string as NSString
|
|
885
1108
|
let paragraphRange = nsString.paragraphRange(for: NSRange(location: result.length - 1, length: 0))
|
|
886
|
-
result.enumerateAttribute(
|
|
1109
|
+
result.enumerateAttribute(
|
|
1110
|
+
.paragraphStyle,
|
|
1111
|
+
in: paragraphRange,
|
|
1112
|
+
options: [.longestEffectiveRangeNotRequired]
|
|
1113
|
+
) { value, range, _ in
|
|
887
1114
|
let sourceStyle = (value as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle
|
|
888
1115
|
?? NSMutableParagraphStyle()
|
|
889
1116
|
sourceStyle.paragraphSpacing = paragraphSpacing
|
|
@@ -891,6 +1118,54 @@ final class RenderBridge {
|
|
|
891
1118
|
}
|
|
892
1119
|
}
|
|
893
1120
|
|
|
1121
|
+
private static func collapseTrailingSpacingBeforeHorizontalRuleIfNeeded(
|
|
1122
|
+
in result: NSMutableAttributedString,
|
|
1123
|
+
pendingParagraphSpacing: inout CGFloat?,
|
|
1124
|
+
nodeType: String,
|
|
1125
|
+
theme: EditorTheme?
|
|
1126
|
+
) {
|
|
1127
|
+
guard nodeType == "horizontalRule" else { return }
|
|
1128
|
+
let horizontalRuleMargin = resolvedHorizontalRuleVerticalMargin(theme: theme)
|
|
1129
|
+
|
|
1130
|
+
if let pendingSpacing = pendingParagraphSpacing {
|
|
1131
|
+
pendingParagraphSpacing = collapsedSpacing(
|
|
1132
|
+
existingSpacing: pendingSpacing,
|
|
1133
|
+
adjacentHorizontalRuleMargin: horizontalRuleMargin
|
|
1134
|
+
)
|
|
1135
|
+
return
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
guard let trailingParagraphSpacing = trailingParagraphSpacing(in: result) else { return }
|
|
1139
|
+
let adjustedSpacing = collapsedSpacing(
|
|
1140
|
+
existingSpacing: trailingParagraphSpacing,
|
|
1141
|
+
adjacentHorizontalRuleMargin: horizontalRuleMargin
|
|
1142
|
+
)
|
|
1143
|
+
guard abs(adjustedSpacing - trailingParagraphSpacing) > 0.01 else { return }
|
|
1144
|
+
overrideTrailingParagraphSpacing(in: result, paragraphSpacing: adjustedSpacing)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
private static func collapsedParagraphSpacingAfterHorizontalRule(
|
|
1148
|
+
in result: NSAttributedString,
|
|
1149
|
+
separatorBlockStack: [BlockContext],
|
|
1150
|
+
theme: EditorTheme?,
|
|
1151
|
+
baseFont: UIFont
|
|
1152
|
+
) -> CGFloat? {
|
|
1153
|
+
guard let horizontalRuleMargin = trailingHorizontalRuleMargin(in: result),
|
|
1154
|
+
let separatorSpacing = separatorParagraphSpacing(
|
|
1155
|
+
for: separatorBlockStack,
|
|
1156
|
+
theme: theme,
|
|
1157
|
+
baseFont: baseFont
|
|
1158
|
+
)
|
|
1159
|
+
else {
|
|
1160
|
+
return nil
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return collapsedSpacing(
|
|
1164
|
+
existingSpacing: separatorSpacing,
|
|
1165
|
+
adjacentHorizontalRuleMargin: horizontalRuleMargin
|
|
1166
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
894
1169
|
@discardableResult
|
|
895
1170
|
private static func applyPendingTrailingParagraphSpacing(
|
|
896
1171
|
in result: NSMutableAttributedString,
|
|
@@ -902,6 +1177,75 @@ final class RenderBridge {
|
|
|
902
1177
|
return true
|
|
903
1178
|
}
|
|
904
1179
|
|
|
1180
|
+
private static func trailingParagraphSpacing(in result: NSAttributedString) -> CGFloat? {
|
|
1181
|
+
guard result.length > 0 else { return nil }
|
|
1182
|
+
|
|
1183
|
+
let nsString = result.string as NSString
|
|
1184
|
+
let paragraphRange = nsString.paragraphRange(for: NSRange(location: result.length - 1, length: 0))
|
|
1185
|
+
var spacing: CGFloat? = nil
|
|
1186
|
+
result.enumerateAttribute(
|
|
1187
|
+
.paragraphStyle,
|
|
1188
|
+
in: paragraphRange,
|
|
1189
|
+
options: [.reverse, .longestEffectiveRangeNotRequired]
|
|
1190
|
+
) { value, _, stop in
|
|
1191
|
+
if let paragraphStyle = value as? NSParagraphStyle {
|
|
1192
|
+
spacing = paragraphStyle.paragraphSpacing
|
|
1193
|
+
stop.pointee = true
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return spacing
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private static func separatorParagraphSpacing(
|
|
1200
|
+
for blockStack: [BlockContext],
|
|
1201
|
+
theme: EditorTheme?,
|
|
1202
|
+
baseFont: UIFont
|
|
1203
|
+
) -> CGFloat? {
|
|
1204
|
+
guard let currentBlock = effectiveBlockContext(blockStack) else { return nil }
|
|
1205
|
+
return paragraphStyleForBlock(
|
|
1206
|
+
currentBlock,
|
|
1207
|
+
blockStack: blockStack,
|
|
1208
|
+
theme: theme,
|
|
1209
|
+
baseFont: baseFont
|
|
1210
|
+
).paragraphSpacing
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private static func trailingHorizontalRuleMargin(in result: NSAttributedString) -> CGFloat? {
|
|
1214
|
+
guard result.length > 0 else { return nil }
|
|
1215
|
+
let nsString = result.string as NSString
|
|
1216
|
+
|
|
1217
|
+
for index in stride(from: result.length - 1, through: 0, by: -1) {
|
|
1218
|
+
let scalar = nsString.character(at: index)
|
|
1219
|
+
if scalar == 0x000A || scalar == 0x000D {
|
|
1220
|
+
continue
|
|
1221
|
+
}
|
|
1222
|
+
guard result.attribute(
|
|
1223
|
+
RenderBridgeAttributes.voidNodeType,
|
|
1224
|
+
at: index,
|
|
1225
|
+
effectiveRange: nil
|
|
1226
|
+
) as? String == "horizontalRule" else {
|
|
1227
|
+
return nil
|
|
1228
|
+
}
|
|
1229
|
+
return (
|
|
1230
|
+
result.attribute(.attachment, at: index, effectiveRange: nil)
|
|
1231
|
+
as? HorizontalRuleAttachment
|
|
1232
|
+
)?.verticalPadding
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return nil
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
private static func resolvedHorizontalRuleVerticalMargin(theme: EditorTheme?) -> CGFloat {
|
|
1239
|
+
theme?.horizontalRule?.verticalMargin ?? LayoutConstants.horizontalRuleVerticalPadding
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
private static func collapsedSpacing(
|
|
1243
|
+
existingSpacing: CGFloat,
|
|
1244
|
+
adjacentHorizontalRuleMargin: CGFloat
|
|
1245
|
+
) -> CGFloat {
|
|
1246
|
+
max(existingSpacing, adjacentHorizontalRuleMargin) - adjacentHorizontalRuleMargin
|
|
1247
|
+
}
|
|
1248
|
+
|
|
905
1249
|
private static func appendTrailingHardBreakPlaceholderIfNeeded(
|
|
906
1250
|
in result: NSMutableAttributedString,
|
|
907
1251
|
endedBlock: BlockContext,
|
|
@@ -1004,6 +1348,7 @@ struct BlockContext {
|
|
|
1004
1348
|
let nodeType: String
|
|
1005
1349
|
let depth: UInt8
|
|
1006
1350
|
var listContext: [String: Any]?
|
|
1351
|
+
var topLevelChildIndex: Int? = nil
|
|
1007
1352
|
var listMarkerContext: [String: Any]? = nil
|
|
1008
1353
|
var markerPending: Bool = false
|
|
1009
1354
|
}
|
|
@@ -1176,25 +1521,15 @@ final class BlockImageAttachment: NSTextAttachment {
|
|
|
1176
1521
|
}
|
|
1177
1522
|
|
|
1178
1523
|
guard let url = URL(string: source) else { return }
|
|
1179
|
-
RenderImageCache.
|
|
1180
|
-
guard let self
|
|
1181
|
-
|
|
1182
|
-
if url.isFileURL {
|
|
1183
|
-
data = try? Data(contentsOf: url)
|
|
1184
|
-
} else {
|
|
1185
|
-
data = try? Data(contentsOf: url)
|
|
1186
|
-
}
|
|
1187
|
-
guard let data,
|
|
1188
|
-
let image = UIImage(data: data)
|
|
1524
|
+
RenderImageCache.load(source: source, url: url) { [weak self] image in
|
|
1525
|
+
guard let self,
|
|
1526
|
+
let image
|
|
1189
1527
|
else {
|
|
1190
1528
|
return
|
|
1191
1529
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
self.image = image
|
|
1196
|
-
NotificationCenter.default.post(name: .editorImageAttachmentDidLoad, object: self)
|
|
1197
|
-
}
|
|
1530
|
+
self.loadedImage = image
|
|
1531
|
+
self.image = image
|
|
1532
|
+
NotificationCenter.default.post(name: .editorImageAttachmentDidLoad, object: self)
|
|
1198
1533
|
}
|
|
1199
1534
|
}
|
|
1200
1535
|
|