@openeditor/react-native-prose-editor 0.0.7 → 0.0.8
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/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- 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 +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- 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 +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
1
|
+
public import ExpoModulesCore
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
4
|
private final class WeakNativeEditorExpoView {
|
|
@@ -2468,7 +2468,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
2468
2468
|
pendingEditableRetryValue = nil
|
|
2469
2469
|
pendingEditableRetryEditorId = nil
|
|
2470
2470
|
pendingEditableRetryScheduled = false
|
|
2471
|
-
richTextView.
|
|
2471
|
+
richTextView.setEditable(editable)
|
|
2472
2472
|
updateAccessoryToolbarVisibility()
|
|
2473
2473
|
}
|
|
2474
2474
|
|
|
@@ -3157,7 +3157,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
3157
3157
|
]
|
|
3158
3158
|
|
|
3159
3159
|
for offset in candidateOffsets where offset >= 0 && offset < textStorage.length {
|
|
3160
|
-
if let nodeType = textStorage.attribute(
|
|
3160
|
+
if let nodeType = textStorage.attribute(InputCoordinatorAttributes.voidNodeType, at: offset, effectiveRange: nil) as? String,
|
|
3161
3161
|
nodeType == "mention" {
|
|
3162
3162
|
return true
|
|
3163
3163
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
1
|
+
public import ExpoModulesCore
|
|
2
2
|
|
|
3
3
|
private func nativeUInt64(_ value: Int) -> UInt64? {
|
|
4
4
|
guard value >= 0 else { return nil }
|
|
@@ -224,6 +224,15 @@ public class NativeEditorModule: Module {
|
|
|
224
224
|
}
|
|
225
225
|
editorSetSelection(id: editorId, anchor: anchor, head: head)
|
|
226
226
|
}
|
|
227
|
+
Function("editorSetCellSelection") { (id: Int, anchor: Int, head: Int) in
|
|
228
|
+
guard let editorId = nativeUInt64(id),
|
|
229
|
+
let anchor = nativeUInt32(anchor),
|
|
230
|
+
let head = nativeUInt32(head)
|
|
231
|
+
else {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
editorSetCellSelection(id: editorId, anchor: anchor, head: head)
|
|
235
|
+
}
|
|
227
236
|
Function("editorSetSelectionScalar") { (id: Int, scalarAnchor: Int, scalarHead: Int) in
|
|
228
237
|
guard let editorId = nativeUInt64(id),
|
|
229
238
|
let scalarAnchor = nativeUInt32(scalarAnchor),
|
|
@@ -443,28 +452,6 @@ public class NativeEditorModule: Module {
|
|
|
443
452
|
guard let editorId = nativeUInt64(id) else { return false }
|
|
444
453
|
return editorCanRedo(id: editorId)
|
|
445
454
|
}
|
|
446
|
-
Function("renderDocumentJson") { (configJson: String, json: String) -> String in
|
|
447
|
-
let editorId = editorCreate(configJson: configJson)
|
|
448
|
-
defer {
|
|
449
|
-
editorDestroy(id: editorId)
|
|
450
|
-
}
|
|
451
|
-
return editorSetJson(id: editorId, json: json)
|
|
452
|
-
}
|
|
453
|
-
Function("measureContentHeight") { (renderJson: String, themeJson: String?, width: Double) -> Double in
|
|
454
|
-
let height = RenderBridge.measureHeight(
|
|
455
|
-
forRenderJSON: renderJson,
|
|
456
|
-
themeJSON: themeJson,
|
|
457
|
-
width: CGFloat(width)
|
|
458
|
-
)
|
|
459
|
-
return Double(height)
|
|
460
|
-
}
|
|
461
|
-
Function("renderDocumentHtml") { (configJson: String, html: String) -> String in
|
|
462
|
-
let editorId = editorCreate(configJson: configJson)
|
|
463
|
-
defer {
|
|
464
|
-
editorDestroy(id: editorId)
|
|
465
|
-
}
|
|
466
|
-
return editorSetHtml(id: editorId, html: html)
|
|
467
|
-
}
|
|
468
455
|
Function("editorReplaceHtml") { (id: Int, html: String) -> String in
|
|
469
456
|
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
470
457
|
return editorReplaceHtml(id: editorId, html: html)
|
|
@@ -509,6 +496,26 @@ public class NativeEditorModule: Module {
|
|
|
509
496
|
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
510
497
|
return editorOutdentListItem(id: editorId)
|
|
511
498
|
}
|
|
499
|
+
Function("editorInsertTableRow") { (id: Int, after: Bool) -> String in
|
|
500
|
+
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
501
|
+
return editorInsertTableRow(id: editorId, after: after)
|
|
502
|
+
}
|
|
503
|
+
Function("editorDeleteTableRow") { (id: Int) -> String in
|
|
504
|
+
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
505
|
+
return editorDeleteTableRow(id: editorId)
|
|
506
|
+
}
|
|
507
|
+
Function("editorInsertTableColumn") { (id: Int, after: Bool) -> String in
|
|
508
|
+
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
509
|
+
return editorInsertTableColumn(id: editorId, after: after)
|
|
510
|
+
}
|
|
511
|
+
Function("editorDeleteTableColumn") { (id: Int) -> String in
|
|
512
|
+
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
513
|
+
return editorDeleteTableColumn(id: editorId)
|
|
514
|
+
}
|
|
515
|
+
Function("editorMoveTableCell") { (id: Int, forward: Bool) -> String in
|
|
516
|
+
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
517
|
+
return editorMoveTableCell(id: editorId, forward: forward)
|
|
518
|
+
}
|
|
512
519
|
Function("editorInsertNode") { (id: Int, nodeType: String) -> String in
|
|
513
520
|
guard let editorId = nativeUInt64(id) else { return nativeArgumentError("editor id") }
|
|
514
521
|
return editorInsertNode(id: editorId, nodeType: nodeType)
|
|
@@ -598,27 +605,5 @@ public class NativeEditorModule: Module {
|
|
|
598
605
|
view.getCaretRectJson()
|
|
599
606
|
}
|
|
600
607
|
}
|
|
601
|
-
|
|
602
|
-
View(NativeProseViewerExpoView.self) {
|
|
603
|
-
ViewName("NativeProseViewer")
|
|
604
|
-
Events("onContentHeightChange", "onPressLink", "onPressMention")
|
|
605
|
-
|
|
606
|
-
Prop("renderJson") { (view: NativeProseViewerExpoView, renderJson: String?) in
|
|
607
|
-
view.setRenderJson(renderJson)
|
|
608
|
-
}
|
|
609
|
-
Prop("themeJson") { (view: NativeProseViewerExpoView, themeJson: String?) in
|
|
610
|
-
view.setThemeJson(themeJson)
|
|
611
|
-
}
|
|
612
|
-
Prop("collapsesWhenEmpty") {
|
|
613
|
-
(view: NativeProseViewerExpoView, collapsesWhenEmpty: Bool?) in
|
|
614
|
-
view.setCollapsesWhenEmpty(collapsesWhenEmpty)
|
|
615
|
-
}
|
|
616
|
-
Prop("enableLinkTaps") { (view: NativeProseViewerExpoView, enableLinkTaps: Bool?) in
|
|
617
|
-
view.setEnableLinkTaps(enableLinkTaps)
|
|
618
|
-
}
|
|
619
|
-
Prop("interceptLinkTaps") { (view: NativeProseViewerExpoView, interceptLinkTaps: Bool?) in
|
|
620
|
-
view.setInterceptLinkTaps(interceptLinkTaps)
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
608
|
}
|
|
624
609
|
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
extension Notification.Name {
|
|
4
|
+
static let editorImageAttachmentDidLoad = Notification.Name(
|
|
5
|
+
"com.openeditor.editor.imageAttachmentDidLoad"
|
|
6
|
+
)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
enum NativeImageLoader {
|
|
10
|
+
static let cache = NSCache<NSString, UIImage>()
|
|
11
|
+
static let stateQueue = DispatchQueue(label: "com.openeditor.editor.image-loader-state")
|
|
12
|
+
static let session: URLSession = {
|
|
13
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
14
|
+
configuration.timeoutIntervalForRequest = 10
|
|
15
|
+
configuration.timeoutIntervalForResource = 15
|
|
16
|
+
configuration.requestCachePolicy = .returnCacheDataElseLoad
|
|
17
|
+
return URLSession(configuration: configuration)
|
|
18
|
+
}()
|
|
19
|
+
private static var inFlight: [String: [(UIImage?) -> Void]] = [:]
|
|
20
|
+
|
|
21
|
+
static func load(source: String, completion: @escaping (UIImage?) -> Void) {
|
|
22
|
+
if let cached = cache.object(forKey: source as NSString) {
|
|
23
|
+
completion(cached)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if let inlineData = decodeDataURL(source), let image = UIImage(data: inlineData) {
|
|
27
|
+
cache.setObject(image, forKey: source as NSString)
|
|
28
|
+
completion(image)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
guard let url = URL(string: source) else {
|
|
32
|
+
completion(nil)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
load(source: source, url: url, completion: completion)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private static func load(
|
|
39
|
+
source: String,
|
|
40
|
+
url: URL,
|
|
41
|
+
completion: @escaping (UIImage?) -> Void
|
|
42
|
+
) {
|
|
43
|
+
if let cached = cache.object(forKey: source as NSString) {
|
|
44
|
+
completion(cached)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var shouldStartLoad = false
|
|
49
|
+
stateQueue.sync {
|
|
50
|
+
if inFlight[source] != nil {
|
|
51
|
+
inFlight[source]?.append(completion)
|
|
52
|
+
} else {
|
|
53
|
+
inFlight[source] = [completion]
|
|
54
|
+
shouldStartLoad = true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
guard shouldStartLoad else { return }
|
|
58
|
+
|
|
59
|
+
let finish: (Data?) -> Void = { data in
|
|
60
|
+
let image = data.flatMap(UIImage.init(data:))
|
|
61
|
+
if let image {
|
|
62
|
+
cache.setObject(image, forKey: source as NSString)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let callbacks: [(UIImage?) -> Void] = stateQueue.sync {
|
|
66
|
+
let callbacks = inFlight.removeValue(forKey: source) ?? []
|
|
67
|
+
return callbacks
|
|
68
|
+
}
|
|
69
|
+
DispatchQueue.main.async {
|
|
70
|
+
callbacks.forEach { $0(image) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if url.isFileURL {
|
|
75
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
76
|
+
finish(try? Data(contentsOf: url))
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
guard url.scheme == "https" || url.scheme == "http" else {
|
|
81
|
+
finish(nil)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
session.dataTask(with: url) { data, _, _ in
|
|
85
|
+
finish(data)
|
|
86
|
+
}.resume()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static func decodeDataURL(_ source: String) -> Data? {
|
|
90
|
+
let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
91
|
+
guard trimmed.lowercased().hasPrefix("data:image/"),
|
|
92
|
+
let commaIndex = trimmed.firstIndex(of: ",")
|
|
93
|
+
else {
|
|
94
|
+
return nil
|
|
95
|
+
}
|
|
96
|
+
let metadata = String(trimmed[..<commaIndex]).lowercased()
|
|
97
|
+
let payload = String(trimmed[trimmed.index(after: commaIndex)...])
|
|
98
|
+
guard metadata.contains(";base64") else { return nil }
|
|
99
|
+
return Data(base64Encoded: payload, options: [.ignoreUnknownCharacters])
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - Constants
|
|
104
|
+
|
|
105
|
+
/// Custom NSAttributedString attribute keys for editor metadata.
|
|
106
|
+
enum InputCoordinatorAttributes {
|
|
107
|
+
/// Marks a character as a void element placeholder (hardBreak, horizontalRule).
|
|
108
|
+
/// The value is the node type string (e.g. "hardBreak", "horizontalRule").
|
|
109
|
+
static let voidNodeType = NSAttributedString.Key("com.openeditor.editor.voidNodeType")
|
|
110
|
+
|
|
111
|
+
/// Stores the Rust document position (UInt32) for void elements.
|
|
112
|
+
static let docPos = NSAttributedString.Key("com.openeditor.editor.docPos")
|
|
113
|
+
|
|
114
|
+
/// Marks a character as a block boundary (for block start/end tracking).
|
|
115
|
+
static let blockBoundary = NSAttributedString.Key("com.openeditor.editor.blockBoundary")
|
|
116
|
+
|
|
117
|
+
/// Stores the block node type (e.g. "paragraph", "listItem").
|
|
118
|
+
static let blockNodeType = NSAttributedString.Key("com.openeditor.editor.blockNodeType")
|
|
119
|
+
|
|
120
|
+
/// Stores the block depth (UInt8).
|
|
121
|
+
static let blockDepth = NSAttributedString.Key("com.openeditor.editor.blockDepth")
|
|
122
|
+
|
|
123
|
+
/// Stores list context info as a dictionary for list items.
|
|
124
|
+
static let listContext = NSAttributedString.Key("com.openeditor.editor.listContext")
|
|
125
|
+
|
|
126
|
+
/// Marks blocks that should render a visible list marker.
|
|
127
|
+
static let listMarkerContext = NSAttributedString.Key("com.openeditor.editor.listMarkerContext")
|
|
128
|
+
|
|
129
|
+
/// Stores the rendered list marker color for the paragraph marker.
|
|
130
|
+
static let listMarkerColor = NSAttributedString.Key("com.openeditor.editor.listMarkerColor")
|
|
131
|
+
|
|
132
|
+
/// Stores the rendered list marker scale for unordered bullets.
|
|
133
|
+
static let listMarkerScale = NSAttributedString.Key("com.openeditor.editor.listMarkerScale")
|
|
134
|
+
|
|
135
|
+
/// Stores the paragraph base font used to render the list marker.
|
|
136
|
+
static let listMarkerBaseFont = NSAttributedString.Key("com.openeditor.editor.listMarkerBaseFont")
|
|
137
|
+
|
|
138
|
+
/// Stores the reserved list marker gutter width.
|
|
139
|
+
static let listMarkerWidth = NSAttributedString.Key("com.openeditor.editor.listMarkerWidth")
|
|
140
|
+
|
|
141
|
+
/// Stores the rendered blockquote border color.
|
|
142
|
+
static let blockquoteBorderColor = NSAttributedString.Key("com.openeditor.editor.blockquoteBorderColor")
|
|
143
|
+
|
|
144
|
+
/// Stores the rendered blockquote border width.
|
|
145
|
+
static let blockquoteBorderWidth = NSAttributedString.Key("com.openeditor.editor.blockquoteBorderWidth")
|
|
146
|
+
|
|
147
|
+
/// Stores the rendered blockquote gap between border and text.
|
|
148
|
+
static let blockquoteMarkerGap = NSAttributedString.Key("com.openeditor.editor.blockquoteMarkerGap")
|
|
149
|
+
|
|
150
|
+
/// Marks code-block paragraphs for custom background drawing.
|
|
151
|
+
static let codeBlockBackgroundColor = NSAttributedString.Key("com.openeditor.editor.codeBlockBackgroundColor")
|
|
152
|
+
static let codeBlockBorderRadius = NSAttributedString.Key("com.openeditor.editor.codeBlockBorderRadius")
|
|
153
|
+
static let codeBlockPaddingHorizontal = NSAttributedString.Key("com.openeditor.editor.codeBlockPaddingHorizontal")
|
|
154
|
+
static let codeBlockPaddingVertical = NSAttributedString.Key("com.openeditor.editor.codeBlockPaddingVertical")
|
|
155
|
+
|
|
156
|
+
/// Marks synthetic zero-width placeholders used only for UIKit layout.
|
|
157
|
+
static let syntheticPlaceholder = NSAttributedString.Key("com.openeditor.editor.syntheticPlaceholder")
|
|
158
|
+
|
|
159
|
+
/// Stores the link href for visually styled link text without enabling UITextView's default link interaction.
|
|
160
|
+
static let linkHref = NSAttributedString.Key("com.openeditor.editor.linkHref")
|
|
161
|
+
|
|
162
|
+
/// Stores the owning top-level document child index for partial native patching.
|
|
163
|
+
static let topLevelChildIndex = NSAttributedString.Key("com.openeditor.editor.topLevelChildIndex")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Layout constants for paragraph styles.
|
|
167
|
+
enum LayoutConstants {
|
|
168
|
+
/// Spacing between paragraphs (points).
|
|
169
|
+
static let paragraphSpacing: CGFloat = 8.0
|
|
170
|
+
|
|
171
|
+
/// Base indentation per depth level (points).
|
|
172
|
+
static let indentPerDepth: CGFloat = 24.0
|
|
173
|
+
|
|
174
|
+
/// Width reserved for the list bullet/number (points).
|
|
175
|
+
static let listMarkerWidth: CGFloat = 36.0
|
|
176
|
+
|
|
177
|
+
/// Gap between the list marker and the text that follows (points).
|
|
178
|
+
static let listMarkerTextGap: CGFloat = 8.0
|
|
179
|
+
|
|
180
|
+
/// Height of the horizontal rule separator line (points).
|
|
181
|
+
static let horizontalRuleHeight: CGFloat = 1.0
|
|
182
|
+
|
|
183
|
+
/// Vertical padding above and below the horizontal rule (points).
|
|
184
|
+
static let horizontalRuleVerticalPadding: CGFloat = 8.0
|
|
185
|
+
|
|
186
|
+
/// Total leading inset reserved for each blockquote depth.
|
|
187
|
+
static let blockquoteIndent: CGFloat = 18.0
|
|
188
|
+
|
|
189
|
+
/// Width of the rendered blockquote border bar.
|
|
190
|
+
static let blockquoteBorderWidth: CGFloat = 3.0
|
|
191
|
+
|
|
192
|
+
/// Gap between the blockquote border bar and the text that follows.
|
|
193
|
+
static let blockquoteMarkerGap: CGFloat = 8.0
|
|
194
|
+
|
|
195
|
+
/// Bullet character for unordered list items.
|
|
196
|
+
static let unorderedListBullet = "\u{2022} "
|
|
197
|
+
|
|
198
|
+
/// Scale factor applied only to unordered list marker glyphs.
|
|
199
|
+
static let unorderedListMarkerFontScale: CGFloat = 2.0
|
|
200
|
+
|
|
201
|
+
/// Object replacement character used for void block elements.
|
|
202
|
+
static let objectReplacementCharacter = "\u{FFFC}"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
enum NativeListMarker {
|
|
206
|
+
static func text(for context: [String: Any]) -> String {
|
|
207
|
+
if context["task"] as? Bool == true {
|
|
208
|
+
return (context["checked"] as? Bool == true) ? "☑︎ " : "☐ "
|
|
209
|
+
}
|
|
210
|
+
if context["ordered"] as? Bool == true {
|
|
211
|
+
let index = context["index"] as? Int ?? 1
|
|
212
|
+
return "\(index). "
|
|
213
|
+
}
|
|
214
|
+
return LayoutConstants.unorderedListBullet
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
// MARK: - BlockContext
|
|
220
|
+
|
|
221
|
+
/// Transient context while rendering block elements. Pushed onto a stack
|
|
222
|
+
/// when a `blockStart` element is encountered and popped on `blockEnd`.
|
|
223
|
+
struct BlockContext {
|
|
224
|
+
let nodeType: String
|
|
225
|
+
let depth: UInt8
|
|
226
|
+
var listContext: [String: Any]?
|
|
227
|
+
var topLevelChildIndex: Int? = nil
|
|
228
|
+
var listMarkerContext: [String: Any]? = nil
|
|
229
|
+
var markerPending: Bool = false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// MARK: - HorizontalRuleAttachment
|
|
233
|
+
|
|
234
|
+
/// NSTextAttachment subclass that draws a horizontal separator line.
|
|
235
|
+
///
|
|
236
|
+
/// The attachment renders as a thin line across the available width with
|
|
237
|
+
/// vertical padding. Used for `horizontalRule` void block elements.
|
|
238
|
+
final class HorizontalRuleAttachment: NSTextAttachment {
|
|
239
|
+
|
|
240
|
+
var lineColor: UIColor = .separator
|
|
241
|
+
var lineHeight: CGFloat = LayoutConstants.horizontalRuleHeight
|
|
242
|
+
var verticalPadding: CGFloat = LayoutConstants.horizontalRuleVerticalPadding
|
|
243
|
+
|
|
244
|
+
override func attachmentBounds(
|
|
245
|
+
for textContainer: NSTextContainer?,
|
|
246
|
+
proposedLineFragment lineFrag: CGRect,
|
|
247
|
+
glyphPosition position: CGPoint,
|
|
248
|
+
characterIndex charIndex: Int
|
|
249
|
+
) -> CGRect {
|
|
250
|
+
let totalHeight = lineHeight + (verticalPadding * 2)
|
|
251
|
+
return CGRect(
|
|
252
|
+
x: 0,
|
|
253
|
+
y: 0,
|
|
254
|
+
width: lineFrag.width,
|
|
255
|
+
height: totalHeight
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
override func image(
|
|
260
|
+
forBounds imageBounds: CGRect,
|
|
261
|
+
textContainer: NSTextContainer?,
|
|
262
|
+
characterIndex charIndex: Int
|
|
263
|
+
) -> UIImage? {
|
|
264
|
+
let renderer = UIGraphicsImageRenderer(bounds: imageBounds)
|
|
265
|
+
return renderer.image { context in
|
|
266
|
+
lineColor.setFill()
|
|
267
|
+
let lineY = imageBounds.midY - (lineHeight / 2)
|
|
268
|
+
let lineRect = CGRect(
|
|
269
|
+
x: imageBounds.origin.x,
|
|
270
|
+
y: lineY,
|
|
271
|
+
width: imageBounds.width,
|
|
272
|
+
height: lineHeight
|
|
273
|
+
)
|
|
274
|
+
context.fill(lineRect)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
final class BlockImageAttachment: NSTextAttachment {
|
|
280
|
+
private let source: String
|
|
281
|
+
private let placeholderTint: UIColor
|
|
282
|
+
private var preferredWidth: CGFloat?
|
|
283
|
+
private var preferredHeight: CGFloat?
|
|
284
|
+
private var loadedImage: UIImage?
|
|
285
|
+
|
|
286
|
+
init(
|
|
287
|
+
source: String,
|
|
288
|
+
placeholderTint: UIColor,
|
|
289
|
+
preferredWidth: CGFloat?,
|
|
290
|
+
preferredHeight: CGFloat?
|
|
291
|
+
) {
|
|
292
|
+
self.source = source
|
|
293
|
+
self.placeholderTint = placeholderTint
|
|
294
|
+
self.preferredWidth = preferredWidth
|
|
295
|
+
self.preferredHeight = preferredHeight
|
|
296
|
+
super.init(data: nil, ofType: nil)
|
|
297
|
+
loadImageIfNeeded()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
required init?(coder: NSCoder) {
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func setPreferredSize(width: CGFloat, height: CGFloat) {
|
|
305
|
+
preferredWidth = width
|
|
306
|
+
preferredHeight = height
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func previewImage() -> UIImage? {
|
|
310
|
+
loadedImage ?? image
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override func attachmentBounds(
|
|
314
|
+
for textContainer: NSTextContainer?,
|
|
315
|
+
proposedLineFragment lineFrag: CGRect,
|
|
316
|
+
glyphPosition position: CGPoint,
|
|
317
|
+
characterIndex charIndex: Int
|
|
318
|
+
) -> CGRect {
|
|
319
|
+
let lineFragmentWidth = lineFrag.width.isFinite ? lineFrag.width : 0
|
|
320
|
+
let containerWidth = textContainer.map {
|
|
321
|
+
max(0, $0.size.width - ($0.lineFragmentPadding * 2))
|
|
322
|
+
} ?? 0
|
|
323
|
+
let widthCandidates = [lineFragmentWidth, containerWidth].filter { $0.isFinite && $0 > 0 }
|
|
324
|
+
let maxWidth = max(160, widthCandidates.min() ?? 160)
|
|
325
|
+
let fallbackAspectRatio = loadedImage.flatMap { image -> CGFloat? in
|
|
326
|
+
let imageSize = image.size
|
|
327
|
+
guard imageSize.width > 0, imageSize.height > 0 else { return nil }
|
|
328
|
+
return imageSize.height / imageSize.width
|
|
329
|
+
} ?? 0.56
|
|
330
|
+
|
|
331
|
+
var resolvedWidth = preferredWidth
|
|
332
|
+
var resolvedHeight = preferredHeight
|
|
333
|
+
|
|
334
|
+
if resolvedWidth == nil, resolvedHeight == nil, let loadedImage {
|
|
335
|
+
let imageSize = loadedImage.size
|
|
336
|
+
if imageSize.width > 0, imageSize.height > 0 {
|
|
337
|
+
resolvedWidth = imageSize.width
|
|
338
|
+
resolvedHeight = imageSize.height
|
|
339
|
+
}
|
|
340
|
+
} else if resolvedWidth == nil, let resolvedHeight {
|
|
341
|
+
resolvedWidth = resolvedHeight / fallbackAspectRatio
|
|
342
|
+
} else if resolvedHeight == nil, let resolvedWidth {
|
|
343
|
+
resolvedHeight = resolvedWidth * fallbackAspectRatio
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let width = max(1, resolvedWidth ?? maxWidth)
|
|
347
|
+
let height = max(1, resolvedHeight ?? min(180, maxWidth * fallbackAspectRatio))
|
|
348
|
+
let scale = min(1, maxWidth / width)
|
|
349
|
+
return CGRect(x: 0, y: 0, width: width * scale, height: height * scale)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
override func image(
|
|
353
|
+
forBounds imageBounds: CGRect,
|
|
354
|
+
textContainer: NSTextContainer?,
|
|
355
|
+
characterIndex charIndex: Int
|
|
356
|
+
) -> UIImage? {
|
|
357
|
+
if let loadedImage {
|
|
358
|
+
return loadedImage
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let renderer = UIGraphicsImageRenderer(bounds: imageBounds)
|
|
362
|
+
return renderer.image { _ in
|
|
363
|
+
let path = UIBezierPath(roundedRect: imageBounds, cornerRadius: 12)
|
|
364
|
+
UIColor.secondarySystemFill.setFill()
|
|
365
|
+
path.fill()
|
|
366
|
+
|
|
367
|
+
let iconSize = min(imageBounds.width, imageBounds.height) * 0.28
|
|
368
|
+
let iconOrigin = CGPoint(
|
|
369
|
+
x: imageBounds.midX - (iconSize / 2),
|
|
370
|
+
y: imageBounds.midY - (iconSize / 2)
|
|
371
|
+
)
|
|
372
|
+
let iconRect = CGRect(origin: iconOrigin, size: CGSize(width: iconSize, height: iconSize))
|
|
373
|
+
|
|
374
|
+
if #available(iOS 13.0, *) {
|
|
375
|
+
let config = UIImage.SymbolConfiguration(pointSize: iconSize, weight: .medium)
|
|
376
|
+
let icon = UIImage(systemName: "photo", withConfiguration: config)?
|
|
377
|
+
.withTintColor(placeholderTint.withAlphaComponent(0.7), renderingMode: .alwaysOriginal)
|
|
378
|
+
icon?.draw(in: iconRect)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private func loadImageIfNeeded() {
|
|
384
|
+
NativeImageLoader.load(source: source) { [weak self] image in
|
|
385
|
+
guard let self,
|
|
386
|
+
let image
|
|
387
|
+
else {
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
self.loadedImage = image
|
|
391
|
+
self.image = image
|
|
392
|
+
NotificationCenter.default.post(name: .editorImageAttachmentDidLoad, object: self)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
}
|
package/ios/PositionBridge.swift
CHANGED
|
@@ -495,11 +495,11 @@ final class PositionBridge {
|
|
|
495
495
|
) { attrs, range, _ in
|
|
496
496
|
guard range.length > 0 else { return }
|
|
497
497
|
|
|
498
|
-
if attrs[
|
|
498
|
+
if attrs[InputCoordinatorAttributes.syntheticPlaceholder] as? Bool == true {
|
|
499
499
|
placeholders.append(range.location)
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
-
guard let listContext = attrs[
|
|
502
|
+
guard let listContext = attrs[InputCoordinatorAttributes.listMarkerContext] as? [String: Any] else {
|
|
503
503
|
return
|
|
504
504
|
}
|
|
505
505
|
|
|
@@ -515,7 +515,7 @@ final class PositionBridge {
|
|
|
515
515
|
guard seenStarts.insert(paragraphStart).inserted else { return }
|
|
516
516
|
|
|
517
517
|
let markerLength = UInt32(
|
|
518
|
-
|
|
518
|
+
NativeListMarker.text(for: listContext).unicodeScalars.count
|
|
519
519
|
)
|
|
520
520
|
markers.append(
|
|
521
521
|
VirtualListMarker(
|
|
@@ -549,7 +549,7 @@ final class PositionBridge {
|
|
|
549
549
|
) -> Bool {
|
|
550
550
|
guard paragraphStart > 0, paragraphStart <= attributedString.length else { return false }
|
|
551
551
|
let previousVoidType = attributedString.attribute(
|
|
552
|
-
|
|
552
|
+
InputCoordinatorAttributes.voidNodeType,
|
|
553
553
|
at: paragraphStart - 1,
|
|
554
554
|
effectiveRange: nil
|
|
555
555
|
) as? String
|