react-native-enriched-markdown-scaffold 0.7.4-scaffold.9 → 1.0.2-scaffold.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 (469) hide show
  1. package/README.md +54 -8
  2. package/ReactNativeEnrichedMarkdown.podspec +82 -73
  3. package/android/build.gradle +186 -1
  4. package/android/consumer-rules.pro +1 -1
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +9 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  7. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +24 -0
  8. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +8 -0
  9. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +9 -0
  10. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -0
  11. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +75 -0
  12. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +63 -0
  13. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +35 -0
  14. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +886 -23
  15. package/android/src/main/baseline-prof.txt +1 -1
  16. package/android/src/main/cpp/jni-adapter.cpp +74 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +189 -13
  18. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +17 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +50 -0
  20. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -1
  21. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +10 -1
  22. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +42 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +82 -4
  24. package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyPressEvent.kt +24 -0
  25. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +54 -4
  26. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +901 -462
  27. package/android/src/main/java/com/swmansion/enriched/markdown/input/MarkdownClipboard.kt +42 -0
  28. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/BlockEditCoordinator.kt +196 -0
  29. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/ClipboardCoordinator.kt +73 -0
  30. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditContext.kt +19 -0
  31. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditPipeline.kt +248 -0
  32. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/EditSession.kt +52 -0
  33. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +101 -7
  34. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/LinkCoordinator.kt +69 -0
  35. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MarkdownTextWatcher.kt +3 -3
  36. package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/MentionCoordinator.kt +118 -0
  37. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnChangeStateEvent.kt +26 -0
  38. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnContextMenuItemPressEvent.kt +26 -0
  39. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnInputKeyPressEvent.kt +24 -0
  40. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
  41. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockStore.kt +254 -0
  42. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +80 -87
  43. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputFormatter.kt +109 -1
  44. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +116 -22
  45. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/MarkdownSerializer.kt +167 -38
  46. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/RangeEditAdjustment.kt +149 -0
  47. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +66 -5
  48. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +4 -2
  49. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockRange.kt +27 -0
  50. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockType.kt +48 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/FormattingRange.kt +9 -4
  52. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/InputFormatterStyle.kt +39 -2
  53. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathAtom.kt +20 -0
  54. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +1 -1
  55. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MutableRangeBounds.kt +11 -0
  56. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputBulletSpan.kt +82 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputHeadingSpan.kt +58 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputListItemSpacingSpan.kt +37 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputOrderedListMarkerSpan.kt +63 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/BlockHandler.kt +54 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/HeadingBlockHandler.kt +27 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/OrderedListBlockHandler.kt +38 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/UnorderedListBlockHandler.kt +37 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +8 -26
  65. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +1 -0
  66. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +1 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +9 -2
  68. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +100 -12
  69. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +16 -1
  70. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +4 -0
  71. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +2 -1
  72. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +74 -18
  73. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +43 -4
  74. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +9 -1
  75. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +16 -0
  76. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +30 -27
  77. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +24 -0
  78. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +174 -24
  79. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +27 -3
  80. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +13 -16
  81. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +1 -1
  82. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +1 -1
  83. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +182 -46
  84. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListItemSpacingSpan.kt +33 -0
  85. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListMarkerAnchorSpan.kt +38 -0
  86. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +2 -0
  87. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +1 -1
  88. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt +3 -2
  89. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +3 -2
  90. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +6 -0
  91. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +26 -0
  92. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +7 -1
  93. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +4 -1
  94. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +13 -4
  95. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TableStyle.kt +6 -0
  96. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockHighlighter.kt +90 -0
  97. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockNode.kt +57 -0
  98. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/FeatureFlags.kt +1 -0
  99. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +9 -0
  100. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +37 -7
  101. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/ReadableMapExtensions.kt +3 -0
  102. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +10 -0
  103. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +1 -0
  104. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter.kt +83 -5
  105. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt +4 -0
  106. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +74 -44
  107. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
  108. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageCache.kt +26 -3
  109. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt +22 -9
  110. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt +161 -0
  111. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/TextPaintExtensions.kt +23 -0
  112. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/CheckboxTouchHelper.kt +7 -0
  113. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/TaskListTapUtils.kt +41 -21
  114. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/span/BlockSpacing.kt +15 -8
  115. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt +58 -27
  116. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +49 -15
  117. package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +45 -0
  118. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +396 -0
  119. package/android/src/main/java/com/swmansion/enriched/markdown/views/ContextMenuPopup.kt +9 -6
  120. package/android/src/main/java/com/swmansion/enriched/markdown/views/CopyGlyph.kt +101 -0
  121. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +36 -9
  122. package/android/src/main/jni/CMakeLists.txt +53 -0
  123. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp +10 -0
  124. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h +11 -2
  125. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +10 -0
  126. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +12 -2
  127. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
  128. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +24 -0
  129. package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +16 -0
  130. package/android/src/math/java/com/swmansion/enriched/markdown/input/math/MathChipControllerImpl.kt +21 -4
  131. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +7 -2
  132. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
  133. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +6 -4
  134. package/cpp/highlight/CodeBlockHighlighter.cpp +284 -0
  135. package/cpp/highlight/CodeBlockHighlighter.hpp +66 -0
  136. package/cpp/highlight/CodeBlockLanguages.cpp +138 -0
  137. package/cpp/highlight/CodeBlockLanguages.hpp +25 -0
  138. package/cpp/highlight/HighlightGrammars.hpp +28 -0
  139. package/cpp/highlight/code_highlight_podspec.rb +211 -0
  140. package/cpp/highlight/gen-registry.mjs +155 -0
  141. package/cpp/highlight/grammar-versions.json +159 -0
  142. package/cpp/highlight/vendor/generated/generated_queries.h +1665 -0
  143. package/cpp/highlight/vendor/generated/generated_registry.cpp +59 -0
  144. package/cpp/parser/MD4CParser.cpp +173 -61
  145. package/cpp/parser/MD4CParser.hpp +1 -0
  146. package/cpp/parser/MarkdownASTNode.hpp +2 -1
  147. package/cpp/wasm/ASTSerializer.cpp +2 -0
  148. package/cpp/wasm/build.sh +1 -0
  149. package/cpp/wasm/md4c_wasm.cpp +3 -1
  150. package/docs/ACCESSIBILITY.md +14 -2
  151. package/docs/API_REFERENCE.md +234 -12
  152. package/docs/BREAKING_CHANGES.md +129 -0
  153. package/docs/CODE_HIGHLIGHT.md +133 -0
  154. package/docs/COPY_OPTIONS.md +47 -3
  155. package/docs/ELEMENTS_STRUCTURE.md +52 -4
  156. package/docs/IMAGE_CACHING.md +30 -0
  157. package/docs/INPUT.md +108 -11
  158. package/docs/LATEX_MATH.md +87 -45
  159. package/docs/MACOS.md +1 -1
  160. package/docs/MARKDOWN_STREAMING.md +27 -1
  161. package/docs/NATIVE_ASSETS.md +127 -0
  162. package/docs/RTL.md +1 -1
  163. package/docs/STYLES.md +46 -6
  164. package/docs/TESTING.md +112 -0
  165. package/docs/TEXT.md +16 -6
  166. package/docs/WEB.md +3 -1
  167. package/ios/EnrichedMarkdown.h +7 -0
  168. package/ios/EnrichedMarkdown.mm +284 -75
  169. package/ios/EnrichedMarkdownText.h +7 -0
  170. package/ios/EnrichedMarkdownText.mm +101 -73
  171. package/ios/attachments/ENRMImageAttachment.h +9 -0
  172. package/ios/attachments/ENRMImageAttachment.m +143 -40
  173. package/ios/attachments/ENRMMathInlineAttachment.h +2 -0
  174. package/ios/attachments/ENRMMathInlineAttachment.m +48 -81
  175. package/ios/attachments/ENRMMathInlineAttachmentShared.h +2 -0
  176. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +75 -0
  177. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +63 -0
  178. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +35 -0
  179. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +886 -23
  180. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +131 -0
  181. package/ios/input/ENRMAutoLinkDetector.h +2 -1
  182. package/ios/input/ENRMAutoLinkDetector.mm +79 -41
  183. package/ios/input/ENRMBlockEditCoordinator.h +71 -0
  184. package/ios/input/ENRMBlockEditCoordinator.mm +195 -0
  185. package/ios/input/ENRMBlockRange.h +28 -0
  186. package/ios/input/ENRMBlockRange.mm +30 -0
  187. package/ios/input/ENRMBlockStore.h +56 -0
  188. package/ios/input/ENRMBlockStore.mm +275 -0
  189. package/ios/input/ENRMClipboardCoordinator.h +45 -0
  190. package/ios/input/ENRMClipboardCoordinator.mm +120 -0
  191. package/ios/input/ENRMDetectorPipeline.h +2 -1
  192. package/ios/input/ENRMEditPipeline.h +76 -0
  193. package/ios/input/ENRMEditPipeline.mm +228 -0
  194. package/ios/input/ENRMEditSession.h +36 -0
  195. package/ios/input/ENRMEditSession.mm +63 -0
  196. package/ios/input/ENRMFormattingStore.h +17 -0
  197. package/ios/input/ENRMFormattingStore.mm +95 -105
  198. package/ios/input/ENRMInputBlockType.h +91 -0
  199. package/ios/input/ENRMInputBlockType.mm +5 -0
  200. package/ios/input/ENRMInputDecorationDrawer.h +18 -0
  201. package/ios/input/ENRMInputEventEmitter.h +56 -0
  202. package/ios/input/ENRMInputEventEmitter.mm +308 -0
  203. package/ios/input/ENRMInputFormatter.h +29 -69
  204. package/ios/input/ENRMInputFormatter.mm +216 -124
  205. package/ios/input/ENRMInputFormatterStyle.h +99 -0
  206. package/ios/input/ENRMInputFormatterStyle.mm +188 -0
  207. package/ios/input/ENRMInputLayoutManager.h +9 -0
  208. package/ios/input/ENRMInputLayoutManager.mm +30 -0
  209. package/ios/input/ENRMInputListMarkerDrawer.h +32 -0
  210. package/ios/input/ENRMInputListMarkerDrawer.mm +426 -0
  211. package/ios/input/ENRMInputParser.h +5 -6
  212. package/ios/input/ENRMInputParser.mm +461 -178
  213. package/ios/input/ENRMInputRemend.mm +2 -2
  214. package/ios/input/ENRMInputStyleStateBuilder.h +40 -0
  215. package/ios/input/ENRMInputStyleStateBuilder.mm +60 -0
  216. package/ios/input/ENRMInputTextView.mm +103 -8
  217. package/ios/input/ENRMInputTypingAttributesController.h +42 -0
  218. package/ios/input/ENRMInputTypingAttributesController.mm +203 -0
  219. package/ios/input/ENRMLinkCoordinator.h +41 -0
  220. package/ios/input/ENRMLinkCoordinator.mm +89 -0
  221. package/ios/input/ENRMMarkdownSerializer.h +29 -5
  222. package/ios/input/ENRMMarkdownSerializer.mm +170 -48
  223. package/ios/input/ENRMMathAtom.h +26 -0
  224. package/ios/input/ENRMMathAtom.mm +12 -0
  225. package/ios/input/ENRMMentionCoordinator.h +45 -0
  226. package/ios/input/ENRMMentionCoordinator.mm +167 -0
  227. package/ios/input/ENRMRangeEditAdjustment.h +38 -0
  228. package/ios/input/ENRMRangeEditAdjustment.mm +91 -0
  229. package/ios/input/ENRMRangeStoreUtils.h +39 -0
  230. package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +6 -6
  231. package/ios/input/EnrichedMarkdownTextInput+Internal.h +11 -3
  232. package/ios/input/EnrichedMarkdownTextInput.h +7 -1
  233. package/ios/input/EnrichedMarkdownTextInput.mm +1089 -1023
  234. package/ios/input/InputStylePropsUtils.h +109 -68
  235. package/ios/input/internals/EnrichedMarkdownTextInputShadowNode.mm +2 -1
  236. package/ios/input/styles/ENRMBlockHandler.h +46 -0
  237. package/ios/input/styles/ENRMHeadingBlockHandler.h +15 -0
  238. package/ios/input/styles/ENRMHeadingBlockHandler.mm +38 -0
  239. package/ios/input/styles/ENRMOrderedListBlockHandler.h +14 -0
  240. package/ios/input/styles/ENRMOrderedListBlockHandler.mm +48 -0
  241. package/ios/input/styles/ENRMUnorderedListBlockHandler.h +15 -0
  242. package/ios/input/styles/ENRMUnorderedListBlockHandler.mm +51 -0
  243. package/ios/internals/ENRMViewFreeMeasurement.h +331 -0
  244. package/ios/internals/EnrichedMarkdownShadowNode.h +1 -3
  245. package/ios/internals/EnrichedMarkdownShadowNode.mm +16 -20
  246. package/ios/internals/EnrichedMarkdownTextShadowNode.h +1 -3
  247. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +15 -20
  248. package/ios/internals/MeasurementCache.h +33 -7
  249. package/ios/internals/ShadowMeasurementUtils.h +63 -58
  250. package/ios/math/ENRMMathChipController.h +74 -0
  251. package/ios/math/ENRMMathChipController.mm +476 -0
  252. package/ios/math/ENRMRaTeXBridge.swift +4 -1
  253. package/ios/parser/ENRMMarkdownParser.h +1 -8
  254. package/ios/parser/ENRMMarkdownParser.mm +2 -11
  255. package/ios/parser/MarkdownASTNode.h +2 -1
  256. package/ios/parser/MarkdownParserBridge.mm +4 -0
  257. package/ios/renderer/AttributedRenderer.m +20 -2
  258. package/ios/renderer/BaseRenderer.h +17 -2
  259. package/ios/renderer/BaseRenderer.m +30 -0
  260. package/ios/renderer/BlockquoteRenderer.h +1 -1
  261. package/ios/renderer/BlockquoteRenderer.m +144 -35
  262. package/ios/renderer/CodeBlockRenderer.h +1 -1
  263. package/ios/renderer/CodeBlockRenderer.m +27 -23
  264. package/ios/renderer/CodeRenderer.h +1 -1
  265. package/ios/renderer/CodeRenderer.m +3 -1
  266. package/ios/renderer/ENRMImageRenderer.h +1 -1
  267. package/ios/renderer/ENRMImageRenderer.m +5 -1
  268. package/ios/renderer/ENRMMathInlineRenderer.h +1 -1
  269. package/ios/renderer/ENRMMathInlineRenderer.m +5 -1
  270. package/ios/renderer/ENRMSpoilerRenderer.h +1 -1
  271. package/ios/renderer/ENRMSpoilerRenderer.m +3 -1
  272. package/ios/renderer/EmphasisRenderer.h +1 -1
  273. package/ios/renderer/EmphasisRenderer.m +3 -1
  274. package/ios/renderer/HeadingRenderer.h +1 -1
  275. package/ios/renderer/HeadingRenderer.m +13 -6
  276. package/ios/renderer/HighlightRenderer.h +1 -1
  277. package/ios/renderer/HighlightRenderer.m +3 -1
  278. package/ios/renderer/LinkRenderer.h +1 -1
  279. package/ios/renderer/LinkRenderer.m +3 -1
  280. package/ios/renderer/ListItemRenderer.h +13 -1
  281. package/ios/renderer/ListItemRenderer.m +102 -44
  282. package/ios/renderer/ListRenderer.h +2 -2
  283. package/ios/renderer/ListRenderer.m +63 -25
  284. package/ios/renderer/ParagraphRenderer.h +1 -1
  285. package/ios/renderer/ParagraphRenderer.m +16 -8
  286. package/ios/renderer/RenderContext.h +10 -0
  287. package/ios/renderer/RenderContext.m +44 -0
  288. package/ios/renderer/RendererFactory.m +14 -0
  289. package/ios/renderer/StrikethroughRenderer.h +1 -1
  290. package/ios/renderer/StrikethroughRenderer.m +3 -1
  291. package/ios/renderer/StrongRenderer.h +1 -1
  292. package/ios/renderer/StrongRenderer.m +3 -1
  293. package/ios/renderer/SubscriptRenderer.h +1 -1
  294. package/ios/renderer/SubscriptRenderer.m +3 -1
  295. package/ios/renderer/SuperscriptRenderer.h +1 -1
  296. package/ios/renderer/SuperscriptRenderer.m +3 -1
  297. package/ios/renderer/TextRenderer.h +2 -2
  298. package/ios/renderer/TextRenderer.m +3 -1
  299. package/ios/renderer/ThematicBreakRenderer.h +1 -1
  300. package/ios/renderer/ThematicBreakRenderer.m +3 -1
  301. package/ios/renderer/UnderlineRenderer.h +1 -1
  302. package/ios/renderer/UnderlineRenderer.m +3 -1
  303. package/ios/styles/StyleConfig.h +22 -0
  304. package/ios/styles/StyleConfig.mm +132 -0
  305. package/ios/utils/BlockquoteBorder.h +6 -0
  306. package/ios/utils/BlockquoteBorder.m +249 -52
  307. package/ios/utils/CodeBlockBackground.m +16 -2
  308. package/ios/utils/ENRMAtomicSize.h +65 -0
  309. package/ios/utils/ENRMBlockMenu.h +57 -0
  310. package/ios/utils/ENRMCodeBlockContent.h +37 -0
  311. package/ios/utils/ENRMCodeBlockContent.mm +79 -0
  312. package/ios/utils/ENRMCodeBlockHighlighter.h +37 -0
  313. package/ios/utils/ENRMCodeBlockHighlighter.mm +51 -0
  314. package/ios/utils/ENRMFeatureFlags.h +7 -0
  315. package/ios/utils/ENRMImageDownloader.h +11 -1
  316. package/ios/utils/ENRMImageDownloader.m +60 -16
  317. package/ios/utils/ENRMLocalImageLoader.h +31 -0
  318. package/ios/utils/ENRMLocalImageLoader.m +68 -0
  319. package/ios/utils/ENRMMathFallback.h +26 -0
  320. package/ios/utils/ENRMMathFallback.m +18 -0
  321. package/ios/utils/ENRMTextViewSetup.h +35 -17
  322. package/ios/utils/EditMenuUtils.h +22 -5
  323. package/ios/utils/EditMenuUtils.m +53 -9
  324. package/ios/utils/ImageRequestHeaderUtils.h +35 -0
  325. package/ios/utils/LastElementUtils.h +1 -0
  326. package/ios/utils/ListMarkerDrawer.m +29 -42
  327. package/ios/utils/MarkdownASTSerializer.m +4 -0
  328. package/ios/utils/ParagraphStyleUtils.h +5 -0
  329. package/ios/utils/ParagraphStyleUtils.m +77 -17
  330. package/ios/utils/RenderedMarkdownSegment.h +13 -1
  331. package/ios/utils/RenderedMarkdownSegment.m +20 -0
  332. package/ios/utils/SegmentRenderer.m +17 -4
  333. package/ios/utils/StreamingMarkdownFilter.h +11 -1
  334. package/ios/utils/StreamingMarkdownFilter.m +98 -2
  335. package/ios/utils/StylePropsUtils.h +63 -0
  336. package/ios/utils/TaskListTapUtils.m +24 -0
  337. package/ios/views/ENRMCodeBlockContainerView.h +53 -0
  338. package/ios/views/ENRMCodeBlockContainerView.m +559 -0
  339. package/ios/views/ENRMMathContainerView.h +16 -0
  340. package/ios/views/ENRMMathContainerView.m +74 -19
  341. package/ios/views/TableContainerView.h +22 -0
  342. package/ios/views/TableContainerView.m +261 -175
  343. package/jest.d.ts +1 -0
  344. package/jest.js +5 -0
  345. package/lib/module/EnrichedMarkdownNativeComponent.ts +73 -0
  346. package/lib/module/EnrichedMarkdownTextInput.js +84 -5
  347. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  348. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
  349. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +74 -0
  350. package/lib/module/headingDefaults.js +34 -0
  351. package/lib/module/headingDefaults.js.map +1 -0
  352. package/lib/module/index.js.map +1 -1
  353. package/lib/module/jest/index.js +191 -0
  354. package/lib/module/jest/index.js.map +1 -0
  355. package/lib/module/native/EnrichedMarkdownText.js +37 -5
  356. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  357. package/lib/module/normalizeMarkdownStyle.js +81 -19
  358. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  359. package/lib/module/normalizeMarkdownStyle.web.js +56 -18
  360. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  361. package/lib/module/normalizeMarkdownTextInputStyle.js +62 -15
  362. package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
  363. package/lib/module/normalizeMenuItem.js +1 -1
  364. package/lib/module/utils/textInputState.js +26 -0
  365. package/lib/module/utils/textInputState.js.map +1 -0
  366. package/lib/module/web/EnrichedMarkdownText.js +9 -5
  367. package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
  368. package/lib/module/web/parseMarkdown.js +4 -3
  369. package/lib/module/web/parseMarkdown.js.map +1 -1
  370. package/lib/module/web/renderers/InlineRenderers.js +6 -0
  371. package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
  372. package/lib/module/web/renderers/ListRenderers.js +7 -2
  373. package/lib/module/web/renderers/ListRenderers.js.map +1 -1
  374. package/lib/module/web/renderers/index.js +7 -4
  375. package/lib/module/web/renderers/index.js.map +1 -1
  376. package/lib/module/web/styles.js +83 -6
  377. package/lib/module/web/styles.js.map +1 -1
  378. package/lib/module/web/utils.js +1 -0
  379. package/lib/module/web/utils.js.map +1 -1
  380. package/lib/module/web/wasm/md4c.js +0 -0
  381. package/lib/module/web/wasm/md4c.js.map +1 -1
  382. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +66 -0
  383. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  384. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +94 -9
  385. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  386. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +69 -5
  387. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  388. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +66 -0
  389. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  390. package/lib/typescript/src/headingDefaults.d.ts +9 -0
  391. package/lib/typescript/src/headingDefaults.d.ts.map +1 -0
  392. package/lib/typescript/src/index.d.ts +2 -2
  393. package/lib/typescript/src/index.d.ts.map +1 -1
  394. package/lib/typescript/src/index.web.d.ts +1 -1
  395. package/lib/typescript/src/index.web.d.ts.map +1 -1
  396. package/lib/typescript/src/jest/index.d.ts +6 -0
  397. package/lib/typescript/src/jest/index.d.ts.map +1 -0
  398. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +3 -3
  399. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  400. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  401. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  402. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +19 -5
  403. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
  404. package/lib/typescript/src/types/MarkdownStyle.d.ts +72 -0
  405. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  406. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +27 -0
  407. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  408. package/lib/typescript/src/types/MarkdownTextProps.d.ts +104 -9
  409. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  410. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +19 -0
  411. package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
  412. package/lib/typescript/src/types/events.d.ts +19 -0
  413. package/lib/typescript/src/types/events.d.ts.map +1 -1
  414. package/lib/typescript/src/utils/textInputState.d.ts +24 -0
  415. package/lib/typescript/src/utils/textInputState.d.ts.map +1 -0
  416. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
  417. package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
  418. package/lib/typescript/src/web/parseMarkdown.d.ts +1 -1
  419. package/lib/typescript/src/web/parseMarkdown.d.ts.map +1 -1
  420. package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
  421. package/lib/typescript/src/web/renderers/ListRenderers.d.ts.map +1 -1
  422. package/lib/typescript/src/web/renderers/index.d.ts +2 -1
  423. package/lib/typescript/src/web/renderers/index.d.ts.map +1 -1
  424. package/lib/typescript/src/web/styles.d.ts +2 -1
  425. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  426. package/lib/typescript/src/web/types.d.ts +6 -2
  427. package/lib/typescript/src/web/types.d.ts.map +1 -1
  428. package/lib/typescript/src/web/utils.d.ts.map +1 -1
  429. package/package.json +36 -9
  430. package/postinstall.mjs +131 -0
  431. package/ratex-version.json +21 -0
  432. package/src/EnrichedMarkdownNativeComponent.ts +73 -0
  433. package/src/EnrichedMarkdownTextInput.tsx +159 -8
  434. package/src/EnrichedMarkdownTextInputNativeComponent.ts +78 -5
  435. package/src/EnrichedMarkdownTextNativeComponent.ts +74 -0
  436. package/src/headingDefaults.ts +21 -0
  437. package/src/index.tsx +9 -0
  438. package/src/index.web.tsx +1 -0
  439. package/src/jest/index.tsx +215 -0
  440. package/src/native/EnrichedMarkdownText.tsx +53 -2
  441. package/src/normalizeMarkdownStyle.ts +102 -16
  442. package/src/normalizeMarkdownStyle.web.ts +63 -15
  443. package/src/normalizeMarkdownTextInputStyle.ts +110 -35
  444. package/src/normalizeMenuItem.ts +1 -1
  445. package/src/types/MarkdownStyle.ts +74 -0
  446. package/src/types/MarkdownStyleInternal.ts +39 -0
  447. package/src/types/MarkdownTextProps.ts +101 -9
  448. package/src/types/MarkdownTextProps.web.ts +19 -0
  449. package/src/types/events.ts +21 -0
  450. package/src/utils/textInputState.ts +35 -0
  451. package/src/web/EnrichedMarkdownText.tsx +16 -2
  452. package/src/web/parseMarkdown.ts +6 -2
  453. package/src/web/renderers/InlineRenderers.tsx +5 -0
  454. package/src/web/renderers/ListRenderers.tsx +14 -2
  455. package/src/web/renderers/index.tsx +6 -2
  456. package/src/web/styles.ts +89 -5
  457. package/src/web/types.ts +6 -1
  458. package/src/web/utils.ts +1 -0
  459. package/src/web/wasm/md4c.js +0 -0
  460. package/vendor-grammars.mjs +511 -0
  461. package/vendor-ratex.mjs +173 -0
  462. package/app.plugin.js +0 -2
  463. package/plugin/build/withAndroidMath.js +0 -19
  464. package/plugin/build/withIosMath.js +0 -28
  465. package/plugin/build/withReactNativeEnrichedMarkdown.js +0 -11
  466. package/plugin/src/withAndroidMath.ts +0 -24
  467. package/plugin/src/withIosMath.ts +0 -35
  468. package/plugin/src/withReactNativeEnrichedMarkdown.ts +0 -17
  469. package/plugin/tsconfig.build.json +0 -16
@@ -1,26 +1,30 @@
1
1
  #import "EnrichedMarkdownTextInput.h"
2
2
  #import "ContextMenuUtils.h"
3
3
  #import "ENRMAutoLinkDetector.h"
4
+ #import "ENRMBlockEditCoordinator.h"
5
+ #import "ENRMBlockHandler.h"
6
+ #import "ENRMBlockStore.h"
7
+ #import "ENRMClipboardCoordinator.h"
4
8
  #import "ENRMDetectorPipeline.h"
5
- #import "ENRMFeatureFlags.h"
9
+ #import "ENRMEditPipeline.h"
10
+ #import "ENRMEditSession.h"
6
11
  #import "ENRMFormattingRange.h"
7
12
  #import "ENRMFormattingStore.h"
13
+ #import "ENRMInputEventEmitter.h"
8
14
  #import "ENRMInputFormatter.h"
9
15
  #import "ENRMInputLayoutManager.h"
10
16
  #import "ENRMInputLinkPrompt.h"
11
- #import "ENRMInputMentionCandidate.h"
17
+ #import "ENRMInputListMarkerDrawer.h"
12
18
  #import "ENRMInputParser.h"
13
19
  #import "ENRMInputTextView.h"
20
+ #import "ENRMInputTypingAttributesController.h"
21
+ #import "ENRMLinkCoordinator.h"
14
22
  #import "ENRMLinkRegexConfig.h"
15
- #import "ENRMMarkdownSerializer.h"
16
- #import "ENRMMathFontWeight.h"
17
- #if ENRICHED_MARKDOWN_MATH
18
- #import "ENRMMathInlineAttachment.h"
19
- #endif
23
+ #import "ENRMMathChipController.h"
24
+ #import "ENRMMentionCoordinator.h"
20
25
  #import "ENRMStyleHandler.h"
21
26
  #import "ENRMStyleMergingConfig.h"
22
27
  #import "ENRMUIKit.h"
23
- #import "ENRMWordsUtils.h"
24
28
  #import "EnrichedMarkdownTextInput+Internal.h"
25
29
  #import "InputStylePropsUtils.h"
26
30
  #import "ParagraphStyleUtils.h"
@@ -45,17 +49,25 @@
45
49
  using namespace facebook::react;
46
50
 
47
51
  #if !TARGET_OS_OSX
52
+ // UIGestureRecognizerDelegate is for the math chip tap: it has to run alongside
53
+ // UITextView's own selection gestures, never instead of them.
48
54
  @interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, UITextViewDelegate,
49
- UIGestureRecognizerDelegate>
55
+ ENRMEditPipelineHost, ENRMInputEventEmitterDataSource,
56
+ ENRMInputTypingAttributesDataSource, UIGestureRecognizerDelegate>
50
57
  #else
51
- @interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, RCTBackedTextInputDelegate>
58
+ @interface EnrichedMarkdownTextInput () <RCTEnrichedMarkdownTextInputViewProtocol, RCTBackedTextInputDelegate,
59
+ ENRMEditPipelineHost, ENRMInputEventEmitterDataSource,
60
+ ENRMInputTypingAttributesDataSource>
52
61
  #endif
53
62
  - (void)setupTextView;
54
63
  - (void)applyFormatting;
64
+ - (void)applyFormattingScopedToEditAtLocation:(NSUInteger)editLocation insertedLength:(NSUInteger)insertedLength;
55
65
  - (void)toggleInlineStyle:(ENRMInputStyleType)styleType;
56
66
  - (void)resetBaseTypingAttributes;
57
67
  @end
58
68
 
69
+ static const NSTimeInterval kENRMAtomicSnapPollInterval = 0.1;
70
+
59
71
  @implementation EnrichedMarkdownTextInput {
60
72
  ENRMPlatformTextView *_textView;
61
73
  ENRMInputLayoutManager *_layoutManager;
@@ -64,26 +76,30 @@ using namespace facebook::react;
64
76
  ENRMInputFormatter *_formatter;
65
77
  ENRMInputFormatterStyle *_formatterStyle;
66
78
  ENRMFormattingStore *_formattingStore;
67
- NSMutableSet<NSNumber *> *_pendingStyles;
68
- NSMutableSet<NSNumber *> *_pendingStyleRemovals;
69
- BOOL _isApplyingFormatting;
70
- BOOL _isTextChanging;
71
- BOOL _emitMarkdown;
79
+ ENRMBlockStore *_blockStore;
80
+ ENRMInputTypingAttributesController *_typingController;
81
+ ENRMInputEventEmitter *_inputEventEmitter;
82
+ ENRMEditSession *_editSession;
72
83
  BOOL _blockSyntax;
73
84
  NSString *_lastImportedMarkdown;
74
- CFTimeInterval _lastTextChangeTime;
75
85
 
76
86
  ENRMPlaceholderLabel *_placeholderLabel;
77
87
 
78
88
  NSUInteger _lastTextLength;
79
89
  NSRange _lastSelectedRange;
80
90
  NSRange _preEditSelectedRange;
81
-
82
- struct {
83
- BOOL bold, italic, underline, strikethrough, spoiler, link, initialized;
84
- } _prevState;
85
-
86
- std::optional<CGRect> _prevCaretRect;
91
+ BOOL _atomicSnapScheduled;
92
+
93
+ // Block type/level of the line being edited, captured before a text change so
94
+ // a Return that continues a list (or an autocorrect/paste that replaces the
95
+ // line) can restore the right block on the resulting line(s).
96
+ ENRMInputBlockType _preEditBlockType;
97
+ NSInteger _preEditBlockLevel;
98
+ BOOL _preEditParagraphWasEmpty;
99
+ // Whether the replaced range contained a line break, captured before the edit
100
+ // applies (the deleted characters are gone afterwards). Drives the full-vs-
101
+ // scoped reformat decision in handleTextChanged.
102
+ BOOL _preEditReplacedNewline;
87
103
 
88
104
  #if TARGET_OS_OSX
89
105
  NSScrollView *_scrollView;
@@ -91,25 +107,24 @@ using namespace facebook::react;
91
107
 
92
108
  NSArray<NSString *> *_contextMenuItemTexts;
93
109
  NSArray<NSString *> *_contextMenuItemIcons;
94
- NSArray<NSString *> *_mentionIndicators;
95
- NSString *_activeMentionIndicator;
96
- NSRange _activeMentionRange;
97
- NSString *_activeMentionText;
98
-
99
110
  ENRMAutoLinkDetector *_autoLinkDetector;
100
111
  ENRMDetectorPipeline *_detectorPipeline;
112
+ ENRMEditPipeline *_editPipeline;
113
+ ENRMBlockEditCoordinator *_blockCoordinator;
114
+ ENRMMentionCoordinator *_mentionCoordinator;
115
+ ENRMLinkCoordinator *_linkCoordinator;
116
+ ENRMClipboardCoordinator *_clipboardCoordinator;
117
+ ENRMMathChipController *_mathChipController;
118
+
119
+ // Buffer position of the math chip currently drawn selected, or NSNotFound.
120
+ NSUInteger _activeMathChipPosition;
121
+ // A caret move made by the chip tap itself must not clear the highlight the tap
122
+ // just set; consumed by the next selection change.
123
+ BOOL _suppressMathChipClearOnce;
101
124
 
102
125
  ENRMWritingDirectionMode _writingDirectionMode;
103
126
  NSWritingDirection _resolvedLayoutDirection;
104
127
 
105
- /// One-shot guard: when YES, the next textViewDidChangeSelection skips
106
- /// clearing math chip selections. Set by handleTapForMathChip right before
107
- /// it moves the caret to the chip, so the caret update doesn't immediately
108
- /// wipe the chip we just selected.
109
- BOOL _suppressMathChipClearOnce;
110
- /// Character index of the currently selected math chip, or NSNotFound.
111
- NSUInteger _activeMathChipIndex;
112
-
113
128
  ENRMInputSelectionMenuConfig _inputSelectionMenuConfig;
114
129
  ENRMFormatMenuConfig _formatMenuConfig;
115
130
 
@@ -125,6 +140,8 @@ using namespace facebook::react;
125
140
  NSString *_formatMenuLinkLabel;
126
141
  }
127
142
 
143
+ @synthesize editSession = _editSession;
144
+
128
145
  #pragma mark - Fabric lifecycle
129
146
 
130
147
  + (ComponentDescriptorProvider)componentDescriptorProvider
@@ -144,20 +161,16 @@ using namespace facebook::react;
144
161
  _props = defaultProps;
145
162
 
146
163
  self.backgroundColor = [RCTUIColor clearColor];
147
- _blockEmitting = NO;
148
164
  _blockSyntax = YES;
149
165
  _lastImportedMarkdown = nil;
150
166
  _heightUpdateCounter = 0;
151
167
  _formatter = [[ENRMInputFormatter alloc] init];
152
168
  _formatterStyle = [[ENRMInputFormatterStyle alloc] init];
153
169
  _formattingStore = [[ENRMFormattingStore alloc] init];
154
- _pendingStyles = [NSMutableSet set];
155
- _pendingStyleRemovals = [NSMutableSet set];
170
+ _blockStore = [[ENRMBlockStore alloc] init];
156
171
  _lastTextLength = 0;
157
172
  _lastSelectedRange = NSMakeRange(0, 0);
158
- _mentionIndicators = @[];
159
- _activeMentionRange = NSMakeRange(NSNotFound, 0);
160
- _activeMentionText = @"";
173
+ _mentionCoordinator = [[ENRMMentionCoordinator alloc] initWithFormattingStore:_formattingStore];
161
174
 
162
175
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
163
176
  _resolvedLayoutDirection =
@@ -166,12 +179,32 @@ using namespace facebook::react;
166
179
  _formatMenuConfig = (ENRMFormatMenuConfig){
167
180
  .bold = YES, .italic = YES, .underline = YES, .strikethrough = YES, .spoiler = YES, .link = YES};
168
181
 
182
+ _activeMathChipPosition = NSNotFound;
169
183
  _suppressMathChipClearOnce = NO;
170
- _activeMathChipIndex = NSNotFound;
171
184
 
172
185
  [self setupTextView];
186
+ _mathChipController = [[ENRMMathChipController alloc] initWithTextView:_textView style:_formatterStyle];
187
+ _editSession = [[ENRMEditSession alloc] initWithTextView:_textView];
188
+ _typingController = [[ENRMInputTypingAttributesController alloc] initWithTextView:_textView
189
+ formatterStyle:_formatterStyle
190
+ dataSource:self
191
+ editSession:_editSession];
192
+ _inputEventEmitter = [[ENRMInputEventEmitter alloc] initWithDataSource:self];
173
193
 
174
194
  [self setupDetectorPipeline];
195
+
196
+ _editPipeline = [[ENRMEditPipeline alloc] initWithFormattingStore:_formattingStore
197
+ blockStore:_blockStore
198
+ formatter:_formatter
199
+ detectorPipeline:_detectorPipeline
200
+ host:self];
201
+ _blockCoordinator = [[ENRMBlockEditCoordinator alloc] initWithBlockStore:_blockStore];
202
+ _linkCoordinator = [[ENRMLinkCoordinator alloc] initWithFormattingStore:_formattingStore
203
+ autoLinkDetector:_autoLinkDetector];
204
+ _clipboardCoordinator = [[ENRMClipboardCoordinator alloc] initWithFormattingStore:_formattingStore
205
+ blockStore:_blockStore
206
+ transientRangeProvider:_detectorPipeline
207
+ formatter:_formatter];
175
208
  }
176
209
  return self;
177
210
  }
@@ -184,7 +217,11 @@ using namespace facebook::react;
184
217
 
185
218
  __weak EnrichedMarkdownTextInput *weakSelf = self;
186
219
  _autoLinkDetector.onLinkDetected = ^(NSString *text, NSString *url, NSRange range) {
187
- [weakSelf emitOnLinkDetectedWithText:text url:url range:range];
220
+ EnrichedMarkdownTextInput *strongSelf = weakSelf;
221
+ if (strongSelf == nil) {
222
+ return;
223
+ }
224
+ [strongSelf->_inputEventEmitter emitOnLinkDetectedWithText:text url:url range:range];
188
225
  };
189
226
 
190
227
  _detectorPipeline = [[ENRMDetectorPipeline alloc] init];
@@ -205,6 +242,8 @@ using namespace facebook::react;
205
242
  ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero textContainer:textContainer];
206
243
  #else
207
244
  ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero];
245
+ _layoutManager = [[ENRMInputLayoutManager alloc] init];
246
+ [inputTextView.textContainer replaceLayoutManager:_layoutManager];
208
247
  #endif
209
248
  inputTextView.markdownTextInput = self;
210
249
  _textView = inputTextView;
@@ -241,124 +280,89 @@ using namespace facebook::react;
241
280
  _placeholderLabel = ENRMCreatePlaceholderLabel(_textView, _formatterStyle.baseFont);
242
281
  #if !TARGET_OS_OSX
243
282
  _placeholderLabel.adjustsFontForContentSizeCategory = YES;
283
+ _placeholderLabel.accessibilityElementsHidden = YES;
284
+ _placeholderLabel.isAccessibilityElement = NO;
244
285
  #endif
245
286
 
246
287
  [self resetBaseTypingAttributes];
247
288
 
248
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
249
- UITapGestureRecognizer *mathTap = [[UITapGestureRecognizer alloc] initWithTarget:self
250
- action:@selector(handleTapForMathChip:)];
251
- mathTap.cancelsTouchesInView = NO;
252
- mathTap.delaysTouchesBegan = NO;
253
- mathTap.delaysTouchesEnded = NO;
254
- mathTap.delegate = self;
255
- [_textView addGestureRecognizer:mathTap];
289
+ #if !TARGET_OS_OSX
290
+ if (ENRMMathChipController.isAvailable) {
291
+ UITapGestureRecognizer *mathTap = [[UITapGestureRecognizer alloc] initWithTarget:self
292
+ action:@selector(handleMathChipTap:)];
293
+ // The text view's own tap gestures must still run: swallowing the touch is
294
+ // what made the first tap on a chip fail to focus the field and raise the
295
+ // keyboard. This recognizer only adds behaviour, it never replaces any.
296
+ mathTap.cancelsTouchesInView = NO;
297
+ mathTap.delaysTouchesBegan = NO;
298
+ mathTap.delaysTouchesEnded = NO;
299
+ mathTap.delegate = self;
300
+ [_textView addGestureRecognizer:mathTap];
301
+ }
256
302
  #endif
257
303
  }
258
304
 
259
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
260
-
261
- // Glyph rect of the chip at `index`, in _textView coordinates (the same space
262
- // `locationInView:_textView` reports). Single source of truth for both the tap
263
- // hit-test and the rect reported to JS.
264
- - (CGRect)mathChipRectInTextViewAtIndex:(NSUInteger)index layoutManager:(NSLayoutManager *)lm
305
+ #if !TARGET_OS_OSX
306
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
307
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
265
308
  {
266
- if (lm == nil)
267
- return CGRectNull;
268
- NSRange glyphRange = [lm glyphRangeForCharacterRange:NSMakeRange(index, 1) actualCharacterRange:NULL];
269
- CGRect rect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
270
- rect.origin.x += _textView.textContainerInset.left;
271
- rect.origin.y += _textView.textContainerInset.top;
272
- return rect;
309
+ return YES;
273
310
  }
311
+ #endif
312
+
313
+ #if !TARGET_OS_OSX
314
+ #pragma mark - Accessibility
274
315
 
275
- - (void)handleTapForMathChip:(UITapGestureRecognizer *)gesture
316
+ // The custom TextKit 1 stack leaves the inner UITextView invisible to VoiceOver, so
317
+ // the host vends the content itself as one readable element.
318
+ - (BOOL)isAccessibilityElement
276
319
  {
277
- if (gesture.state != UIGestureRecognizerStateRecognized)
278
- return;
279
- CGPoint loc = [gesture locationInView:_textView];
280
- UITextPosition *pos = [_textView closestPositionToPoint:loc];
281
- if (!pos)
282
- return;
283
- NSInteger idx = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:pos];
284
- if (idx < 0)
285
- return;
286
- NSTextStorage *storage = _textView.textStorage;
287
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
288
- // The closest position can land just past the chip, so probe both `idx` and `idx-1`.
289
- // `closestPositionToPoint:` has no distance bound — it clamps a tap anywhere in the
290
- // text view (blank space past the text, textContainerInset padding) onto the nearest
291
- // character. Requiring the touch to fall inside the chip's own glyph rect is what
292
- // keeps a tap beside a chip from reading as a tap on it.
293
- ENRMMathInlineAttachment *att = nil;
294
- NSUInteger chipIdx = NSNotFound;
295
- for (NSInteger probe = idx; probe >= idx - 1; probe--) {
296
- if (probe < 0 || (NSUInteger)probe >= storage.length)
297
- continue;
298
- id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)probe effectiveRange:NULL];
299
- if (![v isKindOfClass:[ENRMMathInlineAttachment class]])
300
- continue;
301
- // An empty rect means the layout could not give us the chip's extent; accept the
302
- // candidate then, rather than making the chip untappable.
303
- CGRect rect = [self mathChipRectInTextViewAtIndex:(NSUInteger)probe layoutManager:lm];
304
- if (!CGRectIsEmpty(rect) && !CGRectContainsPoint(rect, loc))
305
- continue;
306
- att = v;
307
- chipIdx = (NSUInteger)probe;
308
- break;
309
- }
310
- if (!att)
311
- return;
320
+ return YES;
321
+ }
312
322
 
313
- NSRange chipRange = NSMakeRange(chipIdx, 1);
314
- [self clearMathChipSelectionsExcept:chipRange];
315
- att.isSelected = YES;
316
- [att invalidateRender];
317
- if (lm) {
318
- [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
319
- [lm ensureLayoutForCharacterRange:chipRange];
323
+ - (NSString *)accessibilityValue
324
+ {
325
+ NSString *text = ENRMGetPlainText(_textView);
326
+ if (text.length > 0) {
327
+ return text;
320
328
  }
321
- ENRMSetNeedsDisplay(_textView);
322
- // Place caret right after the chip (length 0) instead of selecting the chip
323
- // — a length-1 selection would make UITextView draw its blue range handles
324
- // on top of our custom selected-chip background.
325
- NSUInteger caretIndex = chipIdx + 1;
326
- if (caretIndex > storage.length)
327
- caretIndex = storage.length;
328
- _activeMathChipIndex = chipIdx;
329
- _suppressMathChipClearOnce = YES;
330
- _textView.selectedRange = NSMakeRange(caretIndex, 0);
329
+ if (_placeholderLabel.text.length > 0) {
330
+ return _placeholderLabel.text;
331
+ }
332
+ return [super accessibilityValue];
333
+ }
334
+
335
+ - (BOOL)accessibilityActivate
336
+ {
337
+ ENRMFocusTextView(_textView);
338
+ [super accessibilityActivate];
339
+ return YES;
340
+ }
341
+ #endif
331
342
 
332
- // Recomputed rather than reused from the hit-test: the layout was invalidated above.
333
- CGRect glyphRect = [self mathChipRectInTextViewAtIndex:chipIdx layoutManager:lm];
334
- CGRect rectInSelf = [self.contentView convertRect:glyphRect fromView:_textView];
343
+ #pragma mark - Paste
335
344
 
336
- [self emitOnMathPressWithLatex:att.latex start:chipIdx end:chipIdx + 1 displayMode:att.displayMode rect:rectInSelf];
345
+ - (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown
346
+ {
347
+ [_inputEventEmitter emitOnPasteWithText:text markdown:markdown];
337
348
  }
338
349
 
339
- - (void)emitOnMathPressWithLatex:(NSString *)latex
340
- start:(NSUInteger)start
341
- end:(NSUInteger)end
342
- displayMode:(BOOL)displayMode
343
- rect:(CGRect)rect
350
+ /// The single exit for an intercepted paste. One command rather than two makes
351
+ /// "never swallow a paste" structural: the branch that turns the clipboard into
352
+ /// something else still has to come through here, with empty strings.
353
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown
344
354
  {
345
- auto emitter = [self getEventEmitter];
346
- if (emitter == nullptr)
355
+ // Round-trips the formatting when the copy came from an input of ours;
356
+ // without this a copy-then-paste inside the app would land as flat text.
357
+ if (markdown.length > 0) {
358
+ [self pasteMarkdown:markdown];
347
359
  return;
348
- emitter->onMathPress({
349
- .latex = std::string([latex UTF8String] ?: ""),
350
- .start = static_cast<int>(start),
351
- .end = static_cast<int>(end),
352
- .displayMode = displayMode ? true : false,
353
- .x = rect.origin.x,
354
- .y = rect.origin.y,
355
- .width = rect.size.width,
356
- .height = rect.size.height,
357
- });
360
+ }
361
+ // Empty text with an empty selection is a zero-length edit: it still emits
362
+ // onChangeText with unchanged content, which the caller reads as a no-op.
363
+ [self replaceSelectedTextWith:text formattingRanges:@[]];
358
364
  }
359
365
 
360
- #endif
361
-
362
366
  #pragma mark - State
363
367
 
364
368
  - (void)updateState:(const facebook::react::State::Shared &)state
@@ -456,6 +460,10 @@ using namespace facebook::react;
456
460
  }
457
461
  }
458
462
 
463
+ if (newViewProps.interceptPaste != oldViewProps.interceptPaste) {
464
+ _interceptPaste = newViewProps.interceptPaste;
465
+ }
466
+
459
467
  if (newViewProps.multiline != oldViewProps.multiline) {
460
468
  _textView.textContainer.maximumNumberOfLines = newViewProps.multiline ? 0 : 1;
461
469
  _textView.textContainer.lineBreakMode =
@@ -483,7 +491,7 @@ using namespace facebook::react;
483
491
  ENRMApplySelectionColor(_textView, newViewProps.selectionColor);
484
492
  }
485
493
 
486
- _emitMarkdown = newViewProps.isOnChangeMarkdownSet;
494
+ _inputEventEmitter.emitMarkdown = newViewProps.isOnChangeMarkdownSet;
487
495
 
488
496
  {
489
497
  auto configFromProp = [](const auto &prop) {
@@ -551,11 +559,8 @@ using namespace facebook::react;
551
559
  [indicators addObject:value];
552
560
  }
553
561
  }
554
- _mentionIndicators = [indicators copy];
555
- if (_activeMentionIndicator != nil && ![_mentionIndicators containsObject:_activeMentionIndicator]) {
556
- [self clearActiveMention:_activeMentionIndicator];
557
- }
558
- [self updateActiveMention];
562
+ [self dispatchMentionEvents:[_mentionCoordinator setIndicators:indicators]];
563
+ [self dispatchMentionUpdate];
559
564
  }
560
565
 
561
566
  BOOL styleChanged = applyInputStyleProps(_formatterStyle, newViewProps, oldViewProps);
@@ -592,9 +597,11 @@ using namespace facebook::react;
592
597
  [self applyFormatting];
593
598
  }
594
599
 
595
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
596
- [self refreshAllMathChips];
597
- #endif
600
+ // Chips cache the style they were built with, so a style pass has to re-stamp
601
+ // them. Not gated on a style diff: every field is a flat scalar on
602
+ // `_formatterStyle`, so there is no cheap "did the math style change" test,
603
+ // and `refreshAll` already no-ops when the buffer holds no chip.
604
+ [_mathChipController refreshAll];
598
605
 
599
606
  [self requestHeightUpdate];
600
607
  } else if (writingDirectionChanged && _textView.textStorage.length > 0) {
@@ -618,20 +625,23 @@ using namespace facebook::react;
618
625
  return;
619
626
  }
620
627
 
621
- dispatch_async(dispatch_get_main_queue(), ^{
622
- NSUInteger textLength = self->_textView.textStorage.length;
623
- if (textLength == 0) {
624
- return;
625
- }
626
- NSRange wholeRange = NSMakeRange(0, textLength);
627
- NSRange actualRange = NSMakeRange(0, 0);
628
- [self->_textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
629
- [self->_textView.layoutManager ensureLayoutForCharacterRange:actualRange];
630
- [self->_textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
631
-
632
- CGSize measuredSize = [self measureSize:self->_textView.frame.size.width];
633
- ENRMSetContentSize(self->_textView, measuredSize);
634
- });
628
+ NSUInteger textLength = _textView.textStorage.length;
629
+ if (textLength == 0) {
630
+ return;
631
+ }
632
+
633
+ NSRange wholeRange = NSMakeRange(0, textLength);
634
+ [_textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:NULL];
635
+ [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer];
636
+ [_textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
637
+
638
+ CGSize measuredSize = [self measureSize:_textView.frame.size.width];
639
+ CGSize currentSize = _textView.contentSize;
640
+ BOOL sizeChanged =
641
+ fabs(currentSize.width - measuredSize.width) > 0.5 || fabs(currentSize.height - measuredSize.height) > 0.5;
642
+ if (sizeChanged) {
643
+ ENRMSetContentSize(_textView, measuredSize);
644
+ }
635
645
  }
636
646
 
637
647
  #pragma mark - Window attachment
@@ -685,14 +695,6 @@ using namespace facebook::react;
685
695
  [_textView mouseDown:event];
686
696
  }
687
697
 
688
- #else // iOS
689
-
690
- - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
691
- shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
692
- {
693
- return YES;
694
- }
695
-
696
698
  #endif
697
699
 
698
700
  #if !TARGET_OS_OSX
@@ -713,11 +715,34 @@ using namespace facebook::react;
713
715
  }
714
716
  #endif
715
717
 
718
+ #pragma mark - ENRMEditPipelineHost
719
+
720
+ /// Backs `onChangeText`. A math chip stays as its raw `kENRMObjectReplacement`
721
+ /// character here — do not strip or expand it. `onChangeSelection`,
722
+ /// `onCaretRectChange` and the three math commands all speak in buffer offsets
723
+ /// where one formula is exactly one character, so removing it would put every
724
+ /// offset JS holds out by one per chip, and expanding it to `$latex$` would be
725
+ /// worse still. Use `onChangeMarkdown` when you want `$...$`.
726
+ - (NSString *)plainText
727
+ {
728
+ return ENRMGetPlainText(_textView);
729
+ }
730
+
731
+ - (NSTextStorage *)textStorage
732
+ {
733
+ return _textView.textStorage;
734
+ }
735
+
716
736
  #pragma mark - Placeholder
717
737
 
718
738
  - (void)updatePlaceholderVisibility
719
739
  {
720
- _placeholderLabel.hidden = (ENRMGetPlainText(_textView).length > 0);
740
+ // Hide the placeholder once there's any content OR any block has been started
741
+ // (e.g. an empty bullet/heading line with only a zero-length anchor): the block
742
+ // marker or indent would otherwise overlap the placeholder text.
743
+ BOOL hasText = ENRMGetPlainText(_textView).length > 0;
744
+ BOOL hasBlock = _blockStore.allRanges.count > 0;
745
+ _placeholderLabel.hidden = hasText || hasBlock;
721
746
  }
722
747
 
723
748
  #pragma mark - Markdown import
@@ -728,167 +753,140 @@ using namespace facebook::react;
728
753
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
729
754
  ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
730
755
 
731
- _blockEmitting = YES;
756
+ [_editSession enterPhase:ENRMEditPhaseImporting];
732
757
 
733
- _isApplyingFormatting = YES;
758
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
734
759
  ENRMSetPlainText(_textView, parsed.plainText);
760
+ [_editSession enterPhase:ENRMEditPhaseImporting];
735
761
 
736
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
737
- if (parsed.mathAtoms.count > 0) {
738
- NSTextStorage *storage = _textView.textStorage;
739
- [storage beginEditing];
740
- for (ENRMMathAtom *atom in parsed.mathAtoms) {
741
- if (atom.position >= storage.length)
742
- continue;
743
- unichar c = [storage.string characterAtIndex:atom.position];
744
- if (c != (unichar)0xFFFC)
745
- continue;
746
- ENRMMathInlineAttachment *att = [self createMathAttachmentWithLatex:atom.latex displayMode:atom.displayMode];
747
- NSAttributedString *replacement = [NSAttributedString attributedStringWithAttachment:att];
748
- [storage replaceCharactersInRange:NSMakeRange(atom.position, 1) withAttributedString:replacement];
749
- if (atom.displayMode && _formatterStyle.mathBlockAlignment != NSTextAlignmentNatural) {
750
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
751
- ps.alignment = _formatterStyle.mathBlockAlignment;
752
- [storage addAttribute:NSParagraphStyleAttributeName value:ps range:NSMakeRange(atom.position, 1)];
753
- }
754
- }
755
- [storage endEditing];
756
- }
757
- #endif
758
-
759
- _isApplyingFormatting = NO;
762
+ // Stamped inside the Importing phase, before the stores are read back: the
763
+ // parser already reserved one placeholder character per formula, so this only
764
+ // hangs an attachment on characters that are part of `parsed.plainText`. No
765
+ // deferral is needed the way Android needs one — `applyInputStyleProps` runs at
766
+ // the top of `updateProps`, so `_formatterStyle` is current before a
767
+ // `defaultValue` import ever reaches here.
768
+ [_mathChipController applyAtoms:parsed.mathAtoms offset:0];
760
769
 
761
770
  [_formattingStore setRanges:parsed.formattingRanges];
771
+ [_blockStore setRanges:parsed.blockRanges];
762
772
  _lastTextLength = parsed.plainText.length;
763
773
  _lastSelectedRange = _textView.selectedRange;
774
+
775
+ [_editSession exitPhase];
764
776
  [self applyFormatting];
765
777
  [self updatePlaceholderVisibility];
766
778
 
767
- _blockEmitting = NO;
768
- }
769
-
770
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
771
-
772
- - (void)applyMathStyleToAttachment:(ENRMMathInlineAttachment *)att
773
- {
774
- if (att.displayMode) {
775
- att.fontSize = _formatterStyle.mathBlockFontSize > 0 ? _formatterStyle.mathBlockFontSize : 20.0;
776
- att.mathTextColor = _formatterStyle.mathBlockColor ?: _formatterStyle.baseTextColor;
777
- att.containerBackgroundColor = _formatterStyle.mathBlockBackgroundColor;
778
- att.containerBorderColor = _formatterStyle.mathBlockBorderColor;
779
- att.containerBorderWidth = _formatterStyle.mathBlockBorderWidth;
780
- att.containerBorderRadius = _formatterStyle.mathBlockBorderRadius;
781
- att.containerPaddingHorizontal = _formatterStyle.mathBlockPaddingHorizontal;
782
- att.containerPaddingVertical = _formatterStyle.mathBlockPaddingVertical;
783
- att.blockMarginTop = _formatterStyle.mathBlockMarginTop;
784
- att.blockMarginBottom = _formatterStyle.mathBlockMarginBottom;
785
- att.blockAlignment = _formatterStyle.mathBlockAlignment;
786
- att.selectedBackgroundColor = _formatterStyle.mathBlockSelectedBackgroundColor;
787
- att.selectedBorderColor = _formatterStyle.mathBlockSelectedBorderColor;
788
- att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathBlockFontWeight);
789
- } else {
790
- att.fontSize = _formatterStyle.mathFontSize > 0 ? _formatterStyle.mathFontSize : 16.0;
791
- att.mathTextColor = _formatterStyle.mathColor ?: _formatterStyle.baseTextColor;
792
- att.containerBackgroundColor = _formatterStyle.mathBackgroundColor;
793
- att.containerBorderColor = _formatterStyle.mathBorderColor;
794
- att.containerBorderWidth = _formatterStyle.mathBorderWidth;
795
- att.containerBorderRadius = _formatterStyle.mathBorderRadius;
796
- att.containerPaddingHorizontal = _formatterStyle.mathPaddingHorizontal;
797
- att.containerPaddingVertical = _formatterStyle.mathPaddingVertical;
798
- att.blockMarginTop = 0;
799
- att.blockMarginBottom = 0;
800
- att.blockAlignment = NSTextAlignmentNatural;
801
- att.selectedBackgroundColor = _formatterStyle.mathSelectedBackgroundColor;
802
- att.selectedBorderColor = _formatterStyle.mathSelectedBorderColor;
803
- att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathFontWeight);
804
- }
805
- }
806
-
807
- - (ENRMMathInlineAttachment *)createMathAttachmentWithLatex:(NSString *)latex displayMode:(BOOL)displayMode
808
- {
809
- ENRMMathInlineAttachment *att = [[ENRMMathInlineAttachment alloc] init];
810
- att.latex = latex ?: @"";
811
- att.displayMode = displayMode;
812
- [self applyMathStyleToAttachment:att];
813
- return att;
814
- }
815
-
816
- - (void)refreshAllMathChips
817
- {
818
- NSTextStorage *storage = _textView.textStorage;
819
- if (storage.length == 0)
820
- return;
821
- __weak EnrichedMarkdownTextInput *weakSelf = self;
822
- [storage beginEditing];
823
- [storage enumerateAttribute:NSAttachmentAttributeName
824
- inRange:NSMakeRange(0, storage.length)
825
- options:0
826
- usingBlock:^(id value, NSRange range, BOOL *stop) {
827
- EnrichedMarkdownTextInput *strongSelf = weakSelf;
828
- if (!strongSelf)
829
- return;
830
- if ([value isKindOfClass:[ENRMMathInlineAttachment class]]) {
831
- ENRMMathInlineAttachment *att = value;
832
- [strongSelf applyMathStyleToAttachment:att];
833
- [att invalidateRender];
834
- }
835
- }];
836
- [storage endEditing];
837
-
838
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
839
- if (lm) {
840
- NSRange fullRange = NSMakeRange(0, storage.length);
841
- [lm invalidateLayoutForCharacterRange:fullRange actualCharacterRange:NULL];
842
- [lm ensureLayoutForCharacterRange:fullRange];
779
+ if (parsed.plainText.length == 0) {
780
+ [self resetBaseTypingAttributes];
843
781
  }
844
- ENRMSetNeedsDisplay(_textView);
845
782
  }
846
783
 
847
- #endif
848
-
849
784
  - (void)replaceTextInRange:(NSRange)selection
850
785
  withText:(NSString *)text
851
786
  formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
787
+ blockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
852
788
  {
853
789
  NSUInteger editLocation = selection.location;
854
790
 
855
- _isApplyingFormatting = YES;
791
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
856
792
  ENRMReplaceTextInRange(_textView, text, selection);
857
- _isApplyingFormatting = NO;
793
+ [_editSession exitPhase];
858
794
 
859
- [_formattingStore adjustForEditAtLocation:editLocation deletedLength:selection.length insertedLength:text.length];
795
+ NSString *plainText = [self adjustStoresForEditAtLocation:editLocation
796
+ deletedLength:selection.length
797
+ insertedLength:text.length];
860
798
 
861
799
  for (ENRMFormattingRange *range in ranges) {
862
800
  NSRange shifted = NSMakeRange(range.range.location + editLocation, range.range.length);
863
801
  [_formattingStore addRange:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
864
802
  }
865
803
 
866
- _lastTextLength = ENRMGetPlainText(_textView).length;
804
+ for (ENRMBlockRange *block in blockRanges) {
805
+ NSRange shifted = NSMakeRange(block.range.location + editLocation, block.range.length);
806
+ [_blockStore setBlockType:block.type level:block.level forParagraphRange:shifted inText:plainText];
807
+ }
808
+
809
+ _lastTextLength = plainText.length;
867
810
  _lastSelectedRange = _textView.selectedRange;
868
811
 
869
812
  [self applyFormatting];
870
813
 
871
- [_detectorPipeline processTextChange:ENRMGetPlainText(_textView)
872
- modificationRange:NSMakeRange(editLocation, text.length)];
814
+ [_detectorPipeline processTextChange:plainText modificationRange:NSMakeRange(editLocation, text.length)];
873
815
 
874
816
  [self updatePlaceholderVisibility];
875
- [self emitOnChangeText];
876
- [self emitOnChangeSelection];
877
- [self emitFormattingChanged];
817
+ [_inputEventEmitter emitOnChangeText];
818
+ [_inputEventEmitter emitOnChangeSelection];
819
+ [_inputEventEmitter emitFormattingChanged];
878
820
  [self requestHeightUpdate];
879
821
  [self scheduleRelayoutIfNeeded];
880
822
  }
881
823
 
824
+ - (void)replaceTextInRange:(NSRange)selection
825
+ withText:(NSString *)text
826
+ formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
827
+ {
828
+ [self replaceTextInRange:selection withText:text formattingRanges:ranges blockRanges:@[]];
829
+ }
830
+
882
831
  - (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
883
832
  {
884
833
  [self replaceTextInRange:_textView.selectedRange withText:text formattingRanges:ranges];
885
834
  }
886
835
 
836
+ /// Insertion is literal: the string's characters land at the cursor exactly as
837
+ /// given. Markdown parsing consumes leading and trailing newlines as block
838
+ /// structure, so they are split off before the parse and re-attached verbatim;
839
+ /// without this, inserting "\n- item\n" mid-paragraph would merge the list line
840
+ /// with the surrounding text. Callers decide separation by including (or
841
+ /// omitting) newlines in the string.
887
842
  - (void)pasteMarkdown:(NSString *)markdown
888
843
  {
844
+ NSUInteger lead = 0;
845
+ while (lead < markdown.length && [markdown characterAtIndex:lead] == '\n') {
846
+ lead++;
847
+ }
848
+ NSUInteger trail = 0;
849
+ while (lead + trail < markdown.length && [markdown characterAtIndex:markdown.length - 1 - trail] == '\n') {
850
+ trail++;
851
+ }
852
+ NSString *core = [markdown substringWithRange:NSMakeRange(lead, markdown.length - lead - trail)];
853
+
889
854
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
890
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
891
- [self replaceSelectedTextWith:parsed.plainText formattingRanges:parsed.formattingRanges];
855
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:core blockSyntax:_blockSyntax];
856
+
857
+ if (lead == 0 && trail == 0) {
858
+ NSUInteger insertLocation = _textView.selectedRange.location;
859
+ [self replaceTextInRange:_textView.selectedRange
860
+ withText:parsed.plainText
861
+ formattingRanges:parsed.formattingRanges
862
+ blockRanges:parsed.blockRanges];
863
+ // Positions are relative to the parsed fragment, so shift them onto the spot
864
+ // it landed. Pasted markdown carries formulas like any other source.
865
+ [_mathChipController applyAtoms:parsed.mathAtoms offset:insertLocation];
866
+ return;
867
+ }
868
+
869
+ NSString *prefix = [@"" stringByPaddingToLength:lead withString:@"\n" startingAtIndex:0];
870
+ NSString *suffix = [@"" stringByPaddingToLength:trail withString:@"\n" startingAtIndex:0];
871
+ NSString *plainText = [NSString stringWithFormat:@"%@%@%@", prefix, parsed.plainText, suffix];
872
+
873
+ NSMutableArray<ENRMFormattingRange *> *formattingRanges =
874
+ [NSMutableArray arrayWithCapacity:parsed.formattingRanges.count];
875
+ for (ENRMFormattingRange *range in parsed.formattingRanges) {
876
+ NSRange shifted = NSMakeRange(range.range.location + lead, range.range.length);
877
+ [formattingRanges addObject:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
878
+ }
879
+ NSMutableArray<ENRMBlockRange *> *blockRanges = [NSMutableArray arrayWithCapacity:parsed.blockRanges.count];
880
+ for (ENRMBlockRange *block in parsed.blockRanges) {
881
+ NSRange shifted = NSMakeRange(block.range.location + lead, block.range.length);
882
+ [blockRanges addObject:[ENRMBlockRange rangeWithType:block.type range:shifted level:block.level]];
883
+ }
884
+ NSUInteger insertLocation = _textView.selectedRange.location;
885
+ [self replaceTextInRange:_textView.selectedRange
886
+ withText:plainText
887
+ formattingRanges:formattingRanges
888
+ blockRanges:blockRanges];
889
+ [_mathChipController applyAtoms:parsed.mathAtoms offset:insertLocation + lead];
892
890
  }
893
891
 
894
892
  #pragma mark - Formatting
@@ -903,17 +901,44 @@ using namespace facebook::react;
903
901
 
904
902
  - (void)applyFormatting
905
903
  {
906
- if (_isApplyingFormatting) {
904
+ [self applyFormattingScopedToRange:NSMakeRange(0, ENRMGetPlainText(_textView).length)];
905
+ }
906
+
907
+ /// Per-keystroke variant: re-applies inline and block attributes only on the
908
+ /// line(s) touched by the edit, mirroring Android's applyFormattingScopedToEdit.
909
+ - (void)applyFormattingScopedToEditAtLocation:(NSUInteger)editLocation insertedLength:(NSUInteger)insertedLength
910
+ {
911
+ NSString *plainText = ENRMGetPlainText(_textView);
912
+ NSUInteger textLength = plainText.length;
913
+ if (textLength == 0) {
914
+ [self applyFormatting];
915
+ return;
916
+ }
917
+
918
+ NSUInteger rawStart = MIN(editLocation, textLength);
919
+ NSUInteger rawEnd = MIN(editLocation + insertedLength, textLength);
920
+ rawEnd = MAX(rawEnd, rawStart);
921
+ NSRange scope = [plainText lineRangeForRange:NSMakeRange(rawStart, rawEnd - rawStart)];
922
+ [self applyFormattingScopedToRange:scope];
923
+ }
924
+
925
+ - (void)applyFormattingScopedToRange:(NSRange)scope
926
+ {
927
+ if (_editSession.shouldSuppressFormatting) {
907
928
  return;
908
929
  }
909
- if (ENRMHasMarkedText(_textView)) {
930
+ if (_editSession.isComposing) {
910
931
  return;
911
932
  }
912
- _isApplyingFormatting = YES;
933
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
913
934
 
914
935
  NSRange savedSelection = _textView.selectedRange;
915
936
 
916
- [_formatter applyFormattingRanges:_formattingStore.allRanges toTextView:_textView style:_formatterStyle];
937
+ [_formatter applyFormattingRanges:_formattingStore.allRanges
938
+ toTextView:_textView
939
+ style:_formatterStyle
940
+ scopedToRange:scope];
941
+ [_formatter applyBlockRanges:_blockStore.allRanges toTextView:_textView style:_formatterStyle scopedToRange:scope];
917
942
  [_detectorPipeline refreshAllStyling];
918
943
  [self applyWritingDirection];
919
944
 
@@ -922,7 +947,7 @@ using namespace facebook::react;
922
947
  _textView.selectedRange = savedSelection;
923
948
  }
924
949
 
925
- _isApplyingFormatting = NO;
950
+ [_editSession exitPhase];
926
951
  }
927
952
 
928
953
  - (void)applyWritingDirection
@@ -952,9 +977,9 @@ using namespace facebook::react;
952
977
  {
953
978
  [self importMarkdown:markdown];
954
979
  _lastSelectedRange = _textView.selectedRange;
955
- [self emitOnChangeText];
956
- [self emitOnChangeSelection];
957
- [self emitOnChangeState];
980
+ [_inputEventEmitter emitOnChangeText];
981
+ [_inputEventEmitter emitOnChangeSelection];
982
+ [_inputEventEmitter emitOnChangeState];
958
983
  [self requestHeightUpdate];
959
984
  }
960
985
 
@@ -964,8 +989,8 @@ using namespace facebook::react;
964
989
  NSInteger clampedStart = MIN(MAX(start, 0), textLen);
965
990
  NSInteger clampedEnd = MIN(MAX(end, clampedStart), textLen);
966
991
  _textView.selectedRange = NSMakeRange((NSUInteger)clampedStart, (NSUInteger)(clampedEnd - clampedStart));
967
- [self emitOnChangeSelection];
968
- [self emitOnChangeState];
992
+ [_inputEventEmitter emitOnChangeSelection];
993
+ [_inputEventEmitter emitOnChangeState];
969
994
  }
970
995
 
971
996
  - (void)toggleBold
@@ -1002,813 +1027,817 @@ using namespace facebook::react;
1002
1027
  ENRMStyleMergingConfig *mergingConfig = handler.mergingConfig;
1003
1028
 
1004
1029
  NSRange selection = _textView.selectedRange;
1005
- NSUInteger cursor = selection.location;
1006
- NSNumber *key = @(styleType);
1007
-
1008
- // Check blocking rules: if any blocking style is active, refuse to toggle on.
1009
- if (mergingConfig.blockingStyles.count > 0) {
1010
- BOOL isCurrentlyActive = [_formattingStore isStyleActive:styleType atPosition:cursor];
1011
- if (!isCurrentlyActive) {
1012
- for (NSNumber *blockerNum in mergingConfig.blockingStyles) {
1013
- if ([_formattingStore isStyleActive:(ENRMInputStyleType)blockerNum.integerValue atPosition:cursor]) {
1014
- return;
1015
- }
1016
- }
1017
- }
1018
- }
1019
1030
 
1020
- if (selection.length > 0) {
1021
- BOOL fullyStyled = YES;
1022
- NSUInteger pos = selection.location;
1023
- NSUInteger selEnd = NSMaxRange(selection);
1024
- while (pos < selEnd) {
1025
- ENRMFormattingRange *match = [_formattingStore rangeOfType:styleType containingPosition:pos];
1026
- if (match == nil) {
1027
- fullyStyled = NO;
1028
- break;
1029
- }
1030
- pos = NSMaxRange(match.range);
1031
- }
1032
- if (fullyStyled) {
1033
- [_formattingStore removeType:styleType inRange:selection];
1034
- } else {
1035
- // Remove conflicting styles from the range before applying.
1036
- for (NSNumber *conflictNum in mergingConfig.conflictingStyles) {
1037
- [_formattingStore removeType:(ENRMInputStyleType)conflictNum.integerValue inRange:selection];
1038
- }
1039
- ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:styleType range:selection];
1040
- [_formattingStore addRange:newRange];
1041
- }
1042
- [_pendingStyles removeObject:key];
1043
- [_pendingStyleRemovals removeObject:key];
1044
- } else {
1045
- BOOL isInsideRange = [_formattingStore isStyleActive:styleType atPosition:cursor];
1046
-
1047
- if ([_pendingStyleRemovals containsObject:key]) {
1048
- [_pendingStyleRemovals removeObject:key];
1049
- } else if ([_pendingStyles containsObject:key]) {
1050
- [_pendingStyles removeObject:key];
1051
- } else if (isInsideRange) {
1052
- [_pendingStyleRemovals addObject:key];
1053
- } else {
1054
- [_pendingStyles addObject:key];
1055
- }
1031
+ if ([_formattingStore isToggleBlocked:styleType
1032
+ atPosition:selection.location
1033
+ blockingStyles:mergingConfig.blockingStyles]) {
1034
+ return;
1056
1035
  }
1057
1036
 
1037
+ BOOL wasActive = [_formattingStore toggleStyle:styleType
1038
+ inRange:selection
1039
+ conflictingStyles:mergingConfig.conflictingStyles];
1040
+
1041
+ [_typingController togglePendingStyle:styleType wasActive:wasActive hasSelection:(selection.length > 0)];
1042
+
1058
1043
  [self applyFormatting];
1059
- [self syncTypingAttributesWithPendingStyles];
1060
- [self emitFormattingChanged];
1044
+ [_typingController syncWithPendingStyles];
1045
+ [_inputEventEmitter emitFormattingChanged];
1061
1046
  }
1062
1047
 
1063
- - (void)setLink:(NSString *)url
1048
+ - (void)toggleHeading:(NSInteger)level
1064
1049
  {
1065
- NSRange selection = _textView.selectedRange;
1066
- NSUInteger cursor = selection.location;
1050
+ if (level < 1 || level > 6) {
1051
+ return;
1052
+ }
1053
+ [self toggleBlockType:ENRMBlockTypeForHeadingLevel(level) level:level];
1054
+ }
1067
1055
 
1068
- ENRMFormattingRange *activeLink = [_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:cursor];
1056
+ - (void)toggleBlockType:(ENRMInputBlockType)type level:(NSInteger)level
1057
+ {
1058
+ NSString *text = ENRMGetPlainText(_textView);
1059
+ BOOL alreadyActive = [_blockCoordinator toggleBlockType:type
1060
+ level:level
1061
+ selectionRange:_textView.selectedRange
1062
+ inText:text];
1069
1063
 
1070
- if (activeLink != nil) {
1071
- activeLink.url = url;
1072
- [_autoLinkDetector clearAutoLinkInRange:activeLink.range];
1073
- } else if (selection.length > 0) {
1074
- ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink range:selection url:url];
1075
- [_formattingStore addRange:linkRange];
1076
- [_autoLinkDetector clearAutoLinkInRange:selection];
1077
- } else {
1078
- return;
1064
+ if (alreadyActive) {
1065
+ // Strip the stored block paragraph style directly: an empty item's line has no
1066
+ // stamped block-marker attribute (zero-length ranges are never stamped), so the
1067
+ // applyFormatting reset below — which is keyed off that marker — cannot reach it.
1068
+ NSRange paragraphRange = [text paragraphRangeForRange:_textView.selectedRange];
1069
+ NSTextStorage *storage = _textView.textStorage;
1070
+ NSRange clamped = NSIntersectionRange(paragraphRange, NSMakeRange(0, storage.length));
1071
+ if (clamped.length > 0) {
1072
+ [storage beginEditing];
1073
+ [storage removeAttribute:NSParagraphStyleAttributeName range:clamped];
1074
+ [storage endEditing];
1075
+ }
1079
1076
  }
1080
1077
 
1081
1078
  [self applyFormatting];
1082
- [self emitFormattingChanged];
1079
+ [_typingController syncWithCursorBlock];
1080
+ [self updatePlaceholderVisibility];
1081
+ if (alreadyActive) {
1082
+ [_typingController clearListParagraphStyle];
1083
+ }
1084
+ [self updateEmptyBulletMarker];
1085
+ [_inputEventEmitter emitFormattingChanged];
1083
1086
  }
1084
1087
 
1085
- - (void)insertLink:(NSString *)text url:(NSString *)url
1088
+ - (void)toggleUnorderedList
1086
1089
  {
1087
- NSString *displayText = text.length > 0 ? text : url;
1088
- NSRange linkRange = NSMakeRange(0, displayText.length);
1089
- ENRMFormattingRange *range = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1090
- range:linkRange
1091
- url:[self sanitizeLinkURL:url]];
1092
- [self replaceSelectedTextWith:displayText formattingRanges:@[ range ]];
1090
+ [self toggleBlockType:ENRMInputBlockTypeUnorderedListItem level:0];
1093
1091
  }
1094
1092
 
1095
- - (NSString *)sanitizeLinkURL:(NSString *)url
1093
+ - (void)toggleOrderedList
1096
1094
  {
1097
- NSString *result = [url stringByReplacingOccurrencesOfString:@"(" withString:@"%28"];
1098
- return [result stringByReplacingOccurrencesOfString:@")" withString:@"%29"];
1095
+ [self toggleBlockType:ENRMInputBlockTypeOrderedListItem level:0];
1099
1096
  }
1100
1097
 
1101
- - (void)startMention:(NSString *)indicator
1098
+ - (void)indentList
1102
1099
  {
1103
- if (indicator.length == 0 || ![_mentionIndicators containsObject:indicator]) {
1104
- return;
1105
- }
1106
-
1107
- [self replaceSelectedTextWith:indicator formattingRanges:@[]];
1108
- [self updateActiveMention];
1100
+ [self changeListDepthBy:1];
1109
1101
  }
1110
1102
 
1111
- - (void)insertMention:(NSString *)displayText url:(NSString *)url
1103
+ - (void)outdentList
1112
1104
  {
1113
- if (displayText.length == 0 || _activeMentionIndicator == nil || _activeMentionRange.location == NSNotFound) {
1114
- return;
1115
- }
1105
+ [self changeListDepthBy:-1];
1106
+ }
1116
1107
 
1117
- NSString *plainText = ENRMGetPlainText(_textView);
1118
- NSUInteger rangeEnd = NSMaxRange(_activeMentionRange);
1119
- if (rangeEnd > plainText.length) {
1108
+ - (void)changeListDepthBy:(NSInteger)delta
1109
+ {
1110
+ NSString *text = ENRMGetPlainText(_textView);
1111
+ ENRMDepthChangeResult result = [_blockCoordinator changeDepthBy:delta
1112
+ cursorPosition:_textView.selectedRange.location
1113
+ selectionRange:_textView.selectedRange
1114
+ inText:text];
1115
+ if (result == ENRMDepthChangeResultNoOp) {
1120
1116
  return;
1121
1117
  }
1122
- BOOL nextCharIsWhitespace = rangeEnd < plainText.length && [[NSCharacterSet whitespaceAndNewlineCharacterSet]
1123
- characterIsMember:[plainText
1124
- characterAtIndex:rangeEnd]];
1125
- NSString *replacement = nextCharIsWhitespace ? displayText : [displayText stringByAppendingString:@" "];
1126
- ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1127
- range:NSMakeRange(0, displayText.length)
1128
- url:[self sanitizeLinkURL:url]];
1129
- NSString *indicator = _activeMentionIndicator;
1130
- NSRange mentionRange = _activeMentionRange;
1118
+ [self applyFormatting];
1119
+ [_typingController syncWithCursorBlock];
1120
+ [self updateEmptyBulletMarker];
1121
+ [_inputEventEmitter emitFormattingChanged];
1122
+ }
1131
1123
 
1132
- [self clearActiveMention:indicator];
1133
- [self replaceTextInRange:mentionRange withText:replacement formattingRanges:@[ linkRange ]];
1134
- _textView.selectedRange = NSMakeRange(mentionRange.location + replacement.length, 0);
1135
- [self emitOnChangeSelection];
1124
+ - (nullable ENRMBlockRange *)listBlockForCursorParagraph
1125
+ {
1126
+ return [self listBlockForParagraphAtPosition:_textView.selectedRange.location];
1136
1127
  }
1137
1128
 
1138
- - (void)removeLink
1129
+ - (nullable ENRMBlockRange *)listBlockForParagraphAtPosition:(NSUInteger)position
1139
1130
  {
1140
- NSUInteger cursor = _textView.selectedRange.location;
1141
- ENRMFormattingRange *activeLink = [_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:cursor];
1142
- if (activeLink == nil) {
1143
- return;
1144
- }
1131
+ return [_blockCoordinator listBlockAtPosition:position inText:_textView.textStorage.string];
1132
+ }
1145
1133
 
1146
- [_formattingStore removeRange:activeLink];
1147
- [self applyFormatting];
1148
- [self emitFormattingChanged];
1134
+ - (nullable ENRMBlockRange *)blockForParagraphAtPosition:(NSUInteger)position
1135
+ {
1136
+ return [_blockCoordinator blockAtPosition:position inText:_textView.textStorage.string];
1149
1137
  }
1150
1138
 
1151
- #pragma mark - Math chip commands
1139
+ - (BOOL)listStateOfType:(ENRMInputBlockType)type forCursorParagraphDepth:(nullable NSInteger *)outDepth
1140
+ {
1141
+ return [_blockCoordinator listStateOfType:type
1142
+ atPosition:_textView.selectedRange.location
1143
+ inText:_textView.textStorage.string
1144
+ depth:outDepth];
1145
+ }
1152
1146
 
1153
- - (void)insertMath:(NSString *)latex displayMode:(BOOL)displayMode
1147
+ /// Records the block kind/level of the line being edited before the change, so a
1148
+ /// Return that continues a list can recover the previous item's depth and an
1149
+ /// in-line replacement (autocorrect/paste) that wipes the line can heal it.
1150
+ - (void)capturePreEditBlockForRange:(NSRange)range
1154
1151
  {
1155
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1156
- if (latex.length == 0)
1157
- return;
1158
- NSTextStorage *storage = _textView.textStorage;
1159
- NSRange selection = _textView.selectedRange;
1160
- NSString *plainText = ENRMGetPlainText(_textView);
1152
+ ENRMBlockRange *block = [self blockForParagraphAtPosition:range.location];
1153
+ _preEditBlockType = block != nil ? block.type : ENRMInputBlockTypeParagraph;
1154
+ _preEditBlockLevel = block != nil ? block.level : 0;
1155
+ }
1161
1156
 
1162
- NSMutableString *prefix = [NSMutableString string];
1163
- NSMutableString *suffix = [NSMutableString string];
1164
- if (displayMode) {
1165
- BOOL atLineStart = (selection.location == 0) || [plainText characterAtIndex:selection.location - 1] == '\n';
1166
- NSUInteger afterSel = selection.location + selection.length;
1167
- BOOL atLineEnd = (afterSel >= plainText.length) || [plainText characterAtIndex:afterSel] == '\n';
1168
- if (!atLineStart)
1169
- [prefix appendString:@"\n"];
1170
- if (!atLineEnd)
1171
- [suffix appendString:@"\n"];
1157
+ /// Whether `range` of the current (pre-edit) text contains a line break. Scans
1158
+ /// only the replaced run, mirroring Android's editTouchedNewline.
1159
+ - (BOOL)replacedRangeTouchesNewline:(NSRange)range
1160
+ {
1161
+ NSString *text = _textView.textStorage.string;
1162
+ if (range.length == 0 || range.location >= text.length) {
1163
+ return NO;
1172
1164
  }
1165
+ NSRange clamped = NSMakeRange(range.location, MIN(range.length, text.length - range.location));
1166
+ return [text rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet] options:0 range:clamped].location !=
1167
+ NSNotFound;
1168
+ }
1173
1169
 
1174
- ENRMMathInlineAttachment *att = [self createMathAttachmentWithLatex:latex displayMode:displayMode];
1175
- NSMutableAttributedString *replacement = [[NSMutableAttributedString alloc] init];
1176
- if (prefix.length) {
1177
- [replacement appendAttributedString:[[NSAttributedString alloc] initWithString:prefix]];
1170
+ /// Whether the caret's paragraph had no glyph content at the start of the edit
1171
+ /// (an empty line). Used to decide whether Return continues or exits the list.
1172
+ - (BOOL)preEditParagraphWasEmpty:(NSRange)range
1173
+ {
1174
+ NSString *text = _textView.textStorage.string;
1175
+ if (text.length == 0) {
1176
+ return YES;
1178
1177
  }
1179
- [replacement appendAttributedString:[NSAttributedString attributedStringWithAttachment:att]];
1180
- if (suffix.length) {
1181
- [replacement appendAttributedString:[[NSAttributedString alloc] initWithString:suffix]];
1178
+ NSRange paragraph = [text paragraphRangeForRange:range];
1179
+ if (paragraph.length == 0) {
1180
+ return YES;
1182
1181
  }
1182
+ return [[text substringWithRange:paragraph] isEqualToString:@"\n"];
1183
+ }
1183
1184
 
1184
- if (displayMode && att.blockAlignment != NSTextAlignmentNatural) {
1185
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
1186
- ps.alignment = att.blockAlignment;
1187
- NSRange chipRange = NSMakeRange(prefix.length, 1);
1188
- [replacement addAttribute:NSParagraphStyleAttributeName value:ps range:chipRange];
1185
+ /// Intercepts Tab (indent) and Backspace at a bullet item's start (outdent, then
1186
+ /// un-list at depth 0). Returns YES when handled, suppressing the default edit.
1187
+ /// Keyed off the caret's own paragraph (NSMaxRange(range)), not range.location —
1188
+ /// a backspace at a line start targets the previous line's newline, and using the
1189
+ /// caret's line lets the next Backspace after un-listing fall through to a merge.
1190
+ - (BOOL)handleListKeyForReplacementRange:(NSRange)range replacementText:(NSString *)text
1191
+ {
1192
+ NSUInteger caret = NSMaxRange(range);
1193
+ NSInteger depth;
1194
+ if (![self listStateForParagraphAtPosition:caret depth:&depth]) {
1195
+ return NO;
1189
1196
  }
1190
1197
 
1191
- _isApplyingFormatting = YES;
1192
- [storage replaceCharactersInRange:selection withAttributedString:replacement];
1193
- _isApplyingFormatting = NO;
1194
-
1195
- NSUInteger insertedLength = replacement.length;
1196
- [_formattingStore adjustForEditAtLocation:selection.location
1197
- deletedLength:selection.length
1198
- insertedLength:insertedLength];
1198
+ // Tab indents the current item.
1199
+ if ([text isEqualToString:@"\t"]) {
1200
+ [self indentList];
1201
+ return YES;
1202
+ }
1199
1203
 
1200
- NSUInteger newCursor = selection.location + insertedLength;
1201
- _textView.selectedRange = NSMakeRange(newCursor, 0);
1202
- _lastTextLength = ENRMGetPlainText(_textView).length;
1203
- _lastSelectedRange = _textView.selectedRange;
1204
+ // Backspace at the very start of the caret's own item: outdent, or remove the
1205
+ // marker entirely once at depth 0 (de-indenting the line to a plain paragraph).
1206
+ if (text.length == 0 && range.length == 1) {
1207
+ NSString *plainText = ENRMGetPlainText(_textView);
1208
+ NSRange paragraphRange = [plainText paragraphRangeForRange:NSMakeRange(caret, 0)];
1209
+ BOOL atItemStart = caret == paragraphRange.location;
1210
+ if (atItemStart) {
1211
+ if (depth > 0) {
1212
+ [self outdentList];
1213
+ } else {
1214
+ ENRMBlockRange *listBlock = [self listBlockForParagraphAtPosition:caret];
1215
+ [self toggleBlockType:listBlock != nil ? listBlock.type : ENRMInputBlockTypeUnorderedListItem level:0];
1216
+ }
1217
+ return YES;
1218
+ }
1219
+ }
1204
1220
 
1205
- [self applyFormatting];
1206
- [self updatePlaceholderVisibility];
1207
- [self emitOnChangeText];
1208
- [self emitOnChangeSelection];
1209
- [self emitFormattingChanged];
1210
- [self requestHeightUpdate];
1211
- [self scheduleRelayoutIfNeeded];
1212
- #endif
1221
+ return NO;
1213
1222
  }
1214
1223
 
1215
- - (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode
1224
+ /// Backspace at the document start (caret at 0) never fires the text-change
1225
+ /// delegate — nothing precedes the caret — so the first line's bullet has to be
1226
+ /// outdented/removed here. Returns YES when handled.
1227
+ - (BOOL)handleBackspaceAtDocumentStart
1216
1228
  {
1217
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1218
- NSTextStorage *storage = _textView.textStorage;
1219
- if (start < 0 || (NSUInteger)start >= storage.length)
1220
- return;
1221
- id value = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)start effectiveRange:NULL];
1222
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
1223
- return;
1224
-
1225
- ENRMMathInlineAttachment *att = value;
1226
- BOOL modeChanged = (att.displayMode != displayMode);
1227
- att.latex = latex ?: @"";
1228
- att.displayMode = displayMode;
1229
- [self applyMathStyleToAttachment:att];
1230
- [att invalidateRender];
1231
-
1232
- NSRange chipRange = NSMakeRange((NSUInteger)start, 1);
1233
- if (modeChanged) {
1234
- [storage beginEditing];
1235
- if (displayMode && att.blockAlignment != NSTextAlignmentNatural) {
1236
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
1237
- ps.alignment = att.blockAlignment;
1238
- [storage addAttribute:NSParagraphStyleAttributeName value:ps range:chipRange];
1239
- } else {
1240
- [storage removeAttribute:NSParagraphStyleAttributeName range:chipRange];
1241
- }
1242
- [storage endEditing];
1229
+ NSRange selection = _textView.selectedRange;
1230
+ if (selection.location != 0 || selection.length != 0) {
1231
+ return NO;
1243
1232
  }
1244
-
1245
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
1246
- if (lm) {
1247
- [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
1248
- [lm ensureLayoutForCharacterRange:chipRange];
1233
+ ENRMBlockRange *listBlock = [self listBlockForCursorParagraph];
1234
+ if (listBlock == nil) {
1235
+ return NO;
1249
1236
  }
1250
- ENRMSetNeedsDisplay(_textView);
1251
-
1252
- if (_emitMarkdown) {
1253
- [self emitOnChangeMarkdown];
1237
+ if (listBlock.level > 0) {
1238
+ [self outdentList];
1239
+ } else {
1240
+ [self toggleBlockType:listBlock.type level:0];
1254
1241
  }
1255
- [self requestHeightUpdate];
1256
- [self scheduleRelayoutIfNeeded];
1257
- #endif
1242
+ return YES;
1258
1243
  }
1259
1244
 
1260
- - (void)removeMath:(NSInteger)start
1245
+ - (BOOL)listStateForParagraphAtPosition:(NSUInteger)position depth:(NSInteger *)outDepth
1261
1246
  {
1262
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1263
- NSTextStorage *storage = _textView.textStorage;
1264
- if (start < 0 || (NSUInteger)start >= storage.length)
1265
- return;
1266
- id value = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)start effectiveRange:NULL];
1267
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
1268
- return;
1269
-
1270
- [self replaceTextInRange:NSMakeRange((NSUInteger)start, 1) withText:@"" formattingRanges:@[]];
1271
- #endif
1247
+ ENRMBlockRange *block = [_blockCoordinator listBlockAtPosition:position inText:_textView.textStorage.string];
1248
+ if (block != nil) {
1249
+ if (outDepth) {
1250
+ *outDepth = block.level;
1251
+ }
1252
+ return YES;
1253
+ }
1254
+ return NO;
1272
1255
  }
1273
1256
 
1274
- - (void)showLinkPrompt
1257
+ /// Drives the layout manager's empty-line bullet: an empty bullet line has no
1258
+ /// character to anchor the marker to, so the manager is told its location/depth
1259
+ /// explicitly; cleared when the caret isn't on an empty bullet line. Runs on
1260
+ /// every selection-change fire, so it early-returns when there is nothing to
1261
+ /// draw or clear and only touches storage when the paragraph style differs,
1262
+ /// avoiding layout churn that jitters the edit menu.
1263
+ - (void)updateEmptyBulletMarker
1275
1264
  {
1276
- NSUInteger cursor = _textView.selectedRange.location;
1277
- ENRMFormattingRange *activeLink = [_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:cursor];
1278
- NSString *existingURL = activeLink != nil ? activeLink.url : nil;
1265
+ NSString *text = ENRMGetPlainText(_textView);
1266
+ NSRange selection = _textView.selectedRange;
1267
+ BOOL show = NO;
1268
+ NSUInteger location = 0;
1269
+ NSInteger depth = 0;
1279
1270
 
1280
- __weak EnrichedMarkdownTextInput *weakSelf = self;
1281
- ENRMShowLinkPrompt(self, existingURL, ^(NSString *url) { [weakSelf setLink:url]; });
1282
- }
1271
+ BOOL ordered = NO;
1272
+ NSInteger ordinal = 1;
1273
+ ENRMBlockRange *cursorListBlock = selection.length == 0 ? [self listBlockForCursorParagraph] : nil;
1283
1274
 
1284
- - (nullable NSString *)markdownForSelectedRange
1285
- {
1286
- NSRange selection = _textView.selectedRange;
1287
- if (selection.length == 0) {
1288
- return nil;
1275
+ ENRMInputListMarkerDrawer *listDrawer = _layoutManager.listMarkerDrawer;
1276
+ BOOL wasShown = listDrawer.emptyBulletDepth >= 0;
1277
+ if (cursorListBlock == nil && !wasShown) {
1278
+ return;
1289
1279
  }
1290
1280
 
1291
- NSAttributedString *full = _textView.textStorage;
1292
- if (NSMaxRange(selection) > full.length) {
1293
- return nil;
1281
+ if (cursorListBlock != nil) {
1282
+ NSInteger cursorDepth = cursorListBlock.level;
1283
+ ordered = cursorListBlock.type == ENRMInputBlockTypeOrderedListItem;
1284
+ ordinal = cursorListBlock.ordinal;
1285
+ NSRange paragraphRange = text.length == 0 ? NSMakeRange(0, 0) : [text paragraphRangeForRange:selection];
1286
+ NSString *paragraphText = text.length == 0 ? @"" : [text substringWithRange:paragraphRange];
1287
+ BOOL empty = paragraphText.length == 0 || [paragraphText isEqualToString:@"\n"];
1288
+ if (empty) {
1289
+ show = YES;
1290
+ location = paragraphRange.location;
1291
+ depth = cursorDepth;
1292
+
1293
+ // A mid-document empty bullet line has no paragraph style and lays out
1294
+ // flush left; stamp the list style so caret and marker indent immediately.
1295
+ // (A trailing empty line uses the extra line fragment instead.)
1296
+ if (paragraphRange.length > 0) {
1297
+ CGFloat indent = (depth + 1) * kENRMListIndentPerDepth;
1298
+ NSTextStorage *storage = _textView.textStorage;
1299
+ NSParagraphStyle *existing = [storage attribute:NSParagraphStyleAttributeName
1300
+ atIndex:paragraphRange.location
1301
+ effectiveRange:NULL];
1302
+ if (existing == nil || existing.firstLineHeadIndent != indent || existing.headIndent != indent ||
1303
+ existing.paragraphSpacingBefore != _formatterStyle.listItemSpacing) {
1304
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
1305
+ paragraph.firstLineHeadIndent = indent;
1306
+ paragraph.headIndent = indent;
1307
+ paragraph.paragraphSpacingBefore = _formatterStyle.listItemSpacing;
1308
+ [storage beginEditing];
1309
+ [storage addAttribute:NSParagraphStyleAttributeName value:paragraph range:paragraphRange];
1310
+ [storage endEditing];
1311
+ }
1312
+ }
1313
+ }
1294
1314
  }
1295
- NSAttributedString *selectedAttr = [full attributedSubstringFromRange:selection];
1296
- NSUInteger selEnd = NSMaxRange(selection);
1297
1315
 
1298
- NSMutableArray<ENRMFormattingRange *> *clippedRanges = [NSMutableArray array];
1299
- for (ENRMFormattingRange *range in [self allRangesIncludingTransient]) {
1300
- NSUInteger rangeStart = range.range.location;
1301
- NSUInteger rangeEnd = NSMaxRange(range.range);
1316
+ if (!show && !wasShown) {
1317
+ return;
1318
+ }
1302
1319
 
1303
- if (rangeEnd <= selection.location || rangeStart >= selEnd) {
1304
- continue;
1305
- }
1320
+ listDrawer.emptyBulletDepth = show ? depth : -1;
1321
+ listDrawer.emptyBulletOrdered = ordered;
1322
+ listDrawer.emptyBulletOrdinal = ordinal;
1323
+ listDrawer.emptyBulletLocation = location;
1324
+ listDrawer.emptyBulletFont = _formatterStyle.baseFont;
1325
+ listDrawer.emptyBulletColor = _formatterStyle.baseTextColor;
1326
+ listDrawer.emptyBulletRTL = [self emptyListLineIsRTL];
1327
+ listDrawer.listItemSpacing = _formatterStyle.listItemSpacing;
1306
1328
 
1307
- NSUInteger clippedStart = MAX(rangeStart, selection.location);
1308
- NSUInteger clippedEnd = MIN(rangeEnd, selEnd);
1309
- NSRange shifted = NSMakeRange(clippedStart - selection.location, clippedEnd - clippedStart);
1329
+ BOOL isTrailing = show && text.length > 0 && location >= text.length;
1310
1330
 
1311
- [clippedRanges addObject:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
1331
+ // Ensure the extra line fragment is laid out so we can read
1332
+ // extraLineFragmentTextContainer / extraLineFragmentUsedRect. The
1333
+ // empty-editor case (length 0) has no glyphs at all; the trailing-line case
1334
+ // (location >= length) has glyphs on preceding lines but the extra line
1335
+ // fragment lives beyond them and isn't covered by ensureLayoutForCharRange:.
1336
+ if (show && (text.length == 0 || location >= text.length)) {
1337
+ [_layoutManager ensureLayoutForTextContainer:_textView.textContainer];
1312
1338
  }
1313
1339
 
1314
- return [ENRMMarkdownSerializer serializeAttributedString:selectedAttr ranges:clippedRanges];
1340
+ #if !TARGET_OS_OSX
1341
+ if (isTrailing && _layoutManager.extraLineFragmentTextContainer != nil) {
1342
+ [listDrawer showTrailingBulletInTextView:_textView
1343
+ textContainer:_layoutManager.extraLineFragmentTextContainer
1344
+ usedRect:_layoutManager.extraLineFragmentUsedRect];
1345
+ } else {
1346
+ [listDrawer hideTrailingBullet];
1347
+ }
1348
+ #endif
1349
+
1350
+ ENRMSetNeedsDisplay(_textView);
1351
+
1352
+ // The empty-editor bullet would otherwise overlap the placeholder.
1353
+ [self updatePlaceholderVisibility];
1315
1354
  }
1316
1355
 
1317
- - (void)copyToClipboard
1356
+ /// Writing direction the empty list line resolves to, for mirroring its marker.
1357
+ /// An empty line has no strong character, so Auto and FirstStrong fall back the
1358
+ /// same way the formatter's direction pass would.
1359
+ - (BOOL)emptyListLineIsRTL
1318
1360
  {
1319
- NSString *plainText = ENRMGetPlainText(_textView);
1320
- if (plainText.length == 0) {
1321
- return;
1322
- }
1323
- NSString *markdown = [ENRMMarkdownSerializer serializePlainText:plainText ranges:[self allRangesIncludingTransient]];
1324
- NSMutableDictionary *items = [NSMutableDictionary dictionary];
1325
- items[kUTIPlainText] = plainText;
1326
- if (markdown.length > 0) {
1327
- items[kENRMMarkdownPasteboardType] = markdown;
1361
+ switch (_writingDirectionMode) {
1362
+ case ENRMWritingDirectionModeLTR:
1363
+ return NO;
1364
+ case ENRMWritingDirectionModeRTL:
1365
+ return YES;
1366
+ case ENRMWritingDirectionModeFirstStrong:
1367
+ return _resolvedLayoutDirection == NSWritingDirectionRightToLeft;
1368
+ case ENRMWritingDirectionModeAuto:
1369
+ default:
1370
+ return ENRMParagraphIsRTL(nil);
1328
1371
  }
1329
- copyItemsToPasteboard(items);
1330
1372
  }
1331
1373
 
1332
- - (void)requestMarkdown:(NSInteger)requestId
1374
+ - (NSInteger)headingLevelForCursorParagraph
1333
1375
  {
1334
- auto emitter = [self getEventEmitter];
1335
- if (emitter == nullptr) {
1336
- return;
1337
- }
1338
- NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
1339
- ranges:[self allRangesIncludingTransient]];
1340
- emitter->onRequestMarkdownResult({
1341
- .requestId = static_cast<int>(requestId),
1342
- .markdown = std::string([markdown UTF8String] ?: ""),
1343
- });
1376
+ return [_blockCoordinator headingLevelAtPosition:_textView.selectedRange.location
1377
+ inText:_textView.textStorage.string];
1344
1378
  }
1345
1379
 
1346
- - (CGRect)computeCaretRect
1380
+ - (NSString *)adjustStoresForEditAtLocation:(NSUInteger)editLocation
1381
+ deletedLength:(NSUInteger)deletedLength
1382
+ insertedLength:(NSUInteger)insertedLength
1347
1383
  {
1348
- CGRect caretRect = CGRectZero;
1349
- #if !TARGET_OS_OSX
1350
- UITextRange *selectedRange = _textView.selectedTextRange;
1351
- if (selectedRange != nil) {
1352
- caretRect = [_textView caretRectForPosition:selectedRange.start];
1353
- }
1354
- #else
1355
- NSRange selection = _textView.selectedRange;
1356
- if (selection.location != NSNotFound) {
1357
- NSRange glyphRange = [_textView.layoutManager glyphRangeForCharacterRange:NSMakeRange(selection.location, 0)
1358
- actualCharacterRange:NULL];
1359
- caretRect = [_textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
1360
- caretRect.origin.x += _textView.textContainerInset.left;
1361
- caretRect.origin.y += _textView.textContainerInset.top;
1362
- }
1363
- #endif
1364
- return caretRect;
1384
+ return [_editPipeline adjustStoresForEditAtLocation:editLocation
1385
+ deletedLength:deletedLength
1386
+ insertedLength:insertedLength];
1365
1387
  }
1366
1388
 
1367
- - (void)requestCaretRect:(NSInteger)requestId
1389
+ - (void)setLink:(NSString *)url
1368
1390
  {
1369
- auto emitter = [self getEventEmitter];
1370
- if (emitter == nullptr) {
1391
+ NSRange selection = _textView.selectedRange;
1392
+ if (![_linkCoordinator setLinkURL:url atCursor:selection.location selection:selection]) {
1371
1393
  return;
1372
1394
  }
1373
-
1374
- CGRect caretRect = [self computeCaretRect];
1375
- emitter->onRequestCaretRectResult({
1376
- .requestId = static_cast<int>(requestId),
1377
- .x = caretRect.origin.x,
1378
- .y = caretRect.origin.y,
1379
- .width = caretRect.size.width,
1380
- .height = caretRect.size.height,
1381
- });
1395
+ [self applyFormatting];
1396
+ [_inputEventEmitter emitFormattingChanged];
1382
1397
  }
1383
1398
 
1384
- - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
1399
+ - (void)insertLink:(NSString *)text url:(NSString *)url
1385
1400
  {
1386
- RCTEnrichedMarkdownTextInputHandleCommand(self, commandName, args);
1401
+ NSString *displayText = text.length > 0 ? text : url;
1402
+ NSRange linkRange = NSMakeRange(0, displayText.length);
1403
+ ENRMFormattingRange *range = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1404
+ range:linkRange
1405
+ url:[_linkCoordinator sanitizeURL:url]];
1406
+ [self replaceSelectedTextWith:displayText formattingRanges:@[ range ]];
1387
1407
  }
1388
1408
 
1389
- #pragma mark - Style state query
1390
-
1391
- - (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1409
+ - (void)insertText:(NSString *)text
1392
1410
  {
1393
- BOOL inRange = [_formattingStore isStyleActive:type atPosition:position];
1394
- NSNumber *key = @(type);
1395
- if ([_pendingStyleRemovals containsObject:key]) {
1396
- return NO;
1397
- }
1398
- if ([_pendingStyles containsObject:key]) {
1399
- return YES;
1411
+ if (text.length == 0) {
1412
+ return;
1400
1413
  }
1401
- return inRange;
1414
+ [self pasteMarkdown:text];
1402
1415
  }
1403
1416
 
1404
- - (void)syncTypingAttributesWithPendingStyles
1417
+ - (void)startMention:(NSString *)indicator
1405
1418
  {
1406
- UIFontDescriptorSymbolicTraits traits = 0;
1407
- if ([_pendingStyles containsObject:@(ENRMInputStyleTypeStrong)]) {
1408
- traits |= UIFontDescriptorTraitBold;
1409
- }
1410
- if ([_pendingStyles containsObject:@(ENRMInputStyleTypeEmphasis)]) {
1411
- traits |= UIFontDescriptorTraitItalic;
1419
+ if (indicator.length == 0 || ![_mentionCoordinator containsIndicator:indicator]) {
1420
+ return;
1412
1421
  }
1413
1422
 
1414
- NSMutableDictionary *attrs = [_textView.typingAttributes mutableCopy];
1415
- attrs[NSFontAttributeName] = [_formatterStyle fontForTraits:traits];
1416
- _textView.typingAttributes = attrs;
1423
+ [self replaceSelectedTextWith:indicator formattingRanges:@[]];
1424
+ [self dispatchMentionUpdate];
1417
1425
  }
1418
1426
 
1419
- - (void)resetPendingStylesForSelectionChange
1427
+ - (void)insertMention:(NSString *)displayText url:(NSString *)url
1420
1428
  {
1421
- // Skip system-driven selection adjustments (e.g., predictive text) that fire
1422
- // immediately after a text edit.
1423
- static const CFTimeInterval kPostEditGracePeriod = 0.1;
1424
- BOOL isPostEditAdjustment =
1425
- (_lastTextChangeTime > 0 && (CACurrentMediaTime() - _lastTextChangeTime) < kPostEditGracePeriod);
1426
- if (isPostEditAdjustment) {
1429
+ if (displayText.length == 0 || !_mentionCoordinator.isActive ||
1430
+ _mentionCoordinator.activeRange.location == NSNotFound) {
1427
1431
  return;
1428
1432
  }
1429
- [_pendingStyles removeAllObjects];
1430
- [_pendingStyleRemovals removeAllObjects];
1431
- [self rebuildPendingStylesFromContext];
1432
- [self syncTypingAttributesWithPendingStyles];
1433
- }
1434
1433
 
1435
- - (void)rebuildPendingStylesFromContext
1436
- {
1437
- NSUInteger cursor = _textView.selectedRange.location;
1438
- if (_textView.selectedRange.length > 0 || cursor == 0) {
1434
+ NSString *plainText = ENRMGetPlainText(_textView);
1435
+ NSRange activeRange = _mentionCoordinator.activeRange;
1436
+ NSUInteger rangeEnd = NSMaxRange(activeRange);
1437
+ if (rangeEnd > plainText.length) {
1439
1438
  return;
1440
1439
  }
1440
+ BOOL nextCharIsWhitespace = rangeEnd < plainText.length && [[NSCharacterSet whitespaceAndNewlineCharacterSet]
1441
+ characterIsMember:[plainText
1442
+ characterAtIndex:rangeEnd]];
1443
+ NSString *replacement = nextCharIsWhitespace ? displayText : [displayText stringByAppendingString:@" "];
1444
+ ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1445
+ range:NSMakeRange(0, displayText.length)
1446
+ url:[_linkCoordinator sanitizeURL:url]];
1447
+ NSString *indicator = _mentionCoordinator.activeIndicator;
1441
1448
 
1442
- static const ENRMInputStyleType inlineStyles[] = {
1443
- ENRMInputStyleTypeStrong, ENRMInputStyleTypeEmphasis, ENRMInputStyleTypeUnderline,
1444
- ENRMInputStyleTypeStrikethrough, ENRMInputStyleTypeSpoiler,
1445
- };
1446
-
1447
- for (NSUInteger i = 0; i < sizeof(inlineStyles) / sizeof(inlineStyles[0]); i++) {
1448
- ENRMInputStyleType type = inlineStyles[i];
1449
- if ([_formattingStore isStyleAdjacentBefore:type position:cursor]) {
1450
- [_pendingStyles addObject:@(type)];
1451
- }
1452
- }
1449
+ [self dispatchMentionEvents:[_mentionCoordinator clearWithIndicatorOverride:indicator]];
1450
+ [self replaceTextInRange:activeRange withText:replacement formattingRanges:@[ linkRange ]];
1451
+ _textView.selectedRange = NSMakeRange(activeRange.location + replacement.length, 0);
1452
+ [_inputEventEmitter emitOnChangeSelection];
1453
1453
  }
1454
1454
 
1455
- #pragma mark - Event emitters
1456
-
1457
- - (void)emitFormattingChanged
1455
+ - (void)removeLink
1458
1456
  {
1459
- [self emitOnChangeState];
1460
- if (_emitMarkdown) {
1461
- [self emitOnChangeMarkdown];
1457
+ if (![_linkCoordinator removeLinkAtPosition:_textView.selectedRange.location]) {
1458
+ return;
1462
1459
  }
1460
+ [self applyFormatting];
1461
+ [_inputEventEmitter emitFormattingChanged];
1463
1462
  }
1464
1463
 
1465
- - (std::shared_ptr<EnrichedMarkdownTextInputEventEmitter const>)getEventEmitter
1464
+ #pragma mark - Math chip commands
1465
+
1466
+ // A chip is one U+FFFC character in the buffer plus an ENRMMathInlineAttachment
1467
+ // carrying the LaTeX (see ENRMMathChipController). Every text mutation below goes
1468
+ // through the same replaceTextInRange/EditPhase machinery as links and mentions —
1469
+ // a chip is an atomic inline character, not a block, so it needs no ENRMBlockHandler
1470
+ // and no phase of its own.
1471
+ //
1472
+ // No ENRICHED_MARKDOWN_MATH guard here on purpose: codegen declares these three on
1473
+ // RCTEnrichedMarkdownTextInputViewProtocol regardless of the math flag, and
1474
+ // RCTEnrichedMarkdownTextInputHandleCommand sends the selector unconditionally, so
1475
+ // conformance must hold in math-disabled builds. The controller is inert there.
1476
+
1477
+ - (void)insertMath:(NSString *)latex displayMode:(BOOL)displayMode
1466
1478
  {
1467
- if (_eventEmitter == nullptr || _blockEmitting) {
1468
- return nullptr;
1479
+ if (latex.length == 0 || !ENRMMathChipController.isAvailable) {
1480
+ return;
1469
1481
  }
1470
- return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
1471
- }
1472
1482
 
1473
- - (NSArray<ENRMFormattingRange *> *)allRangesIncludingTransient
1474
- {
1475
- NSArray<ENRMFormattingRange *> *transient = [_detectorPipeline allTransientFormattingRanges];
1476
- if (transient.count == 0) {
1477
- return _formattingStore.allRanges;
1483
+ NSString *plainText = ENRMGetPlainText(_textView);
1484
+ NSRange selection = _textView.selectedRange;
1485
+ if (NSMaxRange(selection) > plainText.length) {
1486
+ return;
1478
1487
  }
1479
- NSMutableArray<ENRMFormattingRange *> *merged = [_formattingStore.allRanges mutableCopy];
1480
- [merged addObjectsFromArray:transient];
1481
- return merged;
1488
+
1489
+ // A block chip claims the full row, so it needs a line of its own: add the
1490
+ // separators the caret position is missing rather than pulling the surrounding
1491
+ // text onto the chip's line.
1492
+ BOOL needsLeadingBreak =
1493
+ displayMode && selection.location > 0 && [plainText characterAtIndex:selection.location - 1] != '\n';
1494
+ NSUInteger afterSelection = NSMaxRange(selection);
1495
+ BOOL needsTrailingBreak =
1496
+ displayMode && afterSelection < plainText.length && [plainText characterAtIndex:afterSelection] != '\n';
1497
+
1498
+ NSString *prefix = needsLeadingBreak ? @"\n" : @"";
1499
+ NSString *suffix = needsTrailingBreak ? @"\n" : @"";
1500
+ NSString *replacement = [NSString stringWithFormat:@"%@%@%@", prefix, ENRMObjectReplacementString(), suffix];
1501
+ NSUInteger chipPosition = selection.location + prefix.length;
1502
+
1503
+ // Plain text first, chip second. Going through replaceTextInRange: is what gets
1504
+ // the formatting store, the block store, orphaned-anchor pruning and the
1505
+ // detectors adjusted for the edit; stamping the attachment afterwards only adds
1506
+ // an attribute, so none of that bookkeeping sees a second edit.
1507
+ [self replaceTextInRange:selection withText:replacement formattingRanges:@[]];
1508
+
1509
+ ENRMMathAtom *atom = [[ENRMMathAtom alloc] init];
1510
+ atom.position = chipPosition;
1511
+ atom.latex = latex;
1512
+ atom.displayMode = displayMode;
1513
+ [_mathChipController applyAtoms:@[ atom ] offset:0];
1514
+
1515
+ NSUInteger caret = selection.location + replacement.length;
1516
+ _textView.selectedRange = NSMakeRange(MIN(caret, ENRMGetPlainText(_textView).length), 0);
1517
+
1518
+ // replaceTextInRange: already emitted text/selection/formatting for the edit, but
1519
+ // the caret moved after it and the chip attribute changed the measured height.
1520
+ [_inputEventEmitter emitOnChangeSelection];
1521
+ [self requestHeightUpdate];
1522
+ [self scheduleRelayoutIfNeeded];
1482
1523
  }
1483
1524
 
1484
- - (void)clearActiveMention:(nullable NSString *)indicatorOverride
1525
+ - (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode
1485
1526
  {
1486
- NSString *indicator = indicatorOverride ?: _activeMentionIndicator;
1487
- _activeMentionIndicator = nil;
1488
- _activeMentionRange = NSMakeRange(NSNotFound, 0);
1489
- _activeMentionText = @"";
1527
+ if (start < 0) {
1528
+ return;
1529
+ }
1530
+ // Formula and mode live on the attachment, so this changes no text: no store
1531
+ // adjustment and no edit phase are needed, only a re-render and a re-measure.
1532
+ if (![_mathChipController updateChipAtPosition:(NSUInteger)start latex:latex displayMode:displayMode]) {
1533
+ return;
1534
+ }
1490
1535
 
1491
- if (indicator.length > 0) {
1492
- [self emitOnEndMention:indicator];
1536
+ // Gated like every other producer: `emitOnChangeMarkdown` is not self-guarding
1537
+ // (the check lives at each call site, e.g. `emitFormattingChanged`), and it
1538
+ // serializes the whole document — firing it for an app that never set
1539
+ // `onChangeMarkdown` would be pure cost. Android gates the same way.
1540
+ if (_inputEventEmitter.emitMarkdown) {
1541
+ [_inputEventEmitter emitOnChangeMarkdown];
1493
1542
  }
1543
+ [self requestHeightUpdate];
1544
+ [self scheduleRelayoutIfNeeded];
1494
1545
  }
1495
1546
 
1496
- - (nullable ENRMInputMentionCandidate *)mentionCandidateAtCursor
1547
+ - (void)removeMath:(NSInteger)start
1497
1548
  {
1498
- NSRange selectedRange = _textView.selectedRange;
1499
- if (_mentionIndicators.count == 0 || selectedRange.length != 0) {
1500
- return nil;
1501
- }
1502
-
1503
- NSString *plainText = ENRMGetPlainText(_textView);
1504
- NSUInteger cursor = selectedRange.location;
1505
- if (cursor > plainText.length) {
1506
- return nil;
1549
+ if (start < 0 || [_mathChipController atomAtPosition:(NSUInteger)start] == nil) {
1550
+ return;
1507
1551
  }
1508
1552
 
1509
- NSUInteger start = [ENRMWordsUtils tokenStartInText:plainText beforePosition:cursor];
1510
- NSString *token = [plainText substringWithRange:NSMakeRange(start, cursor - start)];
1511
- NSString *matchedIndicator = nil;
1512
- for (NSString *indicator in _mentionIndicators) {
1513
- if ([token hasPrefix:indicator]) {
1514
- matchedIndicator = indicator;
1515
- break;
1516
- }
1517
- }
1518
- if (matchedIndicator == nil) {
1519
- return nil;
1520
- }
1553
+ // Deleting the character takes the attachment and its paragraph style with it,
1554
+ // so the ordinary text path is enough — no controller-side cleanup like Android
1555
+ // needs for its separate AlignmentSpan, and replaceTextInRange: already emits
1556
+ // the events and re-requests the height.
1557
+ [self replaceTextInRange:NSMakeRange((NSUInteger)start, 1) withText:@"" formattingRanges:@[]];
1521
1558
 
1522
- if ([_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:start] != nil) {
1523
- return nil;
1559
+ if (_activeMathChipPosition == (NSUInteger)start) {
1560
+ _activeMathChipPosition = NSNotFound;
1524
1561
  }
1562
+ }
1525
1563
 
1526
- return [ENRMInputMentionCandidate candidateWithIndicator:matchedIndicator
1527
- start:start
1528
- end:cursor
1529
- text:[token substringFromIndex:matchedIndicator.length]];
1564
+ /// Chips in document order, as plain data for the serializer. Every path that
1565
+ /// serializes the live buffer must pass this, or each formula leaves the editor as
1566
+ /// a bare placeholder character instead of `$latex$`.
1567
+ - (NSArray<ENRMMathAtom *> *)mathAtomsForSerialization
1568
+ {
1569
+ return [_mathChipController allAtoms];
1530
1570
  }
1531
1571
 
1532
- - (void)updateActiveMention
1572
+ #if !TARGET_OS_OSX
1573
+
1574
+ - (void)handleMathChipTap:(UITapGestureRecognizer *)gesture
1533
1575
  {
1534
- ENRMInputMentionCandidate *candidate = [self mentionCandidateAtCursor];
1535
- if (candidate == nil) {
1536
- [self clearActiveMention:nil];
1576
+ if (gesture.state != UIGestureRecognizerStateRecognized) {
1577
+ return;
1578
+ }
1579
+ CGPoint location = [gesture locationInView:_textView];
1580
+ NSUInteger chipPosition = [_mathChipController chipPositionAtPoint:location];
1581
+ if (chipPosition == NSNotFound) {
1582
+ return;
1583
+ }
1584
+ ENRMMathAtom *atom = [_mathChipController atomAtPosition:chipPosition];
1585
+ if (atom == nil) {
1537
1586
  return;
1538
1587
  }
1539
1588
 
1540
- NSString *indicator = candidate.indicator;
1541
- NSUInteger start = candidate.start;
1542
- NSUInteger end = candidate.end;
1543
- NSString *query = candidate.text;
1589
+ [_mathChipController clearSelectionsExceptPosition:chipPosition];
1590
+ [_mathChipController setSelected:YES atPosition:chipPosition];
1544
1591
 
1545
- if (_activeMentionIndicator == nil || ![_activeMentionIndicator isEqualToString:indicator] ||
1546
- _activeMentionRange.location != start) {
1547
- if (_activeMentionIndicator != nil) {
1548
- [self emitOnEndMention:_activeMentionIndicator];
1549
- }
1550
- _activeMentionIndicator = indicator;
1551
- [self emitOnStartMention:indicator];
1552
- }
1553
- _activeMentionRange = NSMakeRange(start, end - start);
1592
+ // Set before moving the caret: textViewDidChangeSelection: reads both to decide
1593
+ // whether the highlight it is about to clear was set by this very tap.
1594
+ _activeMathChipPosition = chipPosition;
1595
+ _suppressMathChipClearOnce = YES;
1596
+ // Caret after the chip, not a length-1 selection: a selection would draw the
1597
+ // system's blue range handles on top of the chip's own selected styling.
1598
+ NSUInteger caret = MIN(chipPosition + 1, ENRMGetPlainText(_textView).length);
1599
+ _textView.selectedRange = NSMakeRange(caret, 0);
1554
1600
 
1555
- if (![_activeMentionText isEqualToString:query]) {
1556
- _activeMentionText = query;
1557
- [self emitOnChangeMentionWithIndicator:indicator text:query];
1558
- }
1601
+ // Recomputed rather than reused from the hit-test: selecting the chip
1602
+ // invalidated its layout above.
1603
+ CGRect glyphRect = [_mathChipController chipRectAtPosition:chipPosition];
1604
+ CGRect rectInSelf =
1605
+ CGRectIsNull(glyphRect) ? CGRectZero : [self.contentView convertRect:glyphRect fromView:_textView];
1606
+
1607
+ [_inputEventEmitter emitOnMathPressWithLatex:atom.latex
1608
+ range:NSMakeRange(chipPosition, 1)
1609
+ displayMode:atom.displayMode
1610
+ rect:rectInSelf];
1559
1611
  }
1560
1612
 
1561
- - (BOOL)deleteLinkForReplacementRange:(NSRange)range replacementText:(NSString *)text
1613
+ #endif
1614
+
1615
+ #pragma mark -
1616
+
1617
+ - (void)showLinkPrompt
1562
1618
  {
1563
- if (text.length > 0) {
1564
- return NO;
1565
- }
1619
+ ENRMFormattingRange *activeLink = [_linkCoordinator linkAtPosition:_textView.selectedRange.location];
1620
+ NSString *existingURL = activeLink != nil ? activeLink.url : nil;
1566
1621
 
1567
- NSUInteger lookupPosition;
1568
- if (range.length > 0) {
1569
- lookupPosition = range.location;
1570
- } else if (range.location > 0) {
1571
- lookupPosition = range.location - 1;
1572
- } else {
1573
- return NO;
1574
- }
1622
+ __weak EnrichedMarkdownTextInput *weakSelf = self;
1623
+ ENRMShowLinkPrompt(self, existingURL, ^(NSString *url) { [weakSelf setLink:url]; });
1624
+ }
1575
1625
 
1576
- ENRMFormattingRange *linkRange = [_formattingStore rangeOfType:ENRMInputStyleTypeLink
1577
- containingPosition:lookupPosition];
1578
- if (linkRange == nil) {
1579
- return NO;
1580
- }
1626
+ /// Serializes plain text with inline + block formatting, resolving each block's
1627
+ /// markdown line prefix through its registered handler. With no block handlers
1628
+ /// registered the prefix provider is never consulted productively and output
1629
+ /// equals the inline-only serialization.
1630
+ - (NSString *)serializeText:(NSString *)text
1631
+ ranges:(NSArray<ENRMFormattingRange *> *)ranges
1632
+ blockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
1633
+ {
1634
+ return [_clipboardCoordinator serializeText:text
1635
+ mathAtoms:[self mathAtomsForSerialization]
1636
+ ranges:ranges
1637
+ blockRanges:blockRanges];
1638
+ }
1581
1639
 
1582
- [self replaceTextInRange:linkRange.range withText:@"" formattingRanges:@[]];
1583
- [self clearActiveMention:nil];
1584
- return YES;
1640
+ - (nullable NSString *)markdownForSelectedRange
1641
+ {
1642
+ return [_clipboardCoordinator serializeSelectedRange:_textView.selectedRange
1643
+ inText:ENRMGetPlainText(_textView)
1644
+ mathAtoms:[self mathAtomsForSerialization]];
1585
1645
  }
1586
1646
 
1587
- - (void)emitOnChangeText
1647
+ - (void)copyToClipboard
1588
1648
  {
1589
- auto emitter = [self getEventEmitter];
1590
- if (emitter == nullptr) {
1649
+ NSString *plainText = ENRMGetPlainText(_textView);
1650
+ if (plainText.length == 0) {
1591
1651
  return;
1592
1652
  }
1593
- NSString *plainText = ENRMGetPlainText(_textView);
1594
- emitter->onChangeText({.value = std::string([plainText UTF8String] ?: "")});
1653
+ NSString *markdown = [_clipboardCoordinator serializeFullDocument:plainText
1654
+ mathAtoms:[self mathAtomsForSerialization]];
1655
+ NSMutableDictionary *items = [NSMutableDictionary dictionary];
1656
+ items[kUTIPlainText] = plainText;
1657
+ if (markdown.length > 0) {
1658
+ items[kENRMMarkdownPasteboardType] = markdown;
1659
+ }
1660
+ copyItemsToPasteboard(items);
1595
1661
  }
1596
1662
 
1597
- - (void)emitOnChangeMarkdown
1663
+ - (void)requestMarkdown:(NSInteger)requestId
1598
1664
  {
1599
- auto emitter = [self getEventEmitter];
1600
- if (emitter == nullptr) {
1601
- return;
1602
- }
1603
- NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
1604
- ranges:[self allRangesIncludingTransient]];
1605
- emitter->onChangeMarkdown({.value = std::string([markdown UTF8String] ?: "")});
1665
+ [_inputEventEmitter requestMarkdown:requestId];
1606
1666
  }
1607
1667
 
1608
- - (void)emitOnChangeSelection
1668
+ - (CGRect)computeCaretRect
1609
1669
  {
1610
- auto emitter = [self getEventEmitter];
1611
- if (emitter == nullptr) {
1612
- return;
1670
+ CGRect caretRect = CGRectZero;
1671
+ #if !TARGET_OS_OSX
1672
+ UITextRange *selectedRange = _textView.selectedTextRange;
1673
+ if (selectedRange != nil) {
1674
+ caretRect = [_textView caretRectForPosition:selectedRange.start];
1613
1675
  }
1676
+ #else
1614
1677
  NSRange selection = _textView.selectedRange;
1615
- emitter->onChangeSelection({
1616
- .start = static_cast<int>(selection.location),
1617
- .end = static_cast<int>(NSMaxRange(selection)),
1618
- });
1678
+ if (selection.location != NSNotFound) {
1679
+ NSRange glyphRange = [_textView.layoutManager glyphRangeForCharacterRange:NSMakeRange(selection.location, 0)
1680
+ actualCharacterRange:NULL];
1681
+ caretRect = [_textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
1682
+ caretRect.origin.x += _textView.textContainerInset.left;
1683
+ caretRect.origin.y += _textView.textContainerInset.top;
1684
+ }
1685
+ #endif
1686
+ return caretRect;
1619
1687
  }
1620
1688
 
1621
- - (void)emitOnChangeState
1689
+ - (void)requestCaretRect:(NSInteger)requestId
1622
1690
  {
1623
- auto emitter = [self getEventEmitter];
1624
- if (emitter == nullptr) {
1625
- return;
1626
- }
1691
+ [_inputEventEmitter requestCaretRect:requestId];
1692
+ }
1693
+
1694
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
1695
+ {
1696
+ RCTEnrichedMarkdownTextInputHandleCommand(self, commandName, args);
1697
+ }
1627
1698
 
1628
- NSUInteger cursor = _textView.selectedRange.location;
1629
- BOOL boldActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrong atPosition:cursor];
1630
- BOOL italicActive = [self isEffectiveStyleActive:ENRMInputStyleTypeEmphasis atPosition:cursor];
1631
- BOOL underlineActive = [self isEffectiveStyleActive:ENRMInputStyleTypeUnderline atPosition:cursor];
1632
- BOOL strikethroughActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrikethrough atPosition:cursor];
1633
- BOOL spoilerActive = [self isEffectiveStyleActive:ENRMInputStyleTypeSpoiler atPosition:cursor];
1634
- BOOL linkActive = [self isEffectiveStyleActive:ENRMInputStyleTypeLink atPosition:cursor];
1699
+ #pragma mark - ENRMInputEventEmitterDataSource
1635
1700
 
1636
- if (_prevState.initialized && _prevState.bold == boldActive && _prevState.italic == italicActive &&
1637
- _prevState.underline == underlineActive && _prevState.strikethrough == strikethroughActive &&
1638
- _prevState.spoiler == spoilerActive && _prevState.link == linkActive) {
1639
- return;
1701
+ - (std::shared_ptr<EnrichedMarkdownTextInputEventEmitter const>)fabricEventEmitter
1702
+ {
1703
+ if (_eventEmitter == nullptr || _editSession.shouldSuppressEvents) {
1704
+ return nullptr;
1640
1705
  }
1706
+ return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
1707
+ }
1641
1708
 
1642
- _prevState.bold = boldActive;
1643
- _prevState.italic = italicActive;
1644
- _prevState.underline = underlineActive;
1645
- _prevState.strikethrough = strikethroughActive;
1646
- _prevState.spoiler = spoilerActive;
1647
- _prevState.link = linkActive;
1648
- _prevState.initialized = YES;
1649
-
1650
- emitter->onChangeState({
1651
- .bold = {.isActive = boldActive},
1652
- .italic = {.isActive = italicActive},
1653
- .underline = {.isActive = underlineActive},
1654
- .strikethrough = {.isActive = strikethroughActive},
1655
- .spoiler = {.isActive = spoilerActive},
1656
- .link = {.isActive = linkActive},
1657
- });
1709
+ - (NSRange)selectedRange
1710
+ {
1711
+ return _textView.selectedRange;
1658
1712
  }
1659
1713
 
1660
- - (void)emitCaretRectChangeIfNeeded
1714
+ - (BOOL)isStyleActive:(ENRMInputStyleType)type inRange:(NSRange)range
1661
1715
  {
1662
- auto emitter = [self getEventEmitter];
1663
- if (emitter == nullptr) {
1664
- return;
1665
- }
1716
+ return [_formattingStore isStyleActive:type inRange:range];
1717
+ }
1666
1718
 
1667
- CGRect caretRect = [self computeCaretRect];
1719
+ - (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1720
+ {
1721
+ return [_typingController isEffectiveStyleActive:type atPosition:position];
1722
+ }
1668
1723
 
1669
- if (_prevCaretRect.has_value() && CGRectEqualToRect(_prevCaretRect.value(), caretRect)) {
1670
- return;
1671
- }
1724
+ #pragma mark - ENRMInputTypingAttributesDataSource
1672
1725
 
1673
- _prevCaretRect = caretRect;
1726
+ - (BOOL)isStyleAdjacentBefore:(ENRMInputStyleType)type position:(NSUInteger)position
1727
+ {
1728
+ return [_formattingStore isStyleAdjacentBefore:type position:position];
1729
+ }
1674
1730
 
1675
- emitter->onCaretRectChange({
1676
- .x = caretRect.origin.x,
1677
- .y = caretRect.origin.y,
1678
- .width = caretRect.size.width,
1679
- .height = caretRect.size.height,
1680
- });
1731
+ - (BOOL)isStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1732
+ {
1733
+ return [_formattingStore isStyleActive:type atPosition:position];
1681
1734
  }
1682
1735
 
1683
- - (NSArray<NSString *> *)contextMenuItemTexts
1736
+ - (NSString *)currentMarkdown
1684
1737
  {
1685
- return _contextMenuItemTexts ?: @[];
1738
+ return [self serializeText:ENRMGetPlainText(_textView)
1739
+ ranges:[self allRangesIncludingTransient]
1740
+ blockRanges:_blockStore.allRanges];
1686
1741
  }
1687
1742
 
1688
- - (NSArray<NSString *> *)contextMenuItemIcons
1743
+ - (NSArray<ENRMFormattingRange *> *)allRangesIncludingTransient
1689
1744
  {
1690
- return _contextMenuItemIcons ?: @[];
1745
+ return [_clipboardCoordinator allRangesIncludingTransient];
1691
1746
  }
1692
1747
 
1693
- - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
1748
+ - (void)updateActiveMention
1694
1749
  {
1695
- return _inputSelectionMenuConfig;
1750
+ [self dispatchMentionUpdate];
1696
1751
  }
1697
1752
 
1698
- - (ENRMFormatMenuConfig)formatMenuConfig
1753
+ - (void)clearActiveMention:(nullable NSString *)indicatorOverride
1699
1754
  {
1700
- return _formatMenuConfig;
1755
+ [self dispatchMentionEvents:[_mentionCoordinator clearWithIndicatorOverride:indicatorOverride]];
1701
1756
  }
1702
1757
 
1703
- - (void)emitContextMenuItemPress:(NSString *)itemText
1758
+ - (void)dispatchMentionUpdate
1704
1759
  {
1705
- auto eventEmitter = [self getEventEmitter];
1706
- if (eventEmitter == nullptr) {
1707
- return;
1760
+ NSString *plainText = ENRMGetPlainText(_textView);
1761
+ [self dispatchMentionEvents:[_mentionCoordinator updateWithText:plainText selectedRange:_textView.selectedRange]];
1762
+ }
1763
+
1764
+ - (void)dispatchMentionEvents:(NSArray<ENRMMentionEvent *> *)events
1765
+ {
1766
+ for (ENRMMentionEvent *event in events) {
1767
+ switch (event.type) {
1768
+ case ENRMMentionEventStart:
1769
+ [_inputEventEmitter emitOnStartMention:event.indicator];
1770
+ break;
1771
+ case ENRMMentionEventChange:
1772
+ [_inputEventEmitter emitOnChangeMentionWithIndicator:event.indicator text:event.text];
1773
+ break;
1774
+ case ENRMMentionEventEnd:
1775
+ [_inputEventEmitter emitOnEndMention:event.indicator];
1776
+ break;
1777
+ }
1708
1778
  }
1779
+ }
1709
1780
 
1710
- NSRange selectedRange = _textView.selectedRange;
1711
- NSString *selectedText =
1712
- selectedRange.length > 0 ? [_textView.textStorage.string substringWithRange:selectedRange] : @"";
1781
+ - (BOOL)deleteLinkForReplacementRange:(NSRange)range replacementText:(NSString *)text
1782
+ {
1783
+ if (text.length > 0) {
1784
+ return NO;
1785
+ }
1713
1786
 
1714
- auto isActive = [&](ENRMInputStyleType type) -> BOOL {
1715
- if (selectedRange.length > 0) {
1716
- return [_formattingStore isStyleActive:type inRange:selectedRange];
1717
- }
1718
- return [self isEffectiveStyleActive:type atPosition:selectedRange.location];
1719
- };
1787
+ NSUInteger lookupPosition;
1788
+ if (range.length > 0) {
1789
+ lookupPosition = range.location;
1790
+ } else if (range.location > 0) {
1791
+ lookupPosition = range.location - 1;
1792
+ } else {
1793
+ return NO;
1794
+ }
1720
1795
 
1721
- BOOL boldActive = isActive(ENRMInputStyleTypeStrong);
1722
- BOOL italicActive = isActive(ENRMInputStyleTypeEmphasis);
1723
- BOOL underlineActive = isActive(ENRMInputStyleTypeUnderline);
1724
- BOOL strikethroughActive = isActive(ENRMInputStyleTypeStrikethrough);
1725
- BOOL spoilerActive = isActive(ENRMInputStyleTypeSpoiler);
1726
- BOOL linkActive = isActive(ENRMInputStyleTypeLink);
1727
-
1728
- eventEmitter->onContextMenuItemPress({
1729
- .itemText = std::string(itemText.UTF8String),
1730
- .selectedText = std::string(selectedText.UTF8String),
1731
- .selectionStart = static_cast<int>(selectedRange.location),
1732
- .selectionEnd = static_cast<int>(NSMaxRange(selectedRange)),
1733
- .styleState =
1734
- {
1735
- .bold = {.isActive = boldActive},
1736
- .italic = {.isActive = italicActive},
1737
- .underline = {.isActive = underlineActive},
1738
- .strikethrough = {.isActive = strikethroughActive},
1739
- .spoiler = {.isActive = spoilerActive},
1740
- .link = {.isActive = linkActive},
1741
- },
1742
- });
1796
+ ENRMFormattingRange *linkRange = [_linkCoordinator linkAtPosition:lookupPosition];
1797
+ if (linkRange == nil) {
1798
+ return NO;
1799
+ }
1800
+
1801
+ [self replaceTextInRange:linkRange.range withText:@"" formattingRanges:@[]];
1802
+ [self clearActiveMention:nil];
1803
+ return YES;
1743
1804
  }
1744
1805
 
1745
- - (void)emitOnFocus
1806
+ - (NSArray<NSString *> *)contextMenuItemTexts
1746
1807
  {
1747
- auto emitter = [self getEventEmitter];
1748
- if (emitter == nullptr) {
1749
- return;
1750
- }
1751
- emitter->onInputFocus({});
1808
+ return _contextMenuItemTexts ?: @[];
1752
1809
  }
1753
1810
 
1754
- - (void)emitOnBlur
1811
+ - (NSArray<NSString *> *)contextMenuItemIcons
1755
1812
  {
1756
- auto emitter = [self getEventEmitter];
1757
- if (emitter == nullptr) {
1758
- return;
1759
- }
1760
- emitter->onInputBlur({});
1813
+ return _contextMenuItemIcons ?: @[];
1761
1814
  }
1762
1815
 
1763
- - (void)emitOnLinkDetectedWithText:(NSString *)text url:(NSString *)url range:(NSRange)range
1816
+ - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
1764
1817
  {
1765
- auto emitter = [self getEventEmitter];
1766
- if (emitter == nullptr) {
1767
- return;
1768
- }
1769
- emitter->onLinkDetected({
1770
- .text = std::string([text UTF8String] ?: ""),
1771
- .url = std::string([url UTF8String] ?: ""),
1772
- .start = static_cast<int>(range.location),
1773
- .end = static_cast<int>(range.location + range.length),
1774
- });
1818
+ return _inputSelectionMenuConfig;
1775
1819
  }
1776
1820
 
1777
- - (void)emitOnStartMention:(NSString *)indicator
1821
+ - (ENRMFormatMenuConfig)formatMenuConfig
1778
1822
  {
1779
- auto emitter = [self getEventEmitter];
1780
- if (emitter == nullptr) {
1781
- return;
1782
- }
1783
- emitter->onStartMention({.indicator = std::string([indicator UTF8String] ?: "")});
1823
+ return _formatMenuConfig;
1784
1824
  }
1785
1825
 
1786
- - (void)emitOnChangeMentionWithIndicator:(NSString *)indicator text:(NSString *)text
1826
+ - (ENRMInputEventEmitter *)inputEventEmitter
1787
1827
  {
1788
- auto emitter = [self getEventEmitter];
1789
- if (emitter == nullptr) {
1790
- return;
1791
- }
1792
- emitter->onChangeMention({
1793
- .indicator = std::string([indicator UTF8String] ?: ""),
1794
- .text = std::string([text UTF8String] ?: ""),
1795
- });
1828
+ return _inputEventEmitter;
1796
1829
  }
1797
1830
 
1798
- - (void)emitOnEndMention:(NSString *)indicator
1831
+ - (ENRMInputTypingAttributesController *)typingController
1799
1832
  {
1800
- auto emitter = [self getEventEmitter];
1801
- if (emitter == nullptr) {
1802
- return;
1803
- }
1804
- emitter->onEndMention({.indicator = std::string([indicator UTF8String] ?: "")});
1833
+ return _typingController;
1805
1834
  }
1806
1835
 
1807
1836
  #pragma mark - Text edit tracking
1808
1837
 
1809
1838
  - (void)handleTextChanged
1810
1839
  {
1811
- if (ENRMHasMarkedText(_textView)) {
1840
+ if (_editSession.isComposing) {
1812
1841
  return;
1813
1842
  }
1814
1843
 
@@ -1838,64 +1867,63 @@ using namespace facebook::react;
1838
1867
  }
1839
1868
  }
1840
1869
 
1841
- [_formattingStore adjustForEditAtLocation:editLocation deletedLength:deletedLength insertedLength:insertedLength];
1870
+ ENRMEditContext *context = [[ENRMEditContext alloc] initWithEditLocation:editLocation
1871
+ deletedLength:deletedLength
1872
+ insertedLength:insertedLength
1873
+ preEditReplacedNewline:_preEditReplacedNewline
1874
+ preEditBlockType:_preEditBlockType
1875
+ preEditBlockLevel:_preEditBlockLevel
1876
+ preEditParagraphWasEmpty:_preEditParagraphWasEmpty
1877
+ pendingStyles:_typingController.pendingStyles
1878
+ pendingStyleRemovals:_typingController.pendingStyleRemovals];
1842
1879
 
1843
- if (insertedLength > 0) {
1844
- NSRange insertedRange = NSMakeRange(editLocation, insertedLength);
1845
-
1846
- // Skip applying pending styles when the insertion is only line breaks —
1847
- // a phantom range over a bare newline corrupts isStyleActive() at the boundary.
1848
- NSString *plainText = ENRMGetPlainText(_textView);
1849
- NSUInteger insertedEnd = NSMaxRange(insertedRange);
1850
- BOOL insertedHasGlyphContent = NO;
1851
- if (insertedEnd <= plainText.length) {
1852
- NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet];
1853
- for (NSUInteger i = insertedRange.location; i < insertedEnd; i++) {
1854
- if (![newlines characterIsMember:[plainText characterAtIndex:i]]) {
1855
- insertedHasGlyphContent = YES;
1856
- break;
1857
- }
1858
- }
1859
- }
1860
-
1861
- if (insertedHasGlyphContent) {
1862
- for (NSNumber *styleNum in _pendingStyles) {
1863
- ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:(ENRMInputStyleType)styleNum.integerValue
1864
- range:insertedRange];
1865
- [_formattingStore addRange:newRange];
1866
- }
1867
- }
1880
+ BOOL touchedNewline = [_editPipeline processTextChangeWithContext:context];
1868
1881
 
1869
- // adjustForEditAtLocation may have expanded an existing range to cover
1870
- // the insertion — carve out the inserted portion for removed styles.
1871
- for (NSNumber *styleNum in _pendingStyleRemovals) {
1872
- [_formattingStore removeType:(ENRMInputStyleType)styleNum.integerValue inRange:insertedRange];
1873
- }
1874
- }
1882
+ // Consumed: clear pre-edit state so stale values can't leak to the next edit.
1883
+ _preEditBlockType = ENRMInputBlockTypeParagraph;
1884
+ _preEditBlockLevel = 0;
1885
+ _preEditParagraphWasEmpty = NO;
1886
+ _preEditReplacedNewline = NO;
1875
1887
 
1876
1888
  _lastTextLength = newLength;
1877
1889
 
1878
1890
  #if !TARGET_OS_OSX
1879
1891
  if (newLength == 0) {
1880
- [self resetBaseTypingAttributes];
1892
+ if ([self headingLevelForCursorParagraph] > 0 || [self listBlockForCursorParagraph] != nil) {
1893
+ [_typingController syncWithCursorBlock];
1894
+ } else {
1895
+ [self resetBaseTypingAttributes];
1896
+ }
1881
1897
  }
1882
1898
  #endif
1883
1899
 
1884
- [self applyFormatting];
1900
+ if (touchedNewline) {
1901
+ [self applyFormatting];
1902
+ } else {
1903
+ [self applyFormattingScopedToEditAtLocation:editLocation insertedLength:insertedLength];
1904
+ }
1905
+
1906
+ if (_textView.selectedRange.length == 0) {
1907
+ [_typingController syncWithCursorBlock];
1908
+ }
1885
1909
 
1886
- NSUInteger clampedEditLocation = MIN(editLocation, newLength);
1887
- NSUInteger clampedInsertedLength = MIN(insertedLength, newLength - clampedEditLocation);
1888
- [_detectorPipeline processTextChange:ENRMGetPlainText(_textView)
1889
- modificationRange:NSMakeRange(clampedEditLocation, clampedInsertedLength)];
1910
+ if ([self listBlockForCursorParagraph] != nil) {
1911
+ [_typingController syncWithCursorBlock];
1912
+ } else if (_textView.typingAttributes[NSParagraphStyleAttributeName] != nil) {
1913
+ [_typingController clearListParagraphStyle];
1914
+ }
1915
+
1916
+ [_editPipeline detectLinksAtLocation:editLocation insertedLength:insertedLength];
1890
1917
 
1891
1918
  [self updatePlaceholderVisibility];
1892
- [self emitOnChangeText];
1893
- [self emitOnChangeSelection];
1894
- [self emitFormattingChanged];
1919
+ [_inputEventEmitter emitOnChangeText];
1920
+ [_inputEventEmitter emitOnChangeSelection];
1921
+ [_inputEventEmitter emitFormattingChanged];
1895
1922
  [self updateActiveMention];
1896
- [self emitCaretRectChangeIfNeeded];
1923
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
1897
1924
  [self requestHeightUpdate];
1898
1925
  [self scheduleRelayoutIfNeeded];
1926
+ [self updateEmptyBulletMarker];
1899
1927
  }
1900
1928
 
1901
1929
  #pragma mark - Text view delegate
@@ -1933,17 +1961,21 @@ using namespace facebook::react;
1933
1961
  {
1934
1962
  [self stripLinkTypingAttributes];
1935
1963
 
1936
- if (_textView.selectedRange.length == 0 && !_isTextChanging) {
1964
+ if (_textView.selectedRange.length == 0 && _editSession.phase == ENRMEditPhaseIdle) {
1937
1965
  NSString *text = ENRMGetPlainText(_textView);
1938
1966
  if (text.length > 0) {
1939
1967
  NSRange paragraphRange = [text paragraphRangeForRange:_textView.selectedRange];
1940
1968
  NSString *paragraphText = [text substringWithRange:paragraphRange];
1941
1969
  BOOL isEmpty = paragraphText.length == 0 || [paragraphText isEqualToString:@"\n"];
1942
1970
  if (isEmpty) {
1943
- NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
1944
- attrs[NSFontAttributeName] = _formatterStyle.baseFont;
1945
- attrs[NSForegroundColorAttributeName] = _formatterStyle.baseTextColor;
1946
- _textView.typingAttributes = attrs;
1971
+ if ([self headingLevelForCursorParagraph] > 0 || [self listBlockForCursorParagraph] != nil) {
1972
+ [_typingController syncWithCursorBlock];
1973
+ } else {
1974
+ NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
1975
+ attrs[NSFontAttributeName] = _formatterStyle.baseFont;
1976
+ attrs[NSForegroundColorAttributeName] = _formatterStyle.baseTextColor;
1977
+ _textView.typingAttributes = attrs;
1978
+ }
1947
1979
  }
1948
1980
  }
1949
1981
  }
@@ -1951,125 +1983,154 @@ using namespace facebook::react;
1951
1983
 
1952
1984
  - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
1953
1985
  {
1986
+ [_inputEventEmitter emitOnKeyPress:text];
1954
1987
  if ([self deleteLinkForReplacementRange:range replacementText:text]) {
1955
1988
  return NO;
1956
1989
  }
1990
+ if ([self handleListKeyForReplacementRange:range replacementText:text]) {
1991
+ return NO;
1992
+ }
1957
1993
  _preEditSelectedRange = _lastSelectedRange;
1958
- _isTextChanging = YES;
1994
+ [self capturePreEditBlockForRange:range];
1995
+ _preEditParagraphWasEmpty = [self preEditParagraphWasEmpty:range];
1996
+ _preEditReplacedNewline = [self replacedRangeTouchesNewline:range];
1997
+ [_editSession enterPhase:ENRMEditPhaseProcessing];
1959
1998
  [self stripLinkTypingAttributes];
1960
1999
  return YES;
1961
2000
  }
1962
2001
 
1963
2002
  - (void)textViewDidChange:(UITextView *)textView
1964
2003
  {
1965
- if (_isApplyingFormatting) {
2004
+ if (_editSession.shouldSuppressFormatting) {
1966
2005
  return;
1967
2006
  }
1968
2007
  [self handleTextChanged];
1969
- _isTextChanging = NO;
1970
- _lastTextChangeTime = CACurrentMediaTime();
2008
+ [_editSession exitPhase];
2009
+ [_editSession recordTextChange];
1971
2010
  _lastSelectedRange = textView.selectedRange;
1972
2011
  }
1973
2012
 
1974
2013
  - (void)textViewDidBeginEditing:(UITextView *)textView
1975
2014
  {
1976
- [self emitOnFocus];
2015
+ [_inputEventEmitter emitOnFocus];
1977
2016
  }
1978
2017
 
1979
2018
  - (void)textViewDidEndEditing:(UITextView *)textView
1980
2019
  {
1981
2020
  [self clearActiveMention:nil];
1982
- [self emitOnBlur];
2021
+ [_inputEventEmitter emitOnBlur];
2022
+ }
2023
+
2024
+ /// Atomic-link snap clamped to the current text length, so a stale link range
2025
+ /// past the text end can't drive an unbounded snap/clamp loop.
2026
+ - (NSRange)clampedAtomicSelectionForSelection:(NSRange)selection
2027
+ {
2028
+ NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:selection];
2029
+ NSUInteger textLength = _textView.textStorage.length;
2030
+ if (adjusted.location > textLength) {
2031
+ return NSMakeRange(textLength, 0);
2032
+ }
2033
+ if (NSMaxRange(adjusted) > textLength) {
2034
+ adjusted.length = textLength - adjusted.location;
2035
+ }
2036
+ return adjusted;
2037
+ }
2038
+
2039
+ /// YES while a UIKit selection gesture (handle drag, long-press loupe) is
2040
+ /// mid-flight; snapping selectedRange then fights the gesture and flickers the
2041
+ /// edit menu, so callers defer the snap until it settles.
2042
+ - (BOOL)selectionGestureIsActive
2043
+ {
2044
+ for (UIGestureRecognizer *recognizer in _textView.gestureRecognizers) {
2045
+ if (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged) {
2046
+ return YES;
2047
+ }
2048
+ }
2049
+ return NO;
2050
+ }
2051
+
2052
+ /// Re-applies the atomic-link snap once the selection gesture ends, polling the
2053
+ /// main queue because UIKit may not re-fire the selection delegate at touch-up.
2054
+ - (void)schedulePostGestureAtomicSnap
2055
+ {
2056
+ if (_atomicSnapScheduled) {
2057
+ return;
2058
+ }
2059
+ _atomicSnapScheduled = YES;
2060
+ __weak __typeof(self) weakSelf = self;
2061
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kENRMAtomicSnapPollInterval * NSEC_PER_SEC)),
2062
+ dispatch_get_main_queue(), ^{
2063
+ __typeof(self) strongSelf = weakSelf;
2064
+ if (strongSelf == nil) {
2065
+ return;
2066
+ }
2067
+ strongSelf->_atomicSnapScheduled = NO;
2068
+ if (strongSelf->_editSession.shouldSuppressSelectionSideEffects ||
2069
+ strongSelf->_editSession.isComposing) {
2070
+ return;
2071
+ }
2072
+ if ([strongSelf selectionGestureIsActive]) {
2073
+ [strongSelf schedulePostGestureAtomicSnap];
2074
+ return;
2075
+ }
2076
+ NSRange selection = strongSelf->_textView.selectedRange;
2077
+ NSRange adjusted = [strongSelf clampedAtomicSelectionForSelection:selection];
2078
+ if (!NSEqualRanges(adjusted, selection)) {
2079
+ strongSelf->_textView.selectedRange = adjusted;
2080
+ }
2081
+ });
1983
2082
  }
1984
2083
 
1985
2084
  - (void)textViewDidChangeSelection:(UITextView *)textView
1986
2085
  {
1987
2086
  NSRange newSelection = textView.selectedRange;
1988
- // Links (e.g. mentions) are atomic: snap a partial selection to the whole link, and a caret inside
1989
- // a link to its end. Returning hands the adjusted selection to the recursive change (no double emit).
1990
- if (!_isApplyingFormatting && !_isTextChanging && !ENRMHasMarkedText(_textView)) {
1991
- NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:newSelection];
1992
- if (!NSEqualRanges(adjusted, newSelection)) {
1993
- _textView.selectedRange = adjusted;
1994
- return;
2087
+ if (!_editSession.shouldSuppressSelectionSideEffects && !_editSession.isComposing) {
2088
+ if ([self selectionGestureIsActive]) {
2089
+ [self schedulePostGestureAtomicSnap];
2090
+ } else {
2091
+ NSRange adjusted = [self clampedAtomicSelectionForSelection:newSelection];
2092
+ if (!NSEqualRanges(adjusted, newSelection)) {
2093
+ _textView.selectedRange = adjusted;
2094
+ return;
2095
+ }
1995
2096
  }
1996
2097
  }
1997
2098
  NSRange previousSelection = _lastSelectedRange;
1998
2099
  _lastSelectedRange = newSelection;
1999
2100
 
2000
- if (_isApplyingFormatting || _isTextChanging) {
2101
+ if (_editSession.shouldSuppressSelectionSideEffects) {
2001
2102
  return;
2002
2103
  }
2003
2104
 
2004
- if (ENRMHasMarkedText(_textView)) {
2105
+ if (_editSession.isComposing) {
2005
2106
  return;
2006
2107
  }
2007
2108
 
2008
- #if ENRICHED_MARKDOWN_MATH
2109
+ BOOL selectionMoved =
2110
+ newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2111
+
2009
2112
  if (_suppressMathChipClearOnce) {
2010
- // Caret update was triggered by handleTapForMathChip — keep that chip
2011
- // selected; consume the suppression flag.
2113
+ // This caret move is the one handleMathChipTap: just made — keep its chip
2114
+ // highlighted and consume the suppression.
2012
2115
  _suppressMathChipClearOnce = NO;
2013
- } else {
2014
- [self clearMathChipSelectionsExcept:NSMakeRange(NSNotFound, 0)];
2015
- _activeMathChipIndex = NSNotFound;
2116
+ } else if (_activeMathChipPosition != NSNotFound) {
2117
+ [_mathChipController clearSelectionsExceptPosition:NSNotFound];
2118
+ _activeMathChipPosition = NSNotFound;
2016
2119
  }
2017
- #endif
2018
-
2019
- BOOL selectionMoved =
2020
- newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2021
2120
 
2022
2121
  if (selectionMoved) {
2023
- [self resetPendingStylesForSelectionChange];
2122
+ [_typingController resetForSelectionChange];
2024
2123
  }
2025
2124
 
2026
2125
  [self manageSelectionBasedChanges];
2027
2126
 
2028
- [self emitOnChangeSelection];
2127
+ [_inputEventEmitter emitOnChangeSelection];
2029
2128
  [self updateActiveMention];
2030
- [self emitOnChangeState];
2031
- [self emitCaretRectChangeIfNeeded];
2129
+ [_inputEventEmitter emitOnChangeState];
2130
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
2131
+ [self updateEmptyBulletMarker];
2032
2132
  }
2033
2133
 
2034
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
2035
- - (void)clearMathChipSelectionsExcept:(NSRange)keepRange
2036
- {
2037
- NSTextStorage *storage = _textView.textStorage;
2038
- if (storage.length == 0)
2039
- return;
2040
- __block BOOL anyChanged = NO;
2041
- __block NSMutableArray<NSValue *> *invalidationRanges = [NSMutableArray array];
2042
- [storage enumerateAttribute:NSAttachmentAttributeName
2043
- inRange:NSMakeRange(0, storage.length)
2044
- options:0
2045
- usingBlock:^(id value, NSRange range, BOOL *stop) {
2046
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
2047
- return;
2048
- ENRMMathInlineAttachment *att = value;
2049
- BOOL keep = NSEqualRanges(range, keepRange);
2050
- if (att.isSelected && !keep) {
2051
- att.isSelected = NO;
2052
- [att invalidateRender];
2053
- [invalidationRanges addObject:[NSValue valueWithRange:range]];
2054
- anyChanged = YES;
2055
- }
2056
- }];
2057
- if (anyChanged) {
2058
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
2059
- for (NSValue *v in invalidationRanges) {
2060
- NSRange r = v.rangeValue;
2061
- [lm invalidateLayoutForCharacterRange:r actualCharacterRange:NULL];
2062
- }
2063
- ENRMSetNeedsDisplay(_textView);
2064
- }
2065
- }
2066
- #else
2067
- - (void)clearMathChipSelectionsExcept:(NSRange)keepRange
2068
- {
2069
- (void)keepRange;
2070
- }
2071
- #endif
2072
-
2073
2134
  #else
2074
2135
 
2075
2136
  #pragma mark - RCTBackedTextInputDelegate (macOS)
@@ -2081,7 +2142,7 @@ using namespace facebook::react;
2081
2142
 
2082
2143
  - (void)textInputDidBeginEditing
2083
2144
  {
2084
- [self emitOnFocus];
2145
+ [_inputEventEmitter emitOnFocus];
2085
2146
  }
2086
2147
 
2087
2148
  - (BOOL)textInputShouldEndEditing
@@ -2092,7 +2153,7 @@ using namespace facebook::react;
2092
2153
  - (void)textInputDidEndEditing
2093
2154
  {
2094
2155
  [self clearActiveMention:nil];
2095
- [self emitOnBlur];
2156
+ [_inputEventEmitter emitOnBlur];
2096
2157
  }
2097
2158
 
2098
2159
  - (BOOL)textInputShouldReturn
@@ -2111,31 +2172,36 @@ using namespace facebook::react;
2111
2172
 
2112
2173
  - (nullable NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
2113
2174
  {
2175
+ [_inputEventEmitter emitOnKeyPress:text];
2114
2176
  if ([self deleteLinkForReplacementRange:range replacementText:text]) {
2115
2177
  return nil;
2116
2178
  }
2117
2179
  _preEditSelectedRange = _lastSelectedRange;
2118
- _isTextChanging = YES;
2180
+ // Capture the same pre-edit block state the iOS path does — otherwise these
2181
+ // ivars carry stale values from a previous edit into reconcileBlockContinuation.
2182
+ [self capturePreEditBlockForRange:range];
2183
+ _preEditParagraphWasEmpty = [self preEditParagraphWasEmpty:range];
2184
+ _preEditReplacedNewline = [self replacedRangeTouchesNewline:range];
2185
+ [_editSession enterPhase:ENRMEditPhaseProcessing];
2119
2186
  return text;
2120
2187
  }
2121
2188
 
2122
2189
  - (void)textInputDidChange
2123
2190
  {
2124
- if (_isApplyingFormatting) {
2125
- _isTextChanging = NO;
2191
+ if (_editSession.shouldSuppressFormatting) {
2192
+ [_editSession exitPhase];
2126
2193
  return;
2127
2194
  }
2128
2195
  [self handleTextChanged];
2129
- _isTextChanging = NO;
2130
- _lastTextChangeTime = CACurrentMediaTime();
2196
+ [_editSession exitPhase];
2197
+ [_editSession recordTextChange];
2131
2198
  _lastSelectedRange = _textView.selectedRange;
2132
2199
  }
2133
2200
 
2134
2201
  - (void)textInputDidChangeSelection
2135
2202
  {
2136
2203
  NSRange newSelection = _textView.selectedRange;
2137
- // Atomic link snapping — same logic as textViewDidChangeSelection: (iOS).
2138
- if (!_isApplyingFormatting && !_isTextChanging && !ENRMHasMarkedText(_textView)) {
2204
+ if (!_editSession.shouldSuppressSelectionSideEffects && !_editSession.isComposing) {
2139
2205
  NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:newSelection];
2140
2206
  if (!NSEqualRanges(adjusted, newSelection)) {
2141
2207
  _textView.selectedRange = adjusted;
@@ -2145,11 +2211,11 @@ using namespace facebook::react;
2145
2211
  NSRange previousSelection = _lastSelectedRange;
2146
2212
  _lastSelectedRange = newSelection;
2147
2213
 
2148
- if (_isApplyingFormatting || _isTextChanging) {
2214
+ if (_editSession.shouldSuppressSelectionSideEffects) {
2149
2215
  return;
2150
2216
  }
2151
2217
 
2152
- if (ENRMHasMarkedText(_textView)) {
2218
+ if (_editSession.isComposing) {
2153
2219
  return;
2154
2220
  }
2155
2221
 
@@ -2157,13 +2223,13 @@ using namespace facebook::react;
2157
2223
  newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2158
2224
 
2159
2225
  if (selectionMoved) {
2160
- [self resetPendingStylesForSelectionChange];
2226
+ [_typingController resetForSelectionChange];
2161
2227
  }
2162
2228
 
2163
- [self emitOnChangeSelection];
2229
+ [_inputEventEmitter emitOnChangeSelection];
2164
2230
  [self updateActiveMention];
2165
- [self emitOnChangeState];
2166
- [self emitCaretRectChangeIfNeeded];
2231
+ [_inputEventEmitter emitOnChangeState];
2232
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
2167
2233
  }
2168
2234
 
2169
2235
  // @required stubs for RCTBackedTextInputDelegate — RCTUITextView's internal adapter