react-native-enriched-markdown-scaffold 0.7.4-scaffold.8 → 1.0.2-scaffold.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/README.md +54 -8
  2. package/ReactNativeEnrichedMarkdown.podspec +82 -73
  3. package/android/build.gradle +191 -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 +27 -0
  8. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +9 -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 +40 -0
  14. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +839 -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 +175 -11
  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 +62 -4
  26. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +911 -460
  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/BlockSyntaxSentinel.kt +69 -0
  43. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/FormattingStore.kt +80 -87
  44. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputFormatter.kt +109 -1
  45. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +180 -27
  46. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/MarkdownSerializer.kt +167 -38
  47. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/RangeEditAdjustment.kt +149 -0
  48. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +66 -5
  49. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -2
  50. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockRange.kt +27 -0
  51. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/BlockType.kt +48 -0
  52. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/FormattingRange.kt +9 -4
  53. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/InputFormatterStyle.kt +39 -2
  54. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathAtom.kt +20 -0
  55. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +1 -1
  56. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MutableRangeBounds.kt +11 -0
  57. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputBulletSpan.kt +82 -0
  58. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputHeadingSpan.kt +58 -0
  59. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputListItemSpacingSpan.kt +37 -0
  60. package/android/src/main/java/com/swmansion/enriched/markdown/input/spans/InputOrderedListMarkerSpan.kt +63 -0
  61. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/BlockHandler.kt +54 -0
  62. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/HeadingBlockHandler.kt +27 -0
  63. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/OrderedListBlockHandler.kt +38 -0
  64. package/android/src/main/java/com/swmansion/enriched/markdown/input/styles/UnorderedListBlockHandler.kt +37 -0
  65. package/android/src/main/java/com/swmansion/enriched/markdown/input/toolbar/InputContextMenu.kt +8 -26
  66. package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +1 -0
  67. package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +1 -0
  68. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +9 -2
  69. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +100 -12
  70. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +16 -1
  71. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +4 -0
  72. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +2 -1
  73. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +74 -18
  74. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +43 -4
  75. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +9 -1
  76. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SoftBreakRenderer.kt +16 -0
  77. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +30 -27
  78. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +24 -0
  79. package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +174 -24
  80. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +27 -3
  81. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +13 -16
  82. package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +1 -1
  83. package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +1 -1
  84. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +182 -46
  85. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListItemSpacingSpan.kt +33 -0
  86. package/android/src/main/java/com/swmansion/enriched/markdown/spans/ListMarkerAnchorSpan.kt +38 -0
  87. package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +2 -0
  88. package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +1 -1
  89. package/android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt +3 -2
  90. package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +3 -2
  91. package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +6 -0
  92. package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +26 -0
  93. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +7 -1
  94. package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +4 -1
  95. package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +13 -4
  96. package/android/src/main/java/com/swmansion/enriched/markdown/styles/TableStyle.kt +6 -0
  97. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockHighlighter.kt +90 -0
  98. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/CodeBlockNode.kt +57 -0
  99. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/FeatureFlags.kt +1 -0
  100. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownSegment.kt +9 -0
  101. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +33 -7
  102. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/ReadableMapExtensions.kt +3 -0
  103. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/RenderedSegment.kt +10 -0
  104. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/SegmentSignature.kt +1 -0
  105. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/StreamingMarkdownFilter.kt +83 -5
  106. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt +4 -0
  107. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +74 -44
  108. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
  109. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageCache.kt +26 -3
  110. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/ImageDownloader.kt +22 -9
  111. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/LocalImageLoader.kt +161 -0
  112. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/TextPaintExtensions.kt +23 -0
  113. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/CheckboxTouchHelper.kt +7 -0
  114. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/interaction/TaskListTapUtils.kt +41 -21
  115. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/span/BlockSpacing.kt +15 -8
  116. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLongPressMovementMethod.kt +58 -27
  117. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +19 -2
  118. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +388 -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 +32 -5
  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 +2 -0
  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 +231 -8
  152. package/docs/BREAKING_CHANGES.md +129 -0
  153. package/docs/CODE_HIGHLIGHT.md +133 -0
  154. package/docs/COPY_OPTIONS.md +21 -3
  155. package/docs/ELEMENTS_STRUCTURE.md +52 -4
  156. package/docs/IMAGE_CACHING.md +30 -0
  157. package/docs/INPUT.md +146 -5
  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 +262 -67
  169. package/ios/EnrichedMarkdownText.h +7 -0
  170. package/ios/EnrichedMarkdownText.mm +97 -68
  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 +40 -0
  179. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +839 -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/ENRMBlockSyntaxSentinel.h +25 -0
  190. package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
  191. package/ios/input/ENRMClipboardCoordinator.h +45 -0
  192. package/ios/input/ENRMClipboardCoordinator.mm +120 -0
  193. package/ios/input/ENRMDetectorPipeline.h +2 -1
  194. package/ios/input/ENRMEditPipeline.h +76 -0
  195. package/ios/input/ENRMEditPipeline.mm +228 -0
  196. package/ios/input/ENRMEditSession.h +36 -0
  197. package/ios/input/ENRMEditSession.mm +63 -0
  198. package/ios/input/ENRMFormattingStore.h +17 -0
  199. package/ios/input/ENRMFormattingStore.mm +95 -105
  200. package/ios/input/ENRMInputBlockType.h +91 -0
  201. package/ios/input/ENRMInputBlockType.mm +5 -0
  202. package/ios/input/ENRMInputDecorationDrawer.h +18 -0
  203. package/ios/input/ENRMInputEventEmitter.h +56 -0
  204. package/ios/input/ENRMInputEventEmitter.mm +308 -0
  205. package/ios/input/ENRMInputFormatter.h +29 -69
  206. package/ios/input/ENRMInputFormatter.mm +216 -124
  207. package/ios/input/ENRMInputFormatterStyle.h +99 -0
  208. package/ios/input/ENRMInputFormatterStyle.mm +188 -0
  209. package/ios/input/ENRMInputLayoutManager.h +9 -0
  210. package/ios/input/ENRMInputLayoutManager.mm +30 -0
  211. package/ios/input/ENRMInputListMarkerDrawer.h +32 -0
  212. package/ios/input/ENRMInputListMarkerDrawer.mm +426 -0
  213. package/ios/input/ENRMInputParser.h +10 -6
  214. package/ios/input/ENRMInputParser.mm +527 -171
  215. package/ios/input/ENRMInputRemend.mm +2 -2
  216. package/ios/input/ENRMInputStyleStateBuilder.h +40 -0
  217. package/ios/input/ENRMInputStyleStateBuilder.mm +60 -0
  218. package/ios/input/ENRMInputTextView.mm +103 -8
  219. package/ios/input/ENRMInputTypingAttributesController.h +42 -0
  220. package/ios/input/ENRMInputTypingAttributesController.mm +203 -0
  221. package/ios/input/ENRMLinkCoordinator.h +41 -0
  222. package/ios/input/ENRMLinkCoordinator.mm +89 -0
  223. package/ios/input/ENRMMarkdownSerializer.h +29 -5
  224. package/ios/input/ENRMMarkdownSerializer.mm +170 -48
  225. package/ios/input/ENRMMathAtom.h +26 -0
  226. package/ios/input/ENRMMathAtom.mm +12 -0
  227. package/ios/input/ENRMMentionCoordinator.h +45 -0
  228. package/ios/input/ENRMMentionCoordinator.mm +167 -0
  229. package/ios/input/ENRMRangeEditAdjustment.h +38 -0
  230. package/ios/input/ENRMRangeEditAdjustment.mm +91 -0
  231. package/ios/input/ENRMRangeStoreUtils.h +39 -0
  232. package/ios/input/EnrichedMarkdownTextInput+ContextMenu.mm +6 -6
  233. package/ios/input/EnrichedMarkdownTextInput+Internal.h +11 -3
  234. package/ios/input/EnrichedMarkdownTextInput.h +7 -1
  235. package/ios/input/EnrichedMarkdownTextInput.mm +1104 -1024
  236. package/ios/input/InputStylePropsUtils.h +109 -68
  237. package/ios/input/internals/EnrichedMarkdownTextInputShadowNode.mm +2 -1
  238. package/ios/input/styles/ENRMBlockHandler.h +46 -0
  239. package/ios/input/styles/ENRMHeadingBlockHandler.h +15 -0
  240. package/ios/input/styles/ENRMHeadingBlockHandler.mm +38 -0
  241. package/ios/input/styles/ENRMOrderedListBlockHandler.h +14 -0
  242. package/ios/input/styles/ENRMOrderedListBlockHandler.mm +48 -0
  243. package/ios/input/styles/ENRMUnorderedListBlockHandler.h +15 -0
  244. package/ios/input/styles/ENRMUnorderedListBlockHandler.mm +51 -0
  245. package/ios/internals/ENRMViewFreeMeasurement.h +331 -0
  246. package/ios/internals/EnrichedMarkdownShadowNode.h +1 -3
  247. package/ios/internals/EnrichedMarkdownShadowNode.mm +16 -20
  248. package/ios/internals/EnrichedMarkdownTextShadowNode.h +1 -3
  249. package/ios/internals/EnrichedMarkdownTextShadowNode.mm +15 -20
  250. package/ios/internals/MeasurementCache.h +33 -7
  251. package/ios/internals/ShadowMeasurementUtils.h +63 -58
  252. package/ios/math/ENRMMathChipController.h +74 -0
  253. package/ios/math/ENRMMathChipController.mm +476 -0
  254. package/ios/math/ENRMRaTeXBridge.swift +4 -1
  255. package/ios/parser/ENRMMarkdownParser.h +1 -8
  256. package/ios/parser/ENRMMarkdownParser.mm +2 -11
  257. package/ios/parser/MarkdownASTNode.h +2 -1
  258. package/ios/parser/MarkdownParserBridge.mm +4 -0
  259. package/ios/renderer/AttributedRenderer.m +20 -2
  260. package/ios/renderer/BaseRenderer.h +17 -2
  261. package/ios/renderer/BaseRenderer.m +30 -0
  262. package/ios/renderer/BlockquoteRenderer.h +1 -1
  263. package/ios/renderer/BlockquoteRenderer.m +144 -35
  264. package/ios/renderer/CodeBlockRenderer.h +1 -1
  265. package/ios/renderer/CodeBlockRenderer.m +27 -23
  266. package/ios/renderer/CodeRenderer.h +1 -1
  267. package/ios/renderer/CodeRenderer.m +3 -1
  268. package/ios/renderer/ENRMImageRenderer.h +1 -1
  269. package/ios/renderer/ENRMImageRenderer.m +5 -1
  270. package/ios/renderer/ENRMMathInlineRenderer.h +1 -1
  271. package/ios/renderer/ENRMMathInlineRenderer.m +5 -1
  272. package/ios/renderer/ENRMSpoilerRenderer.h +1 -1
  273. package/ios/renderer/ENRMSpoilerRenderer.m +3 -1
  274. package/ios/renderer/EmphasisRenderer.h +1 -1
  275. package/ios/renderer/EmphasisRenderer.m +3 -1
  276. package/ios/renderer/HeadingRenderer.h +1 -1
  277. package/ios/renderer/HeadingRenderer.m +13 -6
  278. package/ios/renderer/HighlightRenderer.h +1 -1
  279. package/ios/renderer/HighlightRenderer.m +3 -1
  280. package/ios/renderer/LinkRenderer.h +1 -1
  281. package/ios/renderer/LinkRenderer.m +3 -1
  282. package/ios/renderer/ListItemRenderer.h +13 -1
  283. package/ios/renderer/ListItemRenderer.m +102 -44
  284. package/ios/renderer/ListRenderer.h +2 -2
  285. package/ios/renderer/ListRenderer.m +63 -25
  286. package/ios/renderer/ParagraphRenderer.h +1 -1
  287. package/ios/renderer/ParagraphRenderer.m +16 -8
  288. package/ios/renderer/RenderContext.h +10 -0
  289. package/ios/renderer/RenderContext.m +44 -0
  290. package/ios/renderer/RendererFactory.m +14 -0
  291. package/ios/renderer/StrikethroughRenderer.h +1 -1
  292. package/ios/renderer/StrikethroughRenderer.m +3 -1
  293. package/ios/renderer/StrongRenderer.h +1 -1
  294. package/ios/renderer/StrongRenderer.m +3 -1
  295. package/ios/renderer/SubscriptRenderer.h +1 -1
  296. package/ios/renderer/SubscriptRenderer.m +3 -1
  297. package/ios/renderer/SuperscriptRenderer.h +1 -1
  298. package/ios/renderer/SuperscriptRenderer.m +3 -1
  299. package/ios/renderer/TextRenderer.h +2 -2
  300. package/ios/renderer/TextRenderer.m +3 -1
  301. package/ios/renderer/ThematicBreakRenderer.h +1 -1
  302. package/ios/renderer/ThematicBreakRenderer.m +3 -1
  303. package/ios/renderer/UnderlineRenderer.h +1 -1
  304. package/ios/renderer/UnderlineRenderer.m +3 -1
  305. package/ios/styles/StyleConfig.h +22 -0
  306. package/ios/styles/StyleConfig.mm +132 -0
  307. package/ios/utils/BlockquoteBorder.h +6 -0
  308. package/ios/utils/BlockquoteBorder.m +249 -52
  309. package/ios/utils/CodeBlockBackground.m +16 -2
  310. package/ios/utils/ENRMAtomicSize.h +65 -0
  311. package/ios/utils/ENRMCodeBlockContent.h +37 -0
  312. package/ios/utils/ENRMCodeBlockContent.mm +79 -0
  313. package/ios/utils/ENRMCodeBlockHighlighter.h +37 -0
  314. package/ios/utils/ENRMCodeBlockHighlighter.mm +51 -0
  315. package/ios/utils/ENRMFeatureFlags.h +7 -0
  316. package/ios/utils/ENRMImageDownloader.h +11 -1
  317. package/ios/utils/ENRMImageDownloader.m +60 -16
  318. package/ios/utils/ENRMLocalImageLoader.h +31 -0
  319. package/ios/utils/ENRMLocalImageLoader.m +68 -0
  320. package/ios/utils/ENRMMathFallback.h +26 -0
  321. package/ios/utils/ENRMMathFallback.m +18 -0
  322. package/ios/utils/ENRMTextViewSetup.h +35 -17
  323. package/ios/utils/EditMenuUtils.m +22 -1
  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 +46 -0
  338. package/ios/views/ENRMCodeBlockContainerView.m +566 -0
  339. package/ios/views/ENRMMathContainerView.h +12 -0
  340. package/ios/views/ENRMMathContainerView.m +71 -1
  341. package/ios/views/TableContainerView.h +18 -0
  342. package/ios/views/TableContainerView.m +259 -159
  343. package/jest.d.ts +1 -0
  344. package/jest.js +5 -0
  345. package/lib/module/EnrichedMarkdownNativeComponent.ts +65 -0
  346. package/lib/module/EnrichedMarkdownTextInput.js +86 -5
  347. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  348. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +89 -5
  349. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +66 -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 +30 -4
  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 +62 -0
  383. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  384. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +95 -9
  385. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  386. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +80 -5
  387. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  388. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +62 -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 +65 -1
  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 +65 -0
  433. package/src/EnrichedMarkdownTextInput.tsx +162 -8
  434. package/src/EnrichedMarkdownTextInputNativeComponent.ts +89 -5
  435. package/src/EnrichedMarkdownTextNativeComponent.ts +66 -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 +40 -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 +65 -0
  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,24 +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;
72
- CFTimeInterval _lastTextChangeTime;
79
+ ENRMBlockStore *_blockStore;
80
+ ENRMInputTypingAttributesController *_typingController;
81
+ ENRMInputEventEmitter *_inputEventEmitter;
82
+ ENRMEditSession *_editSession;
83
+ BOOL _blockSyntax;
84
+ NSString *_lastImportedMarkdown;
73
85
 
74
86
  ENRMPlaceholderLabel *_placeholderLabel;
75
87
 
76
88
  NSUInteger _lastTextLength;
77
89
  NSRange _lastSelectedRange;
78
90
  NSRange _preEditSelectedRange;
79
-
80
- struct {
81
- BOOL bold, italic, underline, strikethrough, spoiler, link, initialized;
82
- } _prevState;
83
-
84
- 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;
85
103
 
86
104
  #if TARGET_OS_OSX
87
105
  NSScrollView *_scrollView;
@@ -89,25 +107,24 @@ using namespace facebook::react;
89
107
 
90
108
  NSArray<NSString *> *_contextMenuItemTexts;
91
109
  NSArray<NSString *> *_contextMenuItemIcons;
92
- NSArray<NSString *> *_mentionIndicators;
93
- NSString *_activeMentionIndicator;
94
- NSRange _activeMentionRange;
95
- NSString *_activeMentionText;
96
-
97
110
  ENRMAutoLinkDetector *_autoLinkDetector;
98
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;
99
124
 
100
125
  ENRMWritingDirectionMode _writingDirectionMode;
101
126
  NSWritingDirection _resolvedLayoutDirection;
102
127
 
103
- /// One-shot guard: when YES, the next textViewDidChangeSelection skips
104
- /// clearing math chip selections. Set by handleTapForMathChip right before
105
- /// it moves the caret to the chip, so the caret update doesn't immediately
106
- /// wipe the chip we just selected.
107
- BOOL _suppressMathChipClearOnce;
108
- /// Character index of the currently selected math chip, or NSNotFound.
109
- NSUInteger _activeMathChipIndex;
110
-
111
128
  ENRMInputSelectionMenuConfig _inputSelectionMenuConfig;
112
129
  ENRMFormatMenuConfig _formatMenuConfig;
113
130
 
@@ -123,6 +140,8 @@ using namespace facebook::react;
123
140
  NSString *_formatMenuLinkLabel;
124
141
  }
125
142
 
143
+ @synthesize editSession = _editSession;
144
+
126
145
  #pragma mark - Fabric lifecycle
127
146
 
128
147
  + (ComponentDescriptorProvider)componentDescriptorProvider
@@ -142,18 +161,16 @@ using namespace facebook::react;
142
161
  _props = defaultProps;
143
162
 
144
163
  self.backgroundColor = [RCTUIColor clearColor];
145
- _blockEmitting = NO;
164
+ _blockSyntax = YES;
165
+ _lastImportedMarkdown = nil;
146
166
  _heightUpdateCounter = 0;
147
167
  _formatter = [[ENRMInputFormatter alloc] init];
148
168
  _formatterStyle = [[ENRMInputFormatterStyle alloc] init];
149
169
  _formattingStore = [[ENRMFormattingStore alloc] init];
150
- _pendingStyles = [NSMutableSet set];
151
- _pendingStyleRemovals = [NSMutableSet set];
170
+ _blockStore = [[ENRMBlockStore alloc] init];
152
171
  _lastTextLength = 0;
153
172
  _lastSelectedRange = NSMakeRange(0, 0);
154
- _mentionIndicators = @[];
155
- _activeMentionRange = NSMakeRange(NSNotFound, 0);
156
- _activeMentionText = @"";
173
+ _mentionCoordinator = [[ENRMMentionCoordinator alloc] initWithFormattingStore:_formattingStore];
157
174
 
158
175
  _writingDirectionMode = ENRMWritingDirectionModeFirstStrong;
159
176
  _resolvedLayoutDirection =
@@ -162,12 +179,32 @@ using namespace facebook::react;
162
179
  _formatMenuConfig = (ENRMFormatMenuConfig){
163
180
  .bold = YES, .italic = YES, .underline = YES, .strikethrough = YES, .spoiler = YES, .link = YES};
164
181
 
182
+ _activeMathChipPosition = NSNotFound;
165
183
  _suppressMathChipClearOnce = NO;
166
- _activeMathChipIndex = NSNotFound;
167
184
 
168
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];
169
193
 
170
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];
171
208
  }
172
209
  return self;
173
210
  }
@@ -180,7 +217,11 @@ using namespace facebook::react;
180
217
 
181
218
  __weak EnrichedMarkdownTextInput *weakSelf = self;
182
219
  _autoLinkDetector.onLinkDetected = ^(NSString *text, NSString *url, NSRange range) {
183
- [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];
184
225
  };
185
226
 
186
227
  _detectorPipeline = [[ENRMDetectorPipeline alloc] init];
@@ -201,6 +242,8 @@ using namespace facebook::react;
201
242
  ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero textContainer:textContainer];
202
243
  #else
203
244
  ENRMInputTextView *inputTextView = [[ENRMInputTextView alloc] initWithFrame:CGRectZero];
245
+ _layoutManager = [[ENRMInputLayoutManager alloc] init];
246
+ [inputTextView.textContainer replaceLayoutManager:_layoutManager];
204
247
  #endif
205
248
  inputTextView.markdownTextInput = self;
206
249
  _textView = inputTextView;
@@ -237,124 +280,89 @@ using namespace facebook::react;
237
280
  _placeholderLabel = ENRMCreatePlaceholderLabel(_textView, _formatterStyle.baseFont);
238
281
  #if !TARGET_OS_OSX
239
282
  _placeholderLabel.adjustsFontForContentSizeCategory = YES;
283
+ _placeholderLabel.accessibilityElementsHidden = YES;
284
+ _placeholderLabel.isAccessibilityElement = NO;
240
285
  #endif
241
286
 
242
287
  [self resetBaseTypingAttributes];
243
288
 
244
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
245
- UITapGestureRecognizer *mathTap = [[UITapGestureRecognizer alloc] initWithTarget:self
246
- action:@selector(handleTapForMathChip:)];
247
- mathTap.cancelsTouchesInView = NO;
248
- mathTap.delaysTouchesBegan = NO;
249
- mathTap.delaysTouchesEnded = NO;
250
- mathTap.delegate = self;
251
- [_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
+ }
252
302
  #endif
253
303
  }
254
304
 
255
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
256
-
257
- // Glyph rect of the chip at `index`, in _textView coordinates (the same space
258
- // `locationInView:_textView` reports). Single source of truth for both the tap
259
- // hit-test and the rect reported to JS.
260
- - (CGRect)mathChipRectInTextViewAtIndex:(NSUInteger)index layoutManager:(NSLayoutManager *)lm
305
+ #if !TARGET_OS_OSX
306
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
307
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
261
308
  {
262
- if (lm == nil)
263
- return CGRectNull;
264
- NSRange glyphRange = [lm glyphRangeForCharacterRange:NSMakeRange(index, 1) actualCharacterRange:NULL];
265
- CGRect rect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
266
- rect.origin.x += _textView.textContainerInset.left;
267
- rect.origin.y += _textView.textContainerInset.top;
268
- return rect;
309
+ return YES;
269
310
  }
311
+ #endif
312
+
313
+ #if !TARGET_OS_OSX
314
+ #pragma mark - Accessibility
270
315
 
271
- - (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
272
319
  {
273
- if (gesture.state != UIGestureRecognizerStateRecognized)
274
- return;
275
- CGPoint loc = [gesture locationInView:_textView];
276
- UITextPosition *pos = [_textView closestPositionToPoint:loc];
277
- if (!pos)
278
- return;
279
- NSInteger idx = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:pos];
280
- if (idx < 0)
281
- return;
282
- NSTextStorage *storage = _textView.textStorage;
283
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
284
- // The closest position can land just past the chip, so probe both `idx` and `idx-1`.
285
- // `closestPositionToPoint:` has no distance bound — it clamps a tap anywhere in the
286
- // text view (blank space past the text, textContainerInset padding) onto the nearest
287
- // character. Requiring the touch to fall inside the chip's own glyph rect is what
288
- // keeps a tap beside a chip from reading as a tap on it.
289
- ENRMMathInlineAttachment *att = nil;
290
- NSUInteger chipIdx = NSNotFound;
291
- for (NSInteger probe = idx; probe >= idx - 1; probe--) {
292
- if (probe < 0 || (NSUInteger)probe >= storage.length)
293
- continue;
294
- id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)probe effectiveRange:NULL];
295
- if (![v isKindOfClass:[ENRMMathInlineAttachment class]])
296
- continue;
297
- // An empty rect means the layout could not give us the chip's extent; accept the
298
- // candidate then, rather than making the chip untappable.
299
- CGRect rect = [self mathChipRectInTextViewAtIndex:(NSUInteger)probe layoutManager:lm];
300
- if (!CGRectIsEmpty(rect) && !CGRectContainsPoint(rect, loc))
301
- continue;
302
- att = v;
303
- chipIdx = (NSUInteger)probe;
304
- break;
305
- }
306
- if (!att)
307
- return;
320
+ return YES;
321
+ }
308
322
 
309
- NSRange chipRange = NSMakeRange(chipIdx, 1);
310
- [self clearMathChipSelectionsExcept:chipRange];
311
- att.isSelected = YES;
312
- [att invalidateRender];
313
- if (lm) {
314
- [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
315
- [lm ensureLayoutForCharacterRange:chipRange];
323
+ - (NSString *)accessibilityValue
324
+ {
325
+ NSString *text = ENRMGetPlainText(_textView);
326
+ if (text.length > 0) {
327
+ return text;
316
328
  }
317
- ENRMSetNeedsDisplay(_textView);
318
- // Place caret right after the chip (length 0) instead of selecting the chip
319
- // — a length-1 selection would make UITextView draw its blue range handles
320
- // on top of our custom selected-chip background.
321
- NSUInteger caretIndex = chipIdx + 1;
322
- if (caretIndex > storage.length)
323
- caretIndex = storage.length;
324
- _activeMathChipIndex = chipIdx;
325
- _suppressMathChipClearOnce = YES;
326
- _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
327
342
 
328
- // Recomputed rather than reused from the hit-test: the layout was invalidated above.
329
- CGRect glyphRect = [self mathChipRectInTextViewAtIndex:chipIdx layoutManager:lm];
330
- CGRect rectInSelf = [self.contentView convertRect:glyphRect fromView:_textView];
343
+ #pragma mark - Paste
331
344
 
332
- [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];
333
348
  }
334
349
 
335
- - (void)emitOnMathPressWithLatex:(NSString *)latex
336
- start:(NSUInteger)start
337
- end:(NSUInteger)end
338
- displayMode:(BOOL)displayMode
339
- 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
340
354
  {
341
- auto emitter = [self getEventEmitter];
342
- 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];
343
359
  return;
344
- emitter->onMathPress({
345
- .latex = std::string([latex UTF8String] ?: ""),
346
- .start = static_cast<int>(start),
347
- .end = static_cast<int>(end),
348
- .displayMode = displayMode ? true : false,
349
- .x = rect.origin.x,
350
- .y = rect.origin.y,
351
- .width = rect.size.width,
352
- .height = rect.size.height,
353
- });
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:@[]];
354
364
  }
355
365
 
356
- #endif
357
-
358
366
  #pragma mark - State
359
367
 
360
368
  - (void)updateState:(const facebook::react::State::Shared &)state
@@ -452,6 +460,10 @@ using namespace facebook::react;
452
460
  }
453
461
  }
454
462
 
463
+ if (newViewProps.interceptPaste != oldViewProps.interceptPaste) {
464
+ _interceptPaste = newViewProps.interceptPaste;
465
+ }
466
+
455
467
  if (newViewProps.multiline != oldViewProps.multiline) {
456
468
  _textView.textContainer.maximumNumberOfLines = newViewProps.multiline ? 0 : 1;
457
469
  _textView.textContainer.lineBreakMode =
@@ -479,7 +491,7 @@ using namespace facebook::react;
479
491
  ENRMApplySelectionColor(_textView, newViewProps.selectionColor);
480
492
  }
481
493
 
482
- _emitMarkdown = newViewProps.isOnChangeMarkdownSet;
494
+ _inputEventEmitter.emitMarkdown = newViewProps.isOnChangeMarkdownSet;
483
495
 
484
496
  {
485
497
  auto configFromProp = [](const auto &prop) {
@@ -547,11 +559,8 @@ using namespace facebook::react;
547
559
  [indicators addObject:value];
548
560
  }
549
561
  }
550
- _mentionIndicators = [indicators copy];
551
- if (_activeMentionIndicator != nil && ![_mentionIndicators containsObject:_activeMentionIndicator]) {
552
- [self clearActiveMention:_activeMentionIndicator];
553
- }
554
- [self updateActiveMention];
562
+ [self dispatchMentionEvents:[_mentionCoordinator setIndicators:indicators]];
563
+ [self dispatchMentionUpdate];
555
564
  }
556
565
 
557
566
  BOOL styleChanged = applyInputStyleProps(_formatterStyle, newViewProps, oldViewProps);
@@ -563,11 +572,20 @@ using namespace facebook::react;
563
572
  writingDirectionChanged = YES;
564
573
  }
565
574
 
575
+ // Must land before the defaultValue import below, so the first parse already sees
576
+ // the right flag. Assigned unconditionally, not just on change: the generated C++
577
+ // default is `false` while the JS default is `true`, so `blockSyntax={false}` on the
578
+ // first render compares equal to the old props and a change-guard would drop it.
579
+ BOOL blockSyntaxChanged = (_blockSyntax != (BOOL)newViewProps.blockSyntax);
580
+ _blockSyntax = newViewProps.blockSyntax;
581
+
566
582
  if (newViewProps.defaultValue != oldViewProps.defaultValue) {
567
583
  if (!newViewProps.defaultValue.empty() && oldViewProps.defaultValue.empty()) {
568
584
  NSString *markdown = [NSString stringWithUTF8String:newViewProps.defaultValue.c_str()];
569
585
  [self importMarkdown:markdown];
570
586
  }
587
+ } else if (blockSyntaxChanged && _lastImportedMarkdown != nil) {
588
+ [self importMarkdown:_lastImportedMarkdown];
571
589
  }
572
590
 
573
591
  if (styleChanged) {
@@ -579,9 +597,11 @@ using namespace facebook::react;
579
597
  [self applyFormatting];
580
598
  }
581
599
 
582
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
583
- [self refreshAllMathChips];
584
- #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];
585
605
 
586
606
  [self requestHeightUpdate];
587
607
  } else if (writingDirectionChanged && _textView.textStorage.length > 0) {
@@ -605,20 +625,23 @@ using namespace facebook::react;
605
625
  return;
606
626
  }
607
627
 
608
- dispatch_async(dispatch_get_main_queue(), ^{
609
- NSUInteger textLength = self->_textView.textStorage.length;
610
- if (textLength == 0) {
611
- return;
612
- }
613
- NSRange wholeRange = NSMakeRange(0, textLength);
614
- NSRange actualRange = NSMakeRange(0, 0);
615
- [self->_textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
616
- [self->_textView.layoutManager ensureLayoutForCharacterRange:actualRange];
617
- [self->_textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
618
-
619
- CGSize measuredSize = [self measureSize:self->_textView.frame.size.width];
620
- ENRMSetContentSize(self->_textView, measuredSize);
621
- });
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
+ }
622
645
  }
623
646
 
624
647
  #pragma mark - Window attachment
@@ -672,14 +695,6 @@ using namespace facebook::react;
672
695
  [_textView mouseDown:event];
673
696
  }
674
697
 
675
- #else // iOS
676
-
677
- - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
678
- shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
679
- {
680
- return YES;
681
- }
682
-
683
698
  #endif
684
699
 
685
700
  #if !TARGET_OS_OSX
@@ -700,181 +715,178 @@ using namespace facebook::react;
700
715
  }
701
716
  #endif
702
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
+
703
736
  #pragma mark - Placeholder
704
737
 
705
738
  - (void)updatePlaceholderVisibility
706
739
  {
707
- _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;
708
746
  }
709
747
 
710
748
  #pragma mark - Markdown import
711
749
 
712
750
  - (void)importMarkdown:(NSString *)markdown
713
751
  {
752
+ _lastImportedMarkdown = [markdown copy];
714
753
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
715
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
754
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
716
755
 
717
- _blockEmitting = YES;
756
+ [_editSession enterPhase:ENRMEditPhaseImporting];
718
757
 
719
- _isApplyingFormatting = YES;
758
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
720
759
  ENRMSetPlainText(_textView, parsed.plainText);
760
+ [_editSession enterPhase:ENRMEditPhaseImporting];
721
761
 
722
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
723
- if (parsed.mathAtoms.count > 0) {
724
- NSTextStorage *storage = _textView.textStorage;
725
- [storage beginEditing];
726
- for (ENRMMathAtom *atom in parsed.mathAtoms) {
727
- if (atom.position >= storage.length)
728
- continue;
729
- unichar c = [storage.string characterAtIndex:atom.position];
730
- if (c != (unichar)0xFFFC)
731
- continue;
732
- ENRMMathInlineAttachment *att = [self createMathAttachmentWithLatex:atom.latex displayMode:atom.displayMode];
733
- NSAttributedString *replacement = [NSAttributedString attributedStringWithAttachment:att];
734
- [storage replaceCharactersInRange:NSMakeRange(atom.position, 1) withAttributedString:replacement];
735
- if (atom.displayMode && _formatterStyle.mathBlockAlignment != NSTextAlignmentNatural) {
736
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
737
- ps.alignment = _formatterStyle.mathBlockAlignment;
738
- [storage addAttribute:NSParagraphStyleAttributeName value:ps range:NSMakeRange(atom.position, 1)];
739
- }
740
- }
741
- [storage endEditing];
742
- }
743
- #endif
744
-
745
- _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];
746
769
 
747
770
  [_formattingStore setRanges:parsed.formattingRanges];
771
+ [_blockStore setRanges:parsed.blockRanges];
748
772
  _lastTextLength = parsed.plainText.length;
749
773
  _lastSelectedRange = _textView.selectedRange;
774
+
775
+ [_editSession exitPhase];
750
776
  [self applyFormatting];
751
777
  [self updatePlaceholderVisibility];
752
778
 
753
- _blockEmitting = NO;
754
- }
755
-
756
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
757
-
758
- - (void)applyMathStyleToAttachment:(ENRMMathInlineAttachment *)att
759
- {
760
- if (att.displayMode) {
761
- att.fontSize = _formatterStyle.mathBlockFontSize > 0 ? _formatterStyle.mathBlockFontSize : 20.0;
762
- att.mathTextColor = _formatterStyle.mathBlockColor ?: _formatterStyle.baseTextColor;
763
- att.containerBackgroundColor = _formatterStyle.mathBlockBackgroundColor;
764
- att.containerBorderColor = _formatterStyle.mathBlockBorderColor;
765
- att.containerBorderWidth = _formatterStyle.mathBlockBorderWidth;
766
- att.containerBorderRadius = _formatterStyle.mathBlockBorderRadius;
767
- att.containerPaddingHorizontal = _formatterStyle.mathBlockPaddingHorizontal;
768
- att.containerPaddingVertical = _formatterStyle.mathBlockPaddingVertical;
769
- att.blockMarginTop = _formatterStyle.mathBlockMarginTop;
770
- att.blockMarginBottom = _formatterStyle.mathBlockMarginBottom;
771
- att.blockAlignment = _formatterStyle.mathBlockAlignment;
772
- att.selectedBackgroundColor = _formatterStyle.mathBlockSelectedBackgroundColor;
773
- att.selectedBorderColor = _formatterStyle.mathBlockSelectedBorderColor;
774
- att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathBlockFontWeight);
775
- } else {
776
- att.fontSize = _formatterStyle.mathFontSize > 0 ? _formatterStyle.mathFontSize : 16.0;
777
- att.mathTextColor = _formatterStyle.mathColor ?: _formatterStyle.baseTextColor;
778
- att.containerBackgroundColor = _formatterStyle.mathBackgroundColor;
779
- att.containerBorderColor = _formatterStyle.mathBorderColor;
780
- att.containerBorderWidth = _formatterStyle.mathBorderWidth;
781
- att.containerBorderRadius = _formatterStyle.mathBorderRadius;
782
- att.containerPaddingHorizontal = _formatterStyle.mathPaddingHorizontal;
783
- att.containerPaddingVertical = _formatterStyle.mathPaddingVertical;
784
- att.blockMarginTop = 0;
785
- att.blockMarginBottom = 0;
786
- att.blockAlignment = NSTextAlignmentNatural;
787
- att.selectedBackgroundColor = _formatterStyle.mathSelectedBackgroundColor;
788
- att.selectedBorderColor = _formatterStyle.mathSelectedBorderColor;
789
- att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathFontWeight);
790
- }
791
- }
792
-
793
- - (ENRMMathInlineAttachment *)createMathAttachmentWithLatex:(NSString *)latex displayMode:(BOOL)displayMode
794
- {
795
- ENRMMathInlineAttachment *att = [[ENRMMathInlineAttachment alloc] init];
796
- att.latex = latex ?: @"";
797
- att.displayMode = displayMode;
798
- [self applyMathStyleToAttachment:att];
799
- return att;
800
- }
801
-
802
- - (void)refreshAllMathChips
803
- {
804
- NSTextStorage *storage = _textView.textStorage;
805
- if (storage.length == 0)
806
- return;
807
- __weak EnrichedMarkdownTextInput *weakSelf = self;
808
- [storage beginEditing];
809
- [storage enumerateAttribute:NSAttachmentAttributeName
810
- inRange:NSMakeRange(0, storage.length)
811
- options:0
812
- usingBlock:^(id value, NSRange range, BOOL *stop) {
813
- EnrichedMarkdownTextInput *strongSelf = weakSelf;
814
- if (!strongSelf)
815
- return;
816
- if ([value isKindOfClass:[ENRMMathInlineAttachment class]]) {
817
- ENRMMathInlineAttachment *att = value;
818
- [strongSelf applyMathStyleToAttachment:att];
819
- [att invalidateRender];
820
- }
821
- }];
822
- [storage endEditing];
823
-
824
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
825
- if (lm) {
826
- NSRange fullRange = NSMakeRange(0, storage.length);
827
- [lm invalidateLayoutForCharacterRange:fullRange actualCharacterRange:NULL];
828
- [lm ensureLayoutForCharacterRange:fullRange];
779
+ if (parsed.plainText.length == 0) {
780
+ [self resetBaseTypingAttributes];
829
781
  }
830
- ENRMSetNeedsDisplay(_textView);
831
782
  }
832
783
 
833
- #endif
834
-
835
784
  - (void)replaceTextInRange:(NSRange)selection
836
785
  withText:(NSString *)text
837
786
  formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
787
+ blockRanges:(NSArray<ENRMBlockRange *> *)blockRanges
838
788
  {
839
789
  NSUInteger editLocation = selection.location;
840
790
 
841
- _isApplyingFormatting = YES;
791
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
842
792
  ENRMReplaceTextInRange(_textView, text, selection);
843
- _isApplyingFormatting = NO;
793
+ [_editSession exitPhase];
844
794
 
845
- [_formattingStore adjustForEditAtLocation:editLocation deletedLength:selection.length insertedLength:text.length];
795
+ NSString *plainText = [self adjustStoresForEditAtLocation:editLocation
796
+ deletedLength:selection.length
797
+ insertedLength:text.length];
846
798
 
847
799
  for (ENRMFormattingRange *range in ranges) {
848
800
  NSRange shifted = NSMakeRange(range.range.location + editLocation, range.range.length);
849
801
  [_formattingStore addRange:[ENRMFormattingRange rangeWithType:range.type range:shifted url:range.url]];
850
802
  }
851
803
 
852
- _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;
853
810
  _lastSelectedRange = _textView.selectedRange;
854
811
 
855
812
  [self applyFormatting];
856
813
 
857
- [_detectorPipeline processTextChange:ENRMGetPlainText(_textView)
858
- modificationRange:NSMakeRange(editLocation, text.length)];
814
+ [_detectorPipeline processTextChange:plainText modificationRange:NSMakeRange(editLocation, text.length)];
859
815
 
860
816
  [self updatePlaceholderVisibility];
861
- [self emitOnChangeText];
862
- [self emitOnChangeSelection];
863
- [self emitFormattingChanged];
817
+ [_inputEventEmitter emitOnChangeText];
818
+ [_inputEventEmitter emitOnChangeSelection];
819
+ [_inputEventEmitter emitFormattingChanged];
864
820
  [self requestHeightUpdate];
865
821
  [self scheduleRelayoutIfNeeded];
866
822
  }
867
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
+
868
831
  - (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray<ENRMFormattingRange *> *)ranges
869
832
  {
870
833
  [self replaceTextInRange:_textView.selectedRange withText:text formattingRanges:ranges];
871
834
  }
872
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.
873
842
  - (void)pasteMarkdown:(NSString *)markdown
874
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
+
875
854
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
876
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
877
- [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];
878
890
  }
879
891
 
880
892
  #pragma mark - Formatting
@@ -889,17 +901,44 @@ using namespace facebook::react;
889
901
 
890
902
  - (void)applyFormatting
891
903
  {
892
- 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) {
893
928
  return;
894
929
  }
895
- if (ENRMHasMarkedText(_textView)) {
930
+ if (_editSession.isComposing) {
896
931
  return;
897
932
  }
898
- _isApplyingFormatting = YES;
933
+ [_editSession enterPhase:ENRMEditPhaseFormatting];
899
934
 
900
935
  NSRange savedSelection = _textView.selectedRange;
901
936
 
902
- [_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];
903
942
  [_detectorPipeline refreshAllStyling];
904
943
  [self applyWritingDirection];
905
944
 
@@ -908,7 +947,7 @@ using namespace facebook::react;
908
947
  _textView.selectedRange = savedSelection;
909
948
  }
910
949
 
911
- _isApplyingFormatting = NO;
950
+ [_editSession exitPhase];
912
951
  }
913
952
 
914
953
  - (void)applyWritingDirection
@@ -938,9 +977,9 @@ using namespace facebook::react;
938
977
  {
939
978
  [self importMarkdown:markdown];
940
979
  _lastSelectedRange = _textView.selectedRange;
941
- [self emitOnChangeText];
942
- [self emitOnChangeSelection];
943
- [self emitOnChangeState];
980
+ [_inputEventEmitter emitOnChangeText];
981
+ [_inputEventEmitter emitOnChangeSelection];
982
+ [_inputEventEmitter emitOnChangeState];
944
983
  [self requestHeightUpdate];
945
984
  }
946
985
 
@@ -950,8 +989,8 @@ using namespace facebook::react;
950
989
  NSInteger clampedStart = MIN(MAX(start, 0), textLen);
951
990
  NSInteger clampedEnd = MIN(MAX(end, clampedStart), textLen);
952
991
  _textView.selectedRange = NSMakeRange((NSUInteger)clampedStart, (NSUInteger)(clampedEnd - clampedStart));
953
- [self emitOnChangeSelection];
954
- [self emitOnChangeState];
992
+ [_inputEventEmitter emitOnChangeSelection];
993
+ [_inputEventEmitter emitOnChangeState];
955
994
  }
956
995
 
957
996
  - (void)toggleBold
@@ -988,813 +1027,817 @@ using namespace facebook::react;
988
1027
  ENRMStyleMergingConfig *mergingConfig = handler.mergingConfig;
989
1028
 
990
1029
  NSRange selection = _textView.selectedRange;
991
- NSUInteger cursor = selection.location;
992
- NSNumber *key = @(styleType);
993
-
994
- // Check blocking rules: if any blocking style is active, refuse to toggle on.
995
- if (mergingConfig.blockingStyles.count > 0) {
996
- BOOL isCurrentlyActive = [_formattingStore isStyleActive:styleType atPosition:cursor];
997
- if (!isCurrentlyActive) {
998
- for (NSNumber *blockerNum in mergingConfig.blockingStyles) {
999
- if ([_formattingStore isStyleActive:(ENRMInputStyleType)blockerNum.integerValue atPosition:cursor]) {
1000
- return;
1001
- }
1002
- }
1003
- }
1004
- }
1005
1030
 
1006
- if (selection.length > 0) {
1007
- BOOL fullyStyled = YES;
1008
- NSUInteger pos = selection.location;
1009
- NSUInteger selEnd = NSMaxRange(selection);
1010
- while (pos < selEnd) {
1011
- ENRMFormattingRange *match = [_formattingStore rangeOfType:styleType containingPosition:pos];
1012
- if (match == nil) {
1013
- fullyStyled = NO;
1014
- break;
1015
- }
1016
- pos = NSMaxRange(match.range);
1017
- }
1018
- if (fullyStyled) {
1019
- [_formattingStore removeType:styleType inRange:selection];
1020
- } else {
1021
- // Remove conflicting styles from the range before applying.
1022
- for (NSNumber *conflictNum in mergingConfig.conflictingStyles) {
1023
- [_formattingStore removeType:(ENRMInputStyleType)conflictNum.integerValue inRange:selection];
1024
- }
1025
- ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:styleType range:selection];
1026
- [_formattingStore addRange:newRange];
1027
- }
1028
- [_pendingStyles removeObject:key];
1029
- [_pendingStyleRemovals removeObject:key];
1030
- } else {
1031
- BOOL isInsideRange = [_formattingStore isStyleActive:styleType atPosition:cursor];
1032
-
1033
- if ([_pendingStyleRemovals containsObject:key]) {
1034
- [_pendingStyleRemovals removeObject:key];
1035
- } else if ([_pendingStyles containsObject:key]) {
1036
- [_pendingStyles removeObject:key];
1037
- } else if (isInsideRange) {
1038
- [_pendingStyleRemovals addObject:key];
1039
- } else {
1040
- [_pendingStyles addObject:key];
1041
- }
1031
+ if ([_formattingStore isToggleBlocked:styleType
1032
+ atPosition:selection.location
1033
+ blockingStyles:mergingConfig.blockingStyles]) {
1034
+ return;
1042
1035
  }
1043
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
+
1044
1043
  [self applyFormatting];
1045
- [self syncTypingAttributesWithPendingStyles];
1046
- [self emitFormattingChanged];
1044
+ [_typingController syncWithPendingStyles];
1045
+ [_inputEventEmitter emitFormattingChanged];
1047
1046
  }
1048
1047
 
1049
- - (void)setLink:(NSString *)url
1048
+ - (void)toggleHeading:(NSInteger)level
1050
1049
  {
1051
- NSRange selection = _textView.selectedRange;
1052
- NSUInteger cursor = selection.location;
1050
+ if (level < 1 || level > 6) {
1051
+ return;
1052
+ }
1053
+ [self toggleBlockType:ENRMBlockTypeForHeadingLevel(level) level:level];
1054
+ }
1053
1055
 
1054
- 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];
1055
1063
 
1056
- if (activeLink != nil) {
1057
- activeLink.url = url;
1058
- [_autoLinkDetector clearAutoLinkInRange:activeLink.range];
1059
- } else if (selection.length > 0) {
1060
- ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink range:selection url:url];
1061
- [_formattingStore addRange:linkRange];
1062
- [_autoLinkDetector clearAutoLinkInRange:selection];
1063
- } else {
1064
- 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
+ }
1065
1076
  }
1066
1077
 
1067
1078
  [self applyFormatting];
1068
- [self emitFormattingChanged];
1079
+ [_typingController syncWithCursorBlock];
1080
+ [self updatePlaceholderVisibility];
1081
+ if (alreadyActive) {
1082
+ [_typingController clearListParagraphStyle];
1083
+ }
1084
+ [self updateEmptyBulletMarker];
1085
+ [_inputEventEmitter emitFormattingChanged];
1069
1086
  }
1070
1087
 
1071
- - (void)insertLink:(NSString *)text url:(NSString *)url
1088
+ - (void)toggleUnorderedList
1072
1089
  {
1073
- NSString *displayText = text.length > 0 ? text : url;
1074
- NSRange linkRange = NSMakeRange(0, displayText.length);
1075
- ENRMFormattingRange *range = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1076
- range:linkRange
1077
- url:[self sanitizeLinkURL:url]];
1078
- [self replaceSelectedTextWith:displayText formattingRanges:@[ range ]];
1090
+ [self toggleBlockType:ENRMInputBlockTypeUnorderedListItem level:0];
1079
1091
  }
1080
1092
 
1081
- - (NSString *)sanitizeLinkURL:(NSString *)url
1093
+ - (void)toggleOrderedList
1082
1094
  {
1083
- NSString *result = [url stringByReplacingOccurrencesOfString:@"(" withString:@"%28"];
1084
- return [result stringByReplacingOccurrencesOfString:@")" withString:@"%29"];
1095
+ [self toggleBlockType:ENRMInputBlockTypeOrderedListItem level:0];
1085
1096
  }
1086
1097
 
1087
- - (void)startMention:(NSString *)indicator
1098
+ - (void)indentList
1088
1099
  {
1089
- if (indicator.length == 0 || ![_mentionIndicators containsObject:indicator]) {
1090
- return;
1091
- }
1092
-
1093
- [self replaceSelectedTextWith:indicator formattingRanges:@[]];
1094
- [self updateActiveMention];
1100
+ [self changeListDepthBy:1];
1095
1101
  }
1096
1102
 
1097
- - (void)insertMention:(NSString *)displayText url:(NSString *)url
1103
+ - (void)outdentList
1098
1104
  {
1099
- if (displayText.length == 0 || _activeMentionIndicator == nil || _activeMentionRange.location == NSNotFound) {
1100
- return;
1101
- }
1105
+ [self changeListDepthBy:-1];
1106
+ }
1102
1107
 
1103
- NSString *plainText = ENRMGetPlainText(_textView);
1104
- NSUInteger rangeEnd = NSMaxRange(_activeMentionRange);
1105
- 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) {
1106
1116
  return;
1107
1117
  }
1108
- BOOL nextCharIsWhitespace = rangeEnd < plainText.length && [[NSCharacterSet whitespaceAndNewlineCharacterSet]
1109
- characterIsMember:[plainText
1110
- characterAtIndex:rangeEnd]];
1111
- NSString *replacement = nextCharIsWhitespace ? displayText : [displayText stringByAppendingString:@" "];
1112
- ENRMFormattingRange *linkRange = [ENRMFormattingRange rangeWithType:ENRMInputStyleTypeLink
1113
- range:NSMakeRange(0, displayText.length)
1114
- url:[self sanitizeLinkURL:url]];
1115
- NSString *indicator = _activeMentionIndicator;
1116
- NSRange mentionRange = _activeMentionRange;
1118
+ [self applyFormatting];
1119
+ [_typingController syncWithCursorBlock];
1120
+ [self updateEmptyBulletMarker];
1121
+ [_inputEventEmitter emitFormattingChanged];
1122
+ }
1117
1123
 
1118
- [self clearActiveMention:indicator];
1119
- [self replaceTextInRange:mentionRange withText:replacement formattingRanges:@[ linkRange ]];
1120
- _textView.selectedRange = NSMakeRange(mentionRange.location + replacement.length, 0);
1121
- [self emitOnChangeSelection];
1124
+ - (nullable ENRMBlockRange *)listBlockForCursorParagraph
1125
+ {
1126
+ return [self listBlockForParagraphAtPosition:_textView.selectedRange.location];
1122
1127
  }
1123
1128
 
1124
- - (void)removeLink
1129
+ - (nullable ENRMBlockRange *)listBlockForParagraphAtPosition:(NSUInteger)position
1125
1130
  {
1126
- NSUInteger cursor = _textView.selectedRange.location;
1127
- ENRMFormattingRange *activeLink = [_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:cursor];
1128
- if (activeLink == nil) {
1129
- return;
1130
- }
1131
+ return [_blockCoordinator listBlockAtPosition:position inText:_textView.textStorage.string];
1132
+ }
1131
1133
 
1132
- [_formattingStore removeRange:activeLink];
1133
- [self applyFormatting];
1134
- [self emitFormattingChanged];
1134
+ - (nullable ENRMBlockRange *)blockForParagraphAtPosition:(NSUInteger)position
1135
+ {
1136
+ return [_blockCoordinator blockAtPosition:position inText:_textView.textStorage.string];
1135
1137
  }
1136
1138
 
1137
- #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
+ }
1138
1146
 
1139
- - (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
1140
1151
  {
1141
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1142
- if (latex.length == 0)
1143
- return;
1144
- NSTextStorage *storage = _textView.textStorage;
1145
- NSRange selection = _textView.selectedRange;
1146
- 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
+ }
1147
1156
 
1148
- NSMutableString *prefix = [NSMutableString string];
1149
- NSMutableString *suffix = [NSMutableString string];
1150
- if (displayMode) {
1151
- BOOL atLineStart = (selection.location == 0) || [plainText characterAtIndex:selection.location - 1] == '\n';
1152
- NSUInteger afterSel = selection.location + selection.length;
1153
- BOOL atLineEnd = (afterSel >= plainText.length) || [plainText characterAtIndex:afterSel] == '\n';
1154
- if (!atLineStart)
1155
- [prefix appendString:@"\n"];
1156
- if (!atLineEnd)
1157
- [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;
1158
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
+ }
1159
1169
 
1160
- ENRMMathInlineAttachment *att = [self createMathAttachmentWithLatex:latex displayMode:displayMode];
1161
- NSMutableAttributedString *replacement = [[NSMutableAttributedString alloc] init];
1162
- if (prefix.length) {
1163
- [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;
1164
1177
  }
1165
- [replacement appendAttributedString:[NSAttributedString attributedStringWithAttachment:att]];
1166
- if (suffix.length) {
1167
- [replacement appendAttributedString:[[NSAttributedString alloc] initWithString:suffix]];
1178
+ NSRange paragraph = [text paragraphRangeForRange:range];
1179
+ if (paragraph.length == 0) {
1180
+ return YES;
1168
1181
  }
1182
+ return [[text substringWithRange:paragraph] isEqualToString:@"\n"];
1183
+ }
1169
1184
 
1170
- if (displayMode && att.blockAlignment != NSTextAlignmentNatural) {
1171
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
1172
- ps.alignment = att.blockAlignment;
1173
- NSRange chipRange = NSMakeRange(prefix.length, 1);
1174
- [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;
1175
1196
  }
1176
1197
 
1177
- _isApplyingFormatting = YES;
1178
- [storage replaceCharactersInRange:selection withAttributedString:replacement];
1179
- _isApplyingFormatting = NO;
1180
-
1181
- NSUInteger insertedLength = replacement.length;
1182
- [_formattingStore adjustForEditAtLocation:selection.location
1183
- deletedLength:selection.length
1184
- insertedLength:insertedLength];
1198
+ // Tab indents the current item.
1199
+ if ([text isEqualToString:@"\t"]) {
1200
+ [self indentList];
1201
+ return YES;
1202
+ }
1185
1203
 
1186
- NSUInteger newCursor = selection.location + insertedLength;
1187
- _textView.selectedRange = NSMakeRange(newCursor, 0);
1188
- _lastTextLength = ENRMGetPlainText(_textView).length;
1189
- _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
+ }
1190
1220
 
1191
- [self applyFormatting];
1192
- [self updatePlaceholderVisibility];
1193
- [self emitOnChangeText];
1194
- [self emitOnChangeSelection];
1195
- [self emitFormattingChanged];
1196
- [self requestHeightUpdate];
1197
- [self scheduleRelayoutIfNeeded];
1198
- #endif
1221
+ return NO;
1199
1222
  }
1200
1223
 
1201
- - (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
1202
1228
  {
1203
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1204
- NSTextStorage *storage = _textView.textStorage;
1205
- if (start < 0 || (NSUInteger)start >= storage.length)
1206
- return;
1207
- id value = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)start effectiveRange:NULL];
1208
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
1209
- return;
1210
-
1211
- ENRMMathInlineAttachment *att = value;
1212
- BOOL modeChanged = (att.displayMode != displayMode);
1213
- att.latex = latex ?: @"";
1214
- att.displayMode = displayMode;
1215
- [self applyMathStyleToAttachment:att];
1216
- [att invalidateRender];
1217
-
1218
- NSRange chipRange = NSMakeRange((NSUInteger)start, 1);
1219
- if (modeChanged) {
1220
- [storage beginEditing];
1221
- if (displayMode && att.blockAlignment != NSTextAlignmentNatural) {
1222
- NSMutableParagraphStyle *ps = [[NSMutableParagraphStyle alloc] init];
1223
- ps.alignment = att.blockAlignment;
1224
- [storage addAttribute:NSParagraphStyleAttributeName value:ps range:chipRange];
1225
- } else {
1226
- [storage removeAttribute:NSParagraphStyleAttributeName range:chipRange];
1227
- }
1228
- [storage endEditing];
1229
+ NSRange selection = _textView.selectedRange;
1230
+ if (selection.location != 0 || selection.length != 0) {
1231
+ return NO;
1229
1232
  }
1230
-
1231
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
1232
- if (lm) {
1233
- [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
1234
- [lm ensureLayoutForCharacterRange:chipRange];
1233
+ ENRMBlockRange *listBlock = [self listBlockForCursorParagraph];
1234
+ if (listBlock == nil) {
1235
+ return NO;
1235
1236
  }
1236
- ENRMSetNeedsDisplay(_textView);
1237
-
1238
- if (_emitMarkdown) {
1239
- [self emitOnChangeMarkdown];
1237
+ if (listBlock.level > 0) {
1238
+ [self outdentList];
1239
+ } else {
1240
+ [self toggleBlockType:listBlock.type level:0];
1240
1241
  }
1241
- [self requestHeightUpdate];
1242
- [self scheduleRelayoutIfNeeded];
1243
- #endif
1242
+ return YES;
1244
1243
  }
1245
1244
 
1246
- - (void)removeMath:(NSInteger)start
1245
+ - (BOOL)listStateForParagraphAtPosition:(NSUInteger)position depth:(NSInteger *)outDepth
1247
1246
  {
1248
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
1249
- NSTextStorage *storage = _textView.textStorage;
1250
- if (start < 0 || (NSUInteger)start >= storage.length)
1251
- return;
1252
- id value = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)start effectiveRange:NULL];
1253
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
1254
- return;
1255
-
1256
- [self replaceTextInRange:NSMakeRange((NSUInteger)start, 1) withText:@"" formattingRanges:@[]];
1257
- #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;
1258
1255
  }
1259
1256
 
1260
- - (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
1261
1264
  {
1262
- NSUInteger cursor = _textView.selectedRange.location;
1263
- ENRMFormattingRange *activeLink = [_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:cursor];
1264
- 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;
1265
1270
 
1266
- __weak EnrichedMarkdownTextInput *weakSelf = self;
1267
- ENRMShowLinkPrompt(self, existingURL, ^(NSString *url) { [weakSelf setLink:url]; });
1268
- }
1271
+ BOOL ordered = NO;
1272
+ NSInteger ordinal = 1;
1273
+ ENRMBlockRange *cursorListBlock = selection.length == 0 ? [self listBlockForCursorParagraph] : nil;
1269
1274
 
1270
- - (nullable NSString *)markdownForSelectedRange
1271
- {
1272
- NSRange selection = _textView.selectedRange;
1273
- if (selection.length == 0) {
1274
- return nil;
1275
+ ENRMInputListMarkerDrawer *listDrawer = _layoutManager.listMarkerDrawer;
1276
+ BOOL wasShown = listDrawer.emptyBulletDepth >= 0;
1277
+ if (cursorListBlock == nil && !wasShown) {
1278
+ return;
1275
1279
  }
1276
1280
 
1277
- NSAttributedString *full = _textView.textStorage;
1278
- if (NSMaxRange(selection) > full.length) {
1279
- 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
+ }
1280
1314
  }
1281
- NSAttributedString *selectedAttr = [full attributedSubstringFromRange:selection];
1282
- NSUInteger selEnd = NSMaxRange(selection);
1283
1315
 
1284
- NSMutableArray<ENRMFormattingRange *> *clippedRanges = [NSMutableArray array];
1285
- for (ENRMFormattingRange *range in [self allRangesIncludingTransient]) {
1286
- NSUInteger rangeStart = range.range.location;
1287
- NSUInteger rangeEnd = NSMaxRange(range.range);
1316
+ if (!show && !wasShown) {
1317
+ return;
1318
+ }
1288
1319
 
1289
- if (rangeEnd <= selection.location || rangeStart >= selEnd) {
1290
- continue;
1291
- }
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;
1292
1328
 
1293
- NSUInteger clippedStart = MAX(rangeStart, selection.location);
1294
- NSUInteger clippedEnd = MIN(rangeEnd, selEnd);
1295
- NSRange shifted = NSMakeRange(clippedStart - selection.location, clippedEnd - clippedStart);
1329
+ BOOL isTrailing = show && text.length > 0 && location >= text.length;
1296
1330
 
1297
- [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];
1298
1338
  }
1299
1339
 
1300
- 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];
1301
1354
  }
1302
1355
 
1303
- - (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
1304
1360
  {
1305
- NSString *plainText = ENRMGetPlainText(_textView);
1306
- if (plainText.length == 0) {
1307
- return;
1308
- }
1309
- NSString *markdown = [ENRMMarkdownSerializer serializePlainText:plainText ranges:[self allRangesIncludingTransient]];
1310
- NSMutableDictionary *items = [NSMutableDictionary dictionary];
1311
- items[kUTIPlainText] = plainText;
1312
- if (markdown.length > 0) {
1313
- 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);
1314
1371
  }
1315
- copyItemsToPasteboard(items);
1316
1372
  }
1317
1373
 
1318
- - (void)requestMarkdown:(NSInteger)requestId
1374
+ - (NSInteger)headingLevelForCursorParagraph
1319
1375
  {
1320
- auto emitter = [self getEventEmitter];
1321
- if (emitter == nullptr) {
1322
- return;
1323
- }
1324
- NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
1325
- ranges:[self allRangesIncludingTransient]];
1326
- emitter->onRequestMarkdownResult({
1327
- .requestId = static_cast<int>(requestId),
1328
- .markdown = std::string([markdown UTF8String] ?: ""),
1329
- });
1376
+ return [_blockCoordinator headingLevelAtPosition:_textView.selectedRange.location
1377
+ inText:_textView.textStorage.string];
1330
1378
  }
1331
1379
 
1332
- - (CGRect)computeCaretRect
1380
+ - (NSString *)adjustStoresForEditAtLocation:(NSUInteger)editLocation
1381
+ deletedLength:(NSUInteger)deletedLength
1382
+ insertedLength:(NSUInteger)insertedLength
1333
1383
  {
1334
- CGRect caretRect = CGRectZero;
1335
- #if !TARGET_OS_OSX
1336
- UITextRange *selectedRange = _textView.selectedTextRange;
1337
- if (selectedRange != nil) {
1338
- caretRect = [_textView caretRectForPosition:selectedRange.start];
1339
- }
1340
- #else
1341
- NSRange selection = _textView.selectedRange;
1342
- if (selection.location != NSNotFound) {
1343
- NSRange glyphRange = [_textView.layoutManager glyphRangeForCharacterRange:NSMakeRange(selection.location, 0)
1344
- actualCharacterRange:NULL];
1345
- caretRect = [_textView.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
1346
- caretRect.origin.x += _textView.textContainerInset.left;
1347
- caretRect.origin.y += _textView.textContainerInset.top;
1348
- }
1349
- #endif
1350
- return caretRect;
1384
+ return [_editPipeline adjustStoresForEditAtLocation:editLocation
1385
+ deletedLength:deletedLength
1386
+ insertedLength:insertedLength];
1351
1387
  }
1352
1388
 
1353
- - (void)requestCaretRect:(NSInteger)requestId
1389
+ - (void)setLink:(NSString *)url
1354
1390
  {
1355
- auto emitter = [self getEventEmitter];
1356
- if (emitter == nullptr) {
1391
+ NSRange selection = _textView.selectedRange;
1392
+ if (![_linkCoordinator setLinkURL:url atCursor:selection.location selection:selection]) {
1357
1393
  return;
1358
1394
  }
1359
-
1360
- CGRect caretRect = [self computeCaretRect];
1361
- emitter->onRequestCaretRectResult({
1362
- .requestId = static_cast<int>(requestId),
1363
- .x = caretRect.origin.x,
1364
- .y = caretRect.origin.y,
1365
- .width = caretRect.size.width,
1366
- .height = caretRect.size.height,
1367
- });
1395
+ [self applyFormatting];
1396
+ [_inputEventEmitter emitFormattingChanged];
1368
1397
  }
1369
1398
 
1370
- - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
1399
+ - (void)insertLink:(NSString *)text url:(NSString *)url
1371
1400
  {
1372
- 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 ]];
1373
1407
  }
1374
1408
 
1375
- #pragma mark - Style state query
1376
-
1377
- - (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1409
+ - (void)insertText:(NSString *)text
1378
1410
  {
1379
- BOOL inRange = [_formattingStore isStyleActive:type atPosition:position];
1380
- NSNumber *key = @(type);
1381
- if ([_pendingStyleRemovals containsObject:key]) {
1382
- return NO;
1383
- }
1384
- if ([_pendingStyles containsObject:key]) {
1385
- return YES;
1411
+ if (text.length == 0) {
1412
+ return;
1386
1413
  }
1387
- return inRange;
1414
+ [self pasteMarkdown:text];
1388
1415
  }
1389
1416
 
1390
- - (void)syncTypingAttributesWithPendingStyles
1417
+ - (void)startMention:(NSString *)indicator
1391
1418
  {
1392
- UIFontDescriptorSymbolicTraits traits = 0;
1393
- if ([_pendingStyles containsObject:@(ENRMInputStyleTypeStrong)]) {
1394
- traits |= UIFontDescriptorTraitBold;
1395
- }
1396
- if ([_pendingStyles containsObject:@(ENRMInputStyleTypeEmphasis)]) {
1397
- traits |= UIFontDescriptorTraitItalic;
1419
+ if (indicator.length == 0 || ![_mentionCoordinator containsIndicator:indicator]) {
1420
+ return;
1398
1421
  }
1399
1422
 
1400
- NSMutableDictionary *attrs = [_textView.typingAttributes mutableCopy];
1401
- attrs[NSFontAttributeName] = [_formatterStyle fontForTraits:traits];
1402
- _textView.typingAttributes = attrs;
1423
+ [self replaceSelectedTextWith:indicator formattingRanges:@[]];
1424
+ [self dispatchMentionUpdate];
1403
1425
  }
1404
1426
 
1405
- - (void)resetPendingStylesForSelectionChange
1427
+ - (void)insertMention:(NSString *)displayText url:(NSString *)url
1406
1428
  {
1407
- // Skip system-driven selection adjustments (e.g., predictive text) that fire
1408
- // immediately after a text edit.
1409
- static const CFTimeInterval kPostEditGracePeriod = 0.1;
1410
- BOOL isPostEditAdjustment =
1411
- (_lastTextChangeTime > 0 && (CACurrentMediaTime() - _lastTextChangeTime) < kPostEditGracePeriod);
1412
- if (isPostEditAdjustment) {
1429
+ if (displayText.length == 0 || !_mentionCoordinator.isActive ||
1430
+ _mentionCoordinator.activeRange.location == NSNotFound) {
1413
1431
  return;
1414
1432
  }
1415
- [_pendingStyles removeAllObjects];
1416
- [_pendingStyleRemovals removeAllObjects];
1417
- [self rebuildPendingStylesFromContext];
1418
- [self syncTypingAttributesWithPendingStyles];
1419
- }
1420
1433
 
1421
- - (void)rebuildPendingStylesFromContext
1422
- {
1423
- NSUInteger cursor = _textView.selectedRange.location;
1424
- 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) {
1425
1438
  return;
1426
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;
1427
1448
 
1428
- static const ENRMInputStyleType inlineStyles[] = {
1429
- ENRMInputStyleTypeStrong, ENRMInputStyleTypeEmphasis, ENRMInputStyleTypeUnderline,
1430
- ENRMInputStyleTypeStrikethrough, ENRMInputStyleTypeSpoiler,
1431
- };
1432
-
1433
- for (NSUInteger i = 0; i < sizeof(inlineStyles) / sizeof(inlineStyles[0]); i++) {
1434
- ENRMInputStyleType type = inlineStyles[i];
1435
- if ([_formattingStore isStyleAdjacentBefore:type position:cursor]) {
1436
- [_pendingStyles addObject:@(type)];
1437
- }
1438
- }
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];
1439
1453
  }
1440
1454
 
1441
- #pragma mark - Event emitters
1442
-
1443
- - (void)emitFormattingChanged
1455
+ - (void)removeLink
1444
1456
  {
1445
- [self emitOnChangeState];
1446
- if (_emitMarkdown) {
1447
- [self emitOnChangeMarkdown];
1457
+ if (![_linkCoordinator removeLinkAtPosition:_textView.selectedRange.location]) {
1458
+ return;
1448
1459
  }
1460
+ [self applyFormatting];
1461
+ [_inputEventEmitter emitFormattingChanged];
1449
1462
  }
1450
1463
 
1451
- - (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
1452
1478
  {
1453
- if (_eventEmitter == nullptr || _blockEmitting) {
1454
- return nullptr;
1479
+ if (latex.length == 0 || !ENRMMathChipController.isAvailable) {
1480
+ return;
1455
1481
  }
1456
- return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
1457
- }
1458
1482
 
1459
- - (NSArray<ENRMFormattingRange *> *)allRangesIncludingTransient
1460
- {
1461
- NSArray<ENRMFormattingRange *> *transient = [_detectorPipeline allTransientFormattingRanges];
1462
- if (transient.count == 0) {
1463
- return _formattingStore.allRanges;
1483
+ NSString *plainText = ENRMGetPlainText(_textView);
1484
+ NSRange selection = _textView.selectedRange;
1485
+ if (NSMaxRange(selection) > plainText.length) {
1486
+ return;
1464
1487
  }
1465
- NSMutableArray<ENRMFormattingRange *> *merged = [_formattingStore.allRanges mutableCopy];
1466
- [merged addObjectsFromArray:transient];
1467
- 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];
1468
1523
  }
1469
1524
 
1470
- - (void)clearActiveMention:(nullable NSString *)indicatorOverride
1525
+ - (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode
1471
1526
  {
1472
- NSString *indicator = indicatorOverride ?: _activeMentionIndicator;
1473
- _activeMentionIndicator = nil;
1474
- _activeMentionRange = NSMakeRange(NSNotFound, 0);
1475
- _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
+ }
1476
1535
 
1477
- if (indicator.length > 0) {
1478
- [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];
1479
1542
  }
1543
+ [self requestHeightUpdate];
1544
+ [self scheduleRelayoutIfNeeded];
1480
1545
  }
1481
1546
 
1482
- - (nullable ENRMInputMentionCandidate *)mentionCandidateAtCursor
1547
+ - (void)removeMath:(NSInteger)start
1483
1548
  {
1484
- NSRange selectedRange = _textView.selectedRange;
1485
- if (_mentionIndicators.count == 0 || selectedRange.length != 0) {
1486
- return nil;
1487
- }
1488
-
1489
- NSString *plainText = ENRMGetPlainText(_textView);
1490
- NSUInteger cursor = selectedRange.location;
1491
- if (cursor > plainText.length) {
1492
- return nil;
1549
+ if (start < 0 || [_mathChipController atomAtPosition:(NSUInteger)start] == nil) {
1550
+ return;
1493
1551
  }
1494
1552
 
1495
- NSUInteger start = [ENRMWordsUtils tokenStartInText:plainText beforePosition:cursor];
1496
- NSString *token = [plainText substringWithRange:NSMakeRange(start, cursor - start)];
1497
- NSString *matchedIndicator = nil;
1498
- for (NSString *indicator in _mentionIndicators) {
1499
- if ([token hasPrefix:indicator]) {
1500
- matchedIndicator = indicator;
1501
- break;
1502
- }
1503
- }
1504
- if (matchedIndicator == nil) {
1505
- return nil;
1506
- }
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:@[]];
1507
1558
 
1508
- if ([_formattingStore rangeOfType:ENRMInputStyleTypeLink containingPosition:start] != nil) {
1509
- return nil;
1559
+ if (_activeMathChipPosition == (NSUInteger)start) {
1560
+ _activeMathChipPosition = NSNotFound;
1510
1561
  }
1562
+ }
1511
1563
 
1512
- return [ENRMInputMentionCandidate candidateWithIndicator:matchedIndicator
1513
- start:start
1514
- end:cursor
1515
- 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];
1516
1570
  }
1517
1571
 
1518
- - (void)updateActiveMention
1572
+ #if !TARGET_OS_OSX
1573
+
1574
+ - (void)handleMathChipTap:(UITapGestureRecognizer *)gesture
1519
1575
  {
1520
- ENRMInputMentionCandidate *candidate = [self mentionCandidateAtCursor];
1521
- if (candidate == nil) {
1522
- [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) {
1523
1586
  return;
1524
1587
  }
1525
1588
 
1526
- NSString *indicator = candidate.indicator;
1527
- NSUInteger start = candidate.start;
1528
- NSUInteger end = candidate.end;
1529
- NSString *query = candidate.text;
1589
+ [_mathChipController clearSelectionsExceptPosition:chipPosition];
1590
+ [_mathChipController setSelected:YES atPosition:chipPosition];
1530
1591
 
1531
- if (_activeMentionIndicator == nil || ![_activeMentionIndicator isEqualToString:indicator] ||
1532
- _activeMentionRange.location != start) {
1533
- if (_activeMentionIndicator != nil) {
1534
- [self emitOnEndMention:_activeMentionIndicator];
1535
- }
1536
- _activeMentionIndicator = indicator;
1537
- [self emitOnStartMention:indicator];
1538
- }
1539
- _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);
1540
1600
 
1541
- if (![_activeMentionText isEqualToString:query]) {
1542
- _activeMentionText = query;
1543
- [self emitOnChangeMentionWithIndicator:indicator text:query];
1544
- }
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];
1545
1611
  }
1546
1612
 
1547
- - (BOOL)deleteLinkForReplacementRange:(NSRange)range replacementText:(NSString *)text
1613
+ #endif
1614
+
1615
+ #pragma mark -
1616
+
1617
+ - (void)showLinkPrompt
1548
1618
  {
1549
- if (text.length > 0) {
1550
- return NO;
1551
- }
1619
+ ENRMFormattingRange *activeLink = [_linkCoordinator linkAtPosition:_textView.selectedRange.location];
1620
+ NSString *existingURL = activeLink != nil ? activeLink.url : nil;
1552
1621
 
1553
- NSUInteger lookupPosition;
1554
- if (range.length > 0) {
1555
- lookupPosition = range.location;
1556
- } else if (range.location > 0) {
1557
- lookupPosition = range.location - 1;
1558
- } else {
1559
- return NO;
1560
- }
1622
+ __weak EnrichedMarkdownTextInput *weakSelf = self;
1623
+ ENRMShowLinkPrompt(self, existingURL, ^(NSString *url) { [weakSelf setLink:url]; });
1624
+ }
1561
1625
 
1562
- ENRMFormattingRange *linkRange = [_formattingStore rangeOfType:ENRMInputStyleTypeLink
1563
- containingPosition:lookupPosition];
1564
- if (linkRange == nil) {
1565
- return NO;
1566
- }
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
+ }
1567
1639
 
1568
- [self replaceTextInRange:linkRange.range withText:@"" formattingRanges:@[]];
1569
- [self clearActiveMention:nil];
1570
- return YES;
1640
+ - (nullable NSString *)markdownForSelectedRange
1641
+ {
1642
+ return [_clipboardCoordinator serializeSelectedRange:_textView.selectedRange
1643
+ inText:ENRMGetPlainText(_textView)
1644
+ mathAtoms:[self mathAtomsForSerialization]];
1571
1645
  }
1572
1646
 
1573
- - (void)emitOnChangeText
1647
+ - (void)copyToClipboard
1574
1648
  {
1575
- auto emitter = [self getEventEmitter];
1576
- if (emitter == nullptr) {
1649
+ NSString *plainText = ENRMGetPlainText(_textView);
1650
+ if (plainText.length == 0) {
1577
1651
  return;
1578
1652
  }
1579
- NSString *plainText = ENRMGetPlainText(_textView);
1580
- 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);
1581
1661
  }
1582
1662
 
1583
- - (void)emitOnChangeMarkdown
1663
+ - (void)requestMarkdown:(NSInteger)requestId
1584
1664
  {
1585
- auto emitter = [self getEventEmitter];
1586
- if (emitter == nullptr) {
1587
- return;
1588
- }
1589
- NSString *markdown = [ENRMMarkdownSerializer serializeAttributedString:_textView.textStorage
1590
- ranges:[self allRangesIncludingTransient]];
1591
- emitter->onChangeMarkdown({.value = std::string([markdown UTF8String] ?: "")});
1665
+ [_inputEventEmitter requestMarkdown:requestId];
1592
1666
  }
1593
1667
 
1594
- - (void)emitOnChangeSelection
1668
+ - (CGRect)computeCaretRect
1595
1669
  {
1596
- auto emitter = [self getEventEmitter];
1597
- if (emitter == nullptr) {
1598
- 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];
1599
1675
  }
1676
+ #else
1600
1677
  NSRange selection = _textView.selectedRange;
1601
- emitter->onChangeSelection({
1602
- .start = static_cast<int>(selection.location),
1603
- .end = static_cast<int>(NSMaxRange(selection)),
1604
- });
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;
1605
1687
  }
1606
1688
 
1607
- - (void)emitOnChangeState
1689
+ - (void)requestCaretRect:(NSInteger)requestId
1608
1690
  {
1609
- auto emitter = [self getEventEmitter];
1610
- if (emitter == nullptr) {
1611
- return;
1612
- }
1691
+ [_inputEventEmitter requestCaretRect:requestId];
1692
+ }
1693
+
1694
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
1695
+ {
1696
+ RCTEnrichedMarkdownTextInputHandleCommand(self, commandName, args);
1697
+ }
1613
1698
 
1614
- NSUInteger cursor = _textView.selectedRange.location;
1615
- BOOL boldActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrong atPosition:cursor];
1616
- BOOL italicActive = [self isEffectiveStyleActive:ENRMInputStyleTypeEmphasis atPosition:cursor];
1617
- BOOL underlineActive = [self isEffectiveStyleActive:ENRMInputStyleTypeUnderline atPosition:cursor];
1618
- BOOL strikethroughActive = [self isEffectiveStyleActive:ENRMInputStyleTypeStrikethrough atPosition:cursor];
1619
- BOOL spoilerActive = [self isEffectiveStyleActive:ENRMInputStyleTypeSpoiler atPosition:cursor];
1620
- BOOL linkActive = [self isEffectiveStyleActive:ENRMInputStyleTypeLink atPosition:cursor];
1699
+ #pragma mark - ENRMInputEventEmitterDataSource
1621
1700
 
1622
- if (_prevState.initialized && _prevState.bold == boldActive && _prevState.italic == italicActive &&
1623
- _prevState.underline == underlineActive && _prevState.strikethrough == strikethroughActive &&
1624
- _prevState.spoiler == spoilerActive && _prevState.link == linkActive) {
1625
- return;
1701
+ - (std::shared_ptr<EnrichedMarkdownTextInputEventEmitter const>)fabricEventEmitter
1702
+ {
1703
+ if (_eventEmitter == nullptr || _editSession.shouldSuppressEvents) {
1704
+ return nullptr;
1626
1705
  }
1706
+ return std::static_pointer_cast<EnrichedMarkdownTextInputEventEmitter const>(_eventEmitter);
1707
+ }
1627
1708
 
1628
- _prevState.bold = boldActive;
1629
- _prevState.italic = italicActive;
1630
- _prevState.underline = underlineActive;
1631
- _prevState.strikethrough = strikethroughActive;
1632
- _prevState.spoiler = spoilerActive;
1633
- _prevState.link = linkActive;
1634
- _prevState.initialized = YES;
1635
-
1636
- emitter->onChangeState({
1637
- .bold = {.isActive = boldActive},
1638
- .italic = {.isActive = italicActive},
1639
- .underline = {.isActive = underlineActive},
1640
- .strikethrough = {.isActive = strikethroughActive},
1641
- .spoiler = {.isActive = spoilerActive},
1642
- .link = {.isActive = linkActive},
1643
- });
1709
+ - (NSRange)selectedRange
1710
+ {
1711
+ return _textView.selectedRange;
1644
1712
  }
1645
1713
 
1646
- - (void)emitCaretRectChangeIfNeeded
1714
+ - (BOOL)isStyleActive:(ENRMInputStyleType)type inRange:(NSRange)range
1647
1715
  {
1648
- auto emitter = [self getEventEmitter];
1649
- if (emitter == nullptr) {
1650
- return;
1651
- }
1716
+ return [_formattingStore isStyleActive:type inRange:range];
1717
+ }
1652
1718
 
1653
- CGRect caretRect = [self computeCaretRect];
1719
+ - (BOOL)isEffectiveStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1720
+ {
1721
+ return [_typingController isEffectiveStyleActive:type atPosition:position];
1722
+ }
1654
1723
 
1655
- if (_prevCaretRect.has_value() && CGRectEqualToRect(_prevCaretRect.value(), caretRect)) {
1656
- return;
1657
- }
1724
+ #pragma mark - ENRMInputTypingAttributesDataSource
1658
1725
 
1659
- _prevCaretRect = caretRect;
1726
+ - (BOOL)isStyleAdjacentBefore:(ENRMInputStyleType)type position:(NSUInteger)position
1727
+ {
1728
+ return [_formattingStore isStyleAdjacentBefore:type position:position];
1729
+ }
1660
1730
 
1661
- emitter->onCaretRectChange({
1662
- .x = caretRect.origin.x,
1663
- .y = caretRect.origin.y,
1664
- .width = caretRect.size.width,
1665
- .height = caretRect.size.height,
1666
- });
1731
+ - (BOOL)isStyleActive:(ENRMInputStyleType)type atPosition:(NSUInteger)position
1732
+ {
1733
+ return [_formattingStore isStyleActive:type atPosition:position];
1667
1734
  }
1668
1735
 
1669
- - (NSArray<NSString *> *)contextMenuItemTexts
1736
+ - (NSString *)currentMarkdown
1670
1737
  {
1671
- return _contextMenuItemTexts ?: @[];
1738
+ return [self serializeText:ENRMGetPlainText(_textView)
1739
+ ranges:[self allRangesIncludingTransient]
1740
+ blockRanges:_blockStore.allRanges];
1672
1741
  }
1673
1742
 
1674
- - (NSArray<NSString *> *)contextMenuItemIcons
1743
+ - (NSArray<ENRMFormattingRange *> *)allRangesIncludingTransient
1675
1744
  {
1676
- return _contextMenuItemIcons ?: @[];
1745
+ return [_clipboardCoordinator allRangesIncludingTransient];
1677
1746
  }
1678
1747
 
1679
- - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
1748
+ - (void)updateActiveMention
1680
1749
  {
1681
- return _inputSelectionMenuConfig;
1750
+ [self dispatchMentionUpdate];
1682
1751
  }
1683
1752
 
1684
- - (ENRMFormatMenuConfig)formatMenuConfig
1753
+ - (void)clearActiveMention:(nullable NSString *)indicatorOverride
1685
1754
  {
1686
- return _formatMenuConfig;
1755
+ [self dispatchMentionEvents:[_mentionCoordinator clearWithIndicatorOverride:indicatorOverride]];
1687
1756
  }
1688
1757
 
1689
- - (void)emitContextMenuItemPress:(NSString *)itemText
1758
+ - (void)dispatchMentionUpdate
1690
1759
  {
1691
- auto eventEmitter = [self getEventEmitter];
1692
- if (eventEmitter == nullptr) {
1693
- 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
+ }
1694
1778
  }
1779
+ }
1695
1780
 
1696
- NSRange selectedRange = _textView.selectedRange;
1697
- NSString *selectedText =
1698
- 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
+ }
1699
1786
 
1700
- auto isActive = [&](ENRMInputStyleType type) -> BOOL {
1701
- if (selectedRange.length > 0) {
1702
- return [_formattingStore isStyleActive:type inRange:selectedRange];
1703
- }
1704
- return [self isEffectiveStyleActive:type atPosition:selectedRange.location];
1705
- };
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
+ }
1706
1795
 
1707
- BOOL boldActive = isActive(ENRMInputStyleTypeStrong);
1708
- BOOL italicActive = isActive(ENRMInputStyleTypeEmphasis);
1709
- BOOL underlineActive = isActive(ENRMInputStyleTypeUnderline);
1710
- BOOL strikethroughActive = isActive(ENRMInputStyleTypeStrikethrough);
1711
- BOOL spoilerActive = isActive(ENRMInputStyleTypeSpoiler);
1712
- BOOL linkActive = isActive(ENRMInputStyleTypeLink);
1713
-
1714
- eventEmitter->onContextMenuItemPress({
1715
- .itemText = std::string(itemText.UTF8String),
1716
- .selectedText = std::string(selectedText.UTF8String),
1717
- .selectionStart = static_cast<int>(selectedRange.location),
1718
- .selectionEnd = static_cast<int>(NSMaxRange(selectedRange)),
1719
- .styleState =
1720
- {
1721
- .bold = {.isActive = boldActive},
1722
- .italic = {.isActive = italicActive},
1723
- .underline = {.isActive = underlineActive},
1724
- .strikethrough = {.isActive = strikethroughActive},
1725
- .spoiler = {.isActive = spoilerActive},
1726
- .link = {.isActive = linkActive},
1727
- },
1728
- });
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;
1729
1804
  }
1730
1805
 
1731
- - (void)emitOnFocus
1806
+ - (NSArray<NSString *> *)contextMenuItemTexts
1732
1807
  {
1733
- auto emitter = [self getEventEmitter];
1734
- if (emitter == nullptr) {
1735
- return;
1736
- }
1737
- emitter->onInputFocus({});
1808
+ return _contextMenuItemTexts ?: @[];
1738
1809
  }
1739
1810
 
1740
- - (void)emitOnBlur
1811
+ - (NSArray<NSString *> *)contextMenuItemIcons
1741
1812
  {
1742
- auto emitter = [self getEventEmitter];
1743
- if (emitter == nullptr) {
1744
- return;
1745
- }
1746
- emitter->onInputBlur({});
1813
+ return _contextMenuItemIcons ?: @[];
1747
1814
  }
1748
1815
 
1749
- - (void)emitOnLinkDetectedWithText:(NSString *)text url:(NSString *)url range:(NSRange)range
1816
+ - (ENRMInputSelectionMenuConfig)inputSelectionMenuConfig
1750
1817
  {
1751
- auto emitter = [self getEventEmitter];
1752
- if (emitter == nullptr) {
1753
- return;
1754
- }
1755
- emitter->onLinkDetected({
1756
- .text = std::string([text UTF8String] ?: ""),
1757
- .url = std::string([url UTF8String] ?: ""),
1758
- .start = static_cast<int>(range.location),
1759
- .end = static_cast<int>(range.location + range.length),
1760
- });
1818
+ return _inputSelectionMenuConfig;
1761
1819
  }
1762
1820
 
1763
- - (void)emitOnStartMention:(NSString *)indicator
1821
+ - (ENRMFormatMenuConfig)formatMenuConfig
1764
1822
  {
1765
- auto emitter = [self getEventEmitter];
1766
- if (emitter == nullptr) {
1767
- return;
1768
- }
1769
- emitter->onStartMention({.indicator = std::string([indicator UTF8String] ?: "")});
1823
+ return _formatMenuConfig;
1770
1824
  }
1771
1825
 
1772
- - (void)emitOnChangeMentionWithIndicator:(NSString *)indicator text:(NSString *)text
1826
+ - (ENRMInputEventEmitter *)inputEventEmitter
1773
1827
  {
1774
- auto emitter = [self getEventEmitter];
1775
- if (emitter == nullptr) {
1776
- return;
1777
- }
1778
- emitter->onChangeMention({
1779
- .indicator = std::string([indicator UTF8String] ?: ""),
1780
- .text = std::string([text UTF8String] ?: ""),
1781
- });
1828
+ return _inputEventEmitter;
1782
1829
  }
1783
1830
 
1784
- - (void)emitOnEndMention:(NSString *)indicator
1831
+ - (ENRMInputTypingAttributesController *)typingController
1785
1832
  {
1786
- auto emitter = [self getEventEmitter];
1787
- if (emitter == nullptr) {
1788
- return;
1789
- }
1790
- emitter->onEndMention({.indicator = std::string([indicator UTF8String] ?: "")});
1833
+ return _typingController;
1791
1834
  }
1792
1835
 
1793
1836
  #pragma mark - Text edit tracking
1794
1837
 
1795
1838
  - (void)handleTextChanged
1796
1839
  {
1797
- if (ENRMHasMarkedText(_textView)) {
1840
+ if (_editSession.isComposing) {
1798
1841
  return;
1799
1842
  }
1800
1843
 
@@ -1824,64 +1867,63 @@ using namespace facebook::react;
1824
1867
  }
1825
1868
  }
1826
1869
 
1827
- [_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];
1828
1879
 
1829
- if (insertedLength > 0) {
1830
- NSRange insertedRange = NSMakeRange(editLocation, insertedLength);
1831
-
1832
- // Skip applying pending styles when the insertion is only line breaks —
1833
- // a phantom range over a bare newline corrupts isStyleActive() at the boundary.
1834
- NSString *plainText = ENRMGetPlainText(_textView);
1835
- NSUInteger insertedEnd = NSMaxRange(insertedRange);
1836
- BOOL insertedHasGlyphContent = NO;
1837
- if (insertedEnd <= plainText.length) {
1838
- NSCharacterSet *newlines = [NSCharacterSet newlineCharacterSet];
1839
- for (NSUInteger i = insertedRange.location; i < insertedEnd; i++) {
1840
- if (![newlines characterIsMember:[plainText characterAtIndex:i]]) {
1841
- insertedHasGlyphContent = YES;
1842
- break;
1843
- }
1844
- }
1845
- }
1846
-
1847
- if (insertedHasGlyphContent) {
1848
- for (NSNumber *styleNum in _pendingStyles) {
1849
- ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:(ENRMInputStyleType)styleNum.integerValue
1850
- range:insertedRange];
1851
- [_formattingStore addRange:newRange];
1852
- }
1853
- }
1880
+ BOOL touchedNewline = [_editPipeline processTextChangeWithContext:context];
1854
1881
 
1855
- // adjustForEditAtLocation may have expanded an existing range to cover
1856
- // the insertion — carve out the inserted portion for removed styles.
1857
- for (NSNumber *styleNum in _pendingStyleRemovals) {
1858
- [_formattingStore removeType:(ENRMInputStyleType)styleNum.integerValue inRange:insertedRange];
1859
- }
1860
- }
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;
1861
1887
 
1862
1888
  _lastTextLength = newLength;
1863
1889
 
1864
1890
  #if !TARGET_OS_OSX
1865
1891
  if (newLength == 0) {
1866
- [self resetBaseTypingAttributes];
1892
+ if ([self headingLevelForCursorParagraph] > 0 || [self listBlockForCursorParagraph] != nil) {
1893
+ [_typingController syncWithCursorBlock];
1894
+ } else {
1895
+ [self resetBaseTypingAttributes];
1896
+ }
1867
1897
  }
1868
1898
  #endif
1869
1899
 
1870
- [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
+ }
1871
1909
 
1872
- NSUInteger clampedEditLocation = MIN(editLocation, newLength);
1873
- NSUInteger clampedInsertedLength = MIN(insertedLength, newLength - clampedEditLocation);
1874
- [_detectorPipeline processTextChange:ENRMGetPlainText(_textView)
1875
- 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];
1876
1917
 
1877
1918
  [self updatePlaceholderVisibility];
1878
- [self emitOnChangeText];
1879
- [self emitOnChangeSelection];
1880
- [self emitFormattingChanged];
1919
+ [_inputEventEmitter emitOnChangeText];
1920
+ [_inputEventEmitter emitOnChangeSelection];
1921
+ [_inputEventEmitter emitFormattingChanged];
1881
1922
  [self updateActiveMention];
1882
- [self emitCaretRectChangeIfNeeded];
1923
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
1883
1924
  [self requestHeightUpdate];
1884
1925
  [self scheduleRelayoutIfNeeded];
1926
+ [self updateEmptyBulletMarker];
1885
1927
  }
1886
1928
 
1887
1929
  #pragma mark - Text view delegate
@@ -1919,17 +1961,21 @@ using namespace facebook::react;
1919
1961
  {
1920
1962
  [self stripLinkTypingAttributes];
1921
1963
 
1922
- if (_textView.selectedRange.length == 0 && !_isTextChanging) {
1964
+ if (_textView.selectedRange.length == 0 && _editSession.phase == ENRMEditPhaseIdle) {
1923
1965
  NSString *text = ENRMGetPlainText(_textView);
1924
1966
  if (text.length > 0) {
1925
1967
  NSRange paragraphRange = [text paragraphRangeForRange:_textView.selectedRange];
1926
1968
  NSString *paragraphText = [text substringWithRange:paragraphRange];
1927
1969
  BOOL isEmpty = paragraphText.length == 0 || [paragraphText isEqualToString:@"\n"];
1928
1970
  if (isEmpty) {
1929
- NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
1930
- attrs[NSFontAttributeName] = _formatterStyle.baseFont;
1931
- attrs[NSForegroundColorAttributeName] = _formatterStyle.baseTextColor;
1932
- _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
+ }
1933
1979
  }
1934
1980
  }
1935
1981
  }
@@ -1937,125 +1983,154 @@ using namespace facebook::react;
1937
1983
 
1938
1984
  - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
1939
1985
  {
1986
+ [_inputEventEmitter emitOnKeyPress:text];
1940
1987
  if ([self deleteLinkForReplacementRange:range replacementText:text]) {
1941
1988
  return NO;
1942
1989
  }
1990
+ if ([self handleListKeyForReplacementRange:range replacementText:text]) {
1991
+ return NO;
1992
+ }
1943
1993
  _preEditSelectedRange = _lastSelectedRange;
1944
- _isTextChanging = YES;
1994
+ [self capturePreEditBlockForRange:range];
1995
+ _preEditParagraphWasEmpty = [self preEditParagraphWasEmpty:range];
1996
+ _preEditReplacedNewline = [self replacedRangeTouchesNewline:range];
1997
+ [_editSession enterPhase:ENRMEditPhaseProcessing];
1945
1998
  [self stripLinkTypingAttributes];
1946
1999
  return YES;
1947
2000
  }
1948
2001
 
1949
2002
  - (void)textViewDidChange:(UITextView *)textView
1950
2003
  {
1951
- if (_isApplyingFormatting) {
2004
+ if (_editSession.shouldSuppressFormatting) {
1952
2005
  return;
1953
2006
  }
1954
2007
  [self handleTextChanged];
1955
- _isTextChanging = NO;
1956
- _lastTextChangeTime = CACurrentMediaTime();
2008
+ [_editSession exitPhase];
2009
+ [_editSession recordTextChange];
1957
2010
  _lastSelectedRange = textView.selectedRange;
1958
2011
  }
1959
2012
 
1960
2013
  - (void)textViewDidBeginEditing:(UITextView *)textView
1961
2014
  {
1962
- [self emitOnFocus];
2015
+ [_inputEventEmitter emitOnFocus];
1963
2016
  }
1964
2017
 
1965
2018
  - (void)textViewDidEndEditing:(UITextView *)textView
1966
2019
  {
1967
2020
  [self clearActiveMention:nil];
1968
- [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
+ });
1969
2082
  }
1970
2083
 
1971
2084
  - (void)textViewDidChangeSelection:(UITextView *)textView
1972
2085
  {
1973
2086
  NSRange newSelection = textView.selectedRange;
1974
- // Links (e.g. mentions) are atomic: snap a partial selection to the whole link, and a caret inside
1975
- // a link to its end. Returning hands the adjusted selection to the recursive change (no double emit).
1976
- if (!_isApplyingFormatting && !_isTextChanging && !ENRMHasMarkedText(_textView)) {
1977
- NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:newSelection];
1978
- if (!NSEqualRanges(adjusted, newSelection)) {
1979
- _textView.selectedRange = adjusted;
1980
- 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
+ }
1981
2096
  }
1982
2097
  }
1983
2098
  NSRange previousSelection = _lastSelectedRange;
1984
2099
  _lastSelectedRange = newSelection;
1985
2100
 
1986
- if (_isApplyingFormatting || _isTextChanging) {
2101
+ if (_editSession.shouldSuppressSelectionSideEffects) {
1987
2102
  return;
1988
2103
  }
1989
2104
 
1990
- if (ENRMHasMarkedText(_textView)) {
2105
+ if (_editSession.isComposing) {
1991
2106
  return;
1992
2107
  }
1993
2108
 
1994
- #if ENRICHED_MARKDOWN_MATH
2109
+ BOOL selectionMoved =
2110
+ newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2111
+
1995
2112
  if (_suppressMathChipClearOnce) {
1996
- // Caret update was triggered by handleTapForMathChip — keep that chip
1997
- // selected; consume the suppression flag.
2113
+ // This caret move is the one handleMathChipTap: just made — keep its chip
2114
+ // highlighted and consume the suppression.
1998
2115
  _suppressMathChipClearOnce = NO;
1999
- } else {
2000
- [self clearMathChipSelectionsExcept:NSMakeRange(NSNotFound, 0)];
2001
- _activeMathChipIndex = NSNotFound;
2116
+ } else if (_activeMathChipPosition != NSNotFound) {
2117
+ [_mathChipController clearSelectionsExceptPosition:NSNotFound];
2118
+ _activeMathChipPosition = NSNotFound;
2002
2119
  }
2003
- #endif
2004
-
2005
- BOOL selectionMoved =
2006
- newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2007
2120
 
2008
2121
  if (selectionMoved) {
2009
- [self resetPendingStylesForSelectionChange];
2122
+ [_typingController resetForSelectionChange];
2010
2123
  }
2011
2124
 
2012
2125
  [self manageSelectionBasedChanges];
2013
2126
 
2014
- [self emitOnChangeSelection];
2127
+ [_inputEventEmitter emitOnChangeSelection];
2015
2128
  [self updateActiveMention];
2016
- [self emitOnChangeState];
2017
- [self emitCaretRectChangeIfNeeded];
2129
+ [_inputEventEmitter emitOnChangeState];
2130
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
2131
+ [self updateEmptyBulletMarker];
2018
2132
  }
2019
2133
 
2020
- #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
2021
- - (void)clearMathChipSelectionsExcept:(NSRange)keepRange
2022
- {
2023
- NSTextStorage *storage = _textView.textStorage;
2024
- if (storage.length == 0)
2025
- return;
2026
- __block BOOL anyChanged = NO;
2027
- __block NSMutableArray<NSValue *> *invalidationRanges = [NSMutableArray array];
2028
- [storage enumerateAttribute:NSAttachmentAttributeName
2029
- inRange:NSMakeRange(0, storage.length)
2030
- options:0
2031
- usingBlock:^(id value, NSRange range, BOOL *stop) {
2032
- if (![value isKindOfClass:[ENRMMathInlineAttachment class]])
2033
- return;
2034
- ENRMMathInlineAttachment *att = value;
2035
- BOOL keep = NSEqualRanges(range, keepRange);
2036
- if (att.isSelected && !keep) {
2037
- att.isSelected = NO;
2038
- [att invalidateRender];
2039
- [invalidationRanges addObject:[NSValue valueWithRange:range]];
2040
- anyChanged = YES;
2041
- }
2042
- }];
2043
- if (anyChanged) {
2044
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
2045
- for (NSValue *v in invalidationRanges) {
2046
- NSRange r = v.rangeValue;
2047
- [lm invalidateLayoutForCharacterRange:r actualCharacterRange:NULL];
2048
- }
2049
- ENRMSetNeedsDisplay(_textView);
2050
- }
2051
- }
2052
- #else
2053
- - (void)clearMathChipSelectionsExcept:(NSRange)keepRange
2054
- {
2055
- (void)keepRange;
2056
- }
2057
- #endif
2058
-
2059
2134
  #else
2060
2135
 
2061
2136
  #pragma mark - RCTBackedTextInputDelegate (macOS)
@@ -2067,7 +2142,7 @@ using namespace facebook::react;
2067
2142
 
2068
2143
  - (void)textInputDidBeginEditing
2069
2144
  {
2070
- [self emitOnFocus];
2145
+ [_inputEventEmitter emitOnFocus];
2071
2146
  }
2072
2147
 
2073
2148
  - (BOOL)textInputShouldEndEditing
@@ -2078,7 +2153,7 @@ using namespace facebook::react;
2078
2153
  - (void)textInputDidEndEditing
2079
2154
  {
2080
2155
  [self clearActiveMention:nil];
2081
- [self emitOnBlur];
2156
+ [_inputEventEmitter emitOnBlur];
2082
2157
  }
2083
2158
 
2084
2159
  - (BOOL)textInputShouldReturn
@@ -2097,31 +2172,36 @@ using namespace facebook::react;
2097
2172
 
2098
2173
  - (nullable NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
2099
2174
  {
2175
+ [_inputEventEmitter emitOnKeyPress:text];
2100
2176
  if ([self deleteLinkForReplacementRange:range replacementText:text]) {
2101
2177
  return nil;
2102
2178
  }
2103
2179
  _preEditSelectedRange = _lastSelectedRange;
2104
- _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];
2105
2186
  return text;
2106
2187
  }
2107
2188
 
2108
2189
  - (void)textInputDidChange
2109
2190
  {
2110
- if (_isApplyingFormatting) {
2111
- _isTextChanging = NO;
2191
+ if (_editSession.shouldSuppressFormatting) {
2192
+ [_editSession exitPhase];
2112
2193
  return;
2113
2194
  }
2114
2195
  [self handleTextChanged];
2115
- _isTextChanging = NO;
2116
- _lastTextChangeTime = CACurrentMediaTime();
2196
+ [_editSession exitPhase];
2197
+ [_editSession recordTextChange];
2117
2198
  _lastSelectedRange = _textView.selectedRange;
2118
2199
  }
2119
2200
 
2120
2201
  - (void)textInputDidChangeSelection
2121
2202
  {
2122
2203
  NSRange newSelection = _textView.selectedRange;
2123
- // Atomic link snapping — same logic as textViewDidChangeSelection: (iOS).
2124
- if (!_isApplyingFormatting && !_isTextChanging && !ENRMHasMarkedText(_textView)) {
2204
+ if (!_editSession.shouldSuppressSelectionSideEffects && !_editSession.isComposing) {
2125
2205
  NSRange adjusted = [_formattingStore selectionAdjustedForAtomicLinks:newSelection];
2126
2206
  if (!NSEqualRanges(adjusted, newSelection)) {
2127
2207
  _textView.selectedRange = adjusted;
@@ -2131,11 +2211,11 @@ using namespace facebook::react;
2131
2211
  NSRange previousSelection = _lastSelectedRange;
2132
2212
  _lastSelectedRange = newSelection;
2133
2213
 
2134
- if (_isApplyingFormatting || _isTextChanging) {
2214
+ if (_editSession.shouldSuppressSelectionSideEffects) {
2135
2215
  return;
2136
2216
  }
2137
2217
 
2138
- if (ENRMHasMarkedText(_textView)) {
2218
+ if (_editSession.isComposing) {
2139
2219
  return;
2140
2220
  }
2141
2221
 
@@ -2143,13 +2223,13 @@ using namespace facebook::react;
2143
2223
  newSelection.location != previousSelection.location || newSelection.length != previousSelection.length;
2144
2224
 
2145
2225
  if (selectionMoved) {
2146
- [self resetPendingStylesForSelectionChange];
2226
+ [_typingController resetForSelectionChange];
2147
2227
  }
2148
2228
 
2149
- [self emitOnChangeSelection];
2229
+ [_inputEventEmitter emitOnChangeSelection];
2150
2230
  [self updateActiveMention];
2151
- [self emitOnChangeState];
2152
- [self emitCaretRectChangeIfNeeded];
2231
+ [_inputEventEmitter emitOnChangeState];
2232
+ [_inputEventEmitter emitCaretRectChangeIfNeeded];
2153
2233
  }
2154
2234
 
2155
2235
  // @required stubs for RCTBackedTextInputDelegate — RCTUITextView's internal adapter