@apollohg/react-native-prose-editor 0.4.0 → 0.4.2

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.
Files changed (33) hide show
  1. package/README.md +21 -2
  2. package/android/build.gradle +23 -0
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +502 -39
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
  7. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
  9. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
  10. package/dist/NativeEditorBridge.d.ts +37 -1
  11. package/dist/NativeEditorBridge.js +192 -97
  12. package/dist/NativeRichTextEditor.d.ts +3 -2
  13. package/dist/NativeRichTextEditor.js +164 -56
  14. package/dist/YjsCollaboration.d.ts +2 -0
  15. package/dist/YjsCollaboration.js +142 -20
  16. package/dist/schemas.d.ts +2 -0
  17. package/dist/schemas.js +63 -0
  18. package/ios/EditorCore.xcframework/Info.plist +5 -5
  19. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  20. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  21. package/ios/EditorLayoutManager.swift +3 -3
  22. package/ios/Generated_editor_core.swift +41 -0
  23. package/ios/NativeEditorExpoView.swift +43 -11
  24. package/ios/NativeEditorModule.swift +6 -0
  25. package/ios/PositionBridge.swift +310 -75
  26. package/ios/RenderBridge.swift +362 -27
  27. package/ios/RichTextEditorView.swift +1983 -187
  28. package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
  29. package/package.json +11 -2
  30. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  31. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  32. package/rust/android/x86_64/libeditor_core.so +0 -0
  33. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +63 -0
package/dist/schemas.js CHANGED
@@ -4,6 +4,8 @@ exports.prosemirrorSchema = exports.tiptapSchema = exports.IMAGE_NODE_NAME = voi
4
4
  exports.imageNodeSpec = imageNodeSpec;
5
5
  exports.withImagesSchema = withImagesSchema;
6
6
  exports.buildImageFragmentJson = buildImageFragmentJson;
7
+ exports.defaultEmptyDocument = defaultEmptyDocument;
8
+ exports.normalizeDocumentJson = normalizeDocumentJson;
7
9
  exports.IMAGE_NODE_NAME = 'image';
8
10
  const HEADING_LEVELS = [1, 2, 3, 4, 5, 6];
9
11
  function imageNodeSpec(name = exports.IMAGE_NODE_NAME) {
@@ -129,6 +131,67 @@ exports.tiptapSchema = {
129
131
  ],
130
132
  marks: MARKS,
131
133
  };
134
+ function acceptingGroupsForChildCount(content, existingChildCount) {
135
+ const tokens = content
136
+ .trim()
137
+ .split(/\s+/)
138
+ .filter(Boolean)
139
+ .map((token) => {
140
+ const quantifier = token[token.length - 1];
141
+ if (quantifier === '+' || quantifier === '*' || quantifier === '?') {
142
+ return {
143
+ group: token.slice(0, -1),
144
+ min: quantifier === '+' ? 1 : 0,
145
+ max: quantifier === '?' ? 1 : null,
146
+ };
147
+ }
148
+ return {
149
+ group: token,
150
+ min: 1,
151
+ max: 1,
152
+ };
153
+ });
154
+ let remaining = existingChildCount;
155
+ const acceptingGroups = [];
156
+ for (const token of tokens) {
157
+ if (remaining >= token.min) {
158
+ const consumed = token.max == null ? remaining : Math.min(remaining, token.max);
159
+ remaining = Math.max(0, remaining - consumed);
160
+ const atMax = token.max != null && consumed >= token.max;
161
+ if (!atMax) {
162
+ acceptingGroups.push(token.group);
163
+ }
164
+ continue;
165
+ }
166
+ acceptingGroups.push(token.group);
167
+ break;
168
+ }
169
+ return acceptingGroups;
170
+ }
171
+ function defaultEmptyDocument(schema = exports.tiptapSchema) {
172
+ const docNode = schema.nodes.find((node) => node.role === 'doc' || node.name === 'doc');
173
+ const acceptingGroups = docNode == null ? [] : acceptingGroupsForChildCount(docNode.content ?? '', 0);
174
+ const matchingTextBlocks = schema.nodes.filter((node) => node.role === 'textBlock' &&
175
+ acceptingGroups.some((group) => node.name === group || node.group === group));
176
+ const preferredTextBlock = matchingTextBlocks.find((node) => node.htmlTag === 'p' || node.name === 'paragraph') ??
177
+ matchingTextBlocks[0] ??
178
+ schema.nodes.find((node) => node.htmlTag === 'p' || node.name === 'paragraph') ??
179
+ schema.nodes.find((node) => node.role === 'textBlock');
180
+ return {
181
+ type: 'doc',
182
+ content: [{ type: preferredTextBlock?.name ?? 'paragraph' }],
183
+ };
184
+ }
185
+ function normalizeDocumentJson(doc, schema = exports.tiptapSchema) {
186
+ const root = doc;
187
+ if (root?.type !== 'doc') {
188
+ return doc;
189
+ }
190
+ if (Array.isArray(root.content) && root.content.length > 0) {
191
+ return doc;
192
+ }
193
+ return defaultEmptyDocument(schema);
194
+ }
132
195
  exports.prosemirrorSchema = {
133
196
  nodes: [
134
197
  {
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>libeditor_core.a</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>libeditor_core.a</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -23,7 +23,7 @@ final class EditorLayoutManager: NSLayoutManager {
23
23
  textStorage.enumerateAttribute(
24
24
  RenderBridgeAttributes.blockquoteBorderColor,
25
25
  in: characterRange,
26
- options: []
26
+ options: [.longestEffectiveRangeNotRequired]
27
27
  ) { value, range, _ in
28
28
  guard range.length > 0, let color = value as? UIColor else { return }
29
29
 
@@ -68,7 +68,7 @@ final class EditorLayoutManager: NSLayoutManager {
68
68
  textStorage.enumerateAttribute(
69
69
  RenderBridgeAttributes.listMarkerContext,
70
70
  in: characterRange,
71
- options: []
71
+ options: [.longestEffectiveRangeNotRequired]
72
72
  ) { value, range, _ in
73
73
  guard range.length > 0, let listContext = value as? [String: Any] else { return }
74
74
 
@@ -90,7 +90,7 @@ final class EditorLayoutManager: NSLayoutManager {
90
90
  textStorage.enumerateAttribute(
91
91
  RenderBridgeAttributes.blockquoteBorderColor,
92
92
  in: characterRange,
93
- options: []
93
+ options: [.longestEffectiveRangeNotRequired]
94
94
  ) { value, range, _ in
95
95
  guard range.length > 0, let color = value as? UIColor else { return }
96
96
 
@@ -694,6 +694,18 @@ public func editorDeleteAndSplitScalar(id: UInt64, scalarFrom: UInt32, scalarTo:
694
694
  )
695
695
  })
696
696
  }
697
+ /**
698
+ * Delete backward relative to an explicit scalar selection. Returns an update JSON string.
699
+ */
700
+ public func editorDeleteBackwardAtSelectionScalar(id: UInt64, scalarAnchor: UInt32, scalarHead: UInt32) -> String {
701
+ return try! FfiConverterString.lift(try! rustCall() {
702
+ uniffi_editor_core_fn_func_editor_delete_backward_at_selection_scalar(
703
+ FfiConverterUInt64.lower(id),
704
+ FfiConverterUInt32.lower(scalarAnchor),
705
+ FfiConverterUInt32.lower(scalarHead),$0
706
+ )
707
+ })
708
+ }
697
709
  /**
698
710
  * Delete a range. Returns an update JSON string.
699
711
  */
@@ -738,6 +750,16 @@ public func editorDocToScalar(id: UInt64, docPos: UInt32) -> UInt32 {
738
750
  )
739
751
  })
740
752
  }
753
+ /**
754
+ * Get both HTML and ProseMirror JSON content in one payload.
755
+ */
756
+ public func editorGetContentSnapshot(id: UInt64) -> String {
757
+ return try! FfiConverterString.lift(try! rustCall() {
758
+ uniffi_editor_core_fn_func_editor_get_content_snapshot(
759
+ FfiConverterUInt64.lower(id),$0
760
+ )
761
+ })
762
+ }
741
763
  /**
742
764
  * Get the current editor state (render elements, selection, active state,
743
765
  * history state) without performing any edits. Used by native views to pull
@@ -780,6 +802,16 @@ public func editorGetSelection(id: UInt64) -> String {
780
802
  )
781
803
  })
782
804
  }
805
+ /**
806
+ * Get the current selection-related editor state without render elements.
807
+ */
808
+ public func editorGetSelectionState(id: UInt64) -> String {
809
+ return try! FfiConverterString.lift(try! rustCall() {
810
+ uniffi_editor_core_fn_func_editor_get_selection_state(
811
+ FfiConverterUInt64.lower(id),$0
812
+ )
813
+ })
814
+ }
783
815
  /**
784
816
  * Indent the current list item into a nested list. Returns an update JSON string.
785
817
  */
@@ -1296,6 +1328,9 @@ private let initializationResult: InitializationResult = {
1296
1328
  if (uniffi_editor_core_checksum_func_editor_delete_and_split_scalar() != 13764) {
1297
1329
  return InitializationResult.apiChecksumMismatch
1298
1330
  }
1331
+ if (uniffi_editor_core_checksum_func_editor_delete_backward_at_selection_scalar() != 7697) {
1332
+ return InitializationResult.apiChecksumMismatch
1333
+ }
1299
1334
  if (uniffi_editor_core_checksum_func_editor_delete_range() != 6109) {
1300
1335
  return InitializationResult.apiChecksumMismatch
1301
1336
  }
@@ -1308,6 +1343,9 @@ private let initializationResult: InitializationResult = {
1308
1343
  if (uniffi_editor_core_checksum_func_editor_doc_to_scalar() != 48291) {
1309
1344
  return InitializationResult.apiChecksumMismatch
1310
1345
  }
1346
+ if (uniffi_editor_core_checksum_func_editor_get_content_snapshot() != 32837) {
1347
+ return InitializationResult.apiChecksumMismatch
1348
+ }
1311
1349
  if (uniffi_editor_core_checksum_func_editor_get_current_state() != 13946) {
1312
1350
  return InitializationResult.apiChecksumMismatch
1313
1351
  }
@@ -1320,6 +1358,9 @@ private let initializationResult: InitializationResult = {
1320
1358
  if (uniffi_editor_core_checksum_func_editor_get_selection() != 20571) {
1321
1359
  return InitializationResult.apiChecksumMismatch
1322
1360
  }
1361
+ if (uniffi_editor_core_checksum_func_editor_get_selection_state() != 16471) {
1362
+ return InitializationResult.apiChecksumMismatch
1363
+ }
1323
1364
  if (uniffi_editor_core_checksum_func_editor_indent_list_item() != 10818) {
1324
1365
  return InitializationResult.apiChecksumMismatch
1325
1366
  }
@@ -1596,6 +1596,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1596
1596
  private var addons = NativeEditorAddons(mentions: nil)
1597
1597
  private var mentionQueryState: MentionQueryState?
1598
1598
  private var lastMentionEventJSON: String?
1599
+ private var lastThemeJSON: String?
1600
+ private var lastAddonsJSON: String?
1601
+ private var lastRemoteSelectionsJSON: String?
1602
+ private var lastToolbarItemsJSON: String?
1603
+ private var lastToolbarFrameJSON: String?
1599
1604
  private var pendingEditorUpdateJSON: String?
1600
1605
  private var pendingEditorUpdateRevision = 0
1601
1606
  private var appliedEditorUpdateRevision = 0
@@ -1624,17 +1629,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1624
1629
  let onToolbarAction = EventDispatcher()
1625
1630
  let onAddonEvent = EventDispatcher()
1626
1631
  private var lastEmittedContentHeight: CGFloat = 0
1632
+ private var cachedAutoGrowContentHeight: CGFloat = 0
1627
1633
 
1628
1634
  // MARK: - Initialization
1629
1635
 
1630
1636
  required init(appContext: AppContext? = nil) {
1631
1637
  richTextView = RichTextEditorView(frame: .zero)
1632
1638
  super.init(appContext: appContext)
1633
- richTextView.onHeightMayChange = { [weak self] in
1639
+ richTextView.onHeightMayChange = { [weak self] measuredHeight in
1634
1640
  guard let self, self.heightBehavior == .autoGrow else { return }
1641
+ self.cachedAutoGrowContentHeight = measuredHeight
1635
1642
  self.invalidateIntrinsicContentSize()
1636
- self.superview?.setNeedsLayout()
1637
- self.emitContentHeightIfNeeded(force: true)
1643
+ self.emitContentHeightIfNeeded(force: true, measuredHeight: measuredHeight)
1638
1644
  }
1639
1645
  richTextView.textView.editorDelegate = self
1640
1646
  configureAccessoryToolbar()
@@ -1666,6 +1672,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1666
1672
  guard heightBehavior == .autoGrow else {
1667
1673
  return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
1668
1674
  }
1675
+ if cachedAutoGrowContentHeight > 0 {
1676
+ return CGSize(width: UIView.noIntrinsicMetric, height: cachedAutoGrowContentHeight)
1677
+ }
1669
1678
  return richTextView.intrinsicContentSize
1670
1679
  }
1671
1680
 
@@ -1676,6 +1685,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1676
1685
  let currentWidth = bounds.width.rounded(.towardZero)
1677
1686
  guard currentWidth != lastAutoGrowWidth else { return }
1678
1687
  lastAutoGrowWidth = currentWidth
1688
+ cachedAutoGrowContentHeight = 0
1679
1689
  invalidateIntrinsicContentSize()
1680
1690
  emitContentHeightIfNeeded(force: true)
1681
1691
  }
@@ -1711,6 +1721,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1711
1721
  }
1712
1722
 
1713
1723
  func setThemeJson(_ themeJson: String?) {
1724
+ guard lastThemeJSON != themeJson else { return }
1725
+ lastThemeJSON = themeJson
1714
1726
  let theme = EditorTheme.from(json: themeJson)
1715
1727
  richTextView.applyTheme(theme)
1716
1728
  accessoryToolbar.apply(theme: theme?.toolbar)
@@ -1724,12 +1736,16 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1724
1736
  }
1725
1737
 
1726
1738
  func setAddonsJson(_ addonsJson: String?) {
1739
+ guard lastAddonsJSON != addonsJson else { return }
1740
+ lastAddonsJSON = addonsJson
1727
1741
  addons = NativeEditorAddons.from(json: addonsJson)
1728
1742
  accessoryToolbar.apply(mentionTheme: richTextView.textView.theme?.mentions ?? addons.mentions?.theme)
1729
1743
  refreshMentionQuery()
1730
1744
  }
1731
1745
 
1732
1746
  func setRemoteSelectionsJson(_ remoteSelectionsJson: String?) {
1747
+ guard lastRemoteSelectionsJSON != remoteSelectionsJson else { return }
1748
+ lastRemoteSelectionsJSON = remoteSelectionsJson
1733
1749
  richTextView.setRemoteSelections(RemoteSelectionDecoration.from(json: remoteSelectionsJson))
1734
1750
  }
1735
1751
 
@@ -1758,6 +1774,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1758
1774
  let nextBehavior = EditorHeightBehavior(rawValue: rawHeightBehavior) ?? .fixed
1759
1775
  guard nextBehavior != heightBehavior else { return }
1760
1776
  heightBehavior = nextBehavior
1777
+ if nextBehavior != .autoGrow {
1778
+ cachedAutoGrowContentHeight = 0
1779
+ }
1761
1780
  richTextView.heightBehavior = nextBehavior
1762
1781
  invalidateIntrinsicContentSize()
1763
1782
  setNeedsLayout()
@@ -1770,22 +1789,29 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1770
1789
  richTextView.allowImageResizing = allowImageResizing
1771
1790
  }
1772
1791
 
1773
- private func emitContentHeightIfNeeded(force: Bool = false) {
1792
+ private func emitContentHeightIfNeeded(force: Bool = false, measuredHeight: CGFloat? = nil) {
1774
1793
  guard heightBehavior == .autoGrow else { return }
1775
- let contentHeight = ceil(richTextView.intrinsicContentSize.height)
1794
+ let resolvedHeight = measuredHeight
1795
+ ?? (cachedAutoGrowContentHeight > 0 ? cachedAutoGrowContentHeight : richTextView.intrinsicContentSize.height)
1796
+ let contentHeight = ceil(resolvedHeight)
1776
1797
  guard contentHeight > 0 else { return }
1777
1798
  guard force || abs(contentHeight - lastEmittedContentHeight) > 0.5 else { return }
1799
+ cachedAutoGrowContentHeight = contentHeight
1778
1800
  lastEmittedContentHeight = contentHeight
1779
1801
  onContentHeightChange(["contentHeight": contentHeight])
1780
1802
  }
1781
1803
 
1782
1804
  func setToolbarButtonsJson(_ toolbarButtonsJson: String?) {
1805
+ guard lastToolbarItemsJSON != toolbarButtonsJson else { return }
1806
+ lastToolbarItemsJSON = toolbarButtonsJson
1783
1807
  toolbarItems = NativeToolbarItem.from(json: toolbarButtonsJson)
1784
1808
  accessoryToolbar.setItems(toolbarItems)
1785
1809
  refreshSystemAssistantToolbarIfNeeded()
1786
1810
  }
1787
1811
 
1788
1812
  func setToolbarFrameJson(_ toolbarFrameJson: String?) {
1813
+ guard lastToolbarFrameJSON != toolbarFrameJson else { return }
1814
+ lastToolbarFrameJSON = toolbarFrameJson
1789
1815
  guard let toolbarFrameJson,
1790
1816
  let data = toolbarFrameJson.data(using: .utf8),
1791
1817
  let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
@@ -1917,11 +1943,15 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1917
1943
  // MARK: - EditorTextViewDelegate
1918
1944
 
1919
1945
  func editorTextView(_ textView: EditorTextView, selectionDidChange anchor: UInt32, head: UInt32) {
1920
- refreshToolbarStateFromEditorSelection()
1946
+ let stateJSON = refreshToolbarStateFromEditorSelection()
1921
1947
  refreshSystemAssistantToolbarIfNeeded()
1922
1948
  refreshMentionQuery()
1923
1949
  richTextView.refreshRemoteSelections()
1924
- onSelectionChange(["anchor": Int(anchor), "head": Int(head)])
1950
+ var event: [String: Any] = ["anchor": Int(anchor), "head": Int(head)]
1951
+ if let stateJSON {
1952
+ event["stateJson"] = stateJSON
1953
+ }
1954
+ onSelectionChange(event)
1925
1955
  }
1926
1956
 
1927
1957
  func editorTextView(_ textView: EditorTextView, didReceiveUpdate updateJSON: String) {
@@ -1936,12 +1966,14 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1936
1966
  onEditorUpdate(["updateJson": updateJSON])
1937
1967
  }
1938
1968
 
1939
- private func refreshToolbarStateFromEditorSelection() {
1940
- guard richTextView.editorId != 0 else { return }
1941
- let stateJSON = editorGetCurrentState(id: richTextView.editorId)
1942
- guard let state = NativeToolbarState(updateJSON: stateJSON) else { return }
1969
+ @discardableResult
1970
+ private func refreshToolbarStateFromEditorSelection() -> String? {
1971
+ guard richTextView.editorId != 0 else { return nil }
1972
+ let stateJSON = editorGetSelectionState(id: richTextView.editorId)
1973
+ guard let state = NativeToolbarState(updateJSON: stateJSON) else { return nil }
1943
1974
  toolbarState = state
1944
1975
  accessoryToolbar.apply(state: state)
1976
+ return stateJSON
1945
1977
  }
1946
1978
 
1947
1979
  private func configureAccessoryToolbar() {
@@ -58,6 +58,9 @@ public class NativeEditorModule: Module {
58
58
  Function("editorGetJson") { (id: Int) -> String in
59
59
  editorGetJson(id: UInt64(id))
60
60
  }
61
+ Function("editorGetContentSnapshot") { (id: Int) -> String in
62
+ editorGetContentSnapshot(id: UInt64(id))
63
+ }
61
64
  Function("editorInsertText") { (id: Int, pos: Int, text: String) -> String in
62
65
  editorInsertText(id: UInt64(id), pos: UInt32(pos), text: text)
63
66
  }
@@ -234,6 +237,9 @@ public class NativeEditorModule: Module {
234
237
  Function("editorGetSelection") { (id: Int) -> String in
235
238
  editorGetSelection(id: UInt64(id))
236
239
  }
240
+ Function("editorGetSelectionState") { (id: Int) -> String in
241
+ editorGetSelectionState(id: UInt64(id))
242
+ }
237
243
  Function("editorDocToScalar") { (id: Int, docPos: Int) -> Int in
238
244
  Int(editorDocToScalar(id: UInt64(id), docPos: UInt32(docPos)))
239
245
  }